Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Valid

CurveAdapter has cumulative slippage issue

Summary

The executeSwapExactInput function in the CurveAdapter contract calculates slippage tolerance (amountOutMinimum) for each intermediate swap step in a multi-hop swap path. This approach leads to cumulative slippage, where the overall slippage exceeds the intended tolerance. For example, if the slippage tolerance is 1% and the swap path is USDT -> WETH -> USDC, the final output could be 98.01 USDC for an input of 100 USDT, resulting in an effective slippage of 1.99% instead of the intended 1%.

Vulnerability Details

The executeSwapExactInput function calculates the minimum acceptable output (amountOutMinimum) for each intermediate swap step using the calculateAmountOutMin function.

for (uint256 i; i < tokens.length - 1; i++) {
// approve the tokenIn to the swap router
IERC20(tokens[i]).approve(curveStrategyRouterCache, amountIn);
// get the expected output amount
uint256 expectedAmountOut = getExpectedOutput(tokens[i], tokens[i + 1], amountIn);
// Calculate the minimum acceptable output based on the slippage tolerance
uint256 amountOutMinimum = calculateAmountOutMin(expectedAmountOut);
// If last swap send received tokens to payload recipient
address receiver = (i == tokens.length - 2) ? swapPayload.recipient : address(this);
// make single exchange
amountIn = ICurveSwapRouter(curveStrategyRouterCache).exchange_with_best_rate({
_from: tokens[i],
_to: tokens[i + 1],
_amount: amountIn,
_expected: amountOutMinimum,
_receiver: receiver
});
}

For example, in a path like USDT -> WETH -> USDC:

  • First swap: 100 USDT -> 0.03 WETH (Let's say the 0.03 WETH is now equal to 99 USD, so this is 1% slippage).

  • Second swap: 0.03 WETH -> 98.01 USDC (1% slippage).

The final output is 98.01 USDC, which represents a total slippage of 1.99%.

Since slippage is applied at each step, the overall slippage accumulates, leading to a higher effective slippage than intended.

Impact

Users may receive significantly less output than expected due to cumulative slippage. The slippage tolerance mechanism fails to provide the intended protection against price fluctuations.

The impact is Medium, the likelihood is Medium, so the severity is Medium.

Tools Used

Manual Review

Recommendations

To fix this issue, the slippage tolerance should be calculated based on the entire swap path's expected output. Consider following code example:

function executeSwapExactInput(SwapExactInputPayload calldata swapPayload) external returns (uint256 amountOut) {
// Transfer the tokenIn from the sender to this contract
IERC20(swapPayload.tokenIn).transferFrom(msg.sender, address(this), swapPayload.amountIn);
// Decode the path
(address[] memory tokens,) = swapPayload.path.decodePath();
// Cache the initial amountIn
uint256 amountIn = swapPayload.amountIn;
// Cache the curve strategy router address to save gas
address curveStrategyRouterCache = curveStrategyRouter;
// Calculate the expected output for the entire path
uint256 expectedAmountOut = getExpectedOutputForPath(tokens, amountIn);
// Calculate the minimum acceptable output based on the slippage tolerance
uint256 amountOutMinimum = calculateAmountOutMin(expectedAmountOut);
// Perform the swaps
for (uint256 i; i < tokens.length - 1; i++) {
// Approve the tokenIn to the swap router
IERC20(tokens[i]).approve(curveStrategyRouterCache, amountIn);
// If it's the last swap, send the tokens to the recipient
address receiver = (i == tokens.length - 2) ? swapPayload.recipient : address(this);
// Execute the swap
amountIn = ICurveSwapRouter(curveStrategyRouterCache).exchange_with_best_rate({
_from: tokens[i],
_to: tokens[i + 1],
_amount: amountIn,
_expected: 0, // No slippage check for intermediate steps
_receiver: receiver
});
}
// Ensure the final output meets the minimum requirement
if (amountIn < amountOutMinimum) {
revert Errors.SlippageExceeded(amountIn, amountOutMinimum);
}
// Return the final amountOut
amountOut = amountIn;
}
/// @notice Calculate the expected output for the entire path
/// @param tokens The array of tokens in the path
/// @param amountIn The initial input amount
/// @return expectedAmountOut The expected output amount for the entire path
function getExpectedOutputForPath(address[] memory tokens, uint256 amountIn) internal view returns (uint256 expectedAmountOut) {
expectedAmountOut = amountIn;
for (uint256 i; i < tokens.length - 1; i++) {
// Get the expected output for the current step
expectedAmountOut = getExpectedOutput(tokens[i], tokens[i + 1], expectedAmountOut);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

CurveAdapter cumulative slippage

Support

FAQs

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