Skip to content

Commit

Permalink
Ensure swaps don't use WETH reserved for the withdrawal queue
Browse files Browse the repository at this point in the history
  • Loading branch information
naddison36 committed Sep 25, 2024
1 parent 4c58acd commit 6554eab
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 22 deletions.
5 changes: 5 additions & 0 deletions src/contracts/AbstractARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,9 @@ abstract contract AbstractARM is OwnableOperable {
function _inDeadline(uint256 deadline) internal view {
require(deadline >= block.timestamp, "ARM: Deadline expired");
}

/// @dev Hook to transfer assets out of the ARM contract
function _transferAsset(address asset, address to, uint256 amount) internal virtual {
IERC20(asset).transfer(to, amount);
}
}
14 changes: 2 additions & 12 deletions src/contracts/FixedPriceARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ abstract contract FixedPriceARM is AbstractARM {
inToken.transferFrom(msg.sender, address(this), amountIn);

// Transfer the output tokens to the recipient
uint256 transferAmountOut = _calcTransferAmount(address(outToken), amountOut);
outToken.transfer(to, transferAmountOut);
_transferAsset(address(outToken), to, amountOut);
}

function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to)
Expand All @@ -82,16 +81,7 @@ abstract contract FixedPriceARM is AbstractARM {
inToken.transferFrom(msg.sender, address(this), amountIn);

// Transfer the output tokens to the recipient
uint256 transferAmountOut = _calcTransferAmount(address(outToken), amountOut);
outToken.transfer(to, transferAmountOut);
}

/**
* @notice Calculate transfer amount for outToken.
* Some tokens like stETH transfer less than the requested amount due to internal mechanics.
*/
function _calcTransferAmount(address, uint256 amount) internal view virtual returns (uint256 transferAmount) {
transferAmount = amount;
_transferAsset(address(outToken), to, amountOut);
}

/**
Expand Down
16 changes: 7 additions & 9 deletions src/contracts/LidoFixedPriceMultiLpARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,17 @@ contract LidoFixedPriceMultiLpARM is
}

/**
* @notice Calculate transfer amount for outToken.
* Due to internal stETH mechanics required for rebasing support,
* @dev Due to internal stETH mechanics required for rebasing support,
* in most cases stETH transfers are performed for the value of 1 wei less than passed to transfer method.
* Larger transfer amounts can be 2 wei less.
*
* The MultiLP implementation ensures any WETH reserved for the withdrawal queue is no used in swaps from stETH to WETH.
*/
function _calcTransferAmount(address outToken, uint256 amount)
internal
view
override
returns (uint256 transferAmount)
{
function _transferAsset(address asset, address to, uint256 amount) internal override(AbstractARM, MultiLP) {
// Add 2 wei if transferring stETH
transferAmount = outToken == address(token0) ? amount + 2 : amount;
uint256 transferAmount = asset == address(token0) ? amount + 2 : amount;

MultiLP._transferAsset(asset, to, transferAmount);
}

function _externalWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) {
Expand Down
29 changes: 28 additions & 1 deletion src/contracts/MultiLP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable {
// Check if the claimable amount is less than the queued amount
uint256 queueShortfall = queue.queued - queue.claimable;

// No need to do anything is the withdrawal queue is full funded
// No need to do anything is the withdrawal queue is fully funded
if (queueShortfall == 0) {
return 0;
}
Expand All @@ -218,6 +218,33 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable {
withdrawalQueueMetadata.claimable = SafeCast.toUint128(newClaimable);
}

/// @dev Calculate how much of the liquidity asset in the ARM is not reserved for the withdrawal queue.
// That is, it is available to be swapped.
function _liquidityAvailable() internal view returns (uint256) {
WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata;

// The amount of WETH that is still to be claimed in the withdrawal queue
uint256 outstandingWithdrawals = queue.queued - queue.claimed;

// The amount of the liquidity asset is in the ARM
uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this));

// If there is not enough liquidity assets in the ARM to cover the outstanding withdrawals
if (liquidityBalance <= outstandingWithdrawals) {
return 0;
}

return liquidityBalance - outstandingWithdrawals;
}

/// @dev Ensure any liquidity assets reserved for the withdrawal queue are not used
/// in swaps that send liquidity assets out of the ARM
function _transferAsset(address asset, address to, uint256 amount) internal virtual override {
require(asset == liquidityAsset && amount <= _liquidityAvailable(), "ARM: Insufficient liquidity");

IERC20(asset).transfer(to, amount);
}

/// @notice The total amount of assets in the ARM and external withdrawal queue,
/// less the liquidity assets reserved for the withdrawal queue
function totalAssets() public view virtual returns (uint256 assets) {
Expand Down

0 comments on commit 6554eab

Please sign in to comment.