Stratax Contracts

First Flight #57
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: medium
Likelihood: medium

Dust debt calculation on unwind

Author Revealed upon completion

Description

  • When unwinding, the contract should repay the entire variable debt so the position can be fully closed in a single transaction.

  • calculateUnwindParams determines debtAmount using the current variable debt token balance. However, Aave variable debt accrues interest continuously. Between (a) computing debtAmount off‑chain or via the view helper and (b) the actual unwindPosition execution mined on‑chain, a small interest delta accrues. Since unwindPosition flash‑loans exactly _debtAmount and immediately calls repay(_asset, _amount, 2, address(this)), this leaves a tiny residual (“dust”) debt un‑repaid. The unwind then proceeds to withdraw collateral proportional to the repaid amount, but the leftover dust prevents a full exit—the position remains open with a minimal debt balance.

(,, address debtToken) = aaveDataProvider.getReserveTokensAddresses(_borrowToken);
debtAmount = IERC20(debtToken).balanceOf(address(this));

Risk

Likelihood: Medium

  • Variable debt increases every second; even a one‑block delay between quoting and inclusion causes non‑zero delta.

  • Busy networks, mempool delays, or multi‑step flows make such delays routine - so dust debt will occur.

Impact: Medium

  • Incomplete unwind / stuck residuals: Users expecting to exit fully end up with a tiny remaining debt and residual collateral still locked.

  • Operational friction: Requires a second unwind or manual repay to clear the dust, increasing gas and UX complexity.

Proof of Concept

  • Conceptual pseudocode:

// t0: Off-chain or pre-tx call
(debtToken) = aaveDataProvider.getReserveTokensAddresses(borrowToken).variableDebtToken;
D0 = IERC20(debtToken).balanceOf(address(this)); // snapshot at t0
// User submits unwindPosition(..., _debtAmount = D0, ...)
// t1: Transaction mined (t1 > t0); variable debt accrued: D1 = D0 + δ
executeOperation(...):
// Step 1: repay flash-loaned tokens to Aave
aavePool.repay(borrowToken, _amount = D0, rateMode=2, onBehalfOf=this); // leaves δ unpaid
// Step 2..: withdraw, swap, repay flash loan...
// End result: a tiny variable debt δ remains -> position not fully closed.

Recommended Mitigation

Add a small repay buffer to the debt you flash‑loan so you always have enough to cover accrual between quote and execution. Any leftover debt token simply reduces the amount you need to receive from the collateral → debt swap (and if still leftover at the end, you can return/supply it).

  • Introduce a parameter/constant, e.g. DEBT_REPAY_BUFFER_BPS (1–5 bps), or an absolute floor of +1 wei.

  • In the view helper and/or in unwindPosition, bump the amount you flash‑loan:

// when preparing UnwindParams (view-side or in unwindPosition)
- debtAmount = IERC20(variableDebtToken).balanceOf(address(this));
+ uint256 rawDebt = IERC20(variableDebtToken).balanceOf(address(this));
+ uint256 buffer = rawDebt * DEBT_REPAY_BUFFER_BPS / 10_000;
+ uint256 debtAmount = rawDebt + buffer + 1; // +1 wei floor

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!