From 5c30a4cf63c8b4b99e2ae3b2dc6defdcefa7a1b4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 14 Oct 2024 17:02:20 +1100 Subject: [PATCH 1/5] put nextWithdrawalIndex into its own slot --- src/contracts/AbstractARM.sol | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index ccfd963..9f9a011 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -72,11 +72,11 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { uint256 public crossPrice; /// @notice Cumulative total of all withdrawal requests including the ones that have already been claimed. - uint120 public withdrawsQueued; + uint128 public withdrawsQueued; /// @notice Total of all the withdrawal requests that have been claimed. - uint120 public withdrawsClaimed; + uint128 public withdrawsClaimed; /// @notice Index of the next withdrawal request starting at 0. - uint16 public nextWithdrawalIndex; + uint256 public nextWithdrawalIndex; struct WithdrawalRequest { address withdrawer; @@ -84,9 +84,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // When the withdrawal can be claimed uint40 claimTimestamp; // Amount of liquidity assets to withdraw. eg WETH - uint120 assets; + uint128 assets; // Cumulative total of all withdrawal requests including this one when the redeem request was made. - uint120 queued; + uint128 queued; } /// @notice Mapping of withdrawal request indices to the user withdrawal request data. @@ -106,7 +106,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice The address of the CapManager contract used to manage the ARM's liquidity provider and total assets caps. address public capManager; - uint256[42] private _gap; + uint256[41] private _gap; //////////////////////////////////////////////////// /// Events @@ -478,9 +478,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { requestId = nextWithdrawalIndex; // Store the next withdrawal request - nextWithdrawalIndex = SafeCast.toUint16(requestId + 1); + nextWithdrawalIndex = requestId + 1; - uint120 queued = SafeCast.toUint120(withdrawsQueued + assets); + uint128 queued = SafeCast.toUint128(withdrawsQueued + assets); // Store the updated queued amount which reserves liquidity assets (WETH) in the withdrawal queue withdrawsQueued = queued; @@ -491,7 +491,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { withdrawer: msg.sender, claimed: false, claimTimestamp: claimTimestamp, - assets: SafeCast.toUint120(assets), + assets: SafeCast.toUint128(assets), queued: queued }); @@ -522,7 +522,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Store the request as claimed withdrawalRequests[requestId].claimed = true; // Store the updated claimed amount - withdrawsClaimed += SafeCast.toUint120(assets); + withdrawsClaimed += SafeCast.toUint128(assets); // transfer the liquidity asset to the withdrawer IERC20(liquidityAsset).transfer(msg.sender, assets); From ec44d0768cff447f7f6e09ddd780981065d594a6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 14 Oct 2024 19:08:16 +1100 Subject: [PATCH 2/5] Updated comments for setCrossPrice --- src/contracts/AbstractARM.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 9f9a011..d42779e 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -390,7 +390,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { * @notice set the price that buy and sell prices can not cross. * That is, the buy prices must be below the cross price * and the sell prices must be above the cross price. - * If the cross price is being lowered, there can not be any base assets in the ARM. eg stETH. + * If the cross price is being lowered, there can not be a significant amount of base assets in the ARM. eg stETH. + * This prevents the ARM making a loss when the base asset is sold at a lower price than it was bought + * before the cross price was lowered. * The base assets should be sent to the withdrawal queue before the cross price can be lowered. * The cross price can be increased with assets in the ARM. * @param newCrossPrice The new cross price scaled to 36 decimals. @@ -399,12 +401,15 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { require(newCrossPrice >= PRICE_SCALE - MAX_CROSS_PRICE_DEVIATION, "ARM: cross price too low"); require(newCrossPrice <= PRICE_SCALE, "ARM: cross price too high"); - // If the new cross price is lower than the current cross price + // If the cross price is being lowered, there can not be a significant amount of base assets in the ARM. eg stETH. + // This prevents the ARM making a loss when the base asset is sold at a lower price than it was bought + // before the cross price was lowered. if (newCrossPrice < crossPrice) { // Check there is not a significant amount of base assets in the ARM require(IERC20(baseAsset).balanceOf(address(this)) < MIN_TOTAL_SUPPLY, "ARM: too many base assets"); } + // Save the new cross price to storage crossPrice = newCrossPrice; emit CrossPriceUpdated(newCrossPrice); From 9257f5084e0f645562fc29179ec5534e37b9f9d0 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 14 Oct 2024 21:01:46 +1100 Subject: [PATCH 3/5] Clearer setPrices comments Fixed comment typos --- .vscode/settings.json | 3 ++- src/contracts/AbstractARM.sol | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5382847..9356b92 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "solidity.packageDefaultDependenciesContractsDirectory": "src", "solidity.packageDefaultDependenciesDirectory": "lib", "solidity.compileUsingRemoteVersion": "v0.8.23+commit.f704f362", - "solidity.formatter": "forge" + "solidity.formatter": "forge", + "cSpell.words": ["traderate"] } diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index d42779e..5bfa1fd 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -17,7 +17,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { uint256 public constant MAX_CROSS_PRICE_DEVIATION = 20e32; /// @notice Scale of the prices. uint256 public constant PRICE_SCALE = 1e36; - /// @dev The amount of shares that are minted to a dead address on initalization + /// @dev The amount of shares that are minted to a dead address on initialization uint256 internal constant MIN_TOTAL_SUPPLY = 1e12; /// @dev The address with no known private key that the initial shares are minted to address internal constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; @@ -30,6 +30,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { //////////////////////////////////////////////////// /// @notice The address of the asset that is used to add and remove liquidity. eg WETH + /// This is also the quote asset when the prices are set. + /// eg the stETH/WETH price has a base asset of stETH and quote asset of WETH. address public immutable liquidityAsset; /// @notice The asset being purchased by the ARM and put in the withdrawal queue. eg stETH address public immutable baseAsset; @@ -163,7 +165,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { __ERC20_init(_name, _symbol); - // Transfer a small bit of liquidity from the intializer to this contract + // Transfer a small bit of liquidity from the initializer to this contract IERC20(liquidityAsset).transferFrom(msg.sender, address(this), MIN_TOTAL_SUPPLY); // mint a small amount of shares to a dead account so the total supply can never be zero @@ -380,8 +382,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { require(sellT1 >= crossPrice, "ARM: sell price too low"); require(buyT1 < crossPrice, "ARM: buy price too high"); - traderate0 = PRICE_SCALE * PRICE_SCALE / sellT1; // base (t0) -> token (t1); - traderate1 = buyT1; // token (t1) -> base (t0) + traderate0 = PRICE_SCALE * PRICE_SCALE / sellT1; // quote (t0) -> base (t1); eg WETH -> stETH + traderate1 = buyT1; // base (t1) -> quote (t0). eg stETH -> WETH emit TraderateChanged(traderate0, traderate1); } @@ -435,7 +437,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } /// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares. - /// Funds will be transfered from msg.sender. + /// Funds will be transferred from msg.sender. /// @param assets The amount of liquidity assets to deposit /// @param receiver The address that will receive shares. /// @return shares The amount of shares that were minted @@ -513,7 +515,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @param requestId The index of the withdrawal request /// @return assets The amount of liquidity assets that were transferred to the redeemer function claimRedeem(uint256 requestId) external returns (uint256 assets) { - // Load the structs from storage into memory + // Load the struct from storage into memory WithdrawalRequest memory request = withdrawalRequests[requestId]; require(request.claimTimestamp <= block.timestamp, "Claim delay not met"); From ce9cf60eb92bd7f2b9b07dd673fd9955cbf0b14f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 14 Oct 2024 21:05:57 +1100 Subject: [PATCH 4/5] Generated latest LidoARM contract diagrams --- docs/LidoARMPublicSquashed.svg | 139 +++++++++++++++++---------------- docs/LidoARMSquashed.svg | 129 +++++++++++++++--------------- 2 files changed, 137 insertions(+), 131 deletions(-) diff --git a/docs/LidoARMPublicSquashed.svg b/docs/LidoARMPublicSquashed.svg index 2bf4713..917334b 100644 --- a/docs/LidoARMPublicSquashed.svg +++ b/docs/LidoARMPublicSquashed.svg @@ -4,79 +4,86 @@ - - + + UmlClassDiagram - + 15 - -LidoARM -../src/contracts/LidoARM.sol - -Public: -   operator: address <<OwnableOperable>> -   MAX_PRICE_DEVIATION: uint256 <<AbstractARM>> -   PRICE_SCALE: uint256 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<AbstractARM>> -   FEE_SCALE: uint256 <<AbstractARM>> -   liquidityAsset: address <<AbstractARM>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   traderate0: uint256 <<AbstractARM>> -   traderate1: uint256 <<AbstractARM>> -   withdrawsQueued: uint120 <<AbstractARM>> -   withdrawsClaimed: uint120 <<AbstractARM>> -   nextWithdrawalIndex: uint16 <<AbstractARM>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> -   fee: uint16 <<AbstractARM>> -   lastAvailableAssets: int128 <<AbstractARM>> -   feeCollector: address <<AbstractARM>> -   capManager: address <<AbstractARM>> -   steth: IERC20 <<LidoARM>> -   weth: IWETH <<LidoARM>> -   withdrawalQueue: IStETHWithdrawal <<LidoARM>> -   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> - -External: -    <<payable>> null() <<LidoARM>> -    owner(): address <<Ownable>> -    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> -    setOperator(newOperator: address) <<onlyOwner>> <<OwnableOperable>> -    swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, amountOutMin: uint256, to: address) <<AbstractARM>> -    swapExactTokensForTokens(amountIn: uint256, amountOutMin: uint256, path: address[], to: address, deadline: uint256): (amounts: uint256[]) <<AbstractARM>> -    swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, amountInMax: uint256, to: address) <<AbstractARM>> -    swapTokensForExactTokens(amountOut: uint256, amountInMax: uint256, path: address[], to: address, deadline: uint256): (amounts: uint256[]) <<AbstractARM>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<AbstractARM>> -    previewDeposit(assets: uint256): (shares: uint256) <<AbstractARM>> -    deposit(assets: uint256): (shares: uint256) <<AbstractARM>> -    previewRedeem(shares: uint256): (assets: uint256) <<AbstractARM>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<AbstractARM>> -    claimRedeem(requestId: uint256): (assets: uint256) <<AbstractARM>> -    setCapManager(_capManager: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    feesAccrued(): (fees: uint256) <<AbstractARM>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _capManager: address) <<initializer>> <<LidoARM>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoARM>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<AbstractARM>> -    <<event>> Deposit(owner: address, assets: uint256, shares: uint256) <<AbstractARM>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<AbstractARM>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<AbstractARM>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>> -    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> -    <<event>> CapManagerUpdated(capManager: address) <<AbstractARM>> + +LidoARM +../src/contracts/LidoARM.sol + +Public: +   operator: address <<OwnableOperable>> +   MAX_CROSS_PRICE_DEVIATION: uint256 <<AbstractARM>> +   PRICE_SCALE: uint256 <<AbstractARM>> +   FEE_SCALE: uint256 <<AbstractARM>> +   liquidityAsset: address <<AbstractARM>> +   baseAsset: address <<AbstractARM>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   claimDelay: uint256 <<AbstractARM>> +   traderate0: uint256 <<AbstractARM>> +   traderate1: uint256 <<AbstractARM>> +   crossPrice: uint256 <<AbstractARM>> +   withdrawsQueued: uint128 <<AbstractARM>> +   withdrawsClaimed: uint128 <<AbstractARM>> +   nextWithdrawalIndex: uint256 <<AbstractARM>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> +   fee: uint16 <<AbstractARM>> +   lastAvailableAssets: int128 <<AbstractARM>> +   feeCollector: address <<AbstractARM>> +   capManager: address <<AbstractARM>> +   steth: IERC20 <<LidoARM>> +   weth: IWETH <<LidoARM>> +   lidoWithdrawalQueue: IStETHWithdrawal <<LidoARM>> +   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> + +External: +    <<payable>> null() <<LidoARM>> +    owner(): address <<Ownable>> +    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +    setOperator(newOperator: address) <<onlyOwner>> <<OwnableOperable>> +    swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, amountOutMin: uint256, to: address) <<AbstractARM>> +    swapExactTokensForTokens(amountIn: uint256, amountOutMin: uint256, path: address[], to: address, deadline: uint256): (amounts: uint256[]) <<AbstractARM>> +    swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, amountInMax: uint256, to: address) <<AbstractARM>> +    swapTokensForExactTokens(amountOut: uint256, amountInMax: uint256, path: address[], to: address, deadline: uint256): (amounts: uint256[]) <<AbstractARM>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<AbstractARM>> +    setCrossPrice(newCrossPrice: uint256) <<onlyOwner>> <<AbstractARM>> +    previewDeposit(assets: uint256): (shares: uint256) <<AbstractARM>> +    deposit(assets: uint256): (shares: uint256) <<AbstractARM>> +    deposit(assets: uint256, receiver: address): (shares: uint256) <<AbstractARM>> +    previewRedeem(shares: uint256): (assets: uint256) <<AbstractARM>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<AbstractARM>> +    claimRedeem(requestId: uint256): (assets: uint256) <<AbstractARM>> +    setCapManager(_capManager: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    feesAccrued(): (fees: uint256) <<AbstractARM>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _capManager: address) <<initializer>> <<LidoARM>> +    requestLidoWithdrawals(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoARM>> +    claimLidoWithdrawals(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<AbstractARM>> +    <<event>> CrossPriceUpdated(crossPrice: uint256) <<AbstractARM>> +    <<event>> Deposit(owner: address, assets: uint256, shares: uint256) <<AbstractARM>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<AbstractARM>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<AbstractARM>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>> +    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> +    <<event>> CapManagerUpdated(capManager: address) <<AbstractARM>> +    <<event>> RequestLidoWithdrawals(amounts: uint256[], requestIds: uint256[]) <<LidoARM>> +    <<event>> ClaimLidoWithdrawals(requestIds: uint256[]) <<LidoARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address, _claimDelay: uint256) <<LidoARM>>    claimable(): uint256 <<AbstractARM>>    totalAssets(): uint256 <<AbstractARM>>    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 22628ef..0d51cac 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,47 +4,46 @@ - - + + UmlClassDiagram - + 15 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[41] <<AbstractARM>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> -   DEAD_ACCOUNT: address <<AbstractARM>> -Public: -   operator: address <<OwnableOperable>> -   MAX_CROSS_PRICE_DEVIATION: uint256 <<AbstractARM>> -   PRICE_SCALE: uint256 <<AbstractARM>> -   FEE_SCALE: uint256 <<AbstractARM>> -   liquidityAsset: address <<AbstractARM>> -   baseAsset: address <<AbstractARM>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   claimDelay: uint256 <<AbstractARM>> -   traderate0: uint256 <<AbstractARM>> -   traderate1: uint256 <<AbstractARM>> -   crossPrice: uint256 <<AbstractARM>> -   withdrawsQueued: uint120 <<AbstractARM>> -   withdrawsClaimed: uint120 <<AbstractARM>> -   nextWithdrawalIndex: uint16 <<AbstractARM>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> -   fee: uint16 <<AbstractARM>> -   lastAvailableAssets: int128 <<AbstractARM>> -   feeCollector: address <<AbstractARM>> -   capManager: address <<AbstractARM>> -   zap: address <<AbstractARM>> + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[41] <<AbstractARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> +   DEAD_ACCOUNT: address <<AbstractARM>> +Public: +   operator: address <<OwnableOperable>> +   MAX_CROSS_PRICE_DEVIATION: uint256 <<AbstractARM>> +   PRICE_SCALE: uint256 <<AbstractARM>> +   FEE_SCALE: uint256 <<AbstractARM>> +   liquidityAsset: address <<AbstractARM>> +   baseAsset: address <<AbstractARM>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   claimDelay: uint256 <<AbstractARM>> +   traderate0: uint256 <<AbstractARM>> +   traderate1: uint256 <<AbstractARM>> +   crossPrice: uint256 <<AbstractARM>> +   withdrawsQueued: uint128 <<AbstractARM>> +   withdrawsClaimed: uint128 <<AbstractARM>> +   nextWithdrawalIndex: uint256 <<AbstractARM>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> +   fee: uint16 <<AbstractARM>> +   lastAvailableAssets: int128 <<AbstractARM>> +   feeCollector: address <<AbstractARM>> +   capManager: address <<AbstractARM>>   steth: IERC20 <<LidoARM>>   weth: IWETH <<LidoARM>>   lidoWithdrawalQueue: IStETHWithdrawal <<LidoARM>> @@ -62,7 +61,7 @@    _transferAssetFrom(asset: address, from: address, to: address, amount: uint256) <<AbstractARM>>    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<AbstractARM>>    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<AbstractARM>> -    _deposit(assets: uint256, liquidityProvider: address): (shares: uint256) <<AbstractARM>> +    _deposit(assets: uint256, receiver: address): (shares: uint256) <<AbstractARM>>    _requireLiquidityAvailable(amount: uint256) <<AbstractARM>>    _availableAssets(): uint256 <<AbstractARM>>    _externalWithdrawQueue(): uint256 <<LidoARM>> @@ -82,36 +81,36 @@    setCrossPrice(newCrossPrice: uint256) <<onlyOwner>> <<AbstractARM>>    previewDeposit(assets: uint256): (shares: uint256) <<AbstractARM>>    deposit(assets: uint256): (shares: uint256) <<AbstractARM>> -    deposit(assets: uint256, liquidityProvider: address): (shares: uint256) <<AbstractARM>> +    deposit(assets: uint256, receiver: address): (shares: uint256) <<AbstractARM>>    previewRedeem(shares: uint256): (assets: uint256) <<AbstractARM>>    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<AbstractARM>>    claimRedeem(requestId: uint256): (assets: uint256) <<AbstractARM>> -    claimable(): uint256 <<AbstractARM>> -    setCapManager(_capManager: address) <<onlyOwner>> <<AbstractARM>> -    setZap(_zap: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    feesAccrued(): (fees: uint256) <<AbstractARM>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _capManager: address) <<initializer>> <<LidoARM>> -    requestLidoWithdrawals(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoARM>> -    claimLidoWithdrawals(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<AbstractARM>> -    <<event>> CrossPriceUpdated(crossPrice: uint256) <<AbstractARM>> -    <<event>> Deposit(owner: address, assets: uint256, shares: uint256) <<AbstractARM>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<AbstractARM>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<AbstractARM>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>> -    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> -    <<event>> CapManagerUpdated(capManager: address) <<AbstractARM>> -    <<event>> ZapUpdated(zap: address) <<AbstractARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address, _claimDelay: uint256) <<LidoARM>> +    setCapManager(_capManager: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    feesAccrued(): (fees: uint256) <<AbstractARM>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _capManager: address) <<initializer>> <<LidoARM>> +    requestLidoWithdrawals(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoARM>> +    claimLidoWithdrawals(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<AbstractARM>> +    <<event>> CrossPriceUpdated(crossPrice: uint256) <<AbstractARM>> +    <<event>> Deposit(owner: address, assets: uint256, shares: uint256) <<AbstractARM>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<AbstractARM>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<AbstractARM>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>> +    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> +    <<event>> CapManagerUpdated(capManager: address) <<AbstractARM>> +    <<event>> RequestLidoWithdrawals(amounts: uint256[], requestIds: uint256[]) <<LidoARM>> +    <<event>> ClaimLidoWithdrawals(requestIds: uint256[]) <<LidoARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address, _claimDelay: uint256) <<LidoARM>> +    claimable(): uint256 <<AbstractARM>>    totalAssets(): uint256 <<AbstractARM>>    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>>    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> From 18b96c893247302473de09d97a41181933051fa3 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 15 Oct 2024 09:16:51 +1100 Subject: [PATCH 5/5] Pre launch changes (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Set buy and sell prices in initialize function setCrossPrice also validates buy and sell prices against the new cross price * fix: use `newCrossPrice` instead of `crossPrice`. * fix: use `<=` instead of `<` for `setCrossPrice`. * test: add reverting test for `setCrossPrice`. * fix: revert commit `a7947f3`. * fix: move `setCrossPrice` test to new file. * test: add more test for `setCrossPrice`. * feat: allow anyone to call claimLidoWithdrawals. * fix: adjust failing test * fix: remove testing stuff. --------- Co-authored-by: ClĂ©ment --- src/contracts/AbstractARM.sol | 10 ++ src/contracts/LidoARM.sol | 2 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 14 ++- .../SetCrossPrice.t.sol | 95 +++++++++++++++++++ .../LidoFixedPriceMultiLpARM/Setters.t.sol | 60 +----------- 5 files changed, 120 insertions(+), 61 deletions(-) create mode 100644 test/fork/LidoFixedPriceMultiLpARM/SetCrossPrice.t.sol diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 5bfa1fd..a64441c 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -172,6 +172,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // This avoids donation attacks when there are no assets in the ARM contract _mint(DEAD_ACCOUNT, MIN_TOTAL_SUPPLY); + // Set the sell price to its highest value. 1.0 + traderate0 = PRICE_SCALE; + // Set the buy price to its lowest value. 0.998 + traderate1 = PRICE_SCALE - MAX_CROSS_PRICE_DEVIATION; + emit TraderateChanged(traderate0, traderate1); + // Initialize the last available assets to the current available assets // This ensures no performance fee is accrued when the performance fee is calculated when the fee is set lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(_availableAssets())); @@ -402,6 +408,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { function setCrossPrice(uint256 newCrossPrice) external onlyOwner { require(newCrossPrice >= PRICE_SCALE - MAX_CROSS_PRICE_DEVIATION, "ARM: cross price too low"); require(newCrossPrice <= PRICE_SCALE, "ARM: cross price too high"); + // The exiting sell price must be greater than or equal to the new cross price + require(PRICE_SCALE * PRICE_SCALE / traderate0 >= newCrossPrice, "ARM: sell price too low"); + // The existing buy price must be less than the new cross price + require(traderate1 < newCrossPrice, "ARM: buy price too high"); // If the cross price is being lowered, there can not be a significant amount of base assets in the ARM. eg stETH. // This prevents the ARM making a loss when the base asset is sold at a lower price than it was bought diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index 4fe485e..c541413 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -93,7 +93,7 @@ contract LidoARM is Initializable, AbstractARM { * @notice Claim the ETH owed from the redemption requests and convert it to WETH. * Before calling this method, caller should check on the request NFTs to ensure the withdrawal was processed. */ - function claimLidoWithdrawals(uint256[] memory requestIds) external onlyOperatorOrOwner { + function claimLidoWithdrawals(uint256[] memory requestIds) external { uint256 etherBefore = address(this).balance; // Claim the NFTs for ETH. diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 97bfec8..7429549 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -322,7 +322,12 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether before"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before"); - assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + int256(MIN_TOTAL_SUPPLY), + STETH_ERROR_ROUNDING, + "last available assets before" + ); assertEq(lidoARM.balanceOf(alice), 0, "alice shares before"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); @@ -351,7 +356,12 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after"); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether after"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount), "last available assets after"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + int256(MIN_TOTAL_SUPPLY + amount), + STETH_ERROR_ROUNDING, + "last available assets after" + ); assertEq(lidoARM.balanceOf(alice), shares, "alice shares after"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); diff --git a/test/fork/LidoFixedPriceMultiLpARM/SetCrossPrice.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SetCrossPrice.t.sol new file mode 100644 index 0000000..866e62f --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/SetCrossPrice.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {AbstractARM} from "contracts/AbstractARM.sol"; + +contract Fork_Concrete_LidoARM_SetCrossPrice_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_SetCrossPrice_Because_NotOwner() public asRandomAddress { + vm.expectRevert("ARM: Only owner can call this function."); + lidoARM.setCrossPrice(0.9998e36); + } + + function test_RevertWhen_SetCrossPrice_Because_Operator() public asOperator { + vm.expectRevert("ARM: Only owner can call this function."); + lidoARM.setCrossPrice(0.9998e36); + } + + function test_RevertWhen_SetCrossPrice_Because_CrossPriceTooLow() public { + vm.expectRevert("ARM: cross price too low"); + lidoARM.setCrossPrice(0); + } + + function test_RevertWhen_SetCrossPrice_Because_CrossPriceTooHigh() public { + uint256 priceScale = 10 ** 36; + vm.expectRevert("ARM: cross price too high"); + lidoARM.setCrossPrice(priceScale + 1); + } + + function test_RevertWhen_SetCrossPrice_Because_BuyPriceTooHigh() public { + lidoARM.setPrices(1e36 - 20e32 + 1, 1000 * 1e33 + 1); + vm.expectRevert("ARM: buy price too high"); + lidoARM.setCrossPrice(1e36 - 20e32); + } + + function test_RevertWhen_SetCrossPrice_Because_SellPriceTooLow() public { + // To make it revert we need to try to make cross price above the sell1. + // But we need to keep cross price below 1e36! + // So first we reduce buy and sell price to minimum values + lidoARM.setPrices(1e36 - 20e32, 1000 * 1e33 + 1); + // This allow us to set a cross price below 1e36 + lidoARM.setCrossPrice(1e36 - 20e32 + 1); + // Then we make both buy and sell price below the 1e36 + lidoARM.setPrices(1e36 - 20e32, 1e36 - 20e32 + 1); + + // Then we try to set cross price above the sell price + vm.expectRevert("ARM: sell price too low"); + lidoARM.setCrossPrice(1e36 - 20e32 + 2); + } + + function test_RevertWhen_SetCrossPrice_Because_TooManyBaseAssets() public { + deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY + 1); + vm.expectRevert("ARM: too many base assets"); + lidoARM.setCrossPrice(1e36 - 1); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SetCrossPrice_No_StETH_Owner() public { + deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY - 1); + + // at 1.0 + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.CrossPriceUpdated(1e36); + lidoARM.setCrossPrice(1e36); + + // 20 basis points lower than 1.0 + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.CrossPriceUpdated(0.998e36); + lidoARM.setCrossPrice(0.998e36); + } + + function test_SetCrossPrice_With_StETH_PriceUp_Owner() public { + // 2 basis points lower than 1.0 + lidoARM.setCrossPrice(0.9998e36); + + deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY + 1); + + // 1 basis points lower than 1.0 + lidoARM.setCrossPrice(0.9999e36); + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 540582b..1a71cfc 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -118,12 +118,12 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setPrices(0, 0); } - function test_SellPriceCannotCrossOneByMoreThanTenBps() public asOperator { + function test_RevertWhen_SetPrices_Because_SellPriceCannotCrossOneByMoreThanTenBps() public asOperator { vm.expectRevert("ARM: sell price too low"); lidoARM.setPrices(0.998 * 1e36, 0.9989 * 1e36); } - function test_BuyPriceCannotCrossOneByMoreThanTenBps() public asOperator { + function test_RevertWhen_SetPrices_Because_BuyPriceCannotCrossOneByMoreThanTenBps() public asOperator { vm.expectRevert("ARM: buy price too high"); lidoARM.setPrices(1.0011 * 1e36, 1.002 * 1e36); } @@ -146,66 +146,10 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.traderate1(), 992 * 1e33); } - ////////////////////////////////////////////////////// - /// --- Set Cross Price - REVERTING TESTS - ////////////////////////////////////////////////////// - function test_RevertWhen_SetCrossPrice_Because_NotOwner() public asRandomAddress { - vm.expectRevert("ARM: Only owner can call this function."); - lidoARM.setCrossPrice(0.9998e36); - } - - function test_RevertWhen_SetCrossPrice_Because_Operator() public asOperator { - vm.expectRevert("ARM: Only owner can call this function."); - lidoARM.setCrossPrice(0.9998e36); - } - - function test_RevertWhen_SetCrossPrice_Because_PriceRange() public asLidoARMOwner { - // 21 basis points lower than 1.0 - vm.expectRevert("ARM: cross price too low"); - lidoARM.setCrossPrice(0.9979e36); - - // 1 basis points higher than 1.0 - vm.expectRevert("ARM: cross price too high"); - lidoARM.setCrossPrice(1.0001e36); - } - - function test_RevertWhen_SetCrossPrice_With_stETH_Because_PriceDrop() public { - deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY + 1); - - vm.expectRevert("ARM: too many base assets"); - lidoARM.setCrossPrice(0.9998e36); - } - ////////////////////////////////////////////////////// /// --- Set Cross Price - PASSING TESTS ////////////////////////////////////////////////////// - function test_SetCrossPrice_No_StETH_Owner() public { - deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY - 1); - - // at 1.0 - vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.CrossPriceUpdated(1e36); - lidoARM.setCrossPrice(1e36); - - // 20 basis points lower than 1.0 - vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.CrossPriceUpdated(0.998e36); - lidoARM.setCrossPrice(0.998e36); - } - - function test_SetCrossPrice_With_StETH_PriceUp_Owner() public { - // 2 basis points lower than 1.0 - lidoARM.setCrossPrice(0.9998e36); - - deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY + 1); - - // 4 basis points lower than 1.0 - // vm.expectEmit({emitter: address(lidoARM)}); - // emit AbstractARM.CrossPriceUpdated(0.9996e36); - lidoARM.setCrossPrice(0.9999e36); - } - ////////////////////////////////////////////////////// /// --- OWNABLE - REVERTING TESTS //////////////////////////////////////////////////////