Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
114 changes: 74 additions & 40 deletions src/contracts/AbstractARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
uint256 internal _deprecatedTraderate1;
uint256 internal _deprecatedCrossPrice;

/// @notice Maximum liquidity assets reserved for outstanding LP withdrawal requests.
/// @dev Reuses the legacy packed `withdrawsQueued`/`withdrawsClaimed` storage slot.
uint256 public reservedWithdrawLiquidity;
/// @dev Legacy asset-denominated queue counters retained for storage layout compatibility.
uint128 internal _deprecatedWithdrawsQueued;
uint128 internal _deprecatedWithdrawsClaimed;
/// @notice Index of the next LP withdrawal request.
uint256 public nextWithdrawalIndex;

Expand Down Expand Up @@ -135,8 +135,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
uint128 public withdrawsClaimedShares;
/// @notice True when user-facing ARM actions are paused.
bool public paused;
/// @notice Maximum liquidity assets reserved for outstanding LP withdrawal requests.
uint256 public reservedWithdrawLiquidity;

uint256[33] private _gap;
uint256[32] private _gap;

////////////////////////////////////////////////////
/// Errors
Expand All @@ -162,8 +164,13 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
error MarketActive();
error InvalidARMBuffer();
error AlreadyMigrated();
error LegacyWithdrawalsPending();
error ContractPaused();
error Insolvent();
error ZeroShares();
error ClaimDelayNotMet();
error QueuePendingLiquidity();
error NotRequesterOrOperator();
error AlreadyClaimed();

////////////////////////////////////////////////////
/// Events
Expand Down Expand Up @@ -740,7 +747,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
/// @param receiver Account that receives minted LP shares.
/// @return shares LP shares minted.
function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) {
require(totalAssets() > MIN_TOTAL_SUPPLY || reservedWithdrawLiquidity == 0, "ARM: insolvent");
if (totalAssets() <= MIN_TOTAL_SUPPLY && reservedWithdrawLiquidity != 0) revert Insolvent();
shares = convertToShares(assets);

// Transfer liquidity from the depositor before minting LP shares.
Expand All @@ -767,7 +774,14 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
/// @param shares LP shares to burn.
/// @return requestId The LP withdrawal request id.
/// @return assets The maximum liquidity assets claimable by the redeemer.
function requestRedeem(uint256 shares) external whenNotPaused nonReentrant returns (uint256 requestId, uint256 assets) {
function requestRedeem(uint256 shares)
external
whenNotPaused
nonReentrant
returns (uint256 requestId, uint256 assets)
{
if (shares == 0) revert ZeroShares();
Comment thread
naddison36 marked this conversation as resolved.

assets = convertToAssets(shares);
requestId = nextWithdrawalIndex;
// Store the next withdrawal request id.
Expand Down Expand Up @@ -801,29 +815,49 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
function claimRedeem(uint256 requestId) external whenNotPaused nonReentrant returns (uint256 assets) {
WithdrawalRequest memory request = withdrawalRequests[requestId];

require(request.claimTimestamp <= block.timestamp, "Claim delay not met");
require(request.queued <= claimable(), "Queue pending liquidity");
require(request.withdrawer == msg.sender || msg.sender == operator, "Not requester or operator");
require(request.claimed == false, "Already claimed");
if (request.claimTimestamp > block.timestamp) revert ClaimDelayNotMet();
bool legacyRequest = request.shares == 0;
if (legacyRequest) {
if (request.queued > _legacyClaimable()) revert QueuePendingLiquidity();
} else {
if (request.queued > claimable()) revert QueuePendingLiquidity();
}
if (request.withdrawer != msg.sender && msg.sender != operator) revert NotRequesterOrOperator();
if (request.claimed) revert AlreadyClaimed();

// In the scenario where the ARM has made a loss after the redeem request, the asset value of
// the redeemed shares at the time of the claim is used.
// This can happen if there was a significant slashing event on the base asset, eg stETH,
// after the redeem request was made.
uint256 assetsAtClaim = request.shares > 0 ? convertToAssets(request.shares) : request.assets;
// Use the minimum of the asset value of the redeemed shares at request or claim.
assets = request.assets < assetsAtClaim ? request.assets : assetsAtClaim;
if (legacyRequest) {
assets = request.assets;
// Legacy requests used asset-denominated cumulative queue accounting.
_deprecatedWithdrawsClaimed += SafeCast.toUint128(assets);
} else {
// In the scenario where the ARM has made a loss after the redeem request, the asset value of
// the redeemed shares at the time of the claim is used.
// This can happen if there was a significant slashing event on the base asset, eg stETH,
// after the redeem request was made.
uint256 assetsAtClaim = convertToAssets(request.shares);
// Use the minimum of the asset value of the redeemed shares at request or claim.
assets = request.assets < assetsAtClaim ? request.assets : assetsAtClaim;
Comment thread
clement-ux marked this conversation as resolved.

// 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;

// Burn the escrowed shares after `assets` was computed so conversion uses the pre-claim supply.
_burn(address(this), request.shares);
}

// Store the request as claimed.
withdrawalRequests[requestId].claimed = true;
// 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;
_pullLiquidityForRedeem(assets);

// Burn the escrowed shares after `assets` was computed so conversion uses the pre-claim supply.
_burn(address(this), request.shares);
// Transfer the liquidity asset to the withdrawer.
IERC20(liquidityAsset).transfer(request.withdrawer, assets);
emit RedeemClaimed(request.withdrawer, requestId, assets);
}

/// @dev Pull liquidity from the active market if the ARM does not hold enough to pay a redeem claim.
function _pullLiquidityForRedeem(uint256 assets) private {
// If there is not enough liquidity assets in the ARM, get from the active market if one is configured.
// Read the active market address from storage once to save gas.
address activeMarketMem = activeMarket;
Expand All @@ -835,10 +869,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
IERC4626(activeMarketMem).withdraw(liquidityFromMarket, address(this), address(this));
}
}

// Transfer the liquidity asset to the withdrawer.
IERC20(liquidityAsset).transfer(request.withdrawer, assets);
emit RedeemClaimed(request.withdrawer, requestId, assets);
}

////////////////////////////////////////////////////
Expand All @@ -861,6 +891,16 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
claimableShares = withdrawsClaimedShares + convertToShares(claimableLiquidity);
}

/// @dev Legacy asset-denominated queue frontier for pre-upgrade withdrawal requests.
function _legacyClaimable() internal view returns (uint256 claimableAmount) {
claimableAmount = _deprecatedWithdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this));

address activeMarketMem = activeMarket;
if (activeMarketMem != address(0)) {
claimableAmount += IERC4626(activeMarketMem).maxWithdraw(address(this));
}
}

/// @notice Get available liquidity and base asset reserves for a supported base asset.
/// @param reserveBaseAsset Supported base asset whose reserve should be returned.
/// @return liquidityAssets Available liquidity assets net of outstanding LP withdrawal claims.
Expand Down Expand Up @@ -1169,21 +1209,15 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
emit ARMBufferUpdated(_armBuffer);
}

/// @notice Clear the legacy packed asset queue counter slot during the Model A upgrade.
/// @dev The reused slot previously packed `withdrawsQueued` in the low 128 bits and
/// `withdrawsClaimed` in the high 128 bits. It may be nonzero even when the old queue
/// is fully drained, so upgrade scripts should call this with `upgradeToAndCall`.
/// @notice Validate legacy queue compatibility during the Model A upgrade.
/// @dev The legacy packed asset queue counter slot is preserved so old pending withdrawal
/// requests can still be claimed after the upgrade.
function migrateLegacyWithdrawQueue() external onlyOwner reinitializer(2) {
_checkNoLegacyWithdrawQueue();

if (withdrawsQueuedShares != 0 || withdrawsClaimedShares != 0) revert AlreadyMigrated();

uint256 packedLegacyQueue = reservedWithdrawLiquidity;
uint128 legacyQueued = uint128(packedLegacyQueue);
uint128 legacyClaimed = uint128(packedLegacyQueue >> 128);
if (legacyQueued != legacyClaimed) revert LegacyWithdrawalsPending();

reservedWithdrawLiquidity = 0;
if (withdrawsQueuedShares != 0 || withdrawsClaimedShares != 0 || reservedWithdrawLiquidity != 0) {
revert AlreadyMigrated();
}
}

/// @dev Hook for protocol-specific legacy withdrawal queue checks before shared queue migration.
Expand Down
4 changes: 3 additions & 1 deletion src/contracts/EtherFiARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {AbstractARM} from "./AbstractARM.sol";
* @author Origin Protocol Inc
*/
contract EtherFiARM is Initializable, AbstractARM {
error LegacyEtherFiWithdrawalsPending();

/// @dev Deprecated queue amount retained for storage layout compatibility.
uint256 internal _deprecatedEtherfiWithdrawalQueueAmount;

Expand Down Expand Up @@ -61,7 +63,7 @@ contract EtherFiARM is Initializable, AbstractARM {

/// @dev Revert if legacy EtherFi withdrawal requests are still outstanding.
function _checkNoLegacyWithdrawQueue() internal view override {
require(_deprecatedEtherfiWithdrawalQueueAmount == 0, "EtherFiARM: withdrawals pending");
if (_deprecatedEtherfiWithdrawalQueueAmount != 0) revert LegacyEtherFiWithdrawalsPending();
}

/// @notice This payable method is necessary for receiving ETH claimed from the EtherFi withdrawal queue.
Expand Down
4 changes: 3 additions & 1 deletion src/contracts/LidoARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {AbstractARM} from "./AbstractARM.sol";
* @author Origin Protocol Inc
*/
contract LidoARM is Initializable, AbstractARM {
error LegacyLidoWithdrawalsPending();

/// @dev Deprecated queue amount retained for storage layout compatibility.
uint256 internal _deprecatedLidoWithdrawalQueueAmount;

Expand Down Expand Up @@ -54,7 +56,7 @@ contract LidoARM is Initializable, AbstractARM {

/// @dev Revert if legacy Lido withdrawal requests are still outstanding.
function _checkNoLegacyWithdrawQueue() internal view override {
require(_deprecatedLidoWithdrawalQueueAmount == 0, "LidoARM: requests pending");
if (_deprecatedLidoWithdrawalQueueAmount != 0) revert LegacyLidoWithdrawalsPending();
}

/// @notice This payable method is necessary for receiving ETH claimed from the Lido withdrawal queue.
Expand Down
21 changes: 10 additions & 11 deletions test/deploy/EthenaUpgradeGuards.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,15 @@ contract EthenaUpgradeGuardsTest is Test {
);
}

function test_UpgradeToAndCallMigratesLegacyWithdrawQueue() external {
function test_UpgradeToAndCallMigratesWithClaimedLegacyWithdrawQueue() external {
(Proxy proxy, EthenaARM newImpl) = _deployInitializedEthenaARMProxy();
vm.store(
address(proxy),
bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT),
bytes32(_packLegacyWithdrawQueue(1 ether, 1 ether))
);
uint256 packedLegacyQueue = _packLegacyWithdrawQueue(1 ether, 1 ether);
vm.store(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(packedLegacyQueue));

proxy.upgradeToAndCall(address(newImpl), script.migrateLegacyWithdrawQueueData());

assertEq(EthenaARM(address(proxy)).reservedWithdrawLiquidity(), 0);
assertEq(uint256(vm.load(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT))), packedLegacyQueue);
}

function test_RevertWhen_UpgradeToAndCall_LegacyEthenaCooldownPending() external {
Expand All @@ -55,15 +53,16 @@ contract EthenaUpgradeGuardsTest is Test {
proxy.upgradeToAndCall(address(newImpl), data);
}

function test_RevertWhen_UpgradeToAndCall_LegacyWithdrawQueuePending() external {
function test_UpgradeToAndCallMigratesWithPendingLegacyWithdrawQueue() external {
(Proxy proxy, EthenaARM newImpl) = _deployInitializedEthenaARMProxy();
bytes memory data = script.migrateLegacyWithdrawQueueData();
vm.store(
address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(_packLegacyWithdrawQueue(1 ether, 0))
);
uint256 packedLegacyQueue = _packLegacyWithdrawQueue(1 ether, 0);
vm.store(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(packedLegacyQueue));

vm.expectRevert();
proxy.upgradeToAndCall(address(newImpl), data);

assertEq(EthenaARM(address(proxy)).reservedWithdrawLiquidity(), 0);
assertEq(uint256(vm.load(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT))), packedLegacyQueue);
}

function test_RevertWhen_MigrateLegacyWithdrawQueue_CalledTwice() external {
Expand Down
21 changes: 10 additions & 11 deletions test/deploy/EtherFiUpgradeGuards.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,15 @@ contract EtherFiUpgradeGuardsTest is Test {
);
}

function test_UpgradeToAndCallMigratesLegacyWithdrawQueue() external {
function test_UpgradeToAndCallMigratesWithClaimedLegacyWithdrawQueue() external {
(Proxy proxy, EtherFiARM newImpl) = _deployInitializedEtherFiARMProxy();
vm.store(
address(proxy),
bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT),
bytes32(_packLegacyWithdrawQueue(1 ether, 1 ether))
);
uint256 packedLegacyQueue = _packLegacyWithdrawQueue(1 ether, 1 ether);
vm.store(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(packedLegacyQueue));

proxy.upgradeToAndCall(address(newImpl), script.migrateLegacyWithdrawQueueData());

assertEq(EtherFiARM(payable(address(proxy))).reservedWithdrawLiquidity(), 0);
assertEq(uint256(vm.load(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT))), packedLegacyQueue);
}

function test_RevertWhen_UpgradeToAndCall_LegacyEtherFiWithdrawalsPending() external {
Expand All @@ -55,15 +53,16 @@ contract EtherFiUpgradeGuardsTest is Test {
proxy.upgradeToAndCall(address(newImpl), data);
}

function test_RevertWhen_UpgradeToAndCall_LegacyWithdrawQueuePending() external {
function test_UpgradeToAndCallMigratesWithPendingLegacyWithdrawQueue() external {
(Proxy proxy, EtherFiARM newImpl) = _deployInitializedEtherFiARMProxy();
bytes memory data = script.migrateLegacyWithdrawQueueData();
vm.store(
address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(_packLegacyWithdrawQueue(1 ether, 0))
);
uint256 packedLegacyQueue = _packLegacyWithdrawQueue(1 ether, 0);
vm.store(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(packedLegacyQueue));

vm.expectRevert();
proxy.upgradeToAndCall(address(newImpl), data);

assertEq(EtherFiARM(payable(address(proxy))).reservedWithdrawLiquidity(), 0);
assertEq(uint256(vm.load(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT))), packedLegacyQueue);
}

function test_RevertWhen_MigrateLegacyWithdrawQueue_CalledTwice() external {
Expand Down
21 changes: 10 additions & 11 deletions test/deploy/LidoUpgradeGuards.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,15 @@ contract LidoUpgradeGuardsTest is Test {
);
}

function test_UpgradeToAndCallMigratesLegacyWithdrawQueue() external {
function test_UpgradeToAndCallMigratesWithClaimedLegacyWithdrawQueue() external {
(Proxy proxy, LidoARM newImpl) = _deployInitializedLidoARMProxy();
vm.store(
address(proxy),
bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT),
bytes32(_packLegacyWithdrawQueue(1 ether, 1 ether))
);
uint256 packedLegacyQueue = _packLegacyWithdrawQueue(1 ether, 1 ether);
vm.store(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(packedLegacyQueue));

proxy.upgradeToAndCall(address(newImpl), script.migrateLegacyWithdrawQueueData());

assertEq(LidoARM(payable(address(proxy))).reservedWithdrawLiquidity(), 0);
assertEq(uint256(vm.load(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT))), packedLegacyQueue);
}

function test_RevertWhen_UpgradeToAndCall_LegacyLidoWithdrawalRequestsPending() external {
Expand All @@ -55,15 +53,16 @@ contract LidoUpgradeGuardsTest is Test {
proxy.upgradeToAndCall(address(newImpl), data);
}

function test_RevertWhen_UpgradeToAndCall_LegacyWithdrawQueuePending() external {
function test_UpgradeToAndCallMigratesWithPendingLegacyWithdrawQueue() external {
(Proxy proxy, LidoARM newImpl) = _deployInitializedLidoARMProxy();
bytes memory data = script.migrateLegacyWithdrawQueueData();
vm.store(
address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(_packLegacyWithdrawQueue(1 ether, 0))
);
uint256 packedLegacyQueue = _packLegacyWithdrawQueue(1 ether, 0);
vm.store(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT), bytes32(packedLegacyQueue));

vm.expectRevert();
proxy.upgradeToAndCall(address(newImpl), data);

assertEq(LidoARM(payable(address(proxy))).reservedWithdrawLiquidity(), 0);
assertEq(uint256(vm.load(address(proxy), bytes32(LEGACY_PACKED_WITHDRAW_QUEUE_SLOT))), packedLegacyQueue);
}

function test_RevertWhen_MigrateLegacyWithdrawQueue_CalledTwice() external {
Expand Down
8 changes: 4 additions & 4 deletions test/fork/LidoARM/ClaimRedeem.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {
requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT)
{
skip(delay - 1);
vm.expectRevert("Claim delay not met");
vm.expectRevert(bytes4(keccak256("ClaimDelayNotMet()")));
lidoARM.claimRedeem(0);
}

Expand All @@ -54,7 +54,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {
skip(delay);

// Expect revert
vm.expectRevert("Queue pending liquidity");
vm.expectRevert(bytes4(keccak256("QueuePendingLiquidity()")));
lidoARM.claimRedeem(0);
}

Expand Down Expand Up @@ -91,7 +91,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {

// Expect revert
vm.startPrank(vm.randomAddress());
vm.expectRevert("Not requester or operator");
vm.expectRevert(bytes4(keccak256("NotRequesterOrOperator()")));
lidoARM.claimRedeem(0);
}

Expand All @@ -105,7 +105,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {
claimRequestOnLidoARM(address(this), 0)
{
// Expect revert
vm.expectRevert("Already claimed");
vm.expectRevert(bytes4(keccak256("AlreadyClaimed()")));
lidoARM.claimRedeem(0);
}

Expand Down
Loading
Loading