Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 33 additions & 40 deletions src/contracts/AbstractARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// This is also the price the base assets, eg stETH, in the ARM contract are priced at in `totalAssets`.
uint256 public crossPrice;

/// @notice Cumulative total of all withdrawal requests including the ones that have already been claimed.
uint128 public withdrawsQueued;
/// @notice Total of all the withdrawal requests that have been claimed.
uint128 public withdrawsClaimed;
/// @notice Maximum amount of liquidity assets reserved for outstanding withdrawal requests.
uint256 public reservedWithdrawLiquidity;
/// @notice Index of the next withdrawal request starting at 0.
uint256 public nextWithdrawalIndex;

Expand Down Expand Up @@ -136,7 +134,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {

/// @notice Cumulative shares queued for redemption, used by the FIFO gate in `claimRedeem`.
uint128 public withdrawsQueuedShares;
/// @notice Cumulative shares claimed (and burned). Mirror of `withdrawsClaimed` in shares.
/// @notice Cumulative shares claimed (and burned).
uint128 public withdrawsClaimedShares;

uint256[34] private _gap;
Expand Down Expand Up @@ -373,8 +371,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// @param amount The amount of liquidity assets being sent out of the ARM.
function _ensureLiquidityAvailableForSwap(uint256 amount) internal {
uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this));
uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed;
uint256 requiredLiquidity = amount + outstandingWithdrawals;
uint256 requiredLiquidity = amount + reservedWithdrawLiquidity;

// If there is enough liquidity in the ARM to cover the swap after reserving liquidity for withdrawals
if (requiredLiquidity <= liquidityBalance) return;
Expand Down Expand Up @@ -529,17 +526,17 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// @return reserve0 The available liquidity for token0
/// @return reserve1 The available liquidity for token1
function getReserves() external view returns (uint256 reserve0, uint256 reserve1) {
// The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue
uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed;

uint256 liquidityAssetsBalance = IERC20(liquidityAsset).balanceOf(address(this));
address activeMarketMem = activeMarket;
if (activeMarketMem != address(0)) {
liquidityAssetsBalance += IERC4626(activeMarketMem).maxWithdraw(address(this));
}

// Ensure there is no negative reserves when there are more outstanding withdrawals than liquidity assets in the ARM
reserve0 = outstandingWithdrawals > liquidityAssetsBalance ? 0 : liquidityAssetsBalance - outstandingWithdrawals;
uint256 reservedWithdrawLiquidityMem = reservedWithdrawLiquidity;
reserve0 = reservedWithdrawLiquidityMem > liquidityAssetsBalance
? 0
: liquidityAssetsBalance - reservedWithdrawLiquidityMem;
reserve1 = IERC20(baseAsset).balanceOf(address(this));

// The previous assignment assumed token0 is be the liquidity asset.
Expand Down Expand Up @@ -647,7 +644,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// @dev Internal logic for depositing liquidity assets in exchange for liquidity provider (LP) shares.
function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) {
// Do not allow deposits if the ARM can not meet all its withdrawal obligations.
require(totalAssets() > MIN_TOTAL_SUPPLY || withdrawsQueued == withdrawsClaimed, "ARM: insolvent");
require(totalAssets() > MIN_TOTAL_SUPPLY || reservedWithdrawLiquidity == 0, "ARM: insolvent");

// Calculate the amount of shares to mint after accrued swap fees have been excluded,
// and before new assets are deposited.
Expand Down Expand Up @@ -691,8 +688,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
// Cumulative shares queued including this request, used for the FIFO gate at claim
uint128 queued = SafeCast.toUint128(withdrawsQueuedShares + shares);
withdrawsQueuedShares = queued;
// Cumulative assets queued (upper bound on liability), used for the swap-side reservation
withdrawsQueued = SafeCast.toUint128(withdrawsQueued + assets);
// Increase the maximum liquidity reserved for outstanding withdrawal requests.
reservedWithdrawLiquidity += assets;

uint40 claimTimestamp = uint40(block.timestamp + claimDelay);

Expand Down Expand Up @@ -725,10 +722,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {

require(request.claimTimestamp <= block.timestamp, "Claim delay not met");
// Is there enough liquidity in the ARM and lending market to claim this request?
// Pending shares are valued at the current (loss-adjusted) share price; matches the actual
// payout below in the loss case and is conservative in the gain case.
uint256 pendingShares = request.queued - withdrawsClaimedShares;
require(convertToAssets(pendingShares) <= claimable(), "Queue pending liquidity");
// `queued` and `claimable()` are both cumulative shares, so the UI can use the same check.
require(request.queued <= claimable(), "Queue pending liquidity");
require(request.withdrawer == msg.sender, "Not requester");
require(request.claimed == false, "Already claimed");

Expand All @@ -741,8 +736,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {

// Store the request as claimed
withdrawalRequests[requestId].claimed = true;
// Cumulative claimed amount in assets (upper bound, used by the swap-side reservation)
withdrawsClaimed += SafeCast.toUint128(request.assets);
// Release the full request-time reservation, even when a loss-adjusted payout is lower.
reservedWithdrawLiquidity -= request.assets;
// Cumulative claimed amount in shares (used by the FIFO gate above)
withdrawsClaimedShares += request.shares;

Expand All @@ -757,7 +752,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {

if (assets > liquidityInARM) {
uint256 liquidityFromMarket = assets - liquidityInARM;
// This should work as we have checked earlier the claimable() amount which includes the active market
// This should work as we have checked earlier the claimable liquidity which includes the active market.
IERC4626(activeMarketMem).withdraw(liquidityFromMarket, address(this), address(this));
}
}
Expand All @@ -768,19 +763,20 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
emit RedeemClaimed(msg.sender, requestId, assets);
}

/// @notice The liquidity currently available to satisfy a withdrawal request.
/// @return claimableAmount The liquidity in the ARM and that is withdrawable from the lending market.
/// Compared in `claimRedeem` against `convertToAssets(pendingShares)`.
function claimable() public view returns (uint256 claimableAmount) {
claimableAmount = IERC20(liquidityAsset).balanceOf(address(this));
/// @notice The cumulative share queue frontier currently backed by claimable liquidity.
/// @return claimableShares Requests with `queued <= claimableShares` can be claimed once their delay has elapsed.
function claimable() public view returns (uint256 claimableShares) {
uint256 claimableLiquidity = IERC20(liquidityAsset).balanceOf(address(this));

// if there is an active lending market, add to the claimable amount
address activeMarketMem = activeMarket;
if (activeMarketMem != address(0)) {
// maxWithdraw is used as during periods of high utilization or temporary pauses,
// maxWithdraw may return less than convertToAssets.
claimableAmount += IERC4626(activeMarketMem).maxWithdraw(address(this));
claimableLiquidity += IERC4626(activeMarketMem).maxWithdraw(address(this));
}

claimableShares = withdrawsClaimedShares + convertToShares(claimableLiquidity);
}

////////////////////////////////////////////////////
Expand All @@ -795,15 +791,13 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
// There is no liquidity guarantee for the fee collector. If there is not enough liquidity assets (WETH) in
// the ARM to collect the accrued fees, then the fee collector will have to wait until there is enough liquidity assets.
function _requireLiquidityAvailable(uint256 amount) internal view {
// The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue
uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed;

// Save gas on an external balanceOf call if there are no outstanding withdrawals
if (outstandingWithdrawals == 0) return;
uint256 reservedWithdrawLiquidityMem = reservedWithdrawLiquidity;
if (reservedWithdrawLiquidityMem == 0) return;

// If there is not enough liquidity assets in the ARM to cover the outstanding withdrawals and the amount
require(
amount + outstandingWithdrawals <= IERC20(liquidityAsset).balanceOf(address(this)),
amount + reservedWithdrawLiquidityMem <= IERC20(liquidityAsset).balanceOf(address(this)),
"ARM: Insufficient liquidity"
);
}
Expand All @@ -814,7 +808,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// The active lending market is valued using ERC-4626 share conversion rather than current redeemable liquidity.
/// @return The total amount of assets in the ARM
function totalAssets() public view virtual returns (uint256) {
(uint256 newAvailableAssets,) = _availableAssets();
uint256 newAvailableAssets = _availableAssets();

// total assets should only go up from the initial deposit amount that is burnt
// but in case of something unforeseen, return at least MIN_TOTAL_SUPPLY.
Expand All @@ -838,7 +832,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// The active lending market is valued using convertToAssets() so market valuation remains
/// consistent across ERC-4626 implementations even when current redeemable liquidity differs.
/// This does not exclude any accrued swap fees.
function _availableAssets() internal view returns (uint256 availableAssets, uint256 outstandingWithdrawals) {
function _availableAssets() internal view returns (uint256 availableAssets) {
// Convert the base assets in the ARM to the amount of liquidity assets
uint256 baseConvertedToLiquid = _convert(baseAsset, IERC20(baseAsset).balanceOf(address(this)));

Expand All @@ -859,9 +853,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
// maxRedeem, withdraw and redeem when current liquidity matters.
availableAssets += IERC4626(activeMarketMem).convertToAssets(allShares);
}

// The amount of liquidity assets, eg WETH, reserved for the withdrawal queue (upper bound)
outstandingWithdrawals = withdrawsQueued - withdrawsClaimed;
}

/// @dev Hook for calculating the amount of liquidity assets in an external withdrawal queue like Lido or OETH.
Expand Down Expand Up @@ -1046,17 +1037,19 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
}

function _allocate() internal returns (int256 targetLiquidityDelta, int256 actualLiquidityDelta) {
(uint256 availableAssets, uint256 outstandingWithdrawals) = _availableAssets();
uint256 availableAssets = _availableAssets();
if (availableAssets == 0) return (0, 0);
uint256 reservedWithdrawLiquidityMem = reservedWithdrawLiquidity;
// Net of the withdrawal queue (queued shares stay in totalSupply but the buffer is sized
// against the liquid assets that actually back free LP shares)
uint256 netAvailable = availableAssets > outstandingWithdrawals ? availableAssets - outstandingWithdrawals : 0;
uint256 netAvailable =
availableAssets > reservedWithdrawLiquidityMem ? availableAssets - reservedWithdrawLiquidityMem : 0;
uint256 targetArmLiquidity = netAvailable * armBuffer / 1e18;

// The current liquidity available in swap is the liquidity asset balance less
// any outstanding withdrawals from the ARM's withdrawal queue
int256 currentArmLiquidity = SafeCast.toInt256(IERC20(liquidityAsset).balanceOf(address(this)))
- SafeCast.toInt256(outstandingWithdrawals);
- SafeCast.toInt256(reservedWithdrawLiquidityMem);

targetLiquidityDelta = currentArmLiquidity - SafeCast.toInt256(targetArmLiquidity);

Expand Down
4 changes: 4 additions & 0 deletions test/fork/EthenaARM/shared/Shared.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ abstract contract Fork_Shared_Test is Base_Test_ {
usde.approve(address(ethenaARM), type(uint256).max);
susde.approve(address(ethenaARM), type(uint256).max);

vm.startPrank(ethenaARM.owner());
ethenaARM.setPrices(ethenaARM.traderate1(), 1e36, type(uint256).max, type(uint256).max);
vm.stopPrank();

// Deposit some usde in the ARM
ethenaARM.deposit(10_000 ether);

Expand Down
Loading
Loading