Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
de25fe2
Merge pull request #306 from Dgetsylver/feat/ui-rebuild-v3
Dgetsylver Jun 15, 2026
2ca3d3f
docs(d1): mainnet deploy readiness checklist — WASMs built, config ve…
Dgetsylver Jun 15, 2026
55e9ee3
fix(landing): remove bug-bounty link (program not live; info stale)
Dgetsylver Jun 15, 2026
dde71e8
Merge pull request #308 from Dgetsylver/fix/remove-bug-bounty-link
Dgetsylver Jun 15, 2026
4e74a14
Merge pull request #307 from Dgetsylver/chore/d1-deploy-readiness
Dgetsylver Jun 15, 2026
0a2b137
fix: fixed unit problem in blend request & submit_deleverage issue
hugo-heer Jun 16, 2026
65f9e50
fix: withrawal use now data from submit_unwind
hugo-heer Jun 16, 2026
88f3263
feat(hf): show Health Factor to 5 decimals for hyperleveraged precision
Dgetsylver Jun 17, 2026
bc1b846
fixup: Health Factor 4 decimals (not 5)
Dgetsylver Jun 17, 2026
20ff575
Merge pull request #309 from Dgetsylver/fix/hf-precision
Dgetsylver Jun 17, 2026
f54c9cb
Merge pull request #310 from Dgetsylver/fix/leverage-issue
hugo-heer Jun 18, 2026
8d32f08
fix: fixed cargo fmt
hugo-heer Jun 23, 2026
6852b66
chore: added tests
hugo-heer Jun 23, 2026
c886009
fix(ci): make Contracts gate green — fmt, clippy
hugo-heer Jun 23, 2026
bad0c2c
fix(audit): bump quinn-proto 0.11.14 -> 0.11.15 (RUSTSEC-2026-0185)
hugo-heer Jun 23, 2026
c348464
Merge pull request #314 from Dgetsylver/fix/ci-tests
hugo-heer Jun 23, 2026
1099f02
feat: added admin sep for loop
hugo-heer Jun 23, 2026
92956eb
feat: added admin sep for vault
hugo-heer Jun 23, 2026
7fc70d1
Merge pull request #315 from Dgetsylver/feat/admin-sep
hugo-heer Jun 23, 2026
b9a1b63
fix(dashboard): Manage / Add leg deep-link to the real pool + asset (…
Dgetsylver Jun 24, 2026
568f5fd
feat(ui): complete V3 design system rebuild and monolith extraction
johdanike Jun 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions contracts/strategies/blend_leverage/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions contracts/strategies/blend_leverage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ soroban-sdk = "25.3.0"
defindex-strategy-core = "0.3.0"
soroban-fixed-point-math = "1.3.0"
blend-contract-sdk = "2.25.0"
admin-sep = { git = "https://github.com/theahaco/admin-sep", rev = "46ed159ff38ee81f4b61b5ddb8ca4b6bdf972028" }

[dev-dependencies]
soroban-sdk = { version = "25.3.0", features = ["testutils"] }
blend-contract-sdk = { version = "2.25.0", features = ["testutils"] }
vault_share_token = { path = "../../tokens/vault_share" }

[profile.release]
opt-level = "z"
Expand Down
96 changes: 58 additions & 38 deletions contracts/strategies/blend_leverage/src/blend_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use soroban_sdk::{
use crate::{
constants::{
REQUEST_TYPE_BORROW, REQUEST_TYPE_REPAY, REQUEST_TYPE_SUPPLY_COLLATERAL,
REQUEST_TYPE_WITHDRAW_COLLATERAL, SCALAR_12,
REQUEST_TYPE_WITHDRAW_COLLATERAL, SCALAR_12, SCALAR_7,
},
leverage::{compute_step, loop_step_count},
soroswap::internal_swap_exact_tokens_for_tokens,
Expand Down Expand Up @@ -153,15 +153,32 @@ pub fn submit_unwind(

let pre_balance = token_client.balance(&strategy);

// Blend request amounts are denominated in the UNDERLYING asset, but the
// caller passes b/d-TOKEN quantities. Convert with the current pool rates
// (underlying = tokens × rate / SCALAR_12). The two are only equal while the
// rates sit at 1.0; once interest accrues they diverge, so skipping this
// conversion makes the unwind repay/withdraw the wrong amounts.
let reserve = pool_client.get_reserve(&config.asset);
let b_rate = reserve.data.b_rate;
let d_rate = reserve.data.d_rate;
let d_underlying = d_tokens_to_remove
.checked_mul(d_rate)
.ok_or(StrategyError::ArithmeticError)?
/ SCALAR_12;
let b_underlying = b_tokens_to_remove
.checked_mul(b_rate)
.ok_or(StrategyError::ArithmeticError)?
/ SCALAR_12;

// Build atomic unwind: [withdraw, repay] × N steps + [withdraw equity].
// Split d_tokens_to_remove evenly across target_loops steps.
// Split the underlying debt evenly across target_loops steps.
// Each step withdraws and repays the same amount, maintaining HF.
// The final withdraw extracts the equity (b - d difference).
let mut requests: Vec<Request> = Vec::new(e);
let mut total_repay = 0i128;

let n_steps = config.target_loops.max(1);
let repay_per_step = d_tokens_to_remove / n_steps as i128;
let repay_per_step = d_underlying / n_steps as i128;

// Check if this is a full close (removing all debt)
let pool_client_inner = BlendPoolClient::new(e, &config.pool);
Expand All @@ -180,18 +197,15 @@ pub fn submit_unwind(
let repay_amount = if is_last && is_full_close {
i64::MAX as i128
} else if is_last {
d_tokens_to_remove - repay_per_step * (n_steps as i128 - 1)
d_underlying - repay_per_step * (n_steps as i128 - 1)
} else {
repay_per_step
};

// Withdraw same amount as repay in each pair — this frees collateral to cover repayment.
// The equity portion (b_tokens - d_tokens) is withdrawn separately at the end.
let withdraw_amount = if is_last && is_full_close {
// For full close, withdraw same as the repay dust-cleaning amount
d_tokens_to_remove - repay_per_step * (n_steps as i128 - 1)
} else if is_last {
d_tokens_to_remove - repay_per_step * (n_steps as i128 - 1)
// The equity portion (b - d, in underlying) is withdrawn separately at the end.
let withdraw_amount = if is_last {
d_underlying - repay_per_step * (n_steps as i128 - 1)
} else {
repay_per_step
};
Expand All @@ -209,10 +223,9 @@ pub fn submit_unwind(
total_repay += repay_amount;
}

// Final: withdraw equity portion (collateral minus debt that was removed)
let equity_withdraw = b_tokens_to_remove
.checked_sub(d_tokens_to_remove)
.unwrap_or(0);
// Final: withdraw equity portion (collateral minus debt that was removed),
// in underlying.
let equity_withdraw = b_underlying.checked_sub(d_underlying).unwrap_or(0);

if equity_withdraw > 0 {
requests.push_back(Request {
Expand Down Expand Up @@ -314,44 +327,51 @@ pub fn submit_deleverage(
return Ok((0, 0));
}

// Build all (withdraw, repay) pairs for a single atomic submit.
// Each layer amount = the borrow amount of the corresponding leverage step.
// Unwind in reverse order (last leverage step unwound first).
let count = loop_step_count(config.target_loops);
let mut layers: Vec<i128> = Vec::new(e);
let mut orig_balance = pre_b; // approximate with total collateral
for i in 0..count {
let is_final = i == config.target_loops.min(20);
let (_, borrow) = compute_step(orig_balance, config.c_factor, is_final);
if borrow > 0 {
layers.push_back(borrow);
}
orig_balance = borrow;
// Size one unwind layer as `debt × (1 - c_factor)`, in UNDERLYING — the same
// definition `compute_partial_unwind` uses to derive `unwind_loops`, so that
// unwinding N loops repays ≈ the intended `repay_underlying`. (The previous
// implementation seeded the layers from total collateral, producing layers
// several times larger than the position, which over-unwound or reverted.)
// Blend request amounts are denominated in the underlying asset, so convert
// the d-token debt with the current d_rate.
let reserve = pool_client.get_reserve(&config.asset);
let debt_underlying = pre_d
.checked_mul(reserve.data.d_rate)
.ok_or(StrategyError::ArithmeticError)?
/ SCALAR_12;
let layer = debt_underlying
.checked_mul(SCALAR_7 - config.c_factor)
.ok_or(StrategyError::ArithmeticError)?
/ SCALAR_7;
if layer <= 0 {
return Ok((0, 0));
}

// Build all (withdraw, repay) pairs for a single atomic submit, each step
// HF-neutral (withdraw == repay). Cap the cumulative repay at the outstanding
// debt so we never over-repay or withdraw more collateral than exists.
let mut requests: Vec<Request> = Vec::new(e);
let mut total_repay = 0i128;
let n_layers = layers.len();
let loops_to_unwind = unwind_loops.min(n_layers);

for i in 0..loops_to_unwind {
let idx = n_layers - 1 - i;
let layer_amount = layers.get(idx).unwrap_or(0);
if layer_amount == 0 {
continue;
let mut remaining_debt = debt_underlying;

for _ in 0..unwind_loops.min(20) {
let amount = layer.min(remaining_debt);
if amount <= 0 {
break;
}

requests.push_back(Request {
address: config.asset.clone(),
amount: layer_amount,
amount,
request_type: REQUEST_TYPE_WITHDRAW_COLLATERAL,
});
requests.push_back(Request {
address: config.asset.clone(),
amount: layer_amount,
amount,
request_type: REQUEST_TYPE_REPAY,
});
total_repay += layer_amount;
total_repay += amount;
remaining_debt -= amount;
}

if total_repay > 0 {
Expand Down
Loading