From 00164c6426c56a2797b88176751019f1342021fe Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 23 Aug 2024 19:10:54 +1000 Subject: [PATCH 001/196] WIP MultiLP --- src/contracts/AbstractARM.sol | 146 +++++++++++++++++++++++++++++++++ src/contracts/MultiLP.sol | 26 ++++++ src/contracts/OethARM.sol | 6 +- src/contracts/OwnerLP.sol | 14 ++++ src/contracts/PeggedARM.sol | 150 +--------------------------------- 5 files changed, 193 insertions(+), 149 deletions(-) create mode 100644 src/contracts/AbstractARM.sol create mode 100644 src/contracts/MultiLP.sol create mode 100644 src/contracts/OwnerLP.sol diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol new file mode 100644 index 0000000..da35ce4 --- /dev/null +++ b/src/contracts/AbstractARM.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {OwnableOperable} from "./OwnableOperable.sol"; +import {IERC20} from "./Interfaces.sol"; + +abstract contract AbstractARM is OwnableOperable { + /// @notice The swap input token that is transferred to this contract. + /// From a User perspective, this is the token being sold. + /// token0 is also compatible with the Uniswap V2 Router interface. + IERC20 public immutable token0; + /// @notice The swap output token that is transferred from this contract. + /// From a User perspective, this is the token being bought. + /// token1 is also compatible with the Uniswap V2 Router interface. + IERC20 public immutable token1; + + constructor(address _inputToken, address _outputToken1) { + require(IERC20(_inputToken).decimals() == 18); + require(IERC20(_outputToken1).decimals() == 18); + + token0 = IERC20(_inputToken); + token1 = IERC20(_outputToken1); + + _setOwner(address(0)); // Revoke owner for implementation contract at deployment + } + + /** + * @notice Swaps an exact amount of input tokens for as many output tokens as possible. + * msg.sender should have already given the ARM contract an allowance of + * at least amountIn on the input token. + * + * @param inToken Input token. + * @param outToken Output token. + * @param amountIn The amount of input tokens to send. + * @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert. + * @param to Recipient of the output tokens. + */ + function swapExactTokensForTokens( + IERC20 inToken, + IERC20 outToken, + uint256 amountIn, + uint256 amountOutMin, + address to + ) external { + require(amountIn >= amountOutMin, "ARM: Insufficient output amount"); + _swap(inToken, outToken, amountIn, to); + } + + /** + * @notice Uniswap V2 Router compatible interface. Swaps an exact amount of + * input tokens for as many output tokens as possible. + * msg.sender should have already given the ARM contract an allowance of + * at least amountIn on the input token. + * + * @param amountIn The amount of input tokens to send. + * @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert. + * @param path The input and output token addresses. + * @param to Recipient of the output tokens. + * @param deadline Unix timestamp after which the transaction will revert. + * @return amounts The input and output token amounts. + */ + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts) { + require(amountIn >= amountOutMin, "ARM: Insufficient output amount"); + require(path.length == 2, "ARM: Invalid path length"); + _inDeadline(deadline); + + IERC20 inToken = IERC20(path[0]); + IERC20 outToken = IERC20(path[1]); + + _swap(inToken, outToken, amountIn, to); + + // Swaps are 1:1 so the input amount is the output amount + amounts = new uint256[](2); + amounts[0] = amountIn; + amounts[1] = amountIn; + } + + /** + * @notice Receive an exact amount of output tokens for as few input tokens as possible. + * msg.sender should have already given the router an allowance of + * at least amountInMax on the input token. + * + * @param inToken Input token. + * @param outToken Output token. + * @param amountOut The amount of output tokens to receive. + * @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts. + * @param to Recipient of the output tokens. + */ + function swapTokensForExactTokens( + IERC20 inToken, + IERC20 outToken, + uint256 amountOut, + uint256 amountInMax, + address to + ) external { + require(amountOut <= amountInMax, "ARM: Excess input amount"); + _swap(inToken, outToken, amountOut, to); + } + + /** + * @notice Uniswap V2 Router compatible interface. Receive an exact amount of + * output tokens for as few input tokens as possible. + * msg.sender should have already given the router an allowance of + * at least amountInMax on the input token. + * + * @param amountOut The amount of output tokens to receive. + * @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts. + * @param path The input and output token addresses. + * @param to Recipient of the output tokens. + * @param deadline Unix timestamp after which the transaction will revert. + * @return amounts The input and output token amounts. + */ + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts) { + require(amountOut <= amountInMax, "ARM: Excess input amount"); + require(path.length == 2, "ARM: Invalid path length"); + _inDeadline(deadline); + + IERC20 inToken = IERC20(path[0]); + IERC20 outToken = IERC20(path[1]); + + _swap(inToken, outToken, amountOut, to); + + // Swaps are 1:1 so the input amount is the output amount + amounts = new uint256[](2); + amounts[0] = amountOut; + amounts[1] = amountOut; + } + + function _swap(IERC20 inToken, IERC20 outToken, uint256 amount, address to) internal virtual; + + function _inDeadline(uint256 deadline) internal view { + require(deadline >= block.timestamp, "ARM: Deadline expired"); + } +} diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol new file mode 100644 index 0000000..70ebe01 --- /dev/null +++ b/src/contracts/MultiLP.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import {AbstractARM} from "./AbstractARM.sol"; +import {IERC20, IOETHVault} from "./Interfaces.sol"; + +abstract contract MultiLP is ERC20 { + address public immutable liquidityToken; + + constructor(address _liquidityToken) { + liquidityToken = _liquidityToken; + } + + function deposit(uint256 amount) external returns (uint256 shares) { + uint256 totalAssets = IERC20(token0).balanceOf(address(this)) + IERC20(token1).balanceOf(address(this)); + + shares = (totalAssets == 0) ? amount : (amount * totalSupply()) / totalAssets; + + // Transfer the liquidity token from the sender to this contract + IERC20(liquidityToken).transferFrom(msg.sender, address(this), amount); + + // mint shares + } +} diff --git a/src/contracts/OethARM.sol b/src/contracts/OethARM.sol index 470dad6..58268a7 100644 --- a/src/contracts/OethARM.sol +++ b/src/contracts/OethARM.sol @@ -1,16 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; +import {AbstractARM} from "./AbstractARM.sol"; import {PeggedARM} from "./PeggedARM.sol"; +import {OwnerLP} from "./OwnerLP.sol"; import {OethLiquidityManager} from "./OethLiquidityManager.sol"; import {Initializable} from "./utils/Initializable.sol"; -contract OethARM is Initializable, PeggedARM, OethLiquidityManager { +contract OethARM is Initializable, PeggedARM, OwnerLP, OethLiquidityManager { /// @param _oeth The address of the OETH token that is being swapped into this contract. /// @param _weth The address of the WETH token that is being swapped out of this contract. /// @param _oethVault The address of the OETH Vault proxy. constructor(address _oeth, address _weth, address _oethVault) - PeggedARM(_oeth, _weth) + AbstractARM(_oeth, _weth) OethLiquidityManager(_oeth, _oethVault) {} diff --git a/src/contracts/OwnerLP.sol b/src/contracts/OwnerLP.sol new file mode 100644 index 0000000..6778172 --- /dev/null +++ b/src/contracts/OwnerLP.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Ownable} from "./Ownable.sol"; +import {IERC20} from "./Interfaces.sol"; + +abstract contract OwnerLP is Ownable { + /** + * @notice Owner can transfer out any ERC20 token. + */ + function transferToken(address token, address to, uint256 amount) external onlyOwner { + IERC20(token).transfer(to, amount); + } +} diff --git a/src/contracts/PeggedARM.sol b/src/contracts/PeggedARM.sol index a8c094b..c153774 100644 --- a/src/contracts/PeggedARM.sol +++ b/src/contracts/PeggedARM.sol @@ -1,144 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {OwnableOperable} from "./OwnableOperable.sol"; +import {AbstractARM} from "./AbstractARM.sol"; import {IERC20} from "./Interfaces.sol"; -contract PeggedARM is OwnableOperable { - /// @notice The swap input token that is transferred to this contract. - /// From a User perspective, this is the token being sold. - /// token0 is also compatible with the Uniswap V2 Router interface. - IERC20 public immutable token0; - /// @notice The swap output token that is transferred from this contract. - /// From a User perspective, this is the token being bought. - /// token1 is also compatible with the Uniswap V2 Router interface. - IERC20 public immutable token1; - - constructor(address _inputToken, address _outputToken1) { - require(IERC20(_inputToken).decimals() == 18); - require(IERC20(_outputToken1).decimals() == 18); - - token0 = IERC20(_inputToken); - token1 = IERC20(_outputToken1); - - _setOwner(address(0)); // Revoke owner for implementation contract at deployment - } - - /** - * @notice Swaps an exact amount of input tokens for as many output tokens as possible. - * msg.sender should have already given the ARM contract an allowance of - * at least amountIn on the input token. - * - * @param inToken Input token. - * @param outToken Output token. - * @param amountIn The amount of input tokens to send. - * @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert. - * @param to Recipient of the output tokens. - */ - function swapExactTokensForTokens( - IERC20 inToken, - IERC20 outToken, - uint256 amountIn, - uint256 amountOutMin, - address to - ) external { - require(amountIn >= amountOutMin, "ARM: Insufficient output amount"); - _swap(inToken, outToken, amountIn, to); - } - - /** - * @notice Uniswap V2 Router compatible interface. Swaps an exact amount of - * input tokens for as many output tokens as possible. - * msg.sender should have already given the ARM contract an allowance of - * at least amountIn on the input token. - * - * @param amountIn The amount of input tokens to send. - * @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert. - * @param path The input and output token addresses. - * @param to Recipient of the output tokens. - * @param deadline Unix timestamp after which the transaction will revert. - * @return amounts The input and output token amounts. - */ - function swapExactTokensForTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts) { - require(amountIn >= amountOutMin, "ARM: Insufficient output amount"); - require(path.length == 2, "ARM: Invalid path length"); - _inDeadline(deadline); - - IERC20 inToken = IERC20(path[0]); - IERC20 outToken = IERC20(path[1]); - - _swap(inToken, outToken, amountIn, to); - - // Swaps are 1:1 so the input amount is the output amount - amounts = new uint256[](2); - amounts[0] = amountIn; - amounts[1] = amountIn; - } - - /** - * @notice Receive an exact amount of output tokens for as few input tokens as possible. - * msg.sender should have already given the router an allowance of - * at least amountInMax on the input token. - * - * @param inToken Input token. - * @param outToken Output token. - * @param amountOut The amount of output tokens to receive. - * @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts. - * @param to Recipient of the output tokens. - */ - function swapTokensForExactTokens( - IERC20 inToken, - IERC20 outToken, - uint256 amountOut, - uint256 amountInMax, - address to - ) external { - require(amountOut <= amountInMax, "ARM: Excess input amount"); - _swap(inToken, outToken, amountOut, to); - } - - /** - * @notice Uniswap V2 Router compatible interface. Receive an exact amount of - * output tokens for as few input tokens as possible. - * msg.sender should have already given the router an allowance of - * at least amountInMax on the input token. - * - * @param amountOut The amount of output tokens to receive. - * @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts. - * @param path The input and output token addresses. - * @param to Recipient of the output tokens. - * @param deadline Unix timestamp after which the transaction will revert. - * @return amounts The input and output token amounts. - */ - function swapTokensForExactTokens( - uint256 amountOut, - uint256 amountInMax, - address[] calldata path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts) { - require(amountOut <= amountInMax, "ARM: Excess input amount"); - require(path.length == 2, "ARM: Invalid path length"); - _inDeadline(deadline); - - IERC20 inToken = IERC20(path[0]); - IERC20 outToken = IERC20(path[1]); - - _swap(inToken, outToken, amountOut, to); - - // Swaps are 1:1 so the input amount is the output amount - amounts = new uint256[](2); - amounts[0] = amountOut; - amounts[1] = amountOut; - } - - function _swap(IERC20 inToken, IERC20 outToken, uint256 amount, address to) internal { +abstract contract PeggedARM is AbstractARM { + function _swap(IERC20 inToken, IERC20 outToken, uint256 amount, address to) internal override { require(inToken == token0 && outToken == token1, "ARM: Invalid swap"); // Transfer the input tokens from the caller to this ARM contract @@ -147,15 +14,4 @@ contract PeggedARM is OwnableOperable { // Transfer the same amount of output tokens to the recipient require(outToken.transfer(to, amount), "failed transfer out"); } - - function _inDeadline(uint256 deadline) internal view { - require(deadline >= block.timestamp, "ARM: Deadline expired"); - } - - /** - * @notice Owner can transfer out any ERC20 token. - */ - function transferToken(address token, address to, uint256 amount) external onlyOwner { - IERC20(token).transfer(to, amount); - } } From 72282ebcadb4aa7862e5bb2f250cee9434b49b35 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 23 Aug 2024 19:11:08 +1000 Subject: [PATCH 002/196] forge install: openzeppelin-contracts v5.0.2 --- .gitmodules | 3 +++ lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 888d42d..690924b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..dbb6104 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 From 081cad13ff2f5e09117abb90223a48dfbfb88334 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 23 Aug 2024 19:39:04 +1000 Subject: [PATCH 003/196] WIP --- src/contracts/MultiLP.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 70ebe01..fbc0b54 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -1,18 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20, ERC20Upgradeable} from "@openzeppelin/contracts/token/ERC20/ERC20Upgradeable.sol"; import {AbstractARM} from "./AbstractARM.sol"; import {IERC20, IOETHVault} from "./Interfaces.sol"; -abstract contract MultiLP is ERC20 { +abstract contract MultiLP is AbstractARM, ERC20Upgradeable { address public immutable liquidityToken; constructor(address _liquidityToken) { liquidityToken = _liquidityToken; } + function _initialize(string _name, string _symbol) external { + __ERC20_init(_name, _symbol); + } + function deposit(uint256 amount) external returns (uint256 shares) { uint256 totalAssets = IERC20(token0).balanceOf(address(this)) + IERC20(token1).balanceOf(address(this)); @@ -22,5 +26,6 @@ abstract contract MultiLP is ERC20 { IERC20(liquidityToken).transferFrom(msg.sender, address(this), amount); // mint shares + _mint(msg.sender, shares); } } From 9732e850d98e7e10a7a06b29747a2a0c70691c62 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 23 Aug 2024 19:39:33 +1000 Subject: [PATCH 004/196] forge install: openzeppelin-contracts-upgradeable v5.0.2 --- .gitmodules | 3 +++ lib/openzeppelin-contracts-upgradeable | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/openzeppelin-contracts-upgradeable diff --git a/.gitmodules b/.gitmodules index 690924b..9296efd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..723f8ca --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 From 80170bd7508a14223fe99aef27243a5f198787a3 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 23 Aug 2024 21:36:06 +1000 Subject: [PATCH 005/196] WIP redeem queue --- foundry.toml | 2 + src/contracts/MultiLP.sol | 152 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 147 insertions(+), 7 deletions(-) diff --git a/foundry.toml b/foundry.toml index 7a15fa0..e76dc03 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,6 +11,8 @@ gas_reports = ["OEthARM", "Proxy" ] remappings = [ "contracts/=./src/contracts", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", "script/=./script", "test/=./test", "utils/=./src/contracts/utils" diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index fbc0b54..7d08e07 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -1,31 +1,169 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {IERC20, ERC20Upgradeable} from "@openzeppelin/contracts/token/ERC20/ERC20Upgradeable.sol"; +import {IERC20, ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AbstractARM} from "./AbstractARM.sol"; -import {IERC20, IOETHVault} from "./Interfaces.sol"; abstract contract MultiLP is AbstractARM, ERC20Upgradeable { + uint256 public constant CLAIM_DELAY = 10 minutes; + address public immutable liquidityToken; + struct WithdrawalQueueMetadata { + // cumulative total of all withdrawal requests included the ones that have already been claimed + uint128 queued; + // cumulative total of all the requests that can be claimed including the ones that have already been claimed + uint128 claimable; + // total of all the requests that have been claimed + uint128 claimed; + // index of the next withdrawal request starting at 0 + uint128 nextWithdrawalIndex; + } + + /// @notice Global metadata for the withdrawal queue including: + /// queued - cumulative total of all withdrawal requests included the ones that have already been claimed + /// claimable - cumulative total of all the requests that can be claimed including the ones already claimed + /// claimed - total of all the requests that have been claimed + /// nextWithdrawalIndex - index of the next withdrawal request starting at 0 + // slither-disable-next-line uninitialized-state + WithdrawalQueueMetadata public withdrawalQueueMetadata; + + struct WithdrawalRequest { + address withdrawer; + bool claimed; + uint40 timestamp; // timestamp of the withdrawal request + // Amount of assets to withdraw + uint128 assets; + // cumulative total of all withdrawal requests including this one. + // this request can be claimed when this queued amount is less than or equal to the queue's claimable amount. + uint128 queued; + } + + /// @notice Mapping of withdrawal request indices to the user withdrawal request data + mapping(uint256 => WithdrawalRequest) public withdrawalRequests; + + event RedeemRequested(address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued); + event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); + constructor(address _liquidityToken) { + require(_liquidityToken == address(token0) || _liquidityToken == address(token1), "invalid liquidity token"); liquidityToken = _liquidityToken; } - function _initialize(string _name, string _symbol) external { + function _initialize(string calldata _name, string calldata _symbol) external { __ERC20_init(_name, _symbol); } - function deposit(uint256 amount) external returns (uint256 shares) { - uint256 totalAssets = IERC20(token0).balanceOf(address(this)) + IERC20(token1).balanceOf(address(this)); + function previewDeposit(uint256 assets) public view returns (uint256 shares) { + uint256 _totalAssets = totalAssets(); + shares = (_totalAssets == 0) ? assets : (assets * totalSupply()) / _totalAssets; + } - shares = (totalAssets == 0) ? amount : (amount * totalSupply()) / totalAssets; + function deposit(uint256 assets) external returns (uint256 shares) { + shares = previewDeposit(assets); // Transfer the liquidity token from the sender to this contract - IERC20(liquidityToken).transferFrom(msg.sender, address(this), amount); + IERC20(liquidityToken).transferFrom(msg.sender, address(this), assets); // mint shares _mint(msg.sender, shares); } + + function previewRedeem(uint256 shares) public view returns (uint256 assets) { + assets = (shares * totalAssets()) / totalSupply(); + } + + function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { + assets = previewRedeem(shares); + + requestId = withdrawalQueueMetadata.nextWithdrawalIndex; + uint256 queued = withdrawalQueueMetadata.queued + assets; + + // Store the next withdrawal request + withdrawalQueueMetadata.nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); + // Store requests + withdrawalRequests[requestId] = WithdrawalRequest({ + withdrawer: msg.sender, + claimed: false, + timestamp: uint40(block.timestamp), + assets: SafeCast.toUint128(assets), + queued: SafeCast.toUint128(queued) + }); + + // burn shares + _burn(msg.sender, shares); + + emit RedeemRequested(msg.sender, requestId, assets, queued); + } + + function claimRedeem(uint256 requestId) external returns (uint256 assets) { + if (withdrawalRequests[requestId].queued > withdrawalQueueMetadata.claimable) { + // Add any WETH from the Dripper to the withdrawal queue + _addWithdrawalQueueLiquidity(); + } + + // Load the structs from storage into memory + WithdrawalRequest memory request = withdrawalRequests[requestId]; + WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; + + require(request.timestamp + CLAIM_DELAY <= block.timestamp, "Claim delay not met"); + // If there isn't enough reserved liquidity in the queue to claim + require(request.queued <= queue.claimable, "Queue pending liquidity"); + require(request.withdrawer == msg.sender, "Not requester"); + require(request.claimed == false, "Already claimed"); + + // Store the request as claimed + withdrawalRequests[requestId].claimed = true; + // Store the updated claimed amount + withdrawalQueueMetadata.claimed = queue.claimed + request.assets; + + assets = request.assets; + + emit RedeemClaimed(msg.sender, requestId, assets); + + // transfer the liquidity token to the withdrawer + IERC20(liquidityToken).transfer(msg.sender, assets); + } + + /// @dev Adds liquidity to the withdrawal queue if there is a funding shortfall. + function _addWithdrawalQueueLiquidity() internal returns (uint256 addedClaimable) { + WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; + + // 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 + if (queueShortfall == 0) { + return 0; + } + + uint256 liquidityBalance = IERC20(liquidityToken).balanceOf(address(this)); + + // Of the claimable withdrawal requests, how much is unclaimed? + // That is, the amount of the liquidity token that is currently allocated for the withdrawal queue + uint256 allocatedLiquidity = queue.claimable - queue.claimed; + + // If there is no unallocated liquidity token then there is nothing to add to the queue + if (liquidityBalance <= allocatedLiquidity) { + return 0; + } + + uint256 unallocatedLiquidity = liquidityBalance - allocatedLiquidity; + + // the new claimable amount is the smaller of the queue shortfall or unallocated weth + addedClaimable = queueShortfall < unallocatedLiquidity ? queueShortfall : unallocatedLiquidity; + uint256 newClaimable = queue.claimable + addedClaimable; + + // Store the new claimable amount back to storage + withdrawalQueueMetadata.claimable = SafeCast.toUint128(newClaimable); + } + + function totalAssets() public view returns (uint256) { + // valuing both assets 1:1 + return token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue(); + } + + function _assetsInWithdrawQueue() internal view virtual returns (uint256); } From 0a987f7c409a4c65b324f7c9b2a36a6063d96b0f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 2 Sep 2024 12:55:52 +1000 Subject: [PATCH 006/196] PeggedARM now support one sided and double sided trades Made the AbstractARM more generic Improved OETH ARM tests --- src/contracts/AbstractARM.sol | 43 +++++++++++-------- src/contracts/OethARM.sol | 1 + src/contracts/PeggedARM.sol | 36 +++++++++++++++- .../mainnet/SwapExactTokensForTokens.t.sol | 19 ++++---- .../mainnet/SwapTokensForExactTokens.t.sol | 21 +++++---- 5 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index da35ce4..8395087 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -41,9 +41,9 @@ abstract contract AbstractARM is OwnableOperable { uint256 amountIn, uint256 amountOutMin, address to - ) external { - require(amountIn >= amountOutMin, "ARM: Insufficient output amount"); - _swap(inToken, outToken, amountIn, to); + ) external virtual { + uint256 amountOut = _swapExactTokensForTokens(inToken, outToken, amountIn, to); + require(amountOut >= amountOutMin, "ARM: Insufficient output amount"); } /** @@ -65,20 +65,20 @@ abstract contract AbstractARM is OwnableOperable { address[] calldata path, address to, uint256 deadline - ) external returns (uint256[] memory amounts) { - require(amountIn >= amountOutMin, "ARM: Insufficient output amount"); + ) external virtual returns (uint256[] memory amounts) { require(path.length == 2, "ARM: Invalid path length"); _inDeadline(deadline); IERC20 inToken = IERC20(path[0]); IERC20 outToken = IERC20(path[1]); - _swap(inToken, outToken, amountIn, to); + uint256 amountOut = _swapExactTokensForTokens(inToken, outToken, amountIn, to); + + require(amountOut >= amountOutMin, "ARM: Insufficient output amount"); - // Swaps are 1:1 so the input amount is the output amount amounts = new uint256[](2); amounts[0] = amountIn; - amounts[1] = amountIn; + amounts[1] = amountOut; } /** @@ -98,9 +98,10 @@ abstract contract AbstractARM is OwnableOperable { uint256 amountOut, uint256 amountInMax, address to - ) external { - require(amountOut <= amountInMax, "ARM: Excess input amount"); - _swap(inToken, outToken, amountOut, to); + ) external virtual { + uint256 amountIn = _swapTokensForExactTokens(inToken, outToken, amountOut, to); + + require(amountIn <= amountInMax, "ARM: Excess input amount"); } /** @@ -122,23 +123,31 @@ abstract contract AbstractARM is OwnableOperable { address[] calldata path, address to, uint256 deadline - ) external returns (uint256[] memory amounts) { - require(amountOut <= amountInMax, "ARM: Excess input amount"); + ) external virtual returns (uint256[] memory amounts) { require(path.length == 2, "ARM: Invalid path length"); _inDeadline(deadline); IERC20 inToken = IERC20(path[0]); IERC20 outToken = IERC20(path[1]); - _swap(inToken, outToken, amountOut, to); + uint256 amountIn = _swapTokensForExactTokens(inToken, outToken, amountOut, to); + + require(amountIn <= amountInMax, "ARM: Excess input amount"); - // Swaps are 1:1 so the input amount is the output amount amounts = new uint256[](2); - amounts[0] = amountOut; + amounts[0] = amountIn; amounts[1] = amountOut; } - function _swap(IERC20 inToken, IERC20 outToken, uint256 amount, address to) internal virtual; + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to) + internal + virtual + returns (uint256 amountOut); + + function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to) + internal + virtual + returns (uint256 amountIn); function _inDeadline(uint256 deadline) internal view { require(deadline >= block.timestamp, "ARM: Deadline expired"); diff --git a/src/contracts/OethARM.sol b/src/contracts/OethARM.sol index 58268a7..9dbfaa7 100644 --- a/src/contracts/OethARM.sol +++ b/src/contracts/OethARM.sol @@ -14,6 +14,7 @@ contract OethARM is Initializable, PeggedARM, OwnerLP, OethLiquidityManager { constructor(address _oeth, address _weth, address _oethVault) AbstractARM(_oeth, _weth) OethLiquidityManager(_oeth, _oethVault) + PeggedARM(false) {} /// @notice Initialize the contract. diff --git a/src/contracts/PeggedARM.sol b/src/contracts/PeggedARM.sol index c153774..0d70326 100644 --- a/src/contracts/PeggedARM.sol +++ b/src/contracts/PeggedARM.sol @@ -5,13 +5,45 @@ import {AbstractARM} from "./AbstractARM.sol"; import {IERC20} from "./Interfaces.sol"; abstract contract PeggedARM is AbstractARM { - function _swap(IERC20 inToken, IERC20 outToken, uint256 amount, address to) internal override { - require(inToken == token0 && outToken == token1, "ARM: Invalid swap"); + /// @notice If true, the ARM contract can swap in both directions between token0 and token1. + bool public immutable bothDirections; + + constructor(bool _bothDirections) { + bothDirections = _bothDirections; + } + + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to) + internal + override + returns (uint256 amountOut) + { + return _swap(inToken, outToken, amountIn, to); + } + + function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to) + internal + override + returns (uint256 amountIn) + { + return _swap(inToken, outToken, amountOut, to); + } + + function _swap(IERC20 inToken, IERC20 outToken, uint256 amount, address to) internal returns (uint256) { + if (bothDirections) { + require( + inToken == token0 && outToken == token1 || inToken == token1 && outToken == token0, "ARM: Invalid swap" + ); + } else { + require(inToken == token0 && outToken == token1, "ARM: Invalid swap"); + } // Transfer the input tokens from the caller to this ARM contract require(inToken.transferFrom(msg.sender, address(this), amount), "failed transfer in"); // Transfer the same amount of output tokens to the recipient require(outToken.transfer(to, amount), "failed transfer out"); + + // 1:1 swaps so the exact amount is returned as the calculated amount + return amount; } } diff --git a/test/fork/mainnet/SwapExactTokensForTokens.t.sol b/test/fork/mainnet/SwapExactTokensForTokens.t.sol index 04e9aeb..fe183de 100644 --- a/test/fork/mainnet/SwapExactTokensForTokens.t.sol +++ b/test/fork/mainnet/SwapExactTokensForTokens.t.sol @@ -9,12 +9,18 @@ import {IERC20} from "contracts/Interfaces.sol"; /// @notice The purpose of this contract is to test the `swapExactTokensForTokens` function in the `OethARM` contract. contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Test_ { + address[] path; + ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// function setUp() public override { super.setUp(); + path = new address[](2); + path[0] = address(oeth); + path[1] = address(weth); + // Deal tokens deal(address(oeth), address(this), 100 ether); deal(address(weth), address(oethARM), 100 ether); @@ -30,7 +36,7 @@ contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Tes function test_RevertWhen_SwapExactTokensForTokens_Simple_Because_InsufficientOutputAmount() public { vm.expectRevert("ARM: Insufficient output amount"); - oethARM.swapExactTokensForTokens(weth, oeth, 10 ether, 11 ether, address(this)); + oethARM.swapExactTokensForTokens(oeth, weth, 10 ether, 11 ether, address(this)); } function test_RevertWhen_SwapExactTokensForTokens_Simple_Because_InvalidSwap_TokenIn() public { @@ -45,7 +51,7 @@ contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Tes function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InsuficientOutputAmount() public { vm.expectRevert("ARM: Insufficient output amount"); - oethARM.swapExactTokensForTokens(10 ether, 11 ether, new address[](2), address(this), 0); + oethARM.swapExactTokensForTokens(10 ether, 11 ether, path, address(this), block.timestamp + 10); } function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalidPathLength() public { @@ -55,23 +61,21 @@ contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Tes function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_DeadlineExpired() public { vm.expectRevert("ARM: Deadline expired"); - oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](2), address(this), 0); + oethARM.swapExactTokensForTokens(10 ether, 10 ether, path, address(this), 0); } function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalidSwap_TokenIn() public { - address[] memory path = new address[](2); path[0] = address(weth); path[1] = address(weth); vm.expectRevert("ARM: Invalid swap"); - oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); + oethARM.swapExactTokensForTokens(10 ether, 10 ether, path, address(this), block.timestamp + 10); } function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalidSwap_TokenOut() public { - address[] memory path = new address[](2); path[0] = address(oeth); path[1] = address(oeth); vm.expectRevert("ARM: Invalid swap"); - oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); + oethARM.swapExactTokensForTokens(10 ether, 10 ether, path, address(this), block.timestamp + 10); } ////////////////////////////////////////////////////// @@ -106,7 +110,6 @@ contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Tes assertEq(weth.balanceOf(address(oethARM)), 100 ether, "OETH balance ARM"); assertEq(weth.balanceOf(address(oethARM)), 100 ether, "WETH balance ARM"); - address[] memory path = new address[](2); path[0] = address(oeth); path[1] = address(weth); diff --git a/test/fork/mainnet/SwapTokensForExactTokens.t.sol b/test/fork/mainnet/SwapTokensForExactTokens.t.sol index 3345df1..ebfb4c0 100644 --- a/test/fork/mainnet/SwapTokensForExactTokens.t.sol +++ b/test/fork/mainnet/SwapTokensForExactTokens.t.sol @@ -9,12 +9,18 @@ import {IERC20} from "contracts/Interfaces.sol"; /// @notice The purpose of this contract is to test the `swapTokensForExactTokens` function in the `OethARM` contract. contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Test_ { + address[] path; + ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// function setUp() public override { super.setUp(); + path = new address[](2); + path[0] = address(oeth); + path[1] = address(weth); + // Deal tokens deal(address(oeth), address(this), 100 ether); deal(address(weth), address(oethARM), 100 ether); @@ -30,7 +36,7 @@ contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Tes function test_RevertWhen_SwapTokensForExactTokens_Simple_Because_InsufficientOutputAmount() public { vm.expectRevert("ARM: Excess input amount"); - oethARM.swapTokensForExactTokens(weth, oeth, 10 ether, 9 ether, address(this)); + oethARM.swapTokensForExactTokens(oeth, weth, 10 ether, 9 ether, address(this)); } function test_RevertWhen_SwapTokensForExactTokens_Simple_Because_InvalidSwap_TokenIn() public { @@ -45,33 +51,31 @@ contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Tes function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InsufficientOutputAmount() public { vm.expectRevert("ARM: Excess input amount"); - oethARM.swapTokensForExactTokens(10 ether, 9 ether, new address[](2), address(this), 0); + oethARM.swapTokensForExactTokens(10 ether, 9 ether, path, address(this), block.timestamp + 10); } function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalidPathLength() public { vm.expectRevert("ARM: Invalid path length"); - oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](3), address(this), 0); + oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](3), address(this), block.timestamp + 10); } function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_DeadlineExpired() public { vm.expectRevert("ARM: Deadline expired"); - oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](2), address(this), 0); + oethARM.swapTokensForExactTokens(10 ether, 10 ether, path, address(this), block.timestamp - 1); } function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalidSwap_TokenIn() public { - address[] memory path = new address[](2); path[0] = address(weth); path[1] = address(weth); vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); + oethARM.swapTokensForExactTokens(10 ether, 10 ether, path, address(this), block.timestamp + 10); } function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalidSwap_TokenOut() public { - address[] memory path = new address[](2); path[0] = address(oeth); path[1] = address(oeth); vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); + oethARM.swapTokensForExactTokens(10 ether, 10 ether, path, address(this), block.timestamp + 10); } ////////////////////////////////////////////////////// @@ -106,7 +110,6 @@ contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Tes assertEq(weth.balanceOf(address(oethARM)), 100 ether, "OETH balance ARM"); assertEq(weth.balanceOf(address(oethARM)), 100 ether, "WETH balance ARM"); - address[] memory path = new address[](2); path[0] = address(oeth); path[1] = address(weth); // Expected events From a39504cec0aa57ae082e3a53043d0233e673d476 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 2 Sep 2024 13:07:26 +1000 Subject: [PATCH 007/196] Added Lido ARM --- docs/LidoARMHierarchy.svg | 193 ++++++++++++++++++++ docs/LidoARMSquashed.svg | 81 +++++++++ docs/LidoARMStorage.svg | 69 ++++++++ docs/OEthARMHierarchy.svg | 235 +++++++++++++++---------- docs/OEthARMSquashed.svg | 110 ++++++------ docs/generate.sh | 8 +- src/contracts/FixedPriceARM.sol | 122 +++++++++++++ src/contracts/Interfaces.sol | 48 +++++ src/contracts/LidoARM.sol | 41 +++++ src/contracts/LidoLiquidityManager.sol | 93 ++++++++++ src/contracts/OethARM.sol | 4 +- 11 files changed, 854 insertions(+), 150 deletions(-) create mode 100644 docs/LidoARMHierarchy.svg create mode 100644 docs/LidoARMSquashed.svg create mode 100644 docs/LidoARMStorage.svg create mode 100644 src/contracts/FixedPriceARM.sol create mode 100644 src/contracts/LidoARM.sol create mode 100644 src/contracts/LidoLiquidityManager.sol diff --git a/docs/LidoARMHierarchy.svg b/docs/LidoARMHierarchy.svg new file mode 100644 index 0000000..169cf5c --- /dev/null +++ b/docs/LidoARMHierarchy.svg @@ -0,0 +1,193 @@ + + + + + + +UmlClassDiagram + + + +0 + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol + + + +2 + +<<Interface>> +IERC20 +../src/contracts/Interfaces.sol + + + +0->2 + + + + + +19 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + + +0->19 + + + + + +1 + +<<Abstract>> +FixedPriceARM +../src/contracts/FixedPriceARM.sol + + + +1->0 + + + + + +1->2 + + + + + +8 + +<<Interface>> +IWETH +../src/contracts/Interfaces.sol + + + +8->2 + + + + + +9 + +<<Interface>> +IStETHWithdrawal +../src/contracts/Interfaces.sol + + + +11 + +LidoARM +../src/contracts/LidoARM.sol + + + +11->1 + + + + + +12 + +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol + + + +11->12 + + + + + +20 + +<<Abstract>> +OwnerLP +../src/contracts/OwnerLP.sol + + + +11->20 + + + + + +30 + +<<Abstract>> +Initializable +../src/contracts/utils/Initializable.sol + + + +11->30 + + + + + +12->2 + + + + + +12->8 + + + + + +12->9 + + + + + +12->19 + + + + + +18 + +Ownable +../src/contracts/Ownable.sol + + + +19->18 + + + + + +20->2 + + + + + +20->18 + + + + + diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg new file mode 100644 index 0000000..7427f42 --- /dev/null +++ b/docs/LidoARMSquashed.svg @@ -0,0 +1,81 @@ + + + + + + +UmlClassDiagram + + + +11 + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   gap: uint256[50] <<Initializable>> +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawal: IStETHWithdrawal <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +    _depositETHForStETH(amount: uint256) <<LidoLiquidityManager>> +    _claimStETHWithdrawalForETH(requestIds: uint256[]) <<LidoLiquidityManager>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    owner(): address <<Ownable>> +    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>> +    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>> <<FixedPriceARM>> +    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    depositETHForStETH(amount: uint256) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    depositWETHForStETH(amount: uint256) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_operator: address) <<initializer>> <<LidoARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> +    constructor(_stEth: address, _weth: address, _stEthWithdrawal: address) <<LidoARM>> + + + diff --git a/docs/LidoARMStorage.svg b/docs/LidoARMStorage.svg new file mode 100644 index 0000000..ac22372 --- /dev/null +++ b/docs/LidoARMStorage.svg @@ -0,0 +1,69 @@ + + + + + + +StorageDiagram + + + +1 + +LidoARM <<Contract>> + +slot + +0 + +1-50 + +51 + +52-101 + +102 + +103 + +104 + +0x360..bbd + +0xb53..104 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.gap (1600) + +unallocated (12) + +address: OwnableOperable.operator (20) + +uint256[50]: OwnableOperable._gap (1600) + +uint256: FixedPriceARM.traderate0 (32) + +uint256: FixedPriceARM.traderate1 (32) + +uint256: FixedPriceARM.minimumFunds (32) + +unallocated (12) + +address: eip1967.proxy.implementation (20) + +unallocated (12) + +address: eip1967.proxy.admin (20) + + + diff --git a/docs/OEthARMHierarchy.svg b/docs/OEthARMHierarchy.svg index ace4558..85db68a 100644 --- a/docs/OEthARMHierarchy.svg +++ b/docs/OEthARMHierarchy.svg @@ -4,123 +4,170 @@ - - + + UmlClassDiagram - + 0 - -<<Interface>> -IERC20 -../src/contracts/Interfaces.sol + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol - + -3 - -<<Interface>> -IOETHVault -../src/contracts/Interfaces.sol +2 + +<<Interface>> +IERC20 +../src/contracts/Interfaces.sol - + + +0->2 + + + + + +19 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + + +0->19 + + + + -7 - -OethARM -../src/contracts/OethARM.sol +5 + +<<Interface>> +IOETHVault +../src/contracts/Interfaces.sol - + -8 - -OethLiquidityManager -../src/contracts/OethLiquidityManager.sol - - - -7->8 - - +16 + +OethARM +../src/contracts/OethARM.sol - - -11 - -PeggedARM -../src/contracts/PeggedARM.sol + + +17 + +OethLiquidityManager +../src/contracts/OethLiquidityManager.sol - - -7->11 - - + + +16->17 + + 20 - -<<Abstract>> -Initializable -../src/contracts/utils/Initializable.sol - - - -7->20 - - + +<<Abstract>> +OwnerLP +../src/contracts/OwnerLP.sol - + + +16->20 + + + + + +21 + +<<Abstract>> +PeggedARM +../src/contracts/PeggedARM.sol + + -8->0 - - - - - -8->3 - - - - - -10 - -OwnableOperable -../src/contracts/OwnableOperable.sol +16->21 + + + + + +30 + +<<Abstract>> +Initializable +../src/contracts/utils/Initializable.sol + + + +16->30 + + - - -8->10 - - + + +17->2 + + - - -9 - -Ownable -../src/contracts/Ownable.sol + + +17->5 + + - + -10->9 - - +17->19 + + - - -11->0 - - - - - -11->10 - - + + +18 + +Ownable +../src/contracts/Ownable.sol + + + +19->18 + + + + + +20->2 + + + + + +20->18 + + + + + +21->0 + + + + + +21->2 + + diff --git a/docs/OEthARMSquashed.svg b/docs/OEthARMSquashed.svg index 0a1c7b8..fbf48d5 100644 --- a/docs/OEthARMSquashed.svg +++ b/docs/OEthARMSquashed.svg @@ -4,62 +4,66 @@ - - + + UmlClassDiagram - - + + -7 - -OethARM -../src/contracts/OethARM.sol - -Private: -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   gap: uint256[50] <<Initializable>> -   _gap: uint256[50] <<OwnableOperable>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<PeggedARM>> -   token1: IERC20 <<PeggedARM>> -   oeth: address <<OethLiquidityManager>> -   oethVault: address <<OethLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swap(inToken: IERC20, outToken: IERC20, amount: uint256, to: address) <<PeggedARM>> -    _inDeadline(deadline: uint256) <<PeggedARM>> -    _approvals() <<OethLiquidityManager>> -External: -    owner(): address <<Ownable>> -    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +16 + +OethARM +../src/contracts/OethARM.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   gap: uint256[50] <<Initializable>> +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   bothDirections: bool <<PeggedARM>> +   oeth: address <<OethLiquidityManager>> +   oethVault: address <<OethLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<PeggedARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<PeggedARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _swap(inToken: IERC20, outToken: IERC20, amount: uint256, to: address): uint256 <<PeggedARM>> +    _approvals() <<OethLiquidityManager>> +External: +    owner(): address <<Ownable>> +    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>>    setOperator(newOperator: address) <<onlyOwner>> <<OwnableOperable>> -    swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, amountOutMin: uint256, to: address) <<PeggedARM>> -    swapExactTokensForTokens(amountIn: uint256, amountOutMin: uint256, path: address[], to: address, deadline: uint256): (amounts: uint256[]) <<PeggedARM>> -    swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, amountInMax: uint256, to: address) <<PeggedARM>> -    swapTokensForExactTokens(amountOut: uint256, amountInMax: uint256, path: address[], to: address, deadline: uint256): (amounts: uint256[]) <<PeggedARM>> -    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<PeggedARM>> -    approvals() <<onlyOwner>> <<OethLiquidityManager>> -    requestWithdrawal(amount: uint256): (requestId: uint256, queued: uint256) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> -    claimWithdrawal(requestId: uint256) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> -    claimWithdrawals(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> -    initialize(_operator: address) <<initializer>> <<OethARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<modifier>> initializer() <<Initializable>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_oeth: address, _oethVault: address) <<OethLiquidityManager>> +    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>> +    approvals() <<onlyOwner>> <<OethLiquidityManager>> +    requestWithdrawal(amount: uint256): (requestId: uint256, queued: uint256) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> +    claimWithdrawal(requestId: uint256) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> +    claimWithdrawals(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> +    initialize(_operator: address) <<initializer>> <<OethARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_oeth: address, _oethVault: address) <<OethLiquidityManager>> +    constructor(_bothDirections: bool) <<PeggedARM>>    constructor(_oeth: address, _weth: address, _oethVault: address) <<OethARM>> diff --git a/docs/generate.sh b/docs/generate.sh index 4089405..aeb1b2a 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -11,4 +11,10 @@ sol2uml ../src/contracts -s -d 0 -b Proxy -o ProxySquashed.svg sol2uml storage ../src/contracts -c Proxy -o ProxyStorage.svg \ -sn eip1967.proxy.implementation,eip1967.proxy.admin \ -st address,address - \ No newline at end of file + +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -b LidoARM -o LidoARMHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b LidoARM -o LidoARMSquashed.svg +sol2uml storage ../src/contracts -c LidoARM -o LidoARMStorage.svg \ + -sn eip1967.proxy.implementation,eip1967.proxy.admin \ + -st address,address \ + --hideExpand gap,_gap diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol new file mode 100644 index 0000000..14dc3db --- /dev/null +++ b/src/contracts/FixedPriceARM.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {AbstractARM} from "./AbstractARM.sol"; +import {IERC20} from "./Interfaces.sol"; + +abstract contract FixedPriceARM is AbstractARM { + /** + * @notice For one `token0` from a Trader, how many `token1` does the pool send. + * For example, if `token0` is WETH and `token1` is stETH then + * `traderate0` is the WETH/stETH price. + * From a Trader's perspective, this is the stETH/WETH buy price. + * Rate is to 36 decimals (1e36). + */ + uint256 public traderate0; + /** + * @notice For one `token1` from a Trader, how many `token0` does the pool send. + * For example, if `token0` is WETH and `token1` is stETH then + * `traderate1` is the stETH/WETH price. + * From a Trader's perspective, this is the stETH/WETH sell price. + * Rate is to 36 decimals (1e36). + */ + uint256 public traderate1; + + /// @dev Maximum operator settable traderate. 1e36 + uint256 internal constant MAX_OPERATOR_RATE = 1005 * 1e33; + /// @dev Minimum funds to allow operator to price changes + uint256 public minimumFunds; + + event TraderateChanged(uint256 traderate0, uint256 traderate1); + + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to) + internal + override + returns (uint256 amountOut) + { + uint256 price; + if (inToken == token0) { + require(outToken == token1, "ARM: Invalid token"); + price = traderate0; + } else if (inToken == token1) { + require(outToken == token0, "ARM: Invalid token"); + price = traderate1; + } else { + revert("ARM: Invalid token"); + } + amountOut = amountIn * price / 1e36; + + // Transfer the input tokens from the caller to this ARM contract + inToken.transferFrom(msg.sender, address(this), amountIn); + + // Transfer the output tokens to the recipient + uint256 transferAmountOut = _calcTransferAmount(address(outToken), amountOut); + outToken.transfer(to, transferAmountOut); + } + + function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to) + internal + override + returns (uint256 amountIn) + { + uint256 price; + if (inToken == token0) { + require(outToken == token1, "ARM: Invalid token"); + price = traderate0; + } else if (inToken == token1) { + require(outToken == token0, "ARM: Invalid token"); + price = traderate1; + } else { + revert("ARM: Invalid token"); + } + amountIn = ((amountOut * 1e36) / price) + 1; // +1 to always round in our favor + + // Transfer the input tokens from the caller to this ARM contract + 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; + } + + /** + * @notice Set exchange rates from an operator account + * @param buyT1 The buy price of Token 1 (t0 -> t1), denominated in Token 0. 1e36 + * @param sellT1 The sell price of Token 1 (t1 -> t0), denominated in Token 0. 1e36 + */ + function setPrices(uint256 buyT1, uint256 sellT1) external onlyOperatorOrOwner { + uint256 _traderate0 = 1e72 / sellT1; // base (t0) -> token (t1) + uint256 _traderate1 = buyT1; // token (t1) -> base (t0) + // Limit funds and loss when called by operator + if (msg.sender == operator) { + uint256 currentFunds = token0.balanceOf(address(this)) + token1.balanceOf(address(this)); + require(currentFunds > minimumFunds, "ARM: Too much loss"); + require(_traderate0 <= MAX_OPERATOR_RATE, "ARM: Traderate too high"); + require(_traderate1 <= MAX_OPERATOR_RATE, "ARM: Traderate too high"); + } + _setTraderates(_traderate0, _traderate1); + } + + /** + * @notice Sets the minimum funds to allow operator price changes + */ + function setMinimumFunds(uint256 _minimumFunds) external onlyOwner { + minimumFunds = _minimumFunds; + } + + function _setTraderates(uint256 _traderate0, uint256 _traderate1) internal { + require((1e72 / (_traderate0)) > _traderate1, "ARM: Price cross"); + traderate0 = _traderate0; + traderate1 = _traderate1; + + emit TraderateChanged(_traderate0, _traderate1); + } +} diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 0dab405..a8af55c 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -183,3 +183,51 @@ interface IWETH is IERC20 { function deposit() external payable; function withdraw(uint256 wad) external; } + +interface IStETHWithdrawal { + event WithdrawalRequested( + uint256 indexed requestId, + address indexed requestor, + address indexed owner, + uint256 amountOfStETH, + uint256 amountOfShares + ); + event WithdrawalsFinalized( + uint256 indexed from, uint256 indexed to, uint256 amountOfETHLocked, uint256 sharesToBurn, uint256 timestamp + ); + event WithdrawalClaimed( + uint256 indexed requestId, address indexed owner, address indexed receiver, uint256 amountOfETH + ); + + struct WithdrawalRequestStatus { + /// @notice stETH token amount that was locked on withdrawal queue for this request + uint256 amountOfStETH; + /// @notice amount of stETH shares locked on withdrawal queue for this request + uint256 amountOfShares; + /// @notice address that can claim or transfer this request + address owner; + /// @notice timestamp of when the request was created, in seconds + uint256 timestamp; + /// @notice true, if request is finalized + bool isFinalized; + /// @notice true, if request is claimed. Request is claimable if (isFinalized && !isClaimed) + bool isClaimed; + } + + function transferFrom(address _from, address _to, uint256 _requestId) external; + function ownerOf(uint256 _requestId) external returns (address); + function requestWithdrawals(uint256[] calldata _amounts, address _owner) + external + returns (uint256[] memory requestIds); + function getLastCheckpointIndex() external view returns (uint256); + function findCheckpointHints(uint256[] calldata _requestIds, uint256 _firstIndex, uint256 _lastIndex) + external + view + returns (uint256[] memory hintIds); + function claimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external; + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses); + function getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestsIds); +} diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol new file mode 100644 index 0000000..ea505d9 --- /dev/null +++ b/src/contracts/LidoARM.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {AbstractARM} from "./AbstractARM.sol"; +import {FixedPriceARM} from "./FixedPriceARM.sol"; +import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; +import {OwnerLP} from "./OwnerLP.sol"; +import {Initializable} from "./utils/Initializable.sol"; + +contract LidoARM is Initializable, OwnerLP, FixedPriceARM, LidoLiquidityManager { + /// @param _stEth The address of the stETH token + /// @param _weth The address of the WETH token + /// @param _stEthWithdrawal The address of the stETH Withdrawal contract + constructor(address _stEth, address _weth, address _stEthWithdrawal) + AbstractARM(_stEth, _weth) + FixedPriceARM() + LidoLiquidityManager(_stEth, _weth, _stEthWithdrawal) + {} + + /// @notice Initialize the contract. + /// @param _operator The address of the account that can request and claim OETH withdrawals. + function initialize(address _operator) external initializer { + _setOperator(_operator); + } + + /** + * @notice Calculate transfer amount for outToken. + * 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. + */ + function _calcTransferAmount(address outToken, uint256 amount) + internal + view + override + returns (uint256 transferAmount) + { + // Add 2 wei if transferring stETH + transferAmount = outToken == address(token0) ? amount + 2 : amount; + } +} diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol new file mode 100644 index 0000000..8a38853 --- /dev/null +++ b/src/contracts/LidoLiquidityManager.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {OwnableOperable} from "./OwnableOperable.sol"; +import {IERC20, IWETH, IStETHWithdrawal} from "./Interfaces.sol"; + +contract LidoLiquidityManager is OwnableOperable { + IERC20 public immutable steth; + IWETH public immutable weth; + IStETHWithdrawal public immutable withdrawal; + + constructor(address _steth, address _weth, address _stEthWithdrawal) { + steth = IERC20(_steth); + weth = IWETH(_weth); + withdrawal = IStETHWithdrawal(_stEthWithdrawal); + } + + /** + * @notice Approve the stETH withdrawal contract. Used for redemption requests. + */ + function approveStETH() external onlyOperatorOrOwner { + steth.approve(address(withdrawal), type(uint256).max); + } + + /** + * @notice Mint stETH with ETH + */ + function depositETHForStETH(uint256 amount) external onlyOperatorOrOwner { + _depositETHForStETH(amount); + } + + /** + * @notice Mint stETH with WETH + */ + function depositWETHForStETH(uint256 amount) external onlyOperatorOrOwner { + // Unwrap the WETH then deposit the ETH. + weth.withdraw(amount); + _depositETHForStETH(amount); + } + + /** + * @notice Mint stETH with ETH. + * Reference: https://docs.lido.fi/contracts/lido#fallback + */ + function _depositETHForStETH(uint256 amount) internal { + require(address(this).balance >= amount, "OSwap: Insufficient ETH balance"); + (bool success,) = address(steth).call{value: amount}(new bytes(0)); + require(success, "OSwap: ETH transfer failed"); + } + + /** + * @notice Request a stETH for ETH withdrawal. + * Reference: https://docs.lido.fi/contracts/withdrawal-queue-erc721/ + * Note: There is a 1k amount limit. Caller should split large withdrawals in chunks of less or equal to 1k each.) + */ + function requestStETHWithdrawalForETH(uint256[] memory amounts) + external + onlyOperatorOrOwner + returns (uint256[] memory requestIds) + { + requestIds = withdrawal.requestWithdrawals(amounts, address(this)); + } + + /** + * @notice Claim the ETH owed from the redemption requests. + * Before calling this method, caller should check on the request NFTs to ensure the withdrawal was processed. + */ + function claimStETHWithdrawalForETH(uint256[] memory requestIds) external onlyOperatorOrOwner { + _claimStETHWithdrawalForETH(requestIds); + } + + /** + * @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 claimStETHWithdrawalForWETH(uint256[] memory requestIds) external onlyOperatorOrOwner { + // Claim the NFTs for ETH. + _claimStETHWithdrawalForETH(requestIds); + + // Wrap all the received ETH to WETH. + (bool success,) = address(weth).call{value: address(this).balance}(new bytes(0)); + require(success, "OSwap: ETH transfer failed"); + } + + function _claimStETHWithdrawalForETH(uint256[] memory requestIds) internal { + uint256 lastIndex = withdrawal.getLastCheckpointIndex(); + uint256[] memory hintIds = withdrawal.findCheckpointHints(requestIds, 1, lastIndex); + withdrawal.claimWithdrawals(requestIds, hintIds); + } + + // This method is necessary for receiving the ETH claimed as part of the withdrawal. + receive() external payable {} +} diff --git a/src/contracts/OethARM.sol b/src/contracts/OethARM.sol index 9dbfaa7..540bce4 100644 --- a/src/contracts/OethARM.sol +++ b/src/contracts/OethARM.sol @@ -7,14 +7,14 @@ import {OwnerLP} from "./OwnerLP.sol"; import {OethLiquidityManager} from "./OethLiquidityManager.sol"; import {Initializable} from "./utils/Initializable.sol"; -contract OethARM is Initializable, PeggedARM, OwnerLP, OethLiquidityManager { +contract OethARM is Initializable, OwnerLP, PeggedARM, OethLiquidityManager { /// @param _oeth The address of the OETH token that is being swapped into this contract. /// @param _weth The address of the WETH token that is being swapped out of this contract. /// @param _oethVault The address of the OETH Vault proxy. constructor(address _oeth, address _weth, address _oethVault) AbstractARM(_oeth, _weth) - OethLiquidityManager(_oeth, _oethVault) PeggedARM(false) + OethLiquidityManager(_oeth, _oethVault) {} /// @notice Initialize the contract. From 8bf3d3345b0842e7b8352a4c058ea027ac613d94 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 2 Sep 2024 20:26:20 +1000 Subject: [PATCH 008/196] Added Lido ARM fork tests --- src/contracts/utils/Addresses.sol | 2 + test/Base.sol | 3 + test/fork/mainnet/LidoARM.t.sol | 267 ++++++++++++++++++++++++++++++ test/fork/shared/Shared.sol | 2 + 4 files changed, 274 insertions(+) create mode 100644 test/fork/mainnet/LidoARM.t.sol diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index b8f3388..40a2820 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -21,6 +21,7 @@ library Mainnet { // Tokens address public constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; // Contracts address public constant OETH_VAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; @@ -64,6 +65,7 @@ contract AddressResolver { // Tokens resolver[MAINNET]["OETH"] = Mainnet.OETH; resolver[MAINNET]["WETH"] = Mainnet.WETH; + resolver[MAINNET]["STETH"] = Mainnet.STETH; // Contracts resolver[MAINNET]["OETH_VAULT"] = Mainnet.OETH_VAULT; diff --git a/test/Base.sol b/test/Base.sol index d0017e1..0625ce5 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -7,6 +7,7 @@ import {Test} from "forge-std/Test.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; @@ -30,8 +31,10 @@ abstract contract Base_Test_ is Test { ////////////////////////////////////////////////////// Proxy public proxy; OethARM public oethARM; + IERC20 public oeth; IERC20 public weth; + IERC20 public steth; IOETHVault public vault; ////////////////////////////////////////////////////// diff --git a/test/fork/mainnet/LidoARM.t.sol b/test/fork/mainnet/LidoARM.t.sol new file mode 100644 index 0000000..a1488e7 --- /dev/null +++ b/test/fork/mainnet/LidoARM.t.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test, console2} from "forge-std/Test.sol"; + +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; +import {Proxy} from "contracts/Proxy.sol"; + +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +contract Fork_Concrete_LidoARM_Test is Fork_Shared_Test_ { + Proxy public lidoProxy; + LidoARM public lidoARM; + IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); + + // Account for stETH rounding errors. + // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case + uint256 constant ROUNDING = 2; + + function setUp() public override { + super.setUp(); + + address lidoWithdrawal = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; + LidoARM lidoImpl = new LidoARM(address(weth), address(steth), lidoWithdrawal); + lidoProxy = new Proxy(); + // Initialize Proxy with LidoARM implementation. + bytes memory data = abi.encodeWithSignature("initialize(address)", operator); + lidoProxy.initialize(address(lidoImpl), address(this), data); + + lidoARM = LidoARM(payable(address(lidoProxy))); + + _dealWETH(address(lidoARM), 100 ether); + _dealStETH(address(lidoARM), 100 ether); + // Contract will trade + // give us 1 WETH, get 0.625 stETH + // give us 1 stETH, get 0.5 WETH + lidoARM.setPrices(500 * 1e33, 1600000000000000000000000000000000000); + + weth.approve(address(lidoARM), type(uint256).max); + steth.approve(address(lidoARM), type(uint256).max); + + // Only fuzz from this address. Big speedup on fork. + targetSender(address(this)); + } + + function test_goodPriceSet() external { + lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + lidoARM.setPrices(1001 * 1e33, 1004 * 1e33); + } + + function test_badPriceSet() external { + vm.expectRevert(bytes("ARM: Price cross")); + lidoARM.setPrices(90 * 1e33, 89 * 1e33); + vm.expectRevert(bytes("ARM: Price cross")); + lidoARM.setPrices(72, 70); + vm.expectRevert(bytes("ARM: Price cross")); + lidoARM.setPrices(1005 * 1e33, 1000 * 1e33); + } + + function test_realistic_swaps() external { + vm.prank(operator); + lidoARM.setPrices(997 * 1e33, 998 * 1e33); + _swapExactTokensForTokens(steth, weth, 10 ether, 9.97 ether); + _swapExactTokensForTokens(weth, steth, 10 ether, 10020040080160320641); + } + + function test_swapExactTokensForTokens_WETH_TO_STETH() external { + _swapExactTokensForTokens(weth, steth, 10 ether, 6.25 ether); + } + + function test_swapExactTokensForTokens_STETH_TO_WETH() external { + _swapExactTokensForTokens(steth, weth, 10 ether, 5 ether); + } + + function test_swapTokensForExactTokens_WETH_TO_STETH() external { + _swapTokensForExactTokens(weth, steth, 10 ether, 6.25 ether); + } + + function test_swapTokensForExactTokens_STETH_TO_WETH() external { + _swapTokensForExactTokens(steth, weth, 10 ether, 5 ether); + } + + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) + internal + { + if (inToken == weth) { + _dealWETH(address(this), amountIn + 1000); + } else { + _dealStETH(address(this), amountIn + 1000); + } + uint256 startIn = inToken.balanceOf(address(this)); + uint256 startOut = outToken.balanceOf(address(this)); + lidoARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); + assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); + assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); + assertGe(outToken.balanceOf(address(this)), startOut + expectedOut - ROUNDING, "Out actual"); + assertLe(outToken.balanceOf(address(this)), startOut + expectedOut + ROUNDING, "Out actual"); + } + + function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) + internal + { + if (inToken == weth) { + _dealWETH(address(this), amountIn + 1000); + } else { + _dealStETH(address(this), amountIn + 1000); + } + uint256 startIn = inToken.balanceOf(address(this)); + lidoARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); + assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); + assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); + assertGe(outToken.balanceOf(address(this)), expectedOut - ROUNDING, "Out actual"); + assertLe(outToken.balanceOf(address(this)), expectedOut + ROUNDING, "Out actual"); + } + + function test_unauthorizedAccess() external { + address RANDOM_ADDRESS = 0xfEEDBeef00000000000000000000000000000000; + vm.startPrank(RANDOM_ADDRESS); + + // Proxy's restricted methods. + vm.expectRevert("ARM: Only owner can call this function."); + proxy.setOwner(RANDOM_ADDRESS); + + vm.expectRevert("ARM: Only owner can call this function."); + proxy.initialize(address(this), address(this), ""); + + vm.expectRevert("ARM: Only owner can call this function."); + proxy.upgradeTo(address(this)); + + vm.expectRevert("ARM: Only owner can call this function."); + proxy.upgradeToAndCall(address(this), ""); + + // Implementation's restricted methods. + vm.expectRevert("ARM: Only owner can call this function."); + lidoARM.setOwner(RANDOM_ADDRESS); + + vm.expectRevert("ARM: Only operator or owner can call this function."); + lidoARM.setPrices(123, 321); + } + + function test_wrongInTokenExactIn() external { + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapExactTokensForTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); + } + + function test_wrongOutTokenExactIn() external { + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); + } + + function test_wrongInTokenExactOut() external { + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); + } + + function test_wrongOutTokenExactOut() external { + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); + } + + function test_collectTokens() external { + lidoARM.transferToken(address(weth), address(this), weth.balanceOf(address(lidoARM))); + assertGt(weth.balanceOf(address(this)), 50 ether); + assertEq(weth.balanceOf(address(lidoARM)), 0); + + lidoARM.transferToken(address(steth), address(this), steth.balanceOf(address(lidoARM))); + assertGt(steth.balanceOf(address(this)), 50 ether); + assertLt(steth.balanceOf(address(lidoARM)), 3); + } + + function _dealStETH(address to, uint256 amount) internal { + vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); + steth.transfer(to, amount); + } + + function _dealWETH(address to, uint256 amount) internal { + deal(address(weth), to, amount); + } + + /* Operator Tests */ + + function test_setOperator() external { + lidoARM.setOperator(address(this)); + assertEq(lidoARM.operator(), address(this)); + } + + function test_nonOwnerCannotSetOperator() external { + vm.expectRevert("ARM: Only owner can call this function."); + vm.prank(operator); + lidoARM.setOperator(operator); + } + + function test_setMinimumFunds() external { + lidoARM.setMinimumFunds(100 ether); + assertEq(lidoARM.minimumFunds(), 100 ether); + } + + function test_setGoodCheckedTraderates() external { + vm.prank(operator); + lidoARM.setPrices(992 * 1e33, 2000 * 1e33); + assertEq(lidoARM.traderate0(), 500 * 1e33); + assertEq(lidoARM.traderate1(), 992 * 1e33); + } + + function test_setBadCheckedTraderates() external { + vm.prank(operator); + vm.expectRevert("ARM: Traderate too high"); + lidoARM.setPrices(1010 * 1e33, 1020 * 1e33); + vm.prank(operator); + vm.expectRevert("ARM: Traderate too high"); + lidoARM.setPrices(993 * 1e33, 994 * 1e33); + } + + function test_checkTraderateFailsMinimumFunds() external { + uint256 currentFunds = + lidoARM.token0().balanceOf(address(lidoARM)) + lidoARM.token1().balanceOf(address(lidoARM)); + lidoARM.setMinimumFunds(currentFunds + 100); + + vm.prank(operator); + vm.expectRevert("ARM: Too much loss"); + lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + } + + function test_checkTraderateWorksMinimumFunds() external { + uint256 currentFunds = + lidoARM.token0().balanceOf(address(lidoARM)) + lidoARM.token1().balanceOf(address(lidoARM)); + lidoARM.setMinimumFunds(currentFunds - 100); + + vm.prank(operator); + lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + } + + // // Slow on fork + // function invariant_nocrossed_trading_exact_eth() external { + // uint256 sumBefore = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); + // _dealWETH(address(this), 1 ether); + // lidoARM.swapExactTokensForTokens(weth, steth, weth.balanceOf(address(lidoARM)), 0, address(this)); + // lidoARM.swapExactTokensForTokens(steth, weth, steth.balanceOf(address(lidoARM)), 0, address(this)); + // uint256 sumAfter = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); + // assertGt(sumBefore, sumAfter, "Lost money swapping"); + // } +} diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index c4da811..c82d616 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -88,6 +88,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { // Contracts. oeth = IERC20(resolver.resolve("OETH")); weth = IERC20(resolver.resolve("WETH")); + steth = IERC20(resolver.resolve("STETH")); vault = IOETHVault(resolver.resolve("OETH_VAULT")); } @@ -110,6 +111,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { function _label() internal { vm.label(address(oeth), "OETH"); vm.label(address(weth), "WETH"); + vm.label(address(steth), "stETH"); vm.label(address(vault), "OETH VAULT"); vm.label(address(oethARM), "OETH ARM"); vm.label(address(proxy), "OETH ARM PROXY"); From ccf598696b26a079694eabe97b825ebc5e208940 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 3 Sep 2024 12:46:14 +1000 Subject: [PATCH 009/196] Added a Multi LP version of the Lido ARM --- src/contracts/LidoLiquidityManager.sol | 68 +++++++------------ src/contracts/LidoMultiLpARM.sol | 50 ++++++++++++++ .../{LidoARM.sol => LidoOwnerLpARM.sol} | 11 +-- src/contracts/MultiLP.sol | 2 +- src/contracts/OethARM.sol | 3 +- src/contracts/utils/Initializable.sol | 39 ----------- test/Base.sol | 1 - .../{LidoARM.t.sol => LidoOwnerLpARM.t.sol} | 10 +-- 8 files changed, 88 insertions(+), 96 deletions(-) create mode 100644 src/contracts/LidoMultiLpARM.sol rename src/contracts/{LidoARM.sol => LidoOwnerLpARM.sol} (75%) delete mode 100644 src/contracts/utils/Initializable.sol rename test/fork/mainnet/{LidoARM.t.sol => LidoOwnerLpARM.t.sol} (97%) diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 8a38853..371a4be 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -7,45 +7,21 @@ import {IERC20, IWETH, IStETHWithdrawal} from "./Interfaces.sol"; contract LidoLiquidityManager is OwnableOperable { IERC20 public immutable steth; IWETH public immutable weth; - IStETHWithdrawal public immutable withdrawal; + IStETHWithdrawal public immutable withdrawalQueue; - constructor(address _steth, address _weth, address _stEthWithdrawal) { + uint256 public outstandingEther; + + constructor(address _steth, address _weth, address _lidoWithdrawalQueue) { steth = IERC20(_steth); weth = IWETH(_weth); - withdrawal = IStETHWithdrawal(_stEthWithdrawal); + withdrawalQueue = IStETHWithdrawal(_lidoWithdrawalQueue); } /** * @notice Approve the stETH withdrawal contract. Used for redemption requests. */ function approveStETH() external onlyOperatorOrOwner { - steth.approve(address(withdrawal), type(uint256).max); - } - - /** - * @notice Mint stETH with ETH - */ - function depositETHForStETH(uint256 amount) external onlyOperatorOrOwner { - _depositETHForStETH(amount); - } - - /** - * @notice Mint stETH with WETH - */ - function depositWETHForStETH(uint256 amount) external onlyOperatorOrOwner { - // Unwrap the WETH then deposit the ETH. - weth.withdraw(amount); - _depositETHForStETH(amount); - } - - /** - * @notice Mint stETH with ETH. - * Reference: https://docs.lido.fi/contracts/lido#fallback - */ - function _depositETHForStETH(uint256 amount) internal { - require(address(this).balance >= amount, "OSwap: Insufficient ETH balance"); - (bool success,) = address(steth).call{value: amount}(new bytes(0)); - require(success, "OSwap: ETH transfer failed"); + steth.approve(address(withdrawalQueue), type(uint256).max); } /** @@ -58,15 +34,12 @@ contract LidoLiquidityManager is OwnableOperable { onlyOperatorOrOwner returns (uint256[] memory requestIds) { - requestIds = withdrawal.requestWithdrawals(amounts, address(this)); - } + requestIds = withdrawalQueue.requestWithdrawals(amounts, address(this)); - /** - * @notice Claim the ETH owed from the redemption requests. - * Before calling this method, caller should check on the request NFTs to ensure the withdrawal was processed. - */ - function claimStETHWithdrawalForETH(uint256[] memory requestIds) external onlyOperatorOrOwner { - _claimStETHWithdrawalForETH(requestIds); + // Increase the Ether outstanding from the Lido Withdrawal Queue + for (uint256 i = 0; i < amounts.length; i++) { + outstandingEther += amounts[i]; + } } /** @@ -74,18 +47,25 @@ contract LidoLiquidityManager is OwnableOperable { * Before calling this method, caller should check on the request NFTs to ensure the withdrawal was processed. */ function claimStETHWithdrawalForWETH(uint256[] memory requestIds) external onlyOperatorOrOwner { + uint256 etherBefore = address(this).balance; + // Claim the NFTs for ETH. - _claimStETHWithdrawalForETH(requestIds); + uint256 lastIndex = withdrawalQueue.getLastCheckpointIndex(); + uint256[] memory hintIds = withdrawalQueue.findCheckpointHints(requestIds, 1, lastIndex); + withdrawalQueue.claimWithdrawals(requestIds, hintIds); + + uint256 etherAfter = address(this).balance; + + // Reduce the Ether outstanding from the Lido Withdrawal Queue + outstandingEther -= etherAfter - etherBefore; // Wrap all the received ETH to WETH. - (bool success,) = address(weth).call{value: address(this).balance}(new bytes(0)); + (bool success,) = address(weth).call{value: etherAfter}(new bytes(0)); require(success, "OSwap: ETH transfer failed"); } - function _claimStETHWithdrawalForETH(uint256[] memory requestIds) internal { - uint256 lastIndex = withdrawal.getLastCheckpointIndex(); - uint256[] memory hintIds = withdrawal.findCheckpointHints(requestIds, 1, lastIndex); - withdrawal.claimWithdrawals(requestIds, hintIds); + function _assetsInWithdrawQueue() internal view virtual returns (uint256) { + return outstandingEther; } // This method is necessary for receiving the ETH claimed as part of the withdrawal. diff --git a/src/contracts/LidoMultiLpARM.sol b/src/contracts/LidoMultiLpARM.sol new file mode 100644 index 0000000..ff6b361 --- /dev/null +++ b/src/contracts/LidoMultiLpARM.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import {AbstractARM} from "./AbstractARM.sol"; +import {FixedPriceARM} from "./FixedPriceARM.sol"; +import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; +import {MultiLP} from "./MultiLP.sol"; + +contract LidoMultiLpARM is Initializable, MultiLP, FixedPriceARM, LidoLiquidityManager { + /// @param _stEth The address of the stETH token + /// @param _weth The address of the WETH token + /// @param _lidoWithdrawalQueue The address of the stETH Withdrawal contract + constructor(address _stEth, address _weth, address _lidoWithdrawalQueue) + AbstractARM(_stEth, _weth) + MultiLP(_weth) + FixedPriceARM() + LidoLiquidityManager(_stEth, _weth, _lidoWithdrawalQueue) + {} + + /// @notice Initialize the contract. + /// @param _name The name of the liquidity provider (LP) token. + /// @param _symbol The symbol of the liquidity provider (LP) token. + /// @param _operator The address of the account that can request and claim OETH withdrawals. + function initialize(string calldata _name, string calldata _symbol, address _operator) external initializer { + _initialize(_name, _symbol); + _setOperator(_operator); + } + + /** + * @notice Calculate transfer amount for outToken. + * 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. + */ + function _calcTransferAmount(address outToken, uint256 amount) + internal + view + override + returns (uint256 transferAmount) + { + // Add 2 wei if transferring stETH + transferAmount = outToken == address(token0) ? amount + 2 : amount; + } + + function _assetsInWithdrawQueue() internal view override(LidoLiquidityManager, MultiLP) returns (uint256) { + return LidoLiquidityManager._assetsInWithdrawQueue(); + } +} diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoOwnerLpARM.sol similarity index 75% rename from src/contracts/LidoARM.sol rename to src/contracts/LidoOwnerLpARM.sol index ea505d9..b56b8eb 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoOwnerLpARM.sol @@ -1,20 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + import {AbstractARM} from "./AbstractARM.sol"; import {FixedPriceARM} from "./FixedPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {OwnerLP} from "./OwnerLP.sol"; -import {Initializable} from "./utils/Initializable.sol"; -contract LidoARM is Initializable, OwnerLP, FixedPriceARM, LidoLiquidityManager { +contract LidoOwnerLpARM is Initializable, OwnerLP, FixedPriceARM, LidoLiquidityManager { /// @param _stEth The address of the stETH token /// @param _weth The address of the WETH token - /// @param _stEthWithdrawal The address of the stETH Withdrawal contract - constructor(address _stEth, address _weth, address _stEthWithdrawal) + /// @param _lidoWithdrawalQueue The address of the Lido Withdrawal Queue contract + constructor(address _stEth, address _weth, address _lidoWithdrawalQueue) AbstractARM(_stEth, _weth) FixedPriceARM() - LidoLiquidityManager(_stEth, _weth, _stEthWithdrawal) + LidoLiquidityManager(_stEth, _weth, _lidoWithdrawalQueue) {} /// @notice Initialize the contract. diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 7d08e07..e818ec5 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -52,7 +52,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { liquidityToken = _liquidityToken; } - function _initialize(string calldata _name, string calldata _symbol) external { + function _initialize(string calldata _name, string calldata _symbol) internal { __ERC20_init(_name, _symbol); } diff --git a/src/contracts/OethARM.sol b/src/contracts/OethARM.sol index 540bce4..6198ddd 100644 --- a/src/contracts/OethARM.sol +++ b/src/contracts/OethARM.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + import {AbstractARM} from "./AbstractARM.sol"; import {PeggedARM} from "./PeggedARM.sol"; import {OwnerLP} from "./OwnerLP.sol"; import {OethLiquidityManager} from "./OethLiquidityManager.sol"; -import {Initializable} from "./utils/Initializable.sol"; contract OethARM is Initializable, OwnerLP, PeggedARM, OethLiquidityManager { /// @param _oeth The address of the OETH token that is being swapped into this contract. diff --git a/src/contracts/utils/Initializable.sol b/src/contracts/utils/Initializable.sol deleted file mode 100644 index efb2d37..0000000 --- a/src/contracts/utils/Initializable.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -/** - * @title Base contract any contracts that need to initialize state after deployment. - * @author Origin Protocol Inc - */ -abstract contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to protect an initializer function from being invoked twice. - */ - modifier initializer() { - require(initializing || !initialized, "Initializable: contract is already initialized"); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - uint256[50] private gap; -} diff --git a/test/Base.sol b/test/Base.sol index 0625ce5..fcf33ab 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -7,7 +7,6 @@ import {Test} from "forge-std/Test.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; -import {LidoARM} from "contracts/LidoARM.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; diff --git a/test/fork/mainnet/LidoARM.t.sol b/test/fork/mainnet/LidoOwnerLpARM.t.sol similarity index 97% rename from test/fork/mainnet/LidoARM.t.sol rename to test/fork/mainnet/LidoOwnerLpARM.t.sol index a1488e7..8c4da2a 100644 --- a/test/fork/mainnet/LidoARM.t.sol +++ b/test/fork/mainnet/LidoOwnerLpARM.t.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.23; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "contracts/Interfaces.sol"; -import {LidoARM} from "contracts/LidoARM.sol"; +import {LidoOwnerLpARM} from "contracts/LidoOwnerLpARM.sol"; import {Proxy} from "contracts/Proxy.sol"; import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; contract Fork_Concrete_LidoARM_Test is Fork_Shared_Test_ { Proxy public lidoProxy; - LidoARM public lidoARM; + LidoOwnerLpARM public lidoARM; IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); // Account for stETH rounding errors. @@ -22,13 +22,13 @@ contract Fork_Concrete_LidoARM_Test is Fork_Shared_Test_ { super.setUp(); address lidoWithdrawal = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; - LidoARM lidoImpl = new LidoARM(address(weth), address(steth), lidoWithdrawal); + LidoOwnerLpARM lidoImpl = new LidoOwnerLpARM(address(weth), address(steth), lidoWithdrawal); lidoProxy = new Proxy(); - // Initialize Proxy with LidoARM implementation. + // Initialize Proxy with LidoOwnerLpARM implementation. bytes memory data = abi.encodeWithSignature("initialize(address)", operator); lidoProxy.initialize(address(lidoImpl), address(this), data); - lidoARM = LidoARM(payable(address(lidoProxy))); + lidoARM = LidoOwnerLpARM(payable(address(lidoProxy))); _dealWETH(address(lidoARM), 100 ether); _dealStETH(address(lidoARM), 100 ether); From 550610c01819db2dabddf24e34757e316b3b94d4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 3 Sep 2024 12:54:40 +1000 Subject: [PATCH 010/196] Updated diagrams --- docs/LidoARMHierarchy.svg | 193 --------------- docs/LidoARMStorage.svg | 69 ------ docs/LidoMultiLpARMHierarchy.svg | 113 +++++++++ docs/LidoMultiLpARMSquashed.svg | 88 +++++++ docs/LidoOwnerLpARMHierarchy.svg | 113 +++++++++ ...quashed.svg => LidoOwnerLpARMSquashed.svg} | 125 +++++----- docs/OEthARMHierarchy.svg | 220 +++++++----------- docs/ProxyHierarchy.svg | 12 +- docs/generate.sh | 26 ++- src/contracts/README.md | 26 ++- 10 files changed, 496 insertions(+), 489 deletions(-) delete mode 100644 docs/LidoARMHierarchy.svg delete mode 100644 docs/LidoARMStorage.svg create mode 100644 docs/LidoMultiLpARMHierarchy.svg create mode 100644 docs/LidoMultiLpARMSquashed.svg create mode 100644 docs/LidoOwnerLpARMHierarchy.svg rename docs/{LidoARMSquashed.svg => LidoOwnerLpARMSquashed.svg} (74%) diff --git a/docs/LidoARMHierarchy.svg b/docs/LidoARMHierarchy.svg deleted file mode 100644 index 169cf5c..0000000 --- a/docs/LidoARMHierarchy.svg +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - -UmlClassDiagram - - - -0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol - - - -2 - -<<Interface>> -IERC20 -../src/contracts/Interfaces.sol - - - -0->2 - - - - - -19 - -OwnableOperable -../src/contracts/OwnableOperable.sol - - - -0->19 - - - - - -1 - -<<Abstract>> -FixedPriceARM -../src/contracts/FixedPriceARM.sol - - - -1->0 - - - - - -1->2 - - - - - -8 - -<<Interface>> -IWETH -../src/contracts/Interfaces.sol - - - -8->2 - - - - - -9 - -<<Interface>> -IStETHWithdrawal -../src/contracts/Interfaces.sol - - - -11 - -LidoARM -../src/contracts/LidoARM.sol - - - -11->1 - - - - - -12 - -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol - - - -11->12 - - - - - -20 - -<<Abstract>> -OwnerLP -../src/contracts/OwnerLP.sol - - - -11->20 - - - - - -30 - -<<Abstract>> -Initializable -../src/contracts/utils/Initializable.sol - - - -11->30 - - - - - -12->2 - - - - - -12->8 - - - - - -12->9 - - - - - -12->19 - - - - - -18 - -Ownable -../src/contracts/Ownable.sol - - - -19->18 - - - - - -20->2 - - - - - -20->18 - - - - - diff --git a/docs/LidoARMStorage.svg b/docs/LidoARMStorage.svg deleted file mode 100644 index ac22372..0000000 --- a/docs/LidoARMStorage.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - -StorageDiagram - - - -1 - -LidoARM <<Contract>> - -slot - -0 - -1-50 - -51 - -52-101 - -102 - -103 - -104 - -0x360..bbd - -0xb53..104 - -type: <inherited contract>.variable (bytes) - -unallocated (30) - -bool: Initializable.initializing (1) - -bool: Initializable.initialized (1) - -uint256[50]: Initializable.gap (1600) - -unallocated (12) - -address: OwnableOperable.operator (20) - -uint256[50]: OwnableOperable._gap (1600) - -uint256: FixedPriceARM.traderate0 (32) - -uint256: FixedPriceARM.traderate1 (32) - -uint256: FixedPriceARM.minimumFunds (32) - -unallocated (12) - -address: eip1967.proxy.implementation (20) - -unallocated (12) - -address: eip1967.proxy.admin (20) - - - diff --git a/docs/LidoMultiLpARMHierarchy.svg b/docs/LidoMultiLpARMHierarchy.svg new file mode 100644 index 0000000..a3e9c75 --- /dev/null +++ b/docs/LidoMultiLpARMHierarchy.svg @@ -0,0 +1,113 @@ + + + + + + +UmlClassDiagram + + + +0 + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol + + + +20 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + + +0->20 + + + + + +1 + +<<Abstract>> +FixedPriceARM +../src/contracts/FixedPriceARM.sol + + + +1->0 + + + + + +11 + +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol + + + +11->20 + + + + + +12 + +LidoMultiLpARM +../src/contracts/LidoMultiLpARM.sol + + + +12->1 + + + + + +12->11 + + + + + +14 + +<<Abstract>> +MultiLP +../src/contracts/MultiLP.sol + + + +12->14 + + + + + +14->0 + + + + + +19 + +Ownable +../src/contracts/Ownable.sol + + + +20->19 + + + + + diff --git a/docs/LidoMultiLpARMSquashed.svg b/docs/LidoMultiLpARMSquashed.svg new file mode 100644 index 0000000..d559c58 --- /dev/null +++ b/docs/LidoMultiLpARMSquashed.svg @@ -0,0 +1,88 @@ + + + + + + +UmlClassDiagram + + + +12 + +LidoMultiLpARM +../src/contracts/LidoMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   liquidityToken: address <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initialize(_name: string, _symbol: string) <<MultiLP>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoMultiLpARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> +    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address) <<initializer>> <<LidoMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> +    constructor(_liquidityToken: address) <<MultiLP>> +    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> +    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> +    totalAssets(): uint256 <<MultiLP>> +    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoMultiLpARM>> + + + diff --git a/docs/LidoOwnerLpARMHierarchy.svg b/docs/LidoOwnerLpARMHierarchy.svg new file mode 100644 index 0000000..84952db --- /dev/null +++ b/docs/LidoOwnerLpARMHierarchy.svg @@ -0,0 +1,113 @@ + + + + + + +UmlClassDiagram + + + +0 + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol + + + +20 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + + +0->20 + + + + + +1 + +<<Abstract>> +FixedPriceARM +../src/contracts/FixedPriceARM.sol + + + +1->0 + + + + + +11 + +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol + + + +11->20 + + + + + +13 + +LidoOwnerLpARM +../src/contracts/LidoOwnerLpARM.sol + + + +13->1 + + + + + +13->11 + + + + + +21 + +<<Abstract>> +OwnerLP +../src/contracts/OwnerLP.sol + + + +13->21 + + + + + +19 + +Ownable +../src/contracts/Ownable.sol + + + +20->19 + + + + + +21->19 + + + + + diff --git a/docs/LidoARMSquashed.svg b/docs/LidoOwnerLpARMSquashed.svg similarity index 74% rename from docs/LidoARMSquashed.svg rename to docs/LidoOwnerLpARMSquashed.svg index 7427f42..32e982f 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoOwnerLpARMSquashed.svg @@ -4,78 +4,71 @@ - - + + UmlClassDiagram - - + + -11 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   gap: uint256[50] <<Initializable>> -   _gap: uint256[50] <<OwnableOperable>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawal: IStETHWithdrawal <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -    _depositETHForStETH(amount: uint256) <<LidoLiquidityManager>> -    _claimStETHWithdrawalForETH(requestIds: uint256[]) <<LidoLiquidityManager>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    owner(): address <<Ownable>> -    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> -    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>> -    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>> <<FixedPriceARM>> -    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    depositETHForStETH(amount: uint256) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    depositWETHForStETH(amount: uint256) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_operator: address) <<initializer>> <<LidoARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> -    <<modifier>> initializer() <<Initializable>> +13 + +LidoOwnerLpARM +../src/contracts/LidoOwnerLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoOwnerLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +    _assetsInWithdrawQueue(): uint256 <<LidoLiquidityManager>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    owner(): address <<Ownable>> +    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>> +    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>> <<FixedPriceARM>> +    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_operator: address) <<initializer>> <<LidoOwnerLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>>    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> -    constructor(_stEth: address, _weth: address, _stEthWithdrawal: address) <<LidoARM>> +    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoOwnerLpARM>> diff --git a/docs/OEthARMHierarchy.svg b/docs/OEthARMHierarchy.svg index 85db68a..d31a4e6 100644 --- a/docs/OEthARMHierarchy.svg +++ b/docs/OEthARMHierarchy.svg @@ -4,170 +4,110 @@ - + UmlClassDiagram - + 0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol - - -2 - -<<Interface>> -IERC20 -../src/contracts/Interfaces.sol - - - -0->2 - - - - - -19 - -OwnableOperable -../src/contracts/OwnableOperable.sol + + +20 + +OwnableOperable +../src/contracts/OwnableOperable.sol - + -0->19 - - - - - -5 - -<<Interface>> -IOETHVault -../src/contracts/Interfaces.sol - - - -16 - -OethARM -../src/contracts/OethARM.sol +0->20 + + - + 17 - -OethLiquidityManager -../src/contracts/OethLiquidityManager.sol - - - -16->17 - - + +OethARM +../src/contracts/OethARM.sol - - -20 - -<<Abstract>> -OwnerLP -../src/contracts/OwnerLP.sol + + +18 + +OethLiquidityManager +../src/contracts/OethLiquidityManager.sol - + -16->20 - - +17->18 + + - + 21 - -<<Abstract>> -PeggedARM -../src/contracts/PeggedARM.sol + +<<Abstract>> +OwnerLP +../src/contracts/OwnerLP.sol - - -16->21 - - - - - -30 - -<<Abstract>> -Initializable -../src/contracts/utils/Initializable.sol - - + + +17->21 + + + + + +22 + +<<Abstract>> +PeggedARM +../src/contracts/PeggedARM.sol + + -16->30 - - +17->22 + + - - -17->2 - - + + +18->20 + + + + + +19 + +Ownable +../src/contracts/Ownable.sol - - -17->5 - - + + +20->19 + + - + -17->19 - - +21->19 + + - - -18 - -Ownable -../src/contracts/Ownable.sol - - - -19->18 - - - - - -20->2 - - - - - -20->18 - - - - - -21->0 - - - - - -21->2 - - + + +22->0 + + diff --git a/docs/ProxyHierarchy.svg b/docs/ProxyHierarchy.svg index 1454642..e224dc9 100644 --- a/docs/ProxyHierarchy.svg +++ b/docs/ProxyHierarchy.svg @@ -9,23 +9,23 @@ UmlClassDiagram - + -9 +19 Ownable ../src/contracts/Ownable.sol - + -12 +23 Proxy ../src/contracts/Proxy.sol - + -12->9 +23->19 diff --git a/docs/generate.sh b/docs/generate.sh index aeb1b2a..1714ccf 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -1,20 +1,28 @@ -sol2uml ../src/contracts -v -hv -hf -he -hs -hl -b OethARM -o OethARMHierarchy.svg + +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b Proxy -o ProxyHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b Proxy -o ProxySquashed.svg +sol2uml storage ../src/contracts -c Proxy -o ProxyStorage.svg \ + -sn eip1967.proxy.implementation,eip1967.proxy.admin \ + -st address,address + +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b OethARM -o OethARMHierarchy.svg sol2uml ../src/contracts -s -d 0 -b OethARM -o OethARMSquashed.svg sol2uml storage ../src/contracts -c OethARM -o OethARMStorage.svg \ -sn eip1967.proxy.implementation,eip1967.proxy.admin \ -st address,address \ --hideExpand gap,_gap -sol2uml ../src/contracts -v -hv -hf -he -hs -hl -b Proxy -o ProxyHierarchy.svg -sol2uml ../src/contracts -s -d 0 -b Proxy -o ProxySquashed.svg -sol2uml storage ../src/contracts -c Proxy -o ProxyStorage.svg \ +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoOwnerLpARM -o LidoOwnerLpARMHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b LidoOwnerLpARM -o LidoOwnerLpARMSquashed.svg +sol2uml storage ../src/contracts,../lib -c LidoOwnerLpARM -o LidoOwnerLpARMStorage.svg \ -sn eip1967.proxy.implementation,eip1967.proxy.admin \ - -st address,address - -sol2uml ../src/contracts -v -hv -hf -he -hs -hl -b LidoARM -o LidoARMHierarchy.svg -sol2uml ../src/contracts -s -d 0 -b LidoARM -o LidoARMSquashed.svg -sol2uml storage ../src/contracts -c LidoARM -o LidoARMStorage.svg \ + -st address,address \ + --hideExpand gap,_gap + +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoMultiLpARM -o LidoMultiLpARMHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b LidoMultiLpARM -o LidoMultiLpARMSquashed.svg +sol2uml storage ../src/contracts,../lib -c LidoMultiLpARM -o LidoMultiLpARMStorage.svg \ -sn eip1967.proxy.implementation,eip1967.proxy.admin \ -st address,address \ --hideExpand gap,_gap diff --git a/src/contracts/README.md b/src/contracts/README.md index 6682dda..020ee09 100644 --- a/src/contracts/README.md +++ b/src/contracts/README.md @@ -1,3 +1,17 @@ +## Proxy + +### Hierarchy + +![Proxy Hierarchy](../../docs/ProxyHierarchy.svg) + +## Proxy Squashed + +![Proxy Squashed](../../docs/ProxySquashed.svg) + +## Proxy Storage + +![Proxy Storage](../../docs/ProxyStorage.svg) + ## OETH ARM ### Hierarchy @@ -12,16 +26,16 @@ ![OETH ARM Storage](../../docs/OEthARMStorage.svg) -## Proxy +## Lido ARM ### Hierarchy -![Proxy Hierarchy](../../docs/ProxyHierarchy.svg) +![Lido ARM Hierarchy](../../docs/LidoMultiLpARMHierarchy.svg) -## Proxy Squashed +## OETH ARM Squashed -![Proxy Squashed](../../docs/ProxySquashed.svg) +![Lido ARM Squashed](../../docs/LidoMultiLpARMSquashed.svg) -## Proxy Storage +## OETH ARM Storage -![Proxy Storage](../../docs/ProxyStorage.svg) +![Lido ARM Storage](../../docs/LidoMultiLpARMStorage.svg) From 680e7ec4ef194fe69ff65c9e231d4f26beb92937 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 3 Sep 2024 17:00:07 +1000 Subject: [PATCH 011/196] Added convertToShares and convertToAssets to MultiLP Added donation attack protection to MultiLP --- docs/LidoMultiLpARMSquashed.svg | 152 ++++++++++++++++---------------- src/contracts/MultiLP.sol | 18 ++++ 2 files changed, 96 insertions(+), 74 deletions(-) diff --git a/docs/LidoMultiLpARMSquashed.svg b/docs/LidoMultiLpARMSquashed.svg index d559c58..dc19e2c 100644 --- a/docs/LidoMultiLpARMSquashed.svg +++ b/docs/LidoMultiLpARMSquashed.svg @@ -4,84 +4,88 @@ - - + + UmlClassDiagram - + 12 - -LidoMultiLpARM -../src/contracts/LidoMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   liquidityToken: address <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initialize(_name: string, _symbol: string) <<MultiLP>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoMultiLpARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> -    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address) <<initializer>> <<LidoMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> -    constructor(_liquidityToken: address) <<MultiLP>> -    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> -    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> -    totalAssets(): uint256 <<MultiLP>> + +LidoMultiLpARM +../src/contracts/LidoMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityToken: address <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initialize(_name: string, _symbol: string) <<MultiLP>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoMultiLpARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> +    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address) <<initializer>> <<LidoMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> +    constructor(_liquidityToken: address) <<MultiLP>> +    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> +    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> +    totalAssets(): uint256 <<MultiLP>> +    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>> +    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>>    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoMultiLpARM>> diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index e818ec5..7f7a326 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -8,6 +8,8 @@ import {AbstractARM} from "./AbstractARM.sol"; abstract contract MultiLP is AbstractARM, ERC20Upgradeable { uint256 public constant CLAIM_DELAY = 10 minutes; + uint256 public constant MIN_TOTAL_SUPPLY = 1e12; + address public constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; address public immutable liquidityToken; @@ -54,6 +56,13 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { function _initialize(string calldata _name, string calldata _symbol) internal { __ERC20_init(_name, _symbol); + + // Transfer a small bit of liquidity from the intializer to this contract + IERC20(liquidityToken).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 + // This avoids donation attacks when there are no assets in the ARM contract + _mint(DEAD_ACCOUNT, MIN_TOTAL_SUPPLY); } function previewDeposit(uint256 assets) public view returns (uint256 shares) { @@ -165,5 +174,14 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { return token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue(); } + function convertToShares(uint256 assets) public view returns (uint256 shares) { + uint256 _totalAssets = totalAssets(); + shares = (_totalAssets == 0) ? assets : (assets * totalSupply()) / _totalAssets; + } + + function convertToAssets(uint256 shares) public view returns (uint256 assets) { + assets = (shares * totalAssets()) / totalSupply(); + } + function _assetsInWithdrawQueue() internal view virtual returns (uint256); } From 8da3061316b50146e7b4bd5cd5527932aa76ef90 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 3 Sep 2024 18:26:12 +1000 Subject: [PATCH 012/196] Added performance fee to MultiLP --- src/contracts/LidoMultiLpARM.sol | 14 ++++++- src/contracts/MultiLP.sol | 65 +++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/contracts/LidoMultiLpARM.sol b/src/contracts/LidoMultiLpARM.sol index ff6b361..477ef74 100644 --- a/src/contracts/LidoMultiLpARM.sol +++ b/src/contracts/LidoMultiLpARM.sol @@ -23,8 +23,18 @@ contract LidoMultiLpARM is Initializable, MultiLP, FixedPriceARM, LidoLiquidityM /// @param _name The name of the liquidity provider (LP) token. /// @param _symbol The symbol of the liquidity provider (LP) token. /// @param _operator The address of the account that can request and claim OETH withdrawals. - function initialize(string calldata _name, string calldata _symbol, address _operator) external initializer { - _initialize(_name, _symbol); + /// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent). + /// 10,000 = 100% performance fee + /// 500 = 5% performance fee + /// @param _feeCollector The account that receives the performance fee as shares + function initialize( + string calldata _name, + string calldata _symbol, + address _operator, + uint256 _fee, + address _feeCollector + ) external initializer { + _initialize(_name, _symbol, _fee, _feeCollector); _setOperator(_operator); } diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 7f7a326..5fa34a4 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -9,10 +9,18 @@ import {AbstractARM} from "./AbstractARM.sol"; abstract contract MultiLP is AbstractARM, ERC20Upgradeable { uint256 public constant CLAIM_DELAY = 10 minutes; uint256 public constant MIN_TOTAL_SUPPLY = 1e12; + uint256 public constant MAX_FEE = 10000; // 100% address public constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; address public immutable liquidityToken; + /// @notice The account that receives the performance fee as shares + address public feeCollector; + /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent) + /// 10,000 = 100% performance fee + /// 500 = 5% performance fee + uint256 public fee; + struct WithdrawalQueueMetadata { // cumulative total of all withdrawal requests included the ones that have already been claimed uint128 queued; @@ -54,9 +62,14 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { liquidityToken = _liquidityToken; } - function _initialize(string calldata _name, string calldata _symbol) internal { + function _initialize(string calldata _name, string calldata _symbol, uint256 _fee, address _feeCollector) + internal + { __ERC20_init(_name, _symbol); + _setFee(_fee); + _setFeeCollector(_feeCollector); + // Transfer a small bit of liquidity from the intializer to this contract IERC20(liquidityToken).transferFrom(msg.sender, address(this), MIN_TOTAL_SUPPLY); @@ -66,25 +79,44 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { } function previewDeposit(uint256 assets) public view returns (uint256 shares) { - uint256 _totalAssets = totalAssets(); - shares = (_totalAssets == 0) ? assets : (assets * totalSupply()) / _totalAssets; + shares = convertToShares(assets) * (MAX_FEE - fee) / MAX_FEE; } function deposit(uint256 assets) external returns (uint256 shares) { - shares = previewDeposit(assets); + uint256 totalSharesMinted = convertToShares(assets); + uint256 depositorShares = totalSharesMinted * (MAX_FEE - fee) / MAX_FEE; + uint256 feeCollectorShares = totalSharesMinted - depositorShares; // Transfer the liquidity token from the sender to this contract IERC20(liquidityToken).transferFrom(msg.sender, address(this), assets); // mint shares - _mint(msg.sender, shares); + _mint(msg.sender, depositorShares); + _mint(feeCollector, feeCollectorShares); + + return depositorShares; } function previewRedeem(uint256 shares) public view returns (uint256 assets) { - assets = (shares * totalAssets()) / totalSupply(); + assets = convertToAssets(shares); } + /// @notice Request to redeem liquidity provider shares for liquidity assets + /// @param shares The amount of shares the redeemer wants to burn for assets function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { + // burn redeemer's shares + _burn(msg.sender, shares); + // if not the fee collector, burn fee collector's shares + if (msg.sender != feeCollector) { + // Burn fee collector's shares + // Total shares to burn = redeemer's shares / (MAX_FEE - fee) + // Fee collector's shares = total shares to burn * fee + // So Fee Collector's shares = redeemer's shares * fee / (MAX_FEE - fee) + uint256 feeCollectorShares = shares * fee / (MAX_FEE - fee); + _burn(feeCollector, feeCollectorShares); + } + + // Calculate the amount of assets to transfer to the redeemer assets = previewRedeem(shares); requestId = withdrawalQueueMetadata.nextWithdrawalIndex; @@ -101,9 +133,6 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { queued: SafeCast.toUint128(queued) }); - // burn shares - _burn(msg.sender, shares); - emit RedeemRequested(msg.sender, requestId, assets, queued); } @@ -184,4 +213,22 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { } function _assetsInWithdrawQueue() internal view virtual returns (uint256); + + function setFee(uint256 _fee) external onlyOwner { + _setFee(_fee); + } + + function setFeeCollector(address _feeCollector) external onlyOwner { + _setFeeCollector(_feeCollector); + } + + function _setFee(uint256 _fee) internal { + require(_fee <= MAX_FEE, "ARM: fee too high"); + fee = _fee; + } + + function _setFeeCollector(address _feeCollector) internal { + require(_feeCollector != address(0), "ARM: invalid fee collector"); + feeCollector = _feeCollector; + } } From ce04236b3e8217a00b34b77484822a5bf207a10f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 3 Sep 2024 21:24:07 +1000 Subject: [PATCH 013/196] Changed implementation of the performance fee --- docs/LidoMultiLpARMSquashed.svg | 145 +++++++++++++++++-------------- docs/LidoOwnerLpARMSquashed.svg | 65 +++++++------- docs/OEthARMSquashed.svg | 67 +++++++------- src/contracts/AbstractARM.sol | 11 +++ src/contracts/LidoMultiLpARM.sol | 4 + src/contracts/MultiLP.sol | 46 ++++++---- 6 files changed, 186 insertions(+), 152 deletions(-) diff --git a/docs/LidoMultiLpARMSquashed.svg b/docs/LidoMultiLpARMSquashed.svg index dc19e2c..8bd5de5 100644 --- a/docs/LidoMultiLpARMSquashed.svg +++ b/docs/LidoMultiLpARMSquashed.svg @@ -4,77 +4,88 @@ - - + + UmlClassDiagram - + 12 - -LidoMultiLpARM -../src/contracts/LidoMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityToken: address <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initialize(_name: string, _symbol: string) <<MultiLP>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoMultiLpARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> -    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address) <<initializer>> <<LidoMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> + +LidoMultiLpARM +../src/contracts/LidoMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   MAX_FEE: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityToken: address <<MultiLP>> +   feeCollector: address <<MultiLP>> +   fee: uint256 <<MultiLP>> +   feesCollected: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoMultiLpARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoMultiLpARM>> +    _setFee(_fee: uint256) <<MultiLP>> +    _setFeeCollector(_feeCollector: address) <<MultiLP>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<MultiLP>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<MultiLP>> +    collectFees(): (fees: uint256) <<MultiLP>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> +    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<MultiLP>>    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> diff --git a/docs/LidoOwnerLpARMSquashed.svg b/docs/LidoOwnerLpARMSquashed.svg index 32e982f..81965ae 100644 --- a/docs/LidoOwnerLpARMSquashed.svg +++ b/docs/LidoOwnerLpARMSquashed.svg @@ -4,42 +4,43 @@ - - + + UmlClassDiagram - + 13 - -LidoOwnerLpARM -../src/contracts/LidoOwnerLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> + +LidoOwnerLpARM +../src/contracts/LidoOwnerLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _accountFee(amountIn: uint256, amountOut: uint256) <<AbstractARM>>    _inDeadline(deadline: uint256) <<AbstractARM>>    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoOwnerLpARM>>    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> diff --git a/docs/OEthARMSquashed.svg b/docs/OEthARMSquashed.svg index fbf48d5..4b578ac 100644 --- a/docs/OEthARMSquashed.svg +++ b/docs/OEthARMSquashed.svg @@ -4,22 +4,19 @@ - - + + UmlClassDiagram - - + + -16 - -OethARM -../src/contracts/OethARM.sol - -Private: -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   gap: uint256[50] <<Initializable>> +17 + +OethARM +../src/contracts/OethARM.sol + +Private:   _gap: uint256[50] <<OwnableOperable>> Internal:   OWNER_SLOT: bytes32 <<Ownable>> @@ -38,27 +35,27 @@    _setOperator(newOperator: address) <<OwnableOperable>>    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<PeggedARM>>    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<PeggedARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _swap(inToken: IERC20, outToken: IERC20, amount: uint256, to: address): uint256 <<PeggedARM>> -    _approvals() <<OethLiquidityManager>> -External: -    owner(): address <<Ownable>> -    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> -    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>> -    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>> -    approvals() <<onlyOwner>> <<OethLiquidityManager>> -    requestWithdrawal(amount: uint256): (requestId: uint256, queued: uint256) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> -    claimWithdrawal(requestId: uint256) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> -    claimWithdrawals(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> -    initialize(_operator: address) <<initializer>> <<OethARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<modifier>> initializer() <<Initializable>> +    _accountFee(amountIn: uint256, amountOut: uint256) <<AbstractARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _swap(inToken: IERC20, outToken: IERC20, amount: uint256, to: address): uint256 <<PeggedARM>> +    _approvals() <<OethLiquidityManager>> +External: +    owner(): address <<Ownable>> +    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>> +    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>> +    approvals() <<onlyOwner>> <<OethLiquidityManager>> +    requestWithdrawal(amount: uint256): (requestId: uint256, queued: uint256) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> +    claimWithdrawal(requestId: uint256) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> +    claimWithdrawals(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<OethLiquidityManager>> +    initialize(_operator: address) <<initializer>> <<OethARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>> diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 8395087..8194491 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -44,6 +44,8 @@ abstract contract AbstractARM is OwnableOperable { ) external virtual { uint256 amountOut = _swapExactTokensForTokens(inToken, outToken, amountIn, to); require(amountOut >= amountOutMin, "ARM: Insufficient output amount"); + + _accountFee(amountIn, amountOut); } /** @@ -76,6 +78,8 @@ abstract contract AbstractARM is OwnableOperable { require(amountOut >= amountOutMin, "ARM: Insufficient output amount"); + _accountFee(amountIn, amountOut); + amounts = new uint256[](2); amounts[0] = amountIn; amounts[1] = amountOut; @@ -102,6 +106,8 @@ abstract contract AbstractARM is OwnableOperable { uint256 amountIn = _swapTokensForExactTokens(inToken, outToken, amountOut, to); require(amountIn <= amountInMax, "ARM: Excess input amount"); + + _accountFee(amountIn, amountOut); } /** @@ -134,6 +140,8 @@ abstract contract AbstractARM is OwnableOperable { require(amountIn <= amountInMax, "ARM: Excess input amount"); + _accountFee(amountIn, amountOut); + amounts = new uint256[](2); amounts[0] = amountIn; amounts[1] = amountOut; @@ -149,6 +157,9 @@ abstract contract AbstractARM is OwnableOperable { virtual returns (uint256 amountIn); + /// @dev Default to no fee being collected + function _accountFee(uint256 amountIn, uint256 amountOut) internal virtual {} + function _inDeadline(uint256 deadline) internal view { require(deadline >= block.timestamp, "ARM: Deadline expired"); } diff --git a/src/contracts/LidoMultiLpARM.sol b/src/contracts/LidoMultiLpARM.sol index 477ef74..486ebb7 100644 --- a/src/contracts/LidoMultiLpARM.sol +++ b/src/contracts/LidoMultiLpARM.sol @@ -57,4 +57,8 @@ contract LidoMultiLpARM is Initializable, MultiLP, FixedPriceARM, LidoLiquidityM function _assetsInWithdrawQueue() internal view override(LidoLiquidityManager, MultiLP) returns (uint256) { return LidoLiquidityManager._assetsInWithdrawQueue(); } + + function _accountFee(uint256 amountIn, uint256 amountOut) internal override(MultiLP, AbstractARM) { + MultiLP._accountFee(amountIn, amountOut); + } } diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 5fa34a4..ec01c59 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -20,6 +20,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { /// 10,000 = 100% performance fee /// 500 = 5% performance fee uint256 public fee; + uint256 public feesCollected; struct WithdrawalQueueMetadata { // cumulative total of all withdrawal requests included the ones that have already been claimed @@ -56,6 +57,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { event RedeemRequested(address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued); event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); + event FeeCollected(address feeCollector, uint256 fee); constructor(address _liquidityToken) { require(_liquidityToken == address(token0) || _liquidityToken == address(token1), "invalid liquidity token"); @@ -79,22 +81,17 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { } function previewDeposit(uint256 assets) public view returns (uint256 shares) { - shares = convertToShares(assets) * (MAX_FEE - fee) / MAX_FEE; + shares = convertToShares(assets); } function deposit(uint256 assets) external returns (uint256 shares) { - uint256 totalSharesMinted = convertToShares(assets); - uint256 depositorShares = totalSharesMinted * (MAX_FEE - fee) / MAX_FEE; - uint256 feeCollectorShares = totalSharesMinted - depositorShares; + shares = convertToShares(assets); // Transfer the liquidity token from the sender to this contract IERC20(liquidityToken).transferFrom(msg.sender, address(this), assets); // mint shares - _mint(msg.sender, depositorShares); - _mint(feeCollector, feeCollectorShares); - - return depositorShares; + _mint(msg.sender, shares); } function previewRedeem(uint256 shares) public view returns (uint256 assets) { @@ -106,15 +103,6 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { // burn redeemer's shares _burn(msg.sender, shares); - // if not the fee collector, burn fee collector's shares - if (msg.sender != feeCollector) { - // Burn fee collector's shares - // Total shares to burn = redeemer's shares / (MAX_FEE - fee) - // Fee collector's shares = total shares to burn * fee - // So Fee Collector's shares = redeemer's shares * fee / (MAX_FEE - fee) - uint256 feeCollectorShares = shares * fee / (MAX_FEE - fee); - _burn(feeCollector, feeCollectorShares); - } // Calculate the amount of assets to transfer to the redeemer assets = previewRedeem(shares); @@ -200,7 +188,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { function totalAssets() public view returns (uint256) { // valuing both assets 1:1 - return token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue(); + return + token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue() - feesCollected; } function convertToShares(uint256 assets) public view returns (uint256 shares) { @@ -231,4 +220,25 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { require(_feeCollector != address(0), "ARM: invalid fee collector"); feeCollector = _feeCollector; } + + /// @dev this assumes there is no exchange rate between the two assets. + /// An asset like rETH with WETH will not work with this function. + function _accountFee(uint256 amountIn, uint256 amountOut) internal virtual override { + uint256 discountAmount = amountIn > amountOut ? amountIn - amountOut : amountOut - amountIn; + feesCollected += (discountAmount * fee) / MAX_FEE; + } + + function collectFees() external returns (uint256 fees) { + require(msg.sender == feeCollector, "ARM: not fee collector"); + + fees = feesCollected; + require(fees <= IERC20(liquidityToken).balanceOf(address(this)), "ARM: insufficient liquidity"); + + // Reset fees collected in storage + feesCollected = 0; + + IERC20(liquidityToken).transfer(feeCollector, fees); + + emit FeeCollected(feeCollector, fees); + } } From 04f547c4ede11dd55aa48f649d44c82ce9a44686 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 9 Sep 2024 20:42:35 +1000 Subject: [PATCH 014/196] Add support for multiple tranches --- ... => LidoFixedPriceMultiLpARMHierarchy.svg} | 68 ++--- ...g => LidoFixedPriceMultiLpARMSquashed.svg} | 104 ++++---- docs/LidoMultiPriceMultiLpARMHierarchy.svg | 113 ++++++++ docs/LidoMultiPriceMultiLpARMSquashed.svg | 108 ++++++++ docs/generate.sh | 13 +- ...LpARM.sol => LidoFixedPriceMultiLpARM.sol} | 10 +- src/contracts/LidoMultiPriceMultiLpARM.sol | 72 +++++ src/contracts/MultiLP.sol | 10 +- src/contracts/MultiPriceARM.sol | 248 ++++++++++++++++++ 9 files changed, 656 insertions(+), 90 deletions(-) rename docs/{LidoMultiLpARMHierarchy.svg => LidoFixedPriceMultiLpARMHierarchy.svg} (88%) rename docs/{LidoMultiLpARMSquashed.svg => LidoFixedPriceMultiLpARMSquashed.svg} (89%) create mode 100644 docs/LidoMultiPriceMultiLpARMHierarchy.svg create mode 100644 docs/LidoMultiPriceMultiLpARMSquashed.svg rename src/contracts/{LidoMultiLpARM.sol => LidoFixedPriceMultiLpARM.sol} (89%) create mode 100644 src/contracts/LidoMultiPriceMultiLpARM.sol create mode 100644 src/contracts/MultiPriceARM.sol diff --git a/docs/LidoMultiLpARMHierarchy.svg b/docs/LidoFixedPriceMultiLpARMHierarchy.svg similarity index 88% rename from docs/LidoMultiLpARMHierarchy.svg rename to docs/LidoFixedPriceMultiLpARMHierarchy.svg index a3e9c75..186d7fc 100644 --- a/docs/LidoMultiLpARMHierarchy.svg +++ b/docs/LidoFixedPriceMultiLpARMHierarchy.svg @@ -17,16 +17,16 @@ AbstractARM ../src/contracts/AbstractARM.sol - + -20 +22 OwnableOperable ../src/contracts/OwnableOperable.sol - + -0->20 +0->22 @@ -47,65 +47,65 @@ 11 - -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol - - -11->20 - - + + +11->1 + + 12 - -LidoMultiLpARM -../src/contracts/LidoMultiLpARM.sol + +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol - + -12->1 - - - - - -12->11 +11->12 - + -14 +15 <<Abstract>> MultiLP ../src/contracts/MultiLP.sol - - -12->14 + + +11->15 - + + +12->22 + + + + -14->0 +15->0 - + -19 +21 Ownable ../src/contracts/Ownable.sol - + -20->19 +22->21 diff --git a/docs/LidoMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg similarity index 89% rename from docs/LidoMultiLpARMSquashed.svg rename to docs/LidoFixedPriceMultiLpARMSquashed.svg index 8bd5de5..873839c 100644 --- a/docs/LidoMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,60 +4,62 @@ - - + + UmlClassDiagram - - + + -12 - -LidoMultiLpARM -../src/contracts/LidoMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   MAX_FEE: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityToken: address <<MultiLP>> -   feeCollector: address <<MultiLP>> -   fee: uint256 <<MultiLP>> -   feesCollected: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoMultiLpARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> +11 + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   liquidityToken: address <<MultiLP>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   MAX_FEE: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   feeCollector: address <<MultiLP>> +   fee: uint256 <<MultiLP>> +   feesCollected: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoFixedPriceMultiLpARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> +    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _postRedeemHook(assets: uint256) <<LidoFixedPriceMultiLpARM>>    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoMultiLpARM>> +    _assetsInWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>>    _setFee(_fee: uint256) <<MultiLP>>    _setFeeCollector(_feeCollector: address) <<MultiLP>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiLpARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>>    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> External:    <<payable>> null() <<LidoLiquidityManager>> @@ -79,7 +81,7 @@    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoMultiLpARM>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> Public:    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>>    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> @@ -97,7 +99,7 @@    totalAssets(): uint256 <<MultiLP>>    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>>    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>> -    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoMultiLpARM>> +    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoFixedPriceMultiLpARM>> diff --git a/docs/LidoMultiPriceMultiLpARMHierarchy.svg b/docs/LidoMultiPriceMultiLpARMHierarchy.svg new file mode 100644 index 0000000..e1d5cab --- /dev/null +++ b/docs/LidoMultiPriceMultiLpARMHierarchy.svg @@ -0,0 +1,113 @@ + + + + + + +UmlClassDiagram + + + +0 + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol + + + +22 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + + +0->22 + + + + + +12 + +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol + + + +12->22 + + + + + +13 + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + + + +13->12 + + + + + +15 + +<<Abstract>> +MultiLP +../src/contracts/MultiLP.sol + + + +13->15 + + + + + +18 + +<<Abstract>> +MultiPriceARM +../src/contracts/MultiPriceARM.sol + + + +13->18 + + + + + +15->0 + + + + + +18->0 + + + + + +21 + +Ownable +../src/contracts/Ownable.sol + + + +22->21 + + + + + diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg new file mode 100644 index 0000000..636411b --- /dev/null +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -0,0 +1,108 @@ + + + + + + +UmlClassDiagram + + + +13 + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   liquidityToken: address <<MultiLP>> +   discountToken: address <<MultiPriceARM>> +   liquidityToken: address <<MultiPriceARM>> +   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> +   tranches: uint16[15] <<MultiPriceARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   MAX_FEE: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   feeCollector: address <<MultiLP>> +   fee: uint256 <<MultiLP>> +   feesCollected: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   PRICE_PRECISION: uint256 <<MultiPriceARM>> +   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> +    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoMultiPriceMultiLpARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> +    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _postRedeemHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> +    _setFee(_fee: uint256) <<MultiLP>> +    _setFeeCollector(_feeCollector: address) <<MultiLP>> +    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<MultiLP>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<MultiLP>> +    collectFees(): (fees: uint256) <<MultiLP>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoMultiPriceMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<MultiLP>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_discountToken: address, _liquidityToken: address) <<MultiPriceARM>> +    constructor(_liquidityToken: address) <<MultiLP>> +    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> +    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> +    totalAssets(): uint256 <<MultiLP>> +    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>> +    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>> +    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoMultiPriceMultiLpARM>> + + + diff --git a/docs/generate.sh b/docs/generate.sh index 1714ccf..a7126c4 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -20,9 +20,16 @@ sol2uml storage ../src/contracts,../lib -c LidoOwnerLpARM -o LidoOwnerLpARMStora -st address,address \ --hideExpand gap,_gap -sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoMultiLpARM -o LidoMultiLpARMHierarchy.svg -sol2uml ../src/contracts -s -d 0 -b LidoMultiLpARM -o LidoMultiLpARMSquashed.svg -sol2uml storage ../src/contracts,../lib -c LidoMultiLpARM -o LidoMultiLpARMStorage.svg \ +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMSquashed.svg +sol2uml storage ../src/contracts,../lib -c LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMStorage.svg \ + -sn eip1967.proxy.implementation,eip1967.proxy.admin \ + -st address,address \ + --hideExpand gap,_gap + +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoMultiPriceMultiLpARM -o LidoMultiPriceMultiLpARMHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b LidoMultiPriceMultiLpARM -o LidoMultiPriceMultiLpARMSquashed.svg +sol2uml storage ../src/contracts,../lib -c LidoMultiPriceMultiLpARM -o LidoMultiPriceMultiLpARMStorage.svg \ -sn eip1967.proxy.implementation,eip1967.proxy.admin \ -st address,address \ --hideExpand gap,_gap diff --git a/src/contracts/LidoMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol similarity index 89% rename from src/contracts/LidoMultiLpARM.sol rename to src/contracts/LidoFixedPriceMultiLpARM.sol index 486ebb7..5798af2 100644 --- a/src/contracts/LidoMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -8,7 +8,7 @@ import {FixedPriceARM} from "./FixedPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {MultiLP} from "./MultiLP.sol"; -contract LidoMultiLpARM is Initializable, MultiLP, FixedPriceARM, LidoLiquidityManager { +contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, FixedPriceARM, LidoLiquidityManager { /// @param _stEth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the stETH Withdrawal contract @@ -61,4 +61,12 @@ contract LidoMultiLpARM is Initializable, MultiLP, FixedPriceARM, LidoLiquidityM function _accountFee(uint256 amountIn, uint256 amountOut) internal override(MultiLP, AbstractARM) { MultiLP._accountFee(amountIn, amountOut); } + + function _postDepositHook(uint256 assets) internal override { + // do nothing + } + + function _postRedeemHook(uint256 assets) internal override { + // do nothing + } } diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol new file mode 100644 index 0000000..0d2bc9a --- /dev/null +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import {AbstractARM} from "./AbstractARM.sol"; +import {MultiPriceARM} from "./MultiPriceARM.sol"; +import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; +import {MultiLP} from "./MultiLP.sol"; + +contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, MultiPriceARM, LidoLiquidityManager { + /// @param _stEth The address of the stETH token + /// @param _weth The address of the WETH token + /// @param _lidoWithdrawalQueue The address of the stETH Withdrawal contract + constructor(address _stEth, address _weth, address _lidoWithdrawalQueue) + AbstractARM(_stEth, _weth) + MultiLP(_weth) + MultiPriceARM(_stEth, _weth) + LidoLiquidityManager(_stEth, _weth, _lidoWithdrawalQueue) + {} + + /// @notice Initialize the contract. + /// @param _name The name of the liquidity provider (LP) token. + /// @param _symbol The symbol of the liquidity provider (LP) token. + /// @param _operator The address of the account that can request and claim OETH withdrawals. + /// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent). + /// 10,000 = 100% performance fee + /// 500 = 5% performance fee + /// @param _feeCollector The account that receives the performance fee as shares + function initialize( + string calldata _name, + string calldata _symbol, + address _operator, + uint256 _fee, + address _feeCollector + ) external initializer { + _initialize(_name, _symbol, _fee, _feeCollector); + _setOperator(_operator); + } + + /** + * @notice Calculate transfer amount for outToken. + * 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. + */ + function _calcTransferAmount(address outToken, uint256 amount) + internal + view + override + returns (uint256 transferAmount) + { + // Add 2 wei if transferring stETH + transferAmount = outToken == address(token0) ? amount + 2 : amount; + } + + function _assetsInWithdrawQueue() internal view override(LidoLiquidityManager, MultiLP) returns (uint256) { + return LidoLiquidityManager._assetsInWithdrawQueue(); + } + + function _accountFee(uint256 amountIn, uint256 amountOut) internal override(MultiLP, AbstractARM) { + MultiLP._accountFee(amountIn, amountOut); + } + + function _postDepositHook(uint256 assets) internal override(MultiLP, MultiPriceARM) { + MultiPriceARM._postDepositHook(assets); + } + + function _postRedeemHook(uint256 assets) internal override(MultiLP, MultiPriceARM) { + MultiPriceARM._postRedeemHook(assets); + } +} diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index ec01c59..452ddbe 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -12,7 +12,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { uint256 public constant MAX_FEE = 10000; // 100% address public constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; - address public immutable liquidityToken; + address private immutable liquidityToken; /// @notice The account that receives the performance fee as shares address public feeCollector; @@ -92,8 +92,12 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // mint shares _mint(msg.sender, shares); + + _postDepositHook(assets); } + function _postDepositHook(uint256 assets) internal virtual; + function previewRedeem(uint256 shares) public view returns (uint256 assets) { assets = convertToAssets(shares); } @@ -121,9 +125,13 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { queued: SafeCast.toUint128(queued) }); + _postRedeemHook(assets); + emit RedeemRequested(msg.sender, requestId, assets, queued); } + function _postRedeemHook(uint256 assets) internal virtual; + function claimRedeem(uint256 requestId) external returns (uint256 assets) { if (withdrawalRequests[requestId].queued > withdrawalQueueMetadata.claimable) { // Add any WETH from the Dripper to the withdrawal queue diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol new file mode 100644 index 0000000..33d30c9 --- /dev/null +++ b/src/contracts/MultiPriceARM.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {AbstractARM} from "./AbstractARM.sol"; +import {IERC20} from "./Interfaces.sol"; + +abstract contract MultiPriceARM is AbstractARM { + /// @notice The token being bought by the ARM at a discount. eg stETH + address private immutable discountToken; + /// @notice The token being sold by the ARM. eg WETH + address private immutable liquidityToken; + + uint256 public constant PRICE_PRECISION = 1e18; + uint256 public constant DISCOUNT_MULTIPLIER = 1e13; + /// @notice The amount of tranche allocation and remaining amounts are multiplied by to get the actual amount. + /// min amount is 0.1 Ether, Max amount is 6,553.6 Ether + uint256 public constant TRANCHE_AMOUNT_MULTIPLIER = 1e17; + + uint256 private constant DISCOUNT_INDEX = 0; + uint256 private constant LIQUIDITY_ALLOCATED_INDEX = 1; + uint256 private constant LIQUIDITY_REMAINING_INDEX = 2; + + /// @notice The five liquidity tranches of the ARM + /// Each tranche is represented by three uint16 values: + /// 0 - the discount from the liquidity token scaled to 1e5 + // eg an 8 basis point discount (0.08%) would be 800 with a price of 0.9992 + /// 1 - the amount of liquidity allocated to this tranche. 1 = 0.1 Ether + /// 2 - the amount of liquidity remaining in this tranche. 1 = 0.1 Ether + /// The three tranche values are repeated five times in the array as follows: + /// [discount, allocated, remaining, discount, allocated, remaining, ...] + /// @dev Five tranches are used as they fit in a single storage slot + uint16[15] private tranches; + + constructor(address _discountToken, address _liquidityToken) { + discountToken = _discountToken; + liquidityToken = _liquidityToken; + } + + function _postDepositHook(uint256 liquidityAmount) internal virtual { + uint256 remainingLiquidity = liquidityAmount; + uint256 unallocatedLiquidity; + uint256 liquidityToAdd; + + // Read the tranches from storage into memory + uint16[15] memory tranchesMem = tranches; + + // Fill the tranches with the new liquidity from first to last + for (uint256 i = 0; i < tranchesMem.length; i + 3) { + unallocatedLiquidity = + tranchesMem[i + LIQUIDITY_ALLOCATED_INDEX] - tranchesMem[i + LIQUIDITY_REMAINING_INDEX]; + + liquidityToAdd = remainingLiquidity <= unallocatedLiquidity ? remainingLiquidity : unallocatedLiquidity; + + // Update the liquidity remaining in memory + tranchesMem[i + LIQUIDITY_REMAINING_INDEX] += SafeCast.toUint16(liquidityToAdd / TRANCHE_AMOUNT_MULTIPLIER); + + remainingLiquidity -= liquidityToAdd; + + if (remainingLiquidity == 0) { + return; + } + } + + // Write back the tranche data to storage once + tranches = tranchesMem; + } + + function _postRedeemHook(uint256 liquidityAmount) internal virtual { + uint256 remainingLiquidity = liquidityAmount; + uint256 liquidityToRemove; + + uint16[15] memory tranchesMem = tranches; + + // Take liquidity from the tranches from last to first + for (uint256 i = tranchesMem.length; i > 2;) { + i = i - 3; + liquidityToRemove = remainingLiquidity <= tranchesMem[i + LIQUIDITY_REMAINING_INDEX] + ? remainingLiquidity + : tranchesMem[i + LIQUIDITY_REMAINING_INDEX]; + + tranchesMem[i + LIQUIDITY_REMAINING_INDEX] -= + SafeCast.toUint16(liquidityToRemove / TRANCHE_AMOUNT_MULTIPLIER); + + remainingLiquidity -= liquidityToRemove; + + if (remainingLiquidity == 0) { + return; + } + } + + // Write back the tranche data to storage once + tranches = tranchesMem; + } + + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to) + internal + override + returns (uint256 amountOut) + { + uint256 price; + if (address(inToken) == discountToken) { + require(address(outToken) == liquidityToken, "ARM: Invalid token"); + price = _calcPriceFromDiscount(amountIn); + } else if (address(inToken) == liquidityToken) { + require(address(outToken) == discountToken, "ARM: Invalid token"); + price = _calcPriceFromLiquidity(amountIn); + } else { + revert("ARM: Invalid token"); + } + + amountOut = amountIn * price / 1e36; + + // Transfer the input tokens from the caller to this ARM contract + inToken.transferFrom(msg.sender, address(this), amountIn); + + // Transfer the output tokens to the recipient + uint256 transferAmountOut = _calcTransferAmount(address(outToken), amountOut); + outToken.transfer(to, transferAmountOut); + } + + function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to) + internal + override + returns (uint256 amountIn) + { + uint256 price; + if (address(inToken) == discountToken) { + require(address(outToken) == liquidityToken, "ARM: Invalid token"); + price = _calcPriceFromLiquidity(amountOut); + } else if (address(inToken) == liquidityToken) { + require(address(outToken) == discountToken, "ARM: Invalid token"); + price = _calcPriceFromDiscount(amountOut); + } else { + revert("ARM: Invalid token"); + } + amountIn = ((amountOut * 1e36) / price) + 1; // +1 to always round in our favor + + // Transfer the input tokens from the caller to this ARM contract + inToken.transferFrom(msg.sender, address(this), amountIn); + + // Transfer the output tokens to the recipient + uint256 transferAmountOut = _calcTransferAmount(address(outToken), amountOut); + outToken.transfer(to, transferAmountOut); + } + + /// @dev Calculate the volume weighted price from the available liquidity amount. eg WETH amount + function _calcPriceFromLiquidity(uint256 liquiditySwapAmount) internal returns (uint256 price) { + uint16[15] memory tranchesMem = tranches; + + uint256 trancheVolume; + uint256 totalPriceVolume; + uint256 remainingSwapVolume = liquiditySwapAmount; + + // For each tranche + for (uint256 i = 0; i < tranchesMem.length; i + 3) { + uint256 actualLiquidityRemainingInTranche = + tranchesMem[i + LIQUIDITY_REMAINING_INDEX] * TRANCHE_AMOUNT_MULTIPLIER; + trancheVolume = remainingSwapVolume <= actualLiquidityRemainingInTranche + ? remainingSwapVolume + : actualLiquidityRemainingInTranche; + + // Update the liquidity remaining in memory + tranchesMem[i + LIQUIDITY_REMAINING_INDEX] = + SafeCast.toUint16((actualLiquidityRemainingInTranche - trancheVolume) / TRANCHE_AMOUNT_MULTIPLIER); + + // If there is no liquidity in the tranche then move to the next tranche + if (trancheVolume == 0) { + continue; + } + + uint256 actualPrice = PRICE_PRECISION - (tranchesMem[i + DISCOUNT_INDEX] * DISCOUNT_MULTIPLIER); + totalPriceVolume += actualPrice * trancheVolume; + remainingSwapVolume -= trancheVolume; + + // Break from the loop if we have enough liquidity + if (remainingSwapVolume == 0) { + break; + } + } + + // If there is not enough liquidity in all the tranches then revert + require(remainingSwapVolume == 0, "ARM: Not enough liquidity"); + + // Write back the tranche data to storage once + tranches = tranchesMem; + + // Calculate the volume weighted average price which is returned + return totalPriceVolume / liquiditySwapAmount; + } + + /// @dev Calculate the volume weighted price from the available discount amount. eg stETH amount + function _calcPriceFromDiscount(uint256 discountSwapAmount) internal returns (uint256 price) { + uint16[15] memory tranchesMem = tranches; + + uint256 discountTrancheVolume; + uint256 totalDiscountPriceVolume; + uint256 remainingDiscountSwapVolume = discountSwapAmount; + + // For each tranche + for (uint256 i = 0; i < tranchesMem.length; i + 3) { + uint256 tranchePrice = (PRICE_PRECISION - (tranchesMem[i + DISCOUNT_INDEX] * DISCOUNT_MULTIPLIER)); + // Convert the tranche liquidity to the discount token + uint256 actualDiscountRemainingInTranche = + tranchesMem[i + LIQUIDITY_REMAINING_INDEX] * TRANCHE_AMOUNT_MULTIPLIER * PRICE_PRECISION / tranchePrice; + discountTrancheVolume = remainingDiscountSwapVolume <= actualDiscountRemainingInTranche + ? remainingDiscountSwapVolume + : actualDiscountRemainingInTranche; + + // Update the liquidity remaining in memory + uint256 liquidityTrancheVolume = discountTrancheVolume * tranchePrice / PRICE_PRECISION; + tranchesMem[i + LIQUIDITY_REMAINING_INDEX] = tranchesMem[i + LIQUIDITY_REMAINING_INDEX] + - SafeCast.toUint16(liquidityTrancheVolume / TRANCHE_AMOUNT_MULTIPLIER); + + // If there is no liquidity in the tranche then move to the next tranche + if (discountTrancheVolume == 0) { + continue; + } + + totalDiscountPriceVolume += discountTrancheVolume * PRICE_PRECISION * PRICE_PRECISION / tranchePrice; + remainingDiscountSwapVolume -= discountTrancheVolume; + + // Break from the loop if we have enough liquidity + if (remainingDiscountSwapVolume == 0) { + break; + } + } + + // If there is not enough liquidity in all the tranches then revert + require(remainingDiscountSwapVolume == 0, "ARM: Not enough liquidity"); + + // Write back the tranche data to storage once + tranches = tranchesMem; + + // Calculate the volume weighted average price + uint256 discountPrice = totalDiscountPriceVolume / discountSwapAmount; + // Convert back to a liquidity price. + return PRICE_PRECISION * PRICE_PRECISION / discountPrice; + } + + /** + * @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; + } +} From 8b9a767a131008b66229a4495a156bf9c00c9baa Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 9 Sep 2024 21:24:33 +1000 Subject: [PATCH 015/196] Added admin functions to update tranche prices and allocations --- src/contracts/MultiPriceARM.sol | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol index 33d30c9..38315d4 100644 --- a/src/contracts/MultiPriceARM.sol +++ b/src/contracts/MultiPriceARM.sol @@ -32,6 +32,9 @@ abstract contract MultiPriceARM is AbstractARM { /// @dev Five tranches are used as they fit in a single storage slot uint16[15] private tranches; + event TranchePricesUpdated(uint256[5] prices); + event TrancheAllocationsUpdated(uint256[5] allocations); + constructor(address _discountToken, address _liquidityToken) { discountToken = _discountToken; liquidityToken = _liquidityToken; @@ -245,4 +248,71 @@ abstract contract MultiPriceARM is AbstractARM { function _calcTransferAmount(address, uint256 amount) internal view virtual returns (uint256 transferAmount) { transferAmount = amount; } + + function updateTranchePrices(uint256[5] calldata prices) external onlyOwner { + uint16[15] memory tranchesMem = tranches; + + for (uint256 i = 0; i < prices.length; i++) { + // TODO add price safely checks + tranchesMem[i * 3 + DISCOUNT_INDEX] = SafeCast.toUint16(prices[i] / DISCOUNT_MULTIPLIER); + } + + // Write back the tranche data to storage once + tranches = tranchesMem; + + emit TranchePricesUpdated(prices); + } + + function updateTrancheAllocations(uint256[5] calldata allocations) external onlyOwner { + uint16[15] memory tranchesMem = tranches; + + for (uint256 i = 0; i < allocations.length; ++i) { + uint256 trancheIndex = i * 3; + // TODO add amount safely checks + tranchesMem[trancheIndex + LIQUIDITY_ALLOCATED_INDEX] = + SafeCast.toUint16(allocations[i] / TRANCHE_AMOUNT_MULTIPLIER); + + // If the allocation is smaller than the remaining liquidity then set the remaining liquidity to the allocation + if ( + tranchesMem[trancheIndex + LIQUIDITY_ALLOCATED_INDEX] + < tranchesMem[trancheIndex + LIQUIDITY_REMAINING_INDEX] + ) { + tranchesMem[trancheIndex + LIQUIDITY_REMAINING_INDEX] = + tranchesMem[trancheIndex + LIQUIDITY_ALLOCATED_INDEX]; + } + } + + // Write back the tranche data to storage once + tranches = tranchesMem; + + emit TrancheAllocationsUpdated(allocations); + } + + function resetRemainingLiquidity() external onlyOwner { + uint256 remainingLiquidity = IERC20(liquidityToken).balanceOf(address(this)); + uint256 allocatedLiquidity; + uint256 trancheLiquidity; + + // Read the tranches from storage into memory + uint16[15] memory tranchesMem = tranches; + + // Fill the tranches with the new liquidity from first to last + for (uint256 i = 0; i < tranchesMem.length; i + 3) { + allocatedLiquidity = tranchesMem[i + LIQUIDITY_ALLOCATED_INDEX]; + + trancheLiquidity = remainingLiquidity <= allocatedLiquidity ? remainingLiquidity : allocatedLiquidity; + + // Update the liquidity remaining in memory + tranchesMem[i + LIQUIDITY_REMAINING_INDEX] = SafeCast.toUint16(trancheLiquidity / TRANCHE_AMOUNT_MULTIPLIER); + + remainingLiquidity -= trancheLiquidity; + + if (remainingLiquidity == 0) { + return; + } + } + + // Write back the tranche data to storage once + tranches = tranchesMem; + } } From f4e7b89edb73c19cb56b9267cfd1112a9d560c67 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 9 Sep 2024 21:25:24 +1000 Subject: [PATCH 016/196] Generated latest diagrams --- docs/LidoMultiPriceMultiLpARMSquashed.svg | 173 +++++++++++----------- 1 file changed, 89 insertions(+), 84 deletions(-) diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index 636411b..6f4ca05 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -4,94 +4,99 @@ - - + + UmlClassDiagram - + 13 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   liquidityToken: address <<MultiLP>> -   discountToken: address <<MultiPriceARM>> -   liquidityToken: address <<MultiPriceARM>> -   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> -   tranches: uint16[15] <<MultiPriceARM>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   MAX_FEE: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   feeCollector: address <<MultiLP>> -   fee: uint256 <<MultiLP>> -   feesCollected: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   PRICE_PRECISION: uint256 <<MultiPriceARM>> -   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> -    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoMultiPriceMultiLpARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> -    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _postRedeemHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> -    _setFee(_fee: uint256) <<MultiLP>> -    _setFeeCollector(_feeCollector: address) <<MultiLP>> -    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<MultiLP>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<MultiLP>> -    collectFees(): (fees: uint256) <<MultiLP>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoMultiPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<MultiLP>> + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   liquidityToken: address <<MultiLP>> +   discountToken: address <<MultiPriceARM>> +   liquidityToken: address <<MultiPriceARM>> +   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> +   tranches: uint16[15] <<MultiPriceARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   MAX_FEE: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   feeCollector: address <<MultiLP>> +   fee: uint256 <<MultiLP>> +   feesCollected: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   PRICE_PRECISION: uint256 <<MultiPriceARM>> +   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> +    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoMultiPriceMultiLpARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> +    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _postRedeemHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> +    _setFee(_fee: uint256) <<MultiLP>> +    _setFeeCollector(_feeCollector: address) <<MultiLP>> +    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<MultiLP>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<MultiLP>> +    collectFees(): (fees: uint256) <<MultiLP>> +    updateTranchePrices(prices: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> +    updateTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> +    resetRemainingLiquidity() <<onlyOwner>> <<MultiPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoMultiPriceMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<MultiLP>> +    <<event>> TranchePricesUpdated(prices: uint256[5]) <<MultiPriceARM>> +    <<event>> TrancheAllocationsUpdated(allocations: uint256[5]) <<MultiPriceARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>> From 5d65c44563793320a1e641ab70b79830dbe273bf Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 10 Sep 2024 10:33:41 +1000 Subject: [PATCH 017/196] Add _claimHook to add liquidity back --- docs/LidoFixedPriceMultiLpARMHierarchy.svg | 15 +-- docs/LidoFixedPriceMultiLpARMSquashed.svg | 107 +++++++++--------- docs/LidoMultiPriceMultiLpARMHierarchy.svg | 15 +-- docs/LidoMultiPriceMultiLpARMSquashed.svg | 121 +++++++++++---------- src/contracts/LidoFixedPriceMultiLpARM.sol | 8 +- src/contracts/LidoLiquidityManager.sol | 6 +- src/contracts/LidoMultiPriceMultiLpARM.sol | 12 +- src/contracts/LidoOwnerLpARM.sol | 4 + src/contracts/MultiLP.sol | 8 +- src/contracts/MultiPriceARM.sol | 7 +- 10 files changed, 163 insertions(+), 140 deletions(-) diff --git a/docs/LidoFixedPriceMultiLpARMHierarchy.svg b/docs/LidoFixedPriceMultiLpARMHierarchy.svg index 186d7fc..1d0c88b 100644 --- a/docs/LidoFixedPriceMultiLpARMHierarchy.svg +++ b/docs/LidoFixedPriceMultiLpARMHierarchy.svg @@ -60,15 +60,16 @@ 12 - -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol + +<<Abstract>> +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol 11->12 - - + + @@ -87,8 +88,8 @@ 12->22 - - + + diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index 873839c..61f28cb 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,63 +4,64 @@ - - + + UmlClassDiagram - + 11 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   liquidityToken: address <<MultiLP>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   MAX_FEE: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   feeCollector: address <<MultiLP>> -   fee: uint256 <<MultiLP>> -   feesCollected: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoFixedPriceMultiLpARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> -    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _postRedeemHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> -    _setFee(_fee: uint256) <<MultiLP>> -    _setFeeCollector(_feeCollector: address) <<MultiLP>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   liquidityToken: address <<MultiLP>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   MAX_FEE: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   feeCollector: address <<MultiLP>> +   fee: uint256 <<MultiLP>> +   feesCollected: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoFixedPriceMultiLpARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> +    _depositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _redeemHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> +    _setFee(_fee: uint256) <<MultiLP>> +    _setFeeCollector(_feeCollector: address) <<MultiLP>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +    _claimHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> External:    <<payable>> null() <<LidoLiquidityManager>>    owner(): address <<Ownable>> diff --git a/docs/LidoMultiPriceMultiLpARMHierarchy.svg b/docs/LidoMultiPriceMultiLpARMHierarchy.svg index e1d5cab..adeeab3 100644 --- a/docs/LidoMultiPriceMultiLpARMHierarchy.svg +++ b/docs/LidoMultiPriceMultiLpARMHierarchy.svg @@ -33,15 +33,16 @@ 12 - -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol + +<<Abstract>> +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol 12->22 - - + + @@ -53,8 +54,8 @@ 13->12 - - + + diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index 6f4ca05..42148da 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -4,69 +4,72 @@ - - + + UmlClassDiagram - + 13 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   liquidityToken: address <<MultiLP>> -   discountToken: address <<MultiPriceARM>> -   liquidityToken: address <<MultiPriceARM>> -   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> -   tranches: uint16[15] <<MultiPriceARM>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   MAX_FEE: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   feeCollector: address <<MultiLP>> -   fee: uint256 <<MultiLP>> -   feesCollected: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   PRICE_PRECISION: uint256 <<MultiPriceARM>> -   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> -    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoMultiPriceMultiLpARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> -    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _postRedeemHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> -    _setFee(_fee: uint256) <<MultiLP>> -    _setFeeCollector(_feeCollector: address) <<MultiLP>> -    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   liquidityToken: address <<MultiLP>> +   discountToken: address <<MultiPriceARM>> +   liquidityToken: address <<MultiPriceARM>> +   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> +   tranches: uint16[15] <<MultiPriceARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   MAX_FEE: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   feeCollector: address <<MultiLP>> +   fee: uint256 <<MultiLP>> +   feesCollected: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   PRICE_PRECISION: uint256 <<MultiPriceARM>> +   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> +    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoMultiPriceMultiLpARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> +    _depositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _redeemHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> +    _setFee(_fee: uint256) <<MultiLP>> +    _setFeeCollector(_feeCollector: address) <<MultiLP>> +    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> +    _removeLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> +    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> +    _claimHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> External:    <<payable>> null() <<LidoLiquidityManager>>    owner(): address <<Ownable>> diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index 5798af2..d6f04cc 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -62,11 +62,15 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, FixedPriceARM, Lido MultiLP._accountFee(amountIn, amountOut); } - function _postDepositHook(uint256 assets) internal override { + function _depositHook(uint256 assets) internal override { // do nothing } - function _postRedeemHook(uint256 assets) internal override { + function _redeemHook(uint256 assets) internal override { + // do nothing + } + + function _claimHook(uint256 assets) internal override { // do nothing } } diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 371a4be..11f148c 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import {OwnableOperable} from "./OwnableOperable.sol"; import {IERC20, IWETH, IStETHWithdrawal} from "./Interfaces.sol"; -contract LidoLiquidityManager is OwnableOperable { +abstract contract LidoLiquidityManager is OwnableOperable { IERC20 public immutable steth; IWETH public immutable weth; IStETHWithdrawal public immutable withdrawalQueue; @@ -62,8 +62,12 @@ contract LidoLiquidityManager is OwnableOperable { // Wrap all the received ETH to WETH. (bool success,) = address(weth).call{value: etherAfter}(new bytes(0)); require(success, "OSwap: ETH transfer failed"); + + _claimHook(etherAfter); } + function _claimHook(uint256 assets) internal virtual; + function _assetsInWithdrawQueue() internal view virtual returns (uint256) { return outstandingEther; } diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index 0d2bc9a..a03d8dc 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -62,11 +62,15 @@ contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, MultiPriceARM, Lido MultiLP._accountFee(amountIn, amountOut); } - function _postDepositHook(uint256 assets) internal override(MultiLP, MultiPriceARM) { - MultiPriceARM._postDepositHook(assets); + function _depositHook(uint256 assets) internal override { + _addLiquidity(assets); } - function _postRedeemHook(uint256 assets) internal override(MultiLP, MultiPriceARM) { - MultiPriceARM._postRedeemHook(assets); + function _redeemHook(uint256 assets) internal override { + _removeLiquidity(assets); + } + + function _claimHook(uint256 assets) internal override { + _addLiquidity(assets); } } diff --git a/src/contracts/LidoOwnerLpARM.sol b/src/contracts/LidoOwnerLpARM.sol index b56b8eb..ccd82e6 100644 --- a/src/contracts/LidoOwnerLpARM.sol +++ b/src/contracts/LidoOwnerLpARM.sol @@ -39,4 +39,8 @@ contract LidoOwnerLpARM is Initializable, OwnerLP, FixedPriceARM, LidoLiquidityM // Add 2 wei if transferring stETH transferAmount = outToken == address(token0) ? amount + 2 : amount; } + + function _claimHook(uint256 assets) internal override { + // do nothing + } } diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 452ddbe..6c0dadb 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -93,10 +93,10 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // mint shares _mint(msg.sender, shares); - _postDepositHook(assets); + _depositHook(assets); } - function _postDepositHook(uint256 assets) internal virtual; + function _depositHook(uint256 assets) internal virtual; function previewRedeem(uint256 shares) public view returns (uint256 assets) { assets = convertToAssets(shares); @@ -125,12 +125,12 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { queued: SafeCast.toUint128(queued) }); - _postRedeemHook(assets); + _redeemHook(assets); emit RedeemRequested(msg.sender, requestId, assets, queued); } - function _postRedeemHook(uint256 assets) internal virtual; + function _redeemHook(uint256 assets) internal virtual; function claimRedeem(uint256 requestId) external returns (uint256 assets) { if (withdrawalRequests[requestId].queued > withdrawalQueueMetadata.claimable) { diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol index 38315d4..6a5f608 100644 --- a/src/contracts/MultiPriceARM.sol +++ b/src/contracts/MultiPriceARM.sol @@ -24,7 +24,8 @@ abstract contract MultiPriceARM is AbstractARM { /// @notice The five liquidity tranches of the ARM /// Each tranche is represented by three uint16 values: /// 0 - the discount from the liquidity token scaled to 1e5 - // eg an 8 basis point discount (0.08%) would be 800 with a price of 0.9992 + // eg a 8 basis point discount (0.08%) would be 800 with a price of 0.9992 + // eg a 1.25 basis point discount (0.0125%) would be 125 with a price of 0.999875 /// 1 - the amount of liquidity allocated to this tranche. 1 = 0.1 Ether /// 2 - the amount of liquidity remaining in this tranche. 1 = 0.1 Ether /// The three tranche values are repeated five times in the array as follows: @@ -40,7 +41,7 @@ abstract contract MultiPriceARM is AbstractARM { liquidityToken = _liquidityToken; } - function _postDepositHook(uint256 liquidityAmount) internal virtual { + function _addLiquidity(uint256 liquidityAmount) internal virtual { uint256 remainingLiquidity = liquidityAmount; uint256 unallocatedLiquidity; uint256 liquidityToAdd; @@ -69,7 +70,7 @@ abstract contract MultiPriceARM is AbstractARM { tranches = tranchesMem; } - function _postRedeemHook(uint256 liquidityAmount) internal virtual { + function _removeLiquidity(uint256 liquidityAmount) internal virtual { uint256 remainingLiquidity = liquidityAmount; uint256 liquidityToRemove; From d117f2b487b39fbbb0c65ee0d1be18f5c9dcfed0 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 11 Sep 2024 09:35:37 +1000 Subject: [PATCH 018/196] Added AccessControlLP Made it clear the deposit and withdraw hooks are post function --- src/contracts/AccessControlLP.sol | 41 ++++++++++++++++++++++ src/contracts/LidoFixedPriceMultiLpARM.sol | 6 ++-- src/contracts/LidoLiquidityManager.sol | 4 +-- src/contracts/LidoMultiPriceMultiLpARM.sol | 13 ++++--- src/contracts/LidoOwnerLpARM.sol | 2 +- src/contracts/MultiLP.sol | 8 ++--- 6 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 src/contracts/AccessControlLP.sol diff --git a/src/contracts/AccessControlLP.sol b/src/contracts/AccessControlLP.sol new file mode 100644 index 0000000..dcacbcd --- /dev/null +++ b/src/contracts/AccessControlLP.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {MultiLP} from "./MultiLP.sol"; + +abstract contract AccessControlLP is MultiLP { + uint256 public totalAssetsCap; + mapping(address lp => uint256 cap) public liquidityProviderCaps; + + event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); + event TotalAssetsCap(uint256 cap); + + function _postDepositHook(uint256 assets) internal virtual override { + require(liquidityProviderCaps[msg.sender] >= assets, "ARM: LP cap exceeded"); + // total assets has already been updated with the new assets + require(totalAssetsCap >= totalAssets(), "ARM: Total assets cap exceeded"); + + // Save the new LP cap to storage + liquidityProviderCaps[msg.sender] -= assets; + } + + /// @dev Adds assets to the liquidity provider's cap when withdrawing assets or redeeming shares. + /// Will not revert if the total assets cap is less than the total assets. + function _postWithdrawHook(uint256 assets) internal virtual override { + liquidityProviderCaps[msg.sender] += assets; + } + + function setLiquidityProviderCap(address liquidityProvider, uint256 cap) external onlyOwner { + liquidityProviderCaps[liquidityProvider] = cap; + + emit LiquidityProviderCap(liquidityProvider, cap); + } + + /// @notice Set the total assets cap for a liquidity provider. + /// Setting to zero will prevent any further deposits. The lp can still withdraw assets. + function setTotalAssetsCap(uint256 _totalAssetsCap) external onlyOwner { + totalAssetsCap = _totalAssetsCap; + + emit TotalAssetsCap(_totalAssetsCap); + } +} diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index d6f04cc..49a5a45 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -62,15 +62,15 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, FixedPriceARM, Lido MultiLP._accountFee(amountIn, amountOut); } - function _depositHook(uint256 assets) internal override { + function _postDepositHook(uint256 assets) internal override { // do nothing } - function _redeemHook(uint256 assets) internal override { + function _postWithdrawHook(uint256 assets) internal override { // do nothing } - function _claimHook(uint256 assets) internal override { + function _postClaimHook(uint256 assets) internal override { // do nothing } } diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 11f148c..5956be4 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -63,10 +63,10 @@ abstract contract LidoLiquidityManager is OwnableOperable { (bool success,) = address(weth).call{value: etherAfter}(new bytes(0)); require(success, "OSwap: ETH transfer failed"); - _claimHook(etherAfter); + _postClaimHook(etherAfter); } - function _claimHook(uint256 assets) internal virtual; + function _postClaimHook(uint256 assets) internal virtual; function _assetsInWithdrawQueue() internal view virtual returns (uint256) { return outstandingEther; diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index a03d8dc..2acec00 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.23; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {AccessControlLP} from "./AccessControlLP.sol"; import {AbstractARM} from "./AbstractARM.sol"; import {MultiPriceARM} from "./MultiPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {MultiLP} from "./MultiLP.sol"; -contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, MultiPriceARM, LidoLiquidityManager { +contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, AccessControlLP, MultiPriceARM, LidoLiquidityManager { /// @param _stEth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the stETH Withdrawal contract @@ -62,15 +63,19 @@ contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, MultiPriceARM, Lido MultiLP._accountFee(amountIn, amountOut); } - function _depositHook(uint256 assets) internal override { + function _postDepositHook(uint256 assets) internal override(AccessControlLP, MultiLP) { _addLiquidity(assets); + + AccessControlLP._postDepositHook(assets); } - function _redeemHook(uint256 assets) internal override { + function _postWithdrawHook(uint256 assets) internal override(AccessControlLP, MultiLP) { _removeLiquidity(assets); + + AccessControlLP._postWithdrawHook(assets); } - function _claimHook(uint256 assets) internal override { + function _postClaimHook(uint256 assets) internal override { _addLiquidity(assets); } } diff --git a/src/contracts/LidoOwnerLpARM.sol b/src/contracts/LidoOwnerLpARM.sol index ccd82e6..9fe19f3 100644 --- a/src/contracts/LidoOwnerLpARM.sol +++ b/src/contracts/LidoOwnerLpARM.sol @@ -40,7 +40,7 @@ contract LidoOwnerLpARM is Initializable, OwnerLP, FixedPriceARM, LidoLiquidityM transferAmount = outToken == address(token0) ? amount + 2 : amount; } - function _claimHook(uint256 assets) internal override { + function _postClaimHook(uint256 assets) internal override { // do nothing } } diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 6c0dadb..75ac1c6 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -93,10 +93,10 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // mint shares _mint(msg.sender, shares); - _depositHook(assets); + _postDepositHook(assets); } - function _depositHook(uint256 assets) internal virtual; + function _postDepositHook(uint256 assets) internal virtual; function previewRedeem(uint256 shares) public view returns (uint256 assets) { assets = convertToAssets(shares); @@ -125,12 +125,12 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { queued: SafeCast.toUint128(queued) }); - _redeemHook(assets); + _postWithdrawHook(assets); emit RedeemRequested(msg.sender, requestId, assets, queued); } - function _redeemHook(uint256 assets) internal virtual; + function _postWithdrawHook(uint256 assets) internal virtual; function claimRedeem(uint256 requestId) external returns (uint256 assets) { if (withdrawalRequests[requestId].queued > withdrawalQueueMetadata.claimable) { From 849eeccec6dc203e5238efa013cd45404b4c70e7 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 11 Sep 2024 10:15:13 +1000 Subject: [PATCH 019/196] Added AccessControlShares --- src/contracts/AccessControlShares.sol | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/contracts/AccessControlShares.sol diff --git a/src/contracts/AccessControlShares.sol b/src/contracts/AccessControlShares.sol new file mode 100644 index 0000000..c5288b1 --- /dev/null +++ b/src/contracts/AccessControlShares.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {MultiLP} from "./MultiLP.sol"; + +abstract contract AccessControlShares is MultiLP { + uint256 public totalSupplyCap; + mapping(address lp => uint256 shares) public liquidityProviderCaps; + + event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); + event TotalSupplyCap(uint256 shares); + + function _postDepositHook(uint256) internal virtual override { + require(liquidityProviderCaps[msg.sender] >= balanceOf(msg.sender), "ARM: LP cap exceeded"); + // total supply has already been updated + require(totalSupplyCap >= totalSupply(), "ARM: Supply cap exceeded"); + } + + function _postWithdrawHook(uint256) internal virtual override { + // Do nothing + } + + function setLiquidityProviderCap(address liquidityProvider, uint256 cap) external onlyOwner { + liquidityProviderCaps[liquidityProvider] = cap; + + emit LiquidityProviderCap(liquidityProvider, cap); + } + + function setTotalSupplyCap(uint256 _totalSupplyCap) external onlyOwner { + totalSupplyCap = _totalSupplyCap; + + emit TotalSupplyCap(_totalSupplyCap); + } +} From 4c54c48f958029f1cad6be911f8aeea0477ddcf3 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 11 Sep 2024 22:20:35 +1000 Subject: [PATCH 020/196] Split performance fee logic into PerformanceFee contract Moved calc of perf fee to LP deposit and withdraw instead of swaps --- docs/LidoFixedPriceMultiLpARMHierarchy.svg | 178 ++++++++++-------- docs/LidoFixedPriceMultiLpARMSquashed.svg | 176 +++++++++--------- docs/LidoMultiPriceMultiLpARMHierarchy.svg | 204 ++++++++++++--------- docs/LidoMultiPriceMultiLpARMSquashed.svg | 200 ++++++++++---------- src/contracts/AbstractARM.sol | 11 -- src/contracts/LidoFixedPriceMultiLpARM.sol | 18 +- src/contracts/LidoMultiPriceMultiLpARM.sol | 35 ++-- src/contracts/MultiLP.sol | 69 +------ src/contracts/OwnableOperable.sol | 4 + src/contracts/PerformanceFee.sol | 118 ++++++++++++ 10 files changed, 584 insertions(+), 429 deletions(-) create mode 100644 src/contracts/PerformanceFee.sol diff --git a/docs/LidoFixedPriceMultiLpARMHierarchy.svg b/docs/LidoFixedPriceMultiLpARMHierarchy.svg index 1d0c88b..21e017a 100644 --- a/docs/LidoFixedPriceMultiLpARMHierarchy.svg +++ b/docs/LidoFixedPriceMultiLpARMHierarchy.svg @@ -4,111 +4,131 @@ - - + + UmlClassDiagram - + 0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol - + -22 - -OwnableOperable -../src/contracts/OwnableOperable.sol +24 + +OwnableOperable +../src/contracts/OwnableOperable.sol - + -0->22 - - +0->24 + + - + -1 - -<<Abstract>> -FixedPriceARM -../src/contracts/FixedPriceARM.sol +3 + +<<Abstract>> +FixedPriceARM +../src/contracts/FixedPriceARM.sol - + -1->0 - - +3->0 + + - + -11 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol +13 + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol - - -11->1 - - + + +13->3 + + - + -12 - -<<Abstract>> -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol +14 + +<<Abstract>> +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol - - -11->12 - - + + +13->14 + + - + -15 - -<<Abstract>> -MultiLP -../src/contracts/MultiLP.sol +17 + +<<Abstract>> +MultiLP +../src/contracts/MultiLP.sol - + -11->15 - - - - - -12->22 - - +13->17 + + + + + +27 + +<<Abstract>> +PerformanceFee +../src/contracts/PerformanceFee.sol + + + +13->27 + + - + -15->0 - - - - - -21 - -Ownable -../src/contracts/Ownable.sol +14->24 + + - + -22->21 - - +17->0 + + + + + +23 + +Ownable +../src/contracts/Ownable.sol + + + +24->23 + + + + + +27->17 + + diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index 61f28cb..df6ef89 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,91 +4,99 @@ - - + + UmlClassDiagram - - + + -11 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   liquidityToken: address <<MultiLP>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   MAX_FEE: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   feeCollector: address <<MultiLP>> -   fee: uint256 <<MultiLP>> -   feesCollected: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoFixedPriceMultiLpARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> -    _depositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _redeemHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> -    _setFee(_fee: uint256) <<MultiLP>> -    _setFeeCollector(_feeCollector: address) <<MultiLP>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -    _claimHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<MultiLP>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<MultiLP>> -    collectFees(): (fees: uint256) <<MultiLP>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> -    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<MultiLP>> +13 + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   liquidityToken: address <<MultiLP>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   MAX_FEE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesCollected: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _setFee(_fee: uint256) <<PerformanceFee>> +    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +    _postClaimHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> +    collectFees(): (fees: uint256) <<PerformanceFee>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> +    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCalculated(mewFeesCollected: uint256, totalAssets: uint256) <<PerformanceFee>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> +    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>>    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> @@ -97,7 +105,7 @@    constructor(_liquidityToken: address) <<MultiLP>>    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>>    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> -    totalAssets(): uint256 <<MultiLP>> +    totalAssets(): uint256 <<LidoFixedPriceMultiLpARM>>    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>>    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>>    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoFixedPriceMultiLpARM>> diff --git a/docs/LidoMultiPriceMultiLpARMHierarchy.svg b/docs/LidoMultiPriceMultiLpARMHierarchy.svg index adeeab3..60447b1 100644 --- a/docs/LidoMultiPriceMultiLpARMHierarchy.svg +++ b/docs/LidoMultiPriceMultiLpARMHierarchy.svg @@ -4,111 +4,151 @@ - - + + UmlClassDiagram - + 0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol - - - -22 - -OwnableOperable -../src/contracts/OwnableOperable.sol - - + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol + + + +24 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + -0->22 - - +0->24 + + - + -12 - -<<Abstract>> -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol +1 + +<<Abstract>> +AccessControlLP +../src/contracts/AccessControlLP.sol + + + +17 + +<<Abstract>> +MultiLP +../src/contracts/MultiLP.sol - + -12->22 - - +1->17 + + - + -13 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol +14 + +<<Abstract>> +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol - - -13->12 - - + + +14->24 + + 15 - -<<Abstract>> -MultiLP -../src/contracts/MultiLP.sol + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol - - -13->15 - - + + +15->1 + + - - -18 - -<<Abstract>> -MultiPriceARM -../src/contracts/MultiPriceARM.sol + + +15->14 + + - + -13->18 - - - - - -15->0 - - - - - -18->0 - - +15->17 + + - + -21 - -Ownable -../src/contracts/Ownable.sol +20 + +<<Abstract>> +MultiPriceARM +../src/contracts/MultiPriceARM.sol - - -22->21 - - + + +15->20 + + + + + +27 + +<<Abstract>> +PerformanceFee +../src/contracts/PerformanceFee.sol + + + +15->27 + + + + + +17->0 + + + + + +20->0 + + + + + +23 + +Ownable +../src/contracts/Ownable.sol + + + +24->23 + + + + + +27->17 + + diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index 42148da..bc0f954 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -4,100 +4,114 @@ - - + + UmlClassDiagram - - + + -13 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   liquidityToken: address <<MultiLP>> -   discountToken: address <<MultiPriceARM>> -   liquidityToken: address <<MultiPriceARM>> -   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> -   tranches: uint16[15] <<MultiPriceARM>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   MAX_FEE: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   feeCollector: address <<MultiLP>> -   fee: uint256 <<MultiLP>> -   feesCollected: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   PRICE_PRECISION: uint256 <<MultiPriceARM>> -   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> -    _accountFee(amountIn: uint256, amountOut: uint256) <<LidoMultiPriceMultiLpARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initialize(_name: string, _symbol: string, _fee: uint256, _feeCollector: address) <<MultiLP>> -    _depositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _redeemHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> -    _setFee(_fee: uint256) <<MultiLP>> -    _setFeeCollector(_feeCollector: address) <<MultiLP>> -    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> -    _removeLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> -    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> -    _claimHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<MultiLP>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<MultiLP>> -    collectFees(): (fees: uint256) <<MultiLP>> -    updateTranchePrices(prices: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> -    updateTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> -    resetRemainingLiquidity() <<onlyOwner>> <<MultiPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoMultiPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<MultiLP>> +15 + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   discountToken: address <<MultiPriceARM>> +   liquidityToken: address <<MultiPriceARM>> +   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> +   tranches: uint16[15] <<MultiPriceARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   liquidityToken: address <<MultiLP>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   MAX_FEE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesCollected: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   totalAssetsCap: uint256 <<AccessControlLP>> +   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> +   PRICE_PRECISION: uint256 <<MultiPriceARM>> +   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _setFee(_fee: uint256) <<PerformanceFee>> +    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> +    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> +    _removeLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> +    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> +    _postClaimHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> +    collectFees(): (fees: uint256) <<PerformanceFee>> +    setLiquidityProviderCap(liquidityProvider: address, cap: uint256) <<onlyOwner>> <<AccessControlLP>> +    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>> +    updateTranchePrices(prices: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> +    updateTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> +    resetRemainingLiquidity() <<onlyOwner>> <<MultiPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoMultiPriceMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCalculated(mewFeesCollected: uint256, totalAssets: uint256) <<PerformanceFee>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> +    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> +    <<event>> LiquidityProviderCap(liquidityProvider: address, cap: uint256) <<AccessControlLP>> +    <<event>> TotalAssetsCap(cap: uint256) <<AccessControlLP>>    <<event>> TranchePricesUpdated(prices: uint256[5]) <<MultiPriceARM>>    <<event>> TrancheAllocationsUpdated(allocations: uint256[5]) <<MultiPriceARM>>    <<modifier>> onlyOwner() <<Ownable>> @@ -107,7 +121,7 @@    constructor(_liquidityToken: address) <<MultiLP>>    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>>    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> -    totalAssets(): uint256 <<MultiLP>> +    totalAssets(): uint256 <<LidoMultiPriceMultiLpARM>>    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>>    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>>    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoMultiPriceMultiLpARM>> diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 8194491..8395087 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -44,8 +44,6 @@ abstract contract AbstractARM is OwnableOperable { ) external virtual { uint256 amountOut = _swapExactTokensForTokens(inToken, outToken, amountIn, to); require(amountOut >= amountOutMin, "ARM: Insufficient output amount"); - - _accountFee(amountIn, amountOut); } /** @@ -78,8 +76,6 @@ abstract contract AbstractARM is OwnableOperable { require(amountOut >= amountOutMin, "ARM: Insufficient output amount"); - _accountFee(amountIn, amountOut); - amounts = new uint256[](2); amounts[0] = amountIn; amounts[1] = amountOut; @@ -106,8 +102,6 @@ abstract contract AbstractARM is OwnableOperable { uint256 amountIn = _swapTokensForExactTokens(inToken, outToken, amountOut, to); require(amountIn <= amountInMax, "ARM: Excess input amount"); - - _accountFee(amountIn, amountOut); } /** @@ -140,8 +134,6 @@ abstract contract AbstractARM is OwnableOperable { require(amountIn <= amountInMax, "ARM: Excess input amount"); - _accountFee(amountIn, amountOut); - amounts = new uint256[](2); amounts[0] = amountIn; amounts[1] = amountOut; @@ -157,9 +149,6 @@ abstract contract AbstractARM is OwnableOperable { virtual returns (uint256 amountIn); - /// @dev Default to no fee being collected - function _accountFee(uint256 amountIn, uint256 amountOut) internal virtual {} - function _inDeadline(uint256 deadline) internal view { require(deadline >= block.timestamp, "ARM: Deadline expired"); } diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index 49a5a45..abc7923 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -7,8 +7,9 @@ import {AbstractARM} from "./AbstractARM.sol"; import {FixedPriceARM} from "./FixedPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {MultiLP} from "./MultiLP.sol"; +import {PerformanceFee} from "./PerformanceFee.sol"; -contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, FixedPriceARM, LidoLiquidityManager { +contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, PerformanceFee, FixedPriceARM, LidoLiquidityManager { /// @param _stEth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the stETH Withdrawal contract @@ -34,8 +35,9 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, FixedPriceARM, Lido uint256 _fee, address _feeCollector ) external initializer { - _initialize(_name, _symbol, _fee, _feeCollector); - _setOperator(_operator); + _initOwnableOperable(_operator); + _initMultiLP(_name, _symbol); + _initPerformanceFee(_fee, _feeCollector); } /** @@ -54,14 +56,10 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, FixedPriceARM, Lido transferAmount = outToken == address(token0) ? amount + 2 : amount; } - function _assetsInWithdrawQueue() internal view override(LidoLiquidityManager, MultiLP) returns (uint256) { + function _assetsInWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { return LidoLiquidityManager._assetsInWithdrawQueue(); } - function _accountFee(uint256 amountIn, uint256 amountOut) internal override(MultiLP, AbstractARM) { - MultiLP._accountFee(amountIn, amountOut); - } - function _postDepositHook(uint256 assets) internal override { // do nothing } @@ -73,4 +71,8 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, FixedPriceARM, Lido function _postClaimHook(uint256 assets) internal override { // do nothing } + + function totalAssets() public view override(MultiLP, PerformanceFee) returns (uint256) { + return PerformanceFee.totalAssets(); + } } diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index 2acec00..4a7c53f 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -8,8 +8,16 @@ import {AbstractARM} from "./AbstractARM.sol"; import {MultiPriceARM} from "./MultiPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {MultiLP} from "./MultiLP.sol"; +import {PerformanceFee} from "./PerformanceFee.sol"; -contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, AccessControlLP, MultiPriceARM, LidoLiquidityManager { +contract LidoMultiPriceMultiLpARM is + Initializable, + MultiLP, + PerformanceFee, + AccessControlLP, + MultiPriceARM, + LidoLiquidityManager +{ /// @param _stEth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the stETH Withdrawal contract @@ -35,8 +43,9 @@ contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, AccessControlLP, Mu uint256 _fee, address _feeCollector ) external initializer { - _initialize(_name, _symbol, _fee, _feeCollector); - _setOperator(_operator); + _initOwnableOperable(_operator); + _initMultiLP(_name, _symbol); + _initPerformanceFee(_fee, _feeCollector); } /** @@ -55,27 +64,27 @@ contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, AccessControlLP, Mu transferAmount = outToken == address(token0) ? amount + 2 : amount; } - function _assetsInWithdrawQueue() internal view override(LidoLiquidityManager, MultiLP) returns (uint256) { + function _assetsInWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { return LidoLiquidityManager._assetsInWithdrawQueue(); } - function _accountFee(uint256 amountIn, uint256 amountOut) internal override(MultiLP, AbstractARM) { - MultiLP._accountFee(amountIn, amountOut); - } - - function _postDepositHook(uint256 assets) internal override(AccessControlLP, MultiLP) { - _addLiquidity(assets); + function _postDepositHook(uint256 assets) internal override(MultiLP, AccessControlLP) { + MultiPriceARM._addLiquidity(assets); AccessControlLP._postDepositHook(assets); } - function _postWithdrawHook(uint256 assets) internal override(AccessControlLP, MultiLP) { - _removeLiquidity(assets); + function _postWithdrawHook(uint256 assets) internal override(MultiLP, AccessControlLP) { + MultiPriceARM._removeLiquidity(assets); AccessControlLP._postWithdrawHook(assets); } function _postClaimHook(uint256 assets) internal override { - _addLiquidity(assets); + MultiPriceARM._addLiquidity(assets); + } + + function totalAssets() public view override(MultiLP, PerformanceFee) returns (uint256) { + return PerformanceFee.totalAssets(); } } diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 75ac1c6..6dcd471 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -9,18 +9,9 @@ import {AbstractARM} from "./AbstractARM.sol"; abstract contract MultiLP is AbstractARM, ERC20Upgradeable { uint256 public constant CLAIM_DELAY = 10 minutes; uint256 public constant MIN_TOTAL_SUPPLY = 1e12; - uint256 public constant MAX_FEE = 10000; // 100% address public constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; - address private immutable liquidityToken; - - /// @notice The account that receives the performance fee as shares - address public feeCollector; - /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent) - /// 10,000 = 100% performance fee - /// 500 = 5% performance fee - uint256 public fee; - uint256 public feesCollected; + address internal immutable liquidityToken; struct WithdrawalQueueMetadata { // cumulative total of all withdrawal requests included the ones that have already been claimed @@ -57,21 +48,15 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { event RedeemRequested(address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued); event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); - event FeeCollected(address feeCollector, uint256 fee); constructor(address _liquidityToken) { require(_liquidityToken == address(token0) || _liquidityToken == address(token1), "invalid liquidity token"); liquidityToken = _liquidityToken; } - function _initialize(string calldata _name, string calldata _symbol, uint256 _fee, address _feeCollector) - internal - { + function _initMultiLP(string calldata _name, string calldata _symbol) internal { __ERC20_init(_name, _symbol); - _setFee(_fee); - _setFeeCollector(_feeCollector); - // Transfer a small bit of liquidity from the intializer to this contract IERC20(liquidityToken).transferFrom(msg.sender, address(this), MIN_TOTAL_SUPPLY); @@ -85,6 +70,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { } function deposit(uint256 assets) external returns (uint256 shares) { + _preDepositHook(); + shares = convertToShares(assets); // Transfer the liquidity token from the sender to this contract @@ -96,6 +83,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { _postDepositHook(assets); } + function _preDepositHook() internal virtual; function _postDepositHook(uint256 assets) internal virtual; function previewRedeem(uint256 shares) public view returns (uint256 assets) { @@ -105,6 +93,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { /// @notice Request to redeem liquidity provider shares for liquidity assets /// @param shares The amount of shares the redeemer wants to burn for assets function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { + _preWithdrawHook(); + // burn redeemer's shares _burn(msg.sender, shares); @@ -130,6 +120,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { emit RedeemRequested(msg.sender, requestId, assets, queued); } + function _preWithdrawHook() internal virtual; function _postWithdrawHook(uint256 assets) internal virtual; function claimRedeem(uint256 requestId) external returns (uint256 assets) { @@ -194,10 +185,9 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { withdrawalQueueMetadata.claimable = SafeCast.toUint128(newClaimable); } - function totalAssets() public view returns (uint256) { + function totalAssets() public view virtual returns (uint256) { // valuing both assets 1:1 - return - token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue() - feesCollected; + return token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue(); } function convertToShares(uint256 assets) public view returns (uint256 shares) { @@ -210,43 +200,4 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { } function _assetsInWithdrawQueue() internal view virtual returns (uint256); - - function setFee(uint256 _fee) external onlyOwner { - _setFee(_fee); - } - - function setFeeCollector(address _feeCollector) external onlyOwner { - _setFeeCollector(_feeCollector); - } - - function _setFee(uint256 _fee) internal { - require(_fee <= MAX_FEE, "ARM: fee too high"); - fee = _fee; - } - - function _setFeeCollector(address _feeCollector) internal { - require(_feeCollector != address(0), "ARM: invalid fee collector"); - feeCollector = _feeCollector; - } - - /// @dev this assumes there is no exchange rate between the two assets. - /// An asset like rETH with WETH will not work with this function. - function _accountFee(uint256 amountIn, uint256 amountOut) internal virtual override { - uint256 discountAmount = amountIn > amountOut ? amountIn - amountOut : amountOut - amountIn; - feesCollected += (discountAmount * fee) / MAX_FEE; - } - - function collectFees() external returns (uint256 fees) { - require(msg.sender == feeCollector, "ARM: not fee collector"); - - fees = feesCollected; - require(fees <= IERC20(liquidityToken).balanceOf(address(this)), "ARM: insufficient liquidity"); - - // Reset fees collected in storage - feesCollected = 0; - - IERC20(liquidityToken).transfer(feeCollector, fees); - - emit FeeCollected(feeCollector, fees); - } } diff --git a/src/contracts/OwnableOperable.sol b/src/contracts/OwnableOperable.sol index 6dfcd2f..4a371d4 100644 --- a/src/contracts/OwnableOperable.sol +++ b/src/contracts/OwnableOperable.sol @@ -11,6 +11,10 @@ contract OwnableOperable is Ownable { event OperatorChanged(address newAdmin); + function _initOwnableOperable(address _operator) internal { + _setOperator(_operator); + } + /// @notice Set the account that can request and claim withdrawals. /// @param newOperator The address of the new operator. function setOperator(address newOperator) external onlyOwner { diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol new file mode 100644 index 0000000..8f805e6 --- /dev/null +++ b/src/contracts/PerformanceFee.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {MultiLP} from "./MultiLP.sol"; + +abstract contract PerformanceFee is MultiLP { + uint256 public constant MAX_FEE = 10000; // 100% + + /// @notice The account/contract that receives the performance fee + address public feeCollector; + /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent) + /// 10,000 = 100% performance fee + /// 500 = 5% performance fee + uint16 public fee; + /// @notice The performance fees collected. This is removed from the total assets. + uint112 public feesCollected; + /// @notice The total assets at the last performance time fees were calculated + uint128 public lastTotalAssets; + + event FeeCalculated(uint256 mewFeesCollected, uint256 totalAssets); + event FeeCollected(address indexed feeCollector, uint256 fee); + event FeeUpdated(uint256 fee); + event FeeCollectorUpdated(address indexed newFeeCollector); + + function _initPerformanceFee(uint256 _fee, address _feeCollector) internal { + _setFee(_fee); + _setFeeCollector(_feeCollector); + } + + function _preDepositHook() internal virtual override { + _calcFee(); + } + + function _preWithdrawHook() internal virtual override { + _calcFee(); + } + + function _calcFee() internal { + uint256 newTotalAssets = + token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue() - feesCollected; + + if (newTotalAssets > lastTotalAssets) { + uint256 newAssets = newTotalAssets - lastTotalAssets; + uint256 newFeesCollected = (newAssets * fee) / MAX_FEE; + + // Save the new values back to storage + feesCollected = SafeCast.toUint112(feesCollected + newFeesCollected); + lastTotalAssets = SafeCast.toUint128(newTotalAssets); + + emit FeeCalculated(newFeesCollected, newTotalAssets); + } + } + + function totalAssets() public view virtual override returns (uint256) { + // valuing both assets 1:1 + uint256 totalAssetsBeforeFees = + token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue() - feesCollected; + + // Calculate new fees from increased assets + uint256 newAssets = totalAssetsBeforeFees > lastTotalAssets ? totalAssetsBeforeFees - lastTotalAssets : 0; + uint256 uncollectedFees = (newAssets * fee) / MAX_FEE; + + return totalAssetsBeforeFees - uncollectedFees; + } + + /// @notice Owner sets the performance fee on increased assets + /// @param _fee The performance fee measured in basis points (1/100th of a percent) + /// 10,000 = 100% performance fee + /// 500 = 5% performance fee + function setFee(uint256 _fee) external onlyOwner { + _setFee(_fee); + } + + /// @notice Owner sets the account/contract that receives the performance fee + function setFeeCollector(address _feeCollector) external onlyOwner { + _setFeeCollector(_feeCollector); + } + + function _setFee(uint256 _fee) internal { + require(_fee <= MAX_FEE, "ARM: fee too high"); + + // Calculate fees up to this point using the old fee + _calcFee(); + + fee = SafeCast.toUint16(_fee); + + emit FeeUpdated(_fee); + } + + function _setFeeCollector(address _feeCollector) internal { + require(_feeCollector != address(0), "ARM: invalid fee collector"); + + feeCollector = _feeCollector; + + emit FeeCollectorUpdated(_feeCollector); + } + + /// @notice Transfer collected performance fees to the fee collector + function collectFees() external returns (uint256 fees) { + require(msg.sender == feeCollector, "ARM: not fee collector"); + + // Calculate any new fees up to this point + _calcFee(); + + fees = feesCollected; + require(fees <= IERC20(liquidityToken).balanceOf(address(this)), "ARM: insufficient liquidity"); + + // Reset fees collected in storage + feesCollected = 0; + + IERC20(liquidityToken).transfer(feeCollector, fees); + + emit FeeCollected(feeCollector, fees); + } +} From 6c49b61c463fdd779ee9b816b109ec305024a9c2 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 16 Sep 2024 22:53:02 +1000 Subject: [PATCH 021/196] Fixed saving of new total assets to after asset transfers --- src/contracts/PerformanceFee.sol | 63 ++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index 8f805e6..47411ec 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -6,21 +6,30 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {MultiLP} from "./MultiLP.sol"; +/** + * @title Added a performance fee to an ARM with Liquidity Providers (LP) + * @author Origin Protocol Inc + */ abstract contract PerformanceFee is MultiLP { - uint256 public constant MAX_FEE = 10000; // 100% + /// @notice The maximum performance fee that can be set + /// 10,000 = 100% performance fee + uint256 public constant MAX_FEE = 10000; - /// @notice The account/contract that receives the performance fee + /// @notice The account that can collect the performance fee address public feeCollector; /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent) /// 10,000 = 100% performance fee + /// 2,000 = 20% performance fee /// 500 = 5% performance fee uint16 public fee; - /// @notice The performance fees collected. This is removed from the total assets. - uint112 public feesCollected; - /// @notice The total assets at the last performance time fees were calculated + /// @notice The performance fees accrued but not collected. + /// This is removed from the total assets. + uint112 public feesAccrued; + /// @notice The total assets at the last time performance fees were calculated. + /// This can only go up so is a high watermark. uint128 public lastTotalAssets; - event FeeCalculated(uint256 mewFeesCollected, uint256 totalAssets); + event FeeCalculated(uint256 newFeesAccrued, uint256 totalAssets); event FeeCollected(address indexed feeCollector, uint256 fee); event FeeUpdated(uint256 fee); event FeeCollectorUpdated(address indexed newFeeCollector); @@ -38,32 +47,48 @@ abstract contract PerformanceFee is MultiLP { _calcFee(); } + /// @dev Save the new total assets after the deposit and performance fee accrued + function _postDepositHook(uint256) internal virtual override { + lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); + } + + /// @dev Save the new total assets after the withdrawal and performance fee accrued + function _postWithdrawHook(uint256) internal virtual override { + lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); + } + + /// @dev Calculate the performance fee based on the increase in total assets + /// Needs to be called before any action that changes the liquidity provider shares. eg deposit and redeem function _calcFee() internal { - uint256 newTotalAssets = - token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue() - feesCollected; + uint256 newTotalAssets = _rawTotalAssets(); + // Only collected a performance fee if the total assets have increased if (newTotalAssets > lastTotalAssets) { uint256 newAssets = newTotalAssets - lastTotalAssets; - uint256 newFeesCollected = (newAssets * fee) / MAX_FEE; + uint256 newFeesAccrued = (newAssets * fee) / MAX_FEE; // Save the new values back to storage - feesCollected = SafeCast.toUint112(feesCollected + newFeesCollected); - lastTotalAssets = SafeCast.toUint128(newTotalAssets); + feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); - emit FeeCalculated(newFeesCollected, newTotalAssets); + emit FeeCalculated(newFeesAccrued, newTotalAssets); } } function totalAssets() public view virtual override returns (uint256) { // valuing both assets 1:1 - uint256 totalAssetsBeforeFees = - token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue() - feesCollected; + uint256 totalAssetsBeforeFees = _rawTotalAssets(); // Calculate new fees from increased assets uint256 newAssets = totalAssetsBeforeFees > lastTotalAssets ? totalAssetsBeforeFees - lastTotalAssets : 0; - uint256 uncollectedFees = (newAssets * fee) / MAX_FEE; + uint256 accruedFees = (newAssets * fee) / MAX_FEE; + + return totalAssetsBeforeFees - accruedFees; + } - return totalAssetsBeforeFees - uncollectedFees; + /// @dev Calculate the total assets in the contract, withdrawal queue less accrued fees + function _rawTotalAssets() internal view returns (uint256) { + return + token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue() - feesAccrued; } /// @notice Owner sets the performance fee on increased assets @@ -98,18 +123,18 @@ abstract contract PerformanceFee is MultiLP { emit FeeCollectorUpdated(_feeCollector); } - /// @notice Transfer collected performance fees to the fee collector + /// @notice Transfer accrued performance fees to the fee collector function collectFees() external returns (uint256 fees) { require(msg.sender == feeCollector, "ARM: not fee collector"); // Calculate any new fees up to this point _calcFee(); - fees = feesCollected; + fees = feesAccrued; require(fees <= IERC20(liquidityToken).balanceOf(address(this)), "ARM: insufficient liquidity"); // Reset fees collected in storage - feesCollected = 0; + feesAccrued = 0; IERC20(liquidityToken).transfer(feeCollector, fees); From 6db1bb24c620deea406fc374f3ecd60174a334bd Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 16 Sep 2024 22:53:24 +1000 Subject: [PATCH 022/196] Natspec updates --- src/contracts/LidoMultiPriceMultiLpARM.sol | 20 +++++++++--- src/contracts/MultiLP.sol | 36 +++++++++++++++++++--- src/contracts/OethARM.sol | 6 +++- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index 4a7c53f..aaa16e6 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -10,6 +10,12 @@ import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {MultiLP} from "./MultiLP.sol"; import {PerformanceFee} from "./PerformanceFee.sol"; +/** + * @title Lido (stETH) Application Redemption Manager (ARM) + * @dev This implementation supports multiple Liquidity Providers (LPs) and multiple liquidity tranches + * with different prices. + * @author Origin Protocol Inc + */ contract LidoMultiPriceMultiLpARM is Initializable, MultiLP, @@ -18,9 +24,9 @@ contract LidoMultiPriceMultiLpARM is MultiPriceARM, LidoLiquidityManager { - /// @param _stEth The address of the stETH token + /// @param _stEth The address of Lido's stETH token /// @param _weth The address of the WETH token - /// @param _lidoWithdrawalQueue The address of the stETH Withdrawal contract + /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract constructor(address _stEth, address _weth, address _lidoWithdrawalQueue) AbstractARM(_stEth, _weth) MultiLP(_weth) @@ -31,11 +37,11 @@ contract LidoMultiPriceMultiLpARM is /// @notice Initialize the contract. /// @param _name The name of the liquidity provider (LP) token. /// @param _symbol The symbol of the liquidity provider (LP) token. - /// @param _operator The address of the account that can request and claim OETH withdrawals. + /// @param _operator The address of the account that can request and claim Lido withdrawals. /// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent). /// 10,000 = 100% performance fee /// 500 = 5% performance fee - /// @param _feeCollector The account that receives the performance fee as shares + /// @param _feeCollector The account that can collect the performance fee function initialize( string calldata _name, string calldata _symbol, @@ -69,22 +75,28 @@ contract LidoMultiPriceMultiLpARM is } function _postDepositHook(uint256 assets) internal override(MultiLP, AccessControlLP) { + // Add assets to the liquidity tranches MultiPriceARM._addLiquidity(assets); + // Check the LP can deposit the assets AccessControlLP._postDepositHook(assets); } function _postWithdrawHook(uint256 assets) internal override(MultiLP, AccessControlLP) { + // Remove assets from the liquidity tranches MultiPriceARM._removeLiquidity(assets); + // Update the LP's assets AccessControlLP._postWithdrawHook(assets); } function _postClaimHook(uint256 assets) internal override { + // Add assets to the liquidity tranches MultiPriceARM._addLiquidity(assets); } function totalAssets() public view override(MultiLP, PerformanceFee) returns (uint256) { + // Return the total assets less the collected performance fee return PerformanceFee.totalAssets(); } } diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 6dcd471..0b54371 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -6,11 +6,19 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AbstractARM} from "./AbstractARM.sol"; +/** + * @title Abstract support to an ARM for multiple Liquidity Providers (LP) + * @author Origin Protocol Inc + */ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { + /// @notice The delay before a withdrawal request can be claimed in seconds uint256 public constant CLAIM_DELAY = 10 minutes; - uint256 public constant MIN_TOTAL_SUPPLY = 1e12; - address public constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; + /// @dev The amount of shares that are minted to a dead address on initalization + uint256 internal constant MIN_TOTAL_SUPPLY = 1e12; + /// @dev The address with no know private key that the initial shares are minted to + address internal constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; + /// @notice The address of the liquidity token. eg WETH address internal immutable liquidityToken; struct WithdrawalQueueMetadata { @@ -44,7 +52,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { } /// @notice Mapping of withdrawal request indices to the user withdrawal request data - mapping(uint256 => WithdrawalRequest) public withdrawalRequests; + mapping(uint256 requestId => WithdrawalRequest) public withdrawalRequests; event RedeemRequested(address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued); event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); @@ -54,6 +62,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { liquidityToken = _liquidityToken; } + /// @dev called by the concrete contract's `initialize` function function _initMultiLP(string calldata _name, string calldata _symbol) internal { __ERC20_init(_name, _symbol); @@ -65,10 +74,17 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { _mint(DEAD_ACCOUNT, MIN_TOTAL_SUPPLY); } + /// @notice Preview the amount of shares that would be minted for a given amount of assets + /// @param assets The amount of liquidity assets to deposit + /// @return shares The amount of shares that would be minted function previewDeposit(uint256 assets) public view returns (uint256 shares) { shares = convertToShares(assets); } + /// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares. + /// The caller needs to have approved the contract to transfer the assets. + /// @param assets The amount of liquidity assets to deposit + /// @return shares The amount of shares that were minted function deposit(uint256 assets) external returns (uint256 shares) { _preDepositHook(); @@ -86,12 +102,17 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { function _preDepositHook() internal virtual; function _postDepositHook(uint256 assets) internal virtual; + /// @notice Preview the amount of assets that would be received for burning a given amount of shares + /// @param shares The amount of shares to burn + /// @return assets The amount of liquidity assets that would be received function previewRedeem(uint256 shares) public view returns (uint256 assets) { assets = convertToAssets(shares); } /// @notice Request to redeem liquidity provider shares for liquidity assets - /// @param shares The amount of shares the redeemer wants to burn for assets + /// @param shares The amount of shares the redeemer wants to burn for liquidity assets + /// @return requestId The index of the withdrawal request + /// @return assets The amount of liquidity assets that will be claimable by the redeemer function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { _preWithdrawHook(); @@ -123,6 +144,9 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { function _preWithdrawHook() internal virtual; function _postWithdrawHook(uint256 assets) internal virtual; + /// @notice Claim liquidity assets from a previous withdrawal request after the claim delay has passed + /// @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) { if (withdrawalRequests[requestId].queued > withdrawalQueueMetadata.claimable) { // Add any WETH from the Dripper to the withdrawal queue @@ -185,19 +209,23 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { withdrawalQueueMetadata.claimable = SafeCast.toUint128(newClaimable); } + /// @notice The total amount of assets in the contract including the withdrawal queue function totalAssets() public view virtual returns (uint256) { // valuing both assets 1:1 return token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue(); } + /// @notice Calculates the amount of shares for a given amount of liquidity assets function convertToShares(uint256 assets) public view returns (uint256 shares) { uint256 _totalAssets = totalAssets(); shares = (_totalAssets == 0) ? assets : (assets * totalSupply()) / _totalAssets; } + /// @notice Calculates the amount of liquidity assets for a given amount of shares function convertToAssets(uint256 shares) public view returns (uint256 assets) { assets = (shares * totalAssets()) / totalSupply(); } + /// @dev Hook for calculating the amount of assets in a withdrawal queue function _assetsInWithdrawQueue() internal view virtual returns (uint256); } diff --git a/src/contracts/OethARM.sol b/src/contracts/OethARM.sol index 6198ddd..64174ff 100644 --- a/src/contracts/OethARM.sol +++ b/src/contracts/OethARM.sol @@ -8,6 +8,10 @@ import {PeggedARM} from "./PeggedARM.sol"; import {OwnerLP} from "./OwnerLP.sol"; import {OethLiquidityManager} from "./OethLiquidityManager.sol"; +/** + * @title Origin Ether (OETH) Application Redemption Manager (ARM) + * @author Origin Protocol Inc + */ contract OethARM is Initializable, OwnerLP, PeggedARM, OethLiquidityManager { /// @param _oeth The address of the OETH token that is being swapped into this contract. /// @param _weth The address of the WETH token that is being swapped out of this contract. @@ -19,7 +23,7 @@ contract OethARM is Initializable, OwnerLP, PeggedARM, OethLiquidityManager { {} /// @notice Initialize the contract. - /// @param _operator The address of the account that can request and claim OETH withdrawals. + /// @param _operator The address of the account that can request and claim OETH withdrawals from the OETH Vault. function initialize(address _operator) external initializer { _setOperator(_operator); _approvals(); From 4dc44d09f0a4a449c868e2d61f13fc843a26b8ee Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 16 Sep 2024 22:55:43 +1000 Subject: [PATCH 023/196] Generated new contract diagrams --- docs/LidoFixedPriceMultiLpARMSquashed.svg | 111 +++++++++---------- docs/LidoMultiPriceMultiLpARMSquashed.svg | 125 +++++++++++----------- 2 files changed, 119 insertions(+), 117 deletions(-) diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index df6ef89..6107d66 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,64 +4,65 @@ - - + + UmlClassDiagram - + 13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   liquidityToken: address <<MultiLP>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   MAX_FEE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesCollected: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityToken: address <<MultiLP>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   MAX_FEE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _rawTotalAssets(): uint256 <<PerformanceFee>>    _setFee(_fee: uint256) <<PerformanceFee>>    _setFeeCollector(_feeCollector: address) <<PerformanceFee>>    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> @@ -93,7 +94,7 @@    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>>    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>>    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(mewFeesCollected: uint256, totalAssets: uint256) <<PerformanceFee>> +    <<event>> FeeCalculated(newFeesAccrued: uint256, totalAssets: uint256) <<PerformanceFee>>    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>>    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>>    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index bc0f954..51e8ed1 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -4,71 +4,72 @@ - - + + UmlClassDiagram - + 15 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   discountToken: address <<MultiPriceARM>> -   liquidityToken: address <<MultiPriceARM>> -   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> -   tranches: uint16[15] <<MultiPriceARM>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   liquidityToken: address <<MultiLP>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   MAX_FEE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesCollected: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   totalAssetsCap: uint256 <<AccessControlLP>> -   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> -   PRICE_PRECISION: uint256 <<MultiPriceARM>> -   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   discountToken: address <<MultiPriceARM>> +   liquidityToken: address <<MultiPriceARM>> +   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> +   tranches: uint16[15] <<MultiPriceARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityToken: address <<MultiLP>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   MAX_FEE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   totalAssetsCap: uint256 <<AccessControlLP>> +   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> +   PRICE_PRECISION: uint256 <<MultiPriceARM>> +   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _rawTotalAssets(): uint256 <<PerformanceFee>>    _setFee(_fee: uint256) <<PerformanceFee>>    _setFeeCollector(_feeCollector: address) <<PerformanceFee>>    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> @@ -106,7 +107,7 @@    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>>    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>>    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(mewFeesCollected: uint256, totalAssets: uint256) <<PerformanceFee>> +    <<event>> FeeCalculated(newFeesAccrued: uint256, totalAssets: uint256) <<PerformanceFee>>    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>>    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>>    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> From 3a19ed850410833cb6519273878a7be5a40f695b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 10:55:08 +1000 Subject: [PATCH 024/196] Added _gap in inherited contracts --- src/contracts/AbstractARM.sol | 2 ++ src/contracts/AccessControlLP.sol | 2 ++ src/contracts/AccessControlShares.sol | 2 ++ src/contracts/FixedPriceARM.sol | 2 ++ src/contracts/LidoLiquidityManager.sol | 2 ++ src/contracts/MultiLP.sol | 2 ++ src/contracts/MultiPriceARM.sol | 2 ++ src/contracts/PerformanceFee.sol | 2 ++ 8 files changed, 16 insertions(+) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 8395087..f56e200 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -14,6 +14,8 @@ abstract contract AbstractARM is OwnableOperable { /// token1 is also compatible with the Uniswap V2 Router interface. IERC20 public immutable token1; + uint256[50] private _gap; + constructor(address _inputToken, address _outputToken1) { require(IERC20(_inputToken).decimals() == 18); require(IERC20(_outputToken1).decimals() == 18); diff --git a/src/contracts/AccessControlLP.sol b/src/contracts/AccessControlLP.sol index dcacbcd..c756e0f 100644 --- a/src/contracts/AccessControlLP.sol +++ b/src/contracts/AccessControlLP.sol @@ -7,6 +7,8 @@ abstract contract AccessControlLP is MultiLP { uint256 public totalAssetsCap; mapping(address lp => uint256 cap) public liquidityProviderCaps; + uint256[50] private _gap; + event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); event TotalAssetsCap(uint256 cap); diff --git a/src/contracts/AccessControlShares.sol b/src/contracts/AccessControlShares.sol index c5288b1..07ef4d7 100644 --- a/src/contracts/AccessControlShares.sol +++ b/src/contracts/AccessControlShares.sol @@ -7,6 +7,8 @@ abstract contract AccessControlShares is MultiLP { uint256 public totalSupplyCap; mapping(address lp => uint256 shares) public liquidityProviderCaps; + uint256[50] private _gap; + event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); event TotalSupplyCap(uint256 shares); diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol index 14dc3db..0a2acdb 100644 --- a/src/contracts/FixedPriceARM.sol +++ b/src/contracts/FixedPriceARM.sol @@ -27,6 +27,8 @@ abstract contract FixedPriceARM is AbstractARM { /// @dev Minimum funds to allow operator to price changes uint256 public minimumFunds; + uint256[50] private _gap; + event TraderateChanged(uint256 traderate0, uint256 traderate1); function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to) diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 5956be4..324225e 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -11,6 +11,8 @@ abstract contract LidoLiquidityManager is OwnableOperable { uint256 public outstandingEther; + uint256[50] private _gap; + constructor(address _steth, address _weth, address _lidoWithdrawalQueue) { steth = IERC20(_steth); weth = IWETH(_weth); diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 0b54371..01380e8 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -54,6 +54,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { /// @notice Mapping of withdrawal request indices to the user withdrawal request data mapping(uint256 requestId => WithdrawalRequest) public withdrawalRequests; + uint256[50] private _gap; + event RedeemRequested(address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued); event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol index 6a5f608..cabea49 100644 --- a/src/contracts/MultiPriceARM.sol +++ b/src/contracts/MultiPriceARM.sol @@ -33,6 +33,8 @@ abstract contract MultiPriceARM is AbstractARM { /// @dev Five tranches are used as they fit in a single storage slot uint16[15] private tranches; + uint256[50] private _gap; + event TranchePricesUpdated(uint256[5] prices); event TrancheAllocationsUpdated(uint256[5] allocations); diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index 47411ec..92c09f1 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -29,6 +29,8 @@ abstract contract PerformanceFee is MultiLP { /// This can only go up so is a high watermark. uint128 public lastTotalAssets; + uint256[50] private _gap; + event FeeCalculated(uint256 newFeesAccrued, uint256 totalAssets); event FeeCollected(address indexed feeCollector, uint256 fee); event FeeUpdated(uint256 fee); From c44d4dea70f981ed9dfbc8d49b87b170b75f55f6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 11:05:13 +1000 Subject: [PATCH 025/196] Changed timestamp that was storing the request time in WithdrawalRequest to claimTimestamp Fixed compilation of contracts using PerformanceFee --- src/contracts/LidoFixedPriceMultiLpARM.sol | 10 ++++++---- src/contracts/LidoMultiPriceMultiLpARM.sol | 10 ++++++++-- src/contracts/MultiLP.sol | 14 +++++++++----- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index abc7923..ccb299e 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -60,12 +60,14 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, PerformanceFee, Fix return LidoLiquidityManager._assetsInWithdrawQueue(); } - function _postDepositHook(uint256 assets) internal override { - // do nothing + function _postDepositHook(uint256 assets) internal override(MultiLP, PerformanceFee) { + // Save the new total assets after the deposit and performance fee accrued + PerformanceFee._postDepositHook(assets); } - function _postWithdrawHook(uint256 assets) internal override { - // do nothing + function _postWithdrawHook(uint256 assets) internal override(MultiLP, PerformanceFee) { + // Save the new total assets after the withdrawal and performance fee accrued + PerformanceFee._postWithdrawHook(assets); } function _postClaimHook(uint256 assets) internal override { diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index aaa16e6..5022e48 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -74,20 +74,26 @@ contract LidoMultiPriceMultiLpARM is return LidoLiquidityManager._assetsInWithdrawQueue(); } - function _postDepositHook(uint256 assets) internal override(MultiLP, AccessControlLP) { + function _postDepositHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { // Add assets to the liquidity tranches MultiPriceARM._addLiquidity(assets); // Check the LP can deposit the assets AccessControlLP._postDepositHook(assets); + + // Store the new total assets after the deposit and performance fee accrued + PerformanceFee._postDepositHook(assets); } - function _postWithdrawHook(uint256 assets) internal override(MultiLP, AccessControlLP) { + function _postWithdrawHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { // Remove assets from the liquidity tranches MultiPriceARM._removeLiquidity(assets); // Update the LP's assets AccessControlLP._postWithdrawHook(assets); + + // Store the new total assets after the withdrawal and performance fee accrued + PerformanceFee._postWithdrawHook(assets); } function _postClaimHook(uint256 assets) internal override { diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 01380e8..8680a3d 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -43,7 +43,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { struct WithdrawalRequest { address withdrawer; bool claimed; - uint40 timestamp; // timestamp of the withdrawal request + // When the withdrawal can be claimed + uint40 claimTimestamp; // Amount of assets to withdraw uint128 assets; // cumulative total of all withdrawal requests including this one. @@ -56,7 +57,9 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { uint256[50] private _gap; - event RedeemRequested(address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued); + event RedeemRequested( + address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp + ); event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); constructor(address _liquidityToken) { @@ -126,6 +129,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { requestId = withdrawalQueueMetadata.nextWithdrawalIndex; uint256 queued = withdrawalQueueMetadata.queued + assets; + uint256 claimTimestamp = block.timestamp + CLAIM_DELAY; // Store the next withdrawal request withdrawalQueueMetadata.nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); @@ -133,14 +137,14 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { withdrawalRequests[requestId] = WithdrawalRequest({ withdrawer: msg.sender, claimed: false, - timestamp: uint40(block.timestamp), + claimTimestamp: uint40(claimTimestamp), assets: SafeCast.toUint128(assets), queued: SafeCast.toUint128(queued) }); _postWithdrawHook(assets); - emit RedeemRequested(msg.sender, requestId, assets, queued); + emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); } function _preWithdrawHook() internal virtual; @@ -159,7 +163,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { WithdrawalRequest memory request = withdrawalRequests[requestId]; WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; - require(request.timestamp + CLAIM_DELAY <= block.timestamp, "Claim delay not met"); + require(request.claimTimestamp <= block.timestamp, "Claim delay not met"); // If there isn't enough reserved liquidity in the queue to claim require(request.queued <= queue.claimable, "Queue pending liquidity"); require(request.withdrawer == msg.sender, "Not requester"); From fd2bc10c8dc4ed52b7f6d0bf0bea7e151a580341 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 15:08:51 +1000 Subject: [PATCH 026/196] Refactor PerformanceFee Renamed liquidityToken to liquidityAsset Renamed _assetsInWithdrawalQueue to _externalWithdrawQueue --- src/contracts/LidoFixedPriceMultiLpARM.sol | 4 +- src/contracts/LidoLiquidityManager.sol | 2 +- src/contracts/LidoMultiPriceMultiLpARM.sol | 4 +- src/contracts/MultiLP.sol | 55 +++++++++++++--------- src/contracts/PerformanceFee.sol | 51 +++++++++++--------- 5 files changed, 69 insertions(+), 47 deletions(-) diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index ccb299e..c318e14 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -56,8 +56,8 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, PerformanceFee, Fix transferAmount = outToken == address(token0) ? amount + 2 : amount; } - function _assetsInWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { - return LidoLiquidityManager._assetsInWithdrawQueue(); + function _externalWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { + return LidoLiquidityManager._externalWithdrawQueue(); } function _postDepositHook(uint256 assets) internal override(MultiLP, PerformanceFee) { diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 324225e..8cd8119 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -70,7 +70,7 @@ abstract contract LidoLiquidityManager is OwnableOperable { function _postClaimHook(uint256 assets) internal virtual; - function _assetsInWithdrawQueue() internal view virtual returns (uint256) { + function _externalWithdrawQueue() internal view virtual returns (uint256 assets) { return outstandingEther; } diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index 5022e48..5b4364e 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -70,8 +70,8 @@ contract LidoMultiPriceMultiLpARM is transferAmount = outToken == address(token0) ? amount + 2 : amount; } - function _assetsInWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { - return LidoLiquidityManager._assetsInWithdrawQueue(); + function _externalWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { + return LidoLiquidityManager._externalWithdrawQueue(); } function _postDepositHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 8680a3d..54eea63 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -18,8 +18,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { /// @dev The address with no know private key that the initial shares are minted to address internal constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; - /// @notice The address of the liquidity token. eg WETH - address internal immutable liquidityToken; + /// @notice The address of the asset that is used to add and remove liquidity. eg WETH + address internal immutable liquidityAsset; struct WithdrawalQueueMetadata { // cumulative total of all withdrawal requests included the ones that have already been claimed @@ -62,9 +62,9 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { ); event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); - constructor(address _liquidityToken) { - require(_liquidityToken == address(token0) || _liquidityToken == address(token1), "invalid liquidity token"); - liquidityToken = _liquidityToken; + constructor(address _liquidityAsset) { + require(_liquidityAsset == address(token0) || _liquidityAsset == address(token1), "invalid liquidity asset"); + liquidityAsset = _liquidityAsset; } /// @dev called by the concrete contract's `initialize` function @@ -72,7 +72,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { __ERC20_init(_name, _symbol); // Transfer a small bit of liquidity from the intializer to this contract - IERC20(liquidityToken).transferFrom(msg.sender, address(this), MIN_TOTAL_SUPPLY); + 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 // This avoids donation attacks when there are no assets in the ARM contract @@ -95,8 +95,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { shares = convertToShares(assets); - // Transfer the liquidity token from the sender to this contract - IERC20(liquidityToken).transferFrom(msg.sender, address(this), assets); + // Transfer the liquidity asset from the sender to this contract + IERC20(liquidityAsset).transferFrom(msg.sender, address(this), assets); // mint shares _mint(msg.sender, shares); @@ -178,8 +178,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { emit RedeemClaimed(msg.sender, requestId, assets); - // transfer the liquidity token to the withdrawer - IERC20(liquidityToken).transfer(msg.sender, assets); + // transfer the liquidity asset to the withdrawer + IERC20(liquidityAsset).transfer(msg.sender, assets); } /// @dev Adds liquidity to the withdrawal queue if there is a funding shortfall. @@ -194,13 +194,13 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { return 0; } - uint256 liquidityBalance = IERC20(liquidityToken).balanceOf(address(this)); + uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this)); // Of the claimable withdrawal requests, how much is unclaimed? - // That is, the amount of the liquidity token that is currently allocated for the withdrawal queue + // That is, the amount of the liquidity assets that is currently allocated for the withdrawal queue uint256 allocatedLiquidity = queue.claimable - queue.claimed; - // If there is no unallocated liquidity token then there is nothing to add to the queue + // If there is no unallocated liquidity assets then there is nothing to add to the queue if (liquidityBalance <= allocatedLiquidity) { return 0; } @@ -215,16 +215,28 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { withdrawalQueueMetadata.claimable = SafeCast.toUint128(newClaimable); } - /// @notice The total amount of assets in the contract including the withdrawal queue - function totalAssets() public view virtual returns (uint256) { - // valuing both assets 1:1 - return token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue(); + /// @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) { + // Get the assets in the ARM and external withdrawal queue + assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); + + WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; + + // If the ARM becomes insolvent enough that the total value in the ARM and external withdrawal queue + // is less than the outstanding withdrawals. + if (assets + queue.claimed < queue.queued) { + return 0; + } + + // Need to remove the liquidity assets that have been reserved for the withdrawal queue + return assets + queue.claimed - queue.queued; } /// @notice Calculates the amount of shares for a given amount of liquidity assets function convertToShares(uint256 assets) public view returns (uint256 shares) { - uint256 _totalAssets = totalAssets(); - shares = (_totalAssets == 0) ? assets : (assets * totalSupply()) / _totalAssets; + uint256 totalAssetsMem = totalAssets(); + shares = (totalAssetsMem == 0) ? assets : (assets * totalSupply()) / totalAssetsMem; } /// @notice Calculates the amount of liquidity assets for a given amount of shares @@ -232,6 +244,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { assets = (shares * totalAssets()) / totalSupply(); } - /// @dev Hook for calculating the amount of assets in a withdrawal queue - function _assetsInWithdrawQueue() internal view virtual returns (uint256); + /// @dev Hook for calculating the amount of assets in an external withdrawal queue like Lido or OETH + /// This is not the ARM's withdrawal queue + function _externalWithdrawQueue() internal view virtual returns (uint256 assets); } diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index 92c09f1..f805f2c 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -31,7 +31,7 @@ abstract contract PerformanceFee is MultiLP { uint256[50] private _gap; - event FeeCalculated(uint256 newFeesAccrued, uint256 totalAssets); + event FeeCalculated(uint256 newFeesAccrued, uint256 assetIncrease); event FeeCollected(address indexed feeCollector, uint256 fee); event FeeUpdated(uint256 fee); event FeeCollectorUpdated(address indexed newFeeCollector); @@ -41,10 +41,14 @@ abstract contract PerformanceFee is MultiLP { _setFeeCollector(_feeCollector); } + /// @dev Calculate the performance fee based on the increase in total assets before + /// the liquidity asset from the deposit is transferred into the ARM function _preDepositHook() internal virtual override { _calcFee(); } + /// @dev Calculate the performance fee based on the increase in total assets before + /// the liquidity asset from the redeem is reserved for the ARM withdrawal queue function _preWithdrawHook() internal virtual override { _calcFee(); } @@ -64,33 +68,36 @@ abstract contract PerformanceFee is MultiLP { function _calcFee() internal { uint256 newTotalAssets = _rawTotalAssets(); - // Only collected a performance fee if the total assets have increased - if (newTotalAssets > lastTotalAssets) { - uint256 newAssets = newTotalAssets - lastTotalAssets; - uint256 newFeesAccrued = (newAssets * fee) / MAX_FEE; + // Do not accrued a performance fee if the total assets has decreased + if (newTotalAssets <= lastTotalAssets) return; - // Save the new values back to storage - feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); + uint256 assetIncrease = newTotalAssets - lastTotalAssets; + uint256 newFeesAccrued = (assetIncrease * fee) / MAX_FEE; - emit FeeCalculated(newFeesAccrued, newTotalAssets); - } + // Save the new accrued fees back to storage + feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); + + emit FeeCalculated(newFeesAccrued, assetIncrease); } function totalAssets() public view virtual override returns (uint256) { - // valuing both assets 1:1 uint256 totalAssetsBeforeFees = _rawTotalAssets(); - // Calculate new fees from increased assets - uint256 newAssets = totalAssetsBeforeFees > lastTotalAssets ? totalAssetsBeforeFees - lastTotalAssets : 0; - uint256 accruedFees = (newAssets * fee) / MAX_FEE; + // If the total assets have decreased, then we don't charge a performance fee + if (totalAssetsBeforeFees <= lastTotalAssets) return totalAssetsBeforeFees; + + // Calculate the increase in assets since the last time fees were calculated + uint256 assetIncrease = totalAssetsBeforeFees - lastTotalAssets; - return totalAssetsBeforeFees - accruedFees; + // Calculate the performance fee and remove from the total assets before new fees are removed + return totalAssetsBeforeFees - ((assetIncrease * fee) / MAX_FEE); } - /// @dev Calculate the total assets in the contract, withdrawal queue less accrued fees + /// @dev Calculate the total assets in the ARM, external withdrawal queue, + /// less liquidity assets reserved for the ARM's withdrawal queue and past accrued fees. + /// The accrued fees are from the last time fees were calculated. function _rawTotalAssets() internal view returns (uint256) { - return - token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _assetsInWithdrawQueue() - feesAccrued; + return super.totalAssets() - feesAccrued; } /// @notice Owner sets the performance fee on increased assets @@ -126,19 +133,21 @@ abstract contract PerformanceFee is MultiLP { } /// @notice Transfer accrued performance fees to the fee collector + /// This requires enough liquidity assets in the ARM to cover the accrued fees. function collectFees() external returns (uint256 fees) { require(msg.sender == feeCollector, "ARM: not fee collector"); - // Calculate any new fees up to this point + // Accrued all fees up to this point _calcFee(); + // Read the updated accrued fees from storage fees = feesAccrued; - require(fees <= IERC20(liquidityToken).balanceOf(address(this)), "ARM: insufficient liquidity"); + require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); - // Reset fees collected in storage + // Reset the accrued fees in storage feesAccrued = 0; - IERC20(liquidityToken).transfer(feeCollector, fees); + IERC20(liquidityAsset).transfer(feeCollector, fees); emit FeeCollected(feeCollector, fees); } From 09e028399043de60a0b47ddada663bc6ef4f9bb4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 15:52:27 +1000 Subject: [PATCH 027/196] Generated latest contract diagrams --- docs/LidoFixedPriceMultiLpARMSquashed.svg | 35 ++++++----- docs/LidoMultiPriceMultiLpARMSquashed.svg | 48 +++++++------- docs/LidoOwnerLpARMHierarchy.svg | 71 ++++++++++----------- docs/LidoOwnerLpARMSquashed.svg | 76 ++++++++++++----------- docs/OEthARMHierarchy.svg | 56 ++++++++--------- docs/OEthARMSquashed.svg | 33 +++++----- 6 files changed, 168 insertions(+), 151 deletions(-) diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index 6107d66..1bbbcb8 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,25 +4,30 @@ - - + + UmlClassDiagram - + 13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<MultiLP>> +   _gap: uint256[50] <<PerformanceFee>> +   _gap: uint256[50] <<FixedPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> Internal:   OWNER_SLOT: bytes32 <<Ownable>>   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>>   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityToken: address <<MultiLP>> +   liquidityAsset: address <<MultiLP>>   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> Public:   operator: address <<OwnableOperable>> @@ -59,7 +64,7 @@    _preWithdrawHook() <<PerformanceFee>>    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>>    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> +    _externalWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>>    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>>    _calcFee() <<PerformanceFee>>    _rawTotalAssets(): uint256 <<PerformanceFee>> @@ -92,9 +97,9 @@ Public:    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>>    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>>    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(newFeesAccrued: uint256, totalAssets: uint256) <<PerformanceFee>> +    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>>    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>>    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>>    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> @@ -103,7 +108,7 @@    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>>    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> -    constructor(_liquidityToken: address) <<MultiLP>> +    constructor(_liquidityAsset: address) <<MultiLP>>    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>>    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>>    totalAssets(): uint256 <<LidoFixedPriceMultiLpARM>> diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index 51e8ed1..3fd290b 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -4,31 +4,37 @@ - - + + UmlClassDiagram - + 15 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   discountToken: address <<MultiPriceARM>> -   liquidityToken: address <<MultiPriceARM>> -   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> -   tranches: uint16[15] <<MultiPriceARM>> + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<MultiLP>> +   _gap: uint256[50] <<PerformanceFee>> +   _gap: uint256[50] <<AccessControlLP>> +   discountToken: address <<MultiPriceARM>> +   liquidityToken: address <<MultiPriceARM>> +   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> +   tranches: uint16[15] <<MultiPriceARM>> +   _gap: uint256[50] <<MultiPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> Internal:   OWNER_SLOT: bytes32 <<Ownable>>   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>>   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityToken: address <<MultiLP>> +   liquidityAsset: address <<MultiLP>> Public:   operator: address <<OwnableOperable>>   token0: IERC20 <<AbstractARM>> @@ -66,7 +72,7 @@    _preWithdrawHook() <<PerformanceFee>>    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>>    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _assetsInWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> +    _externalWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>>    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>>    _calcFee() <<PerformanceFee>>    _rawTotalAssets(): uint256 <<PerformanceFee>> @@ -105,9 +111,9 @@ Public:    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>>    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256) <<MultiLP>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>>    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(newFeesAccrued: uint256, totalAssets: uint256) <<PerformanceFee>> +    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>>    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>>    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>>    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> @@ -119,7 +125,7 @@    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>>    constructor(_discountToken: address, _liquidityToken: address) <<MultiPriceARM>> -    constructor(_liquidityToken: address) <<MultiLP>> +    constructor(_liquidityAsset: address) <<MultiLP>>    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>>    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>>    totalAssets(): uint256 <<LidoMultiPriceMultiLpARM>> diff --git a/docs/LidoOwnerLpARMHierarchy.svg b/docs/LidoOwnerLpARMHierarchy.svg index 84952db..542284d 100644 --- a/docs/LidoOwnerLpARMHierarchy.svg +++ b/docs/LidoOwnerLpARMHierarchy.svg @@ -17,95 +17,96 @@ AbstractARM ../src/contracts/AbstractARM.sol - + -20 +24 OwnableOperable ../src/contracts/OwnableOperable.sol - + -0->20 +0->24 - + -1 +3 <<Abstract>> FixedPriceARM ../src/contracts/FixedPriceARM.sol - + -1->0 +3->0 - + -11 - -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol +14 + +<<Abstract>> +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol - + -11->20 - - +14->24 + + - + -13 +16 LidoOwnerLpARM ../src/contracts/LidoOwnerLpARM.sol - + -13->1 +16->3 - + -13->11 - - +16->14 + + - + -21 +25 <<Abstract>> OwnerLP ../src/contracts/OwnerLP.sol - + -13->21 +16->25 - + -19 +23 Ownable ../src/contracts/Ownable.sol - + -20->19 +24->23 - + -21->19 +25->23 diff --git a/docs/LidoOwnerLpARMSquashed.svg b/docs/LidoOwnerLpARMSquashed.svg index 81965ae..abfdfb3 100644 --- a/docs/LidoOwnerLpARMSquashed.svg +++ b/docs/LidoOwnerLpARMSquashed.svg @@ -4,47 +4,51 @@ - - + + UmlClassDiagram - - + + -13 - -LidoOwnerLpARM -../src/contracts/LidoOwnerLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> +16 + +LidoOwnerLpARM +../src/contracts/LidoOwnerLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<FixedPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>>    _setOperator(newOperator: address) <<OwnableOperable>>    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>>    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _accountFee(amountIn: uint256, amountOut: uint256) <<AbstractARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoOwnerLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -    _assetsInWithdrawQueue(): uint256 <<LidoLiquidityManager>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoOwnerLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +    _postClaimHook(assets: uint256) <<LidoOwnerLpARM>> +    _externalWithdrawQueue(): (assets: uint256) <<LidoLiquidityManager>> External:    <<payable>> null() <<LidoLiquidityManager>>    owner(): address <<Ownable>> diff --git a/docs/OEthARMHierarchy.svg b/docs/OEthARMHierarchy.svg index d31a4e6..2f99897 100644 --- a/docs/OEthARMHierarchy.svg +++ b/docs/OEthARMHierarchy.svg @@ -17,95 +17,95 @@ AbstractARM ../src/contracts/AbstractARM.sol - + -20 +24 OwnableOperable ../src/contracts/OwnableOperable.sol - + -0->20 +0->24 - + -17 +21 OethARM ../src/contracts/OethARM.sol - + -18 +22 OethLiquidityManager ../src/contracts/OethLiquidityManager.sol - + -17->18 +21->22 - + -21 +25 <<Abstract>> OwnerLP ../src/contracts/OwnerLP.sol - + -17->21 +21->25 - + -22 +26 <<Abstract>> PeggedARM ../src/contracts/PeggedARM.sol - + -17->22 +21->26 - + -18->20 +22->24 - + -19 +23 Ownable ../src/contracts/Ownable.sol - + -20->19 +24->23 - + -21->19 +25->23 - + -22->0 +26->0 diff --git a/docs/OEthARMSquashed.svg b/docs/OEthARMSquashed.svg index 4b578ac..516cd18 100644 --- a/docs/OEthARMSquashed.svg +++ b/docs/OEthARMSquashed.svg @@ -4,20 +4,21 @@ - - + + UmlClassDiagram - - + + -17 - -OethARM -../src/contracts/OethARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> +21 + +OethARM +../src/contracts/OethARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> Internal:   OWNER_SLOT: bytes32 <<Ownable>> Public: @@ -32,10 +33,10 @@    _owner(): (ownerOut: address) <<Ownable>>    _setOwner(newOwner: address) <<Ownable>>    _onlyOwner() <<Ownable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<PeggedARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<PeggedARM>> -    _accountFee(amountIn: uint256, amountOut: uint256) <<AbstractARM>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<PeggedARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<PeggedARM>>    _inDeadline(deadline: uint256) <<AbstractARM>>    _swap(inToken: IERC20, outToken: IERC20, amount: uint256, to: address): uint256 <<PeggedARM>>    _approvals() <<OethLiquidityManager>> From 4b59dc0c173a1c1b1adc65cfab81dc59c6e6953e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 16:34:14 +1000 Subject: [PATCH 028/196] Fixed OETH ARM fork tests --- test/fork/mainnet/Withdraw.t.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/fork/mainnet/Withdraw.t.sol b/test/fork/mainnet/Withdraw.t.sol index 50c4753..f63728e 100644 --- a/test/fork/mainnet/Withdraw.t.sol +++ b/test/fork/mainnet/Withdraw.t.sol @@ -63,8 +63,8 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { (uint256 requestId,) = oethARM.requestWithdrawal(1 ether); // Add more liquidity to facilitate withdrawal - (, uint128 claimable, uint128 claimed,) = vault.withdrawalQueueMetadata(); - deal(address(weth), address(vault), claimable - claimed + 1 ether); + (uint128 queued,, uint128 claimed,) = vault.withdrawalQueueMetadata(); + deal(address(weth), address(vault), queued - claimed + 1 ether); // Add liquidity to the withdrawal queue vault.addWithdrawalQueueLiquidity(); @@ -87,8 +87,8 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { oethARM.requestWithdrawal(1 ether); // Add more liquidity to facilitate withdrawal - (, uint128 claimable, uint128 claimed,) = vault.withdrawalQueueMetadata(); - deal(address(weth), address(vault), claimable - claimed + 2 ether); + (uint128 queued,, uint128 claimed,) = vault.withdrawalQueueMetadata(); + deal(address(weth), address(vault), queued - claimed + 2 ether); // Skip withdrawal queue delay skip(10 minutes); From 9d003dd126512b05c910cc575740b5005ad38afe Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 18:32:09 +1000 Subject: [PATCH 029/196] Refactor MultiPriceARM Started LidoMultiPriceMultiLpARM fork test --- src/contracts/MultiPriceARM.sol | 24 +- .../mainnet/LidoMultiPriceMultiLpARM.t.sol | 277 ++++++++++++++++++ 2 files changed, 294 insertions(+), 7 deletions(-) create mode 100644 test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol index cabea49..57ae552 100644 --- a/src/contracts/MultiPriceARM.sol +++ b/src/contracts/MultiPriceARM.sol @@ -35,7 +35,7 @@ abstract contract MultiPriceARM is AbstractARM { uint256[50] private _gap; - event TranchePricesUpdated(uint256[5] prices); + event TranchePricesUpdated(uint16[5] discounts); event TrancheAllocationsUpdated(uint256[5] allocations); constructor(address _discountToken, address _liquidityToken) { @@ -252,21 +252,31 @@ abstract contract MultiPriceARM is AbstractARM { transferAmount = amount; } - function updateTranchePrices(uint256[5] calldata prices) external onlyOwner { + /// @notice sets the tranche discounts from the liquidity asset scaled to 1e5. + /// For example: + /// a 8 basis point discount (0.08%) would be 800 with a price of 0.9992 + /// a 1.25 basis point discount (0.0125%) would be 125 with a price of 0.999875 + function setTrancheDiscounts(uint16[5] calldata discounts) external onlyOwner { uint16[15] memory tranchesMem = tranches; - for (uint256 i = 0; i < prices.length; i++) { - // TODO add price safely checks - tranchesMem[i * 3 + DISCOUNT_INDEX] = SafeCast.toUint16(prices[i] / DISCOUNT_MULTIPLIER); + for (uint256 i = 0; i < discounts.length; i++) { + require(discounts[i] > 0, "ARM: zero discount"); + tranchesMem[i * 3 + DISCOUNT_INDEX] = discounts[i]; } // Write back the tranche data to storage once tranches = tranchesMem; - emit TranchePricesUpdated(prices); + emit TranchePricesUpdated(discounts); } - function updateTrancheAllocations(uint256[5] calldata allocations) external onlyOwner { + function getTrancheDiscounts() external view returns (uint16[5] memory discounts) { + for (uint256 i = 0; i < discounts.length; i++) { + discounts[i] = tranches[i * 3 + DISCOUNT_INDEX]; + } + } + + function setTrancheAllocations(uint256[5] calldata allocations) external onlyOwner { uint16[15] memory tranchesMem = tranches; for (uint256 i = 0; i < allocations.length; ++i) { diff --git a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol new file mode 100644 index 0000000..e227f2c --- /dev/null +++ b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test, console2} from "forge-std/Test.sol"; + +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoMultiPriceMultiLpARM} from "contracts/LidoMultiPriceMultiLpARM.sol"; +import {Proxy} from "contracts/Proxy.sol"; + +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { + Proxy public lidoProxy; + LidoMultiPriceMultiLpARM public lidoARM; + IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); + uint256 performanceFee = 2000; // 20% + address feeCollector = 0x000000000000000000000000000000Feec011ec1; + + // Account for stETH rounding errors. + // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case + uint256 constant ROUNDING = 2; + + function setUp() public override { + super.setUp(); + + address lidoWithdrawal = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; + LidoMultiPriceMultiLpARM lidoImpl = new LidoMultiPriceMultiLpARM(address(steth), address(weth), lidoWithdrawal); + lidoProxy = new Proxy(); + + // The deployer needs a tiny amount of WETH to initialize the ARM + _dealWETH(address(this), 1e12); + weth.approve(address(lidoProxy), type(uint256).max); + steth.approve(address(lidoProxy), type(uint256).max); + + // Initialize Proxy with LidoMultiPriceMultiLpARM implementation. + bytes memory data = abi.encodeWithSignature( + "initialize(string,string,address,uint256,address)", + "Lido ARM", + "ARM-ST", + operator, + performanceFee, + feeCollector + ); + lidoProxy.initialize(address(lidoImpl), address(this), data); + + lidoARM = LidoMultiPriceMultiLpARM(payable(address(lidoProxy))); + + // Set the tranche discounts for each tranche + // 8 basis point discount (0.08%) would be 800 with a price of 0.9992 + lidoARM.setTrancheDiscounts([uint16(200), 375, 800, 1400, 1800]); + lidoARM.setTrancheAllocations([uint256(8e18), 5e18, 3e18, 2e18, 1e18]); + + // Only fuzz from this address. Big speedup on fork. + targetSender(address(this)); + } + + function _dealStETH(address to, uint256 amount) internal { + vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); + steth.transfer(to, amount); + } + + function _dealWETH(address to, uint256 amount) internal { + deal(address(weth), to, amount); + } + + /// @dev ARM owner sets valid trance discounts ranging from 1 to MAX_DISCOUNT + function test_setValidTrancheDiscounts() external { + lidoARM.setTrancheDiscounts([uint16(1), 20, 300, 9999, 65535]); + uint16[5] memory discounts = lidoARM.getTrancheDiscounts(); + assertEq(discounts[0], 1); + assertEq(discounts[1], 20); + assertEq(discounts[2], 300); + assertEq(discounts[3], 9999); + assertEq(discounts[4], 65535); + } + // Revert when a tranche discount is zero + // Revert when a tranche discount is greater than the MAX_DISCOUNT + // Revert when non owner tries to set tranche discounts + + // function test_realistic_swaps() external { + // // vm.prank(operator); + // // lidoARM.setPrices(997 * 1e33, 998 * 1e33); + // _swapExactTokensForTokens(steth, weth, 10 ether, 9.97 ether); + // // _swapExactTokensForTokens(weth, steth, 10 ether, 10020040080160320641); + // } + + // function test_swapExactTokensForTokens_WETH_TO_STETH() external { + // _swapExactTokensForTokens(weth, steth, 10 ether, 6.25 ether); + // } + + // function test_swapExactTokensForTokens_STETH_TO_WETH() external { + // _swapExactTokensForTokens(steth, weth, 10 ether, 5 ether); + // } + + // function test_swapTokensForExactTokens_WETH_TO_STETH() external { + // _swapTokensForExactTokens(weth, steth, 10 ether, 6.25 ether); + // } + + // function test_swapTokensForExactTokens_STETH_TO_WETH() external { + // _swapTokensForExactTokens(steth, weth, 10 ether, 5 ether); + // } + + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) + internal + { + if (inToken == weth) { + _dealWETH(address(this), amountIn + 1000); + } else { + _dealStETH(address(this), amountIn + 1000); + } + uint256 startIn = inToken.balanceOf(address(this)); + uint256 startOut = outToken.balanceOf(address(this)); + lidoARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); + assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); + assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); + assertGe(outToken.balanceOf(address(this)), startOut + expectedOut - ROUNDING, "Out actual"); + assertLe(outToken.balanceOf(address(this)), startOut + expectedOut + ROUNDING, "Out actual"); + } + + // function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) + // internal + // { + // if (inToken == weth) { + // _dealWETH(address(this), amountIn + 1000); + // } else { + // _dealStETH(address(this), amountIn + 1000); + // } + // uint256 startIn = inToken.balanceOf(address(this)); + // lidoARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); + // assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); + // assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); + // assertGe(outToken.balanceOf(address(this)), expectedOut - ROUNDING, "Out actual"); + // assertLe(outToken.balanceOf(address(this)), expectedOut + ROUNDING, "Out actual"); + // } + + // function test_unauthorizedAccess() external { + // address RANDOM_ADDRESS = 0xfEEDBeef00000000000000000000000000000000; + // vm.startPrank(RANDOM_ADDRESS); + + // // Proxy's restricted methods. + // vm.expectRevert("ARM: Only owner can call this function."); + // proxy.setOwner(RANDOM_ADDRESS); + + // vm.expectRevert("ARM: Only owner can call this function."); + // proxy.initialize(address(this), address(this), ""); + + // vm.expectRevert("ARM: Only owner can call this function."); + // proxy.upgradeTo(address(this)); + + // vm.expectRevert("ARM: Only owner can call this function."); + // proxy.upgradeToAndCall(address(this), ""); + + // // Implementation's restricted methods. + // vm.expectRevert("ARM: Only owner can call this function."); + // lidoARM.setOwner(RANDOM_ADDRESS); + + // vm.expectRevert("ARM: Only operator or owner can call this function."); + // lidoARM.setPrices(123, 321); + // } + + // function test_wrongInTokenExactIn() external { + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapExactTokensForTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); + // } + + // function test_wrongOutTokenExactIn() external { + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); + // } + + // function test_wrongInTokenExactOut() external { + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); + // } + + // function test_wrongOutTokenExactOut() external { + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); + // vm.expectRevert("ARM: Invalid token"); + // lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); + // } + + // function test_collectTokens() external { + // lidoARM.transferToken(address(weth), address(this), weth.balanceOf(address(lidoARM))); + // assertGt(weth.balanceOf(address(this)), 50 ether); + // assertEq(weth.balanceOf(address(lidoARM)), 0); + + // lidoARM.transferToken(address(steth), address(this), steth.balanceOf(address(lidoARM))); + // assertGt(steth.balanceOf(address(this)), 50 ether); + // assertLt(steth.balanceOf(address(lidoARM)), 3); + // } + + // /* Operator Tests */ + + // function test_setOperator() external { + // lidoARM.setOperator(address(this)); + // assertEq(lidoARM.operator(), address(this)); + // } + + // function test_nonOwnerCannotSetOperator() external { + // vm.expectRevert("ARM: Only owner can call this function."); + // vm.prank(operator); + // lidoARM.setOperator(operator); + // } + + // function test_setMinimumFunds() external { + // lidoARM.setMinimumFunds(100 ether); + // assertEq(lidoARM.minimumFunds(), 100 ether); + // } + + // function test_setGoodCheckedTraderates() external { + // vm.prank(operator); + // lidoARM.setPrices(992 * 1e33, 2000 * 1e33); + // assertEq(lidoARM.traderate0(), 500 * 1e33); + // assertEq(lidoARM.traderate1(), 992 * 1e33); + // } + + // function test_setBadCheckedTraderates() external { + // vm.prank(operator); + // vm.expectRevert("ARM: Traderate too high"); + // lidoARM.setPrices(1010 * 1e33, 1020 * 1e33); + // vm.prank(operator); + // vm.expectRevert("ARM: Traderate too high"); + // lidoARM.setPrices(993 * 1e33, 994 * 1e33); + // } + + // function test_checkTraderateFailsMinimumFunds() external { + // uint256 currentFunds = + // lidoARM.token0().balanceOf(address(lidoARM)) + lidoARM.token1().balanceOf(address(lidoARM)); + // lidoARM.setMinimumFunds(currentFunds + 100); + + // vm.prank(operator); + // vm.expectRevert("ARM: Too much loss"); + // lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + // } + + // function test_checkTraderateWorksMinimumFunds() external { + // uint256 currentFunds = + // lidoARM.token0().balanceOf(address(lidoARM)) + lidoARM.token1().balanceOf(address(lidoARM)); + // lidoARM.setMinimumFunds(currentFunds - 100); + + // vm.prank(operator); + // lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + // } + + // // Slow on fork + // function invariant_nocrossed_trading_exact_eth() external { + // uint256 sumBefore = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); + // _dealWETH(address(this), 1 ether); + // lidoARM.swapExactTokensForTokens(weth, steth, weth.balanceOf(address(lidoARM)), 0, address(this)); + // lidoARM.swapExactTokensForTokens(steth, weth, steth.balanceOf(address(lidoARM)), 0, address(this)); + // uint256 sumAfter = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); + // assertGt(sumBefore, sumAfter, "Lost money swapping"); + // } +} From d00856bc05841e563b69d95a3fa0cd73a74deb16 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 18:33:03 +1000 Subject: [PATCH 030/196] Updated contract diagrams --- docs/LidoMultiPriceMultiLpARMSquashed.svg | 193 +++++++++++----------- 1 file changed, 97 insertions(+), 96 deletions(-) diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index 3fd290b..2968288 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -4,105 +4,106 @@ - - + + UmlClassDiagram - + 15 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<MultiLP>> -   _gap: uint256[50] <<PerformanceFee>> -   _gap: uint256[50] <<AccessControlLP>> -   discountToken: address <<MultiPriceARM>> -   liquidityToken: address <<MultiPriceARM>> -   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> -   tranches: uint16[15] <<MultiPriceARM>> -   _gap: uint256[50] <<MultiPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityAsset: address <<MultiLP>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   MAX_FEE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   totalAssetsCap: uint256 <<AccessControlLP>> -   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> -   PRICE_PRECISION: uint256 <<MultiPriceARM>> -   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _externalWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> -    _rawTotalAssets(): uint256 <<PerformanceFee>> -    _setFee(_fee: uint256) <<PerformanceFee>> -    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> -    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> -    _removeLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> -    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> -    _postClaimHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> -    collectFees(): (fees: uint256) <<PerformanceFee>> -    setLiquidityProviderCap(liquidityProvider: address, cap: uint256) <<onlyOwner>> <<AccessControlLP>> -    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>> -    updateTranchePrices(prices: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> -    updateTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<MultiLP>> +   _gap: uint256[50] <<PerformanceFee>> +   _gap: uint256[50] <<AccessControlLP>> +   discountToken: address <<MultiPriceARM>> +   liquidityToken: address <<MultiPriceARM>> +   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> +   tranches: uint16[15] <<MultiPriceARM>> +   _gap: uint256[50] <<MultiPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityAsset: address <<MultiLP>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   MAX_FEE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   totalAssetsCap: uint256 <<AccessControlLP>> +   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> +   PRICE_PRECISION: uint256 <<MultiPriceARM>> +   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _externalWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _rawTotalAssets(): uint256 <<PerformanceFee>> +    _setFee(_fee: uint256) <<PerformanceFee>> +    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> +    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> +    _removeLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> +    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> +    _postClaimHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> +    collectFees(): (fees: uint256) <<PerformanceFee>> +    setLiquidityProviderCap(liquidityProvider: address, cap: uint256) <<onlyOwner>> <<AccessControlLP>> +    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>> +    setTrancheDiscounts(discounts: uint16[5]) <<onlyOwner>> <<MultiPriceARM>> +    getTrancheDiscounts(): (discounts: uint16[5]) <<MultiPriceARM>> +    setTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>>    resetRemainingLiquidity() <<onlyOwner>> <<MultiPriceARM>>    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> @@ -119,7 +120,7 @@    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>>    <<event>> LiquidityProviderCap(liquidityProvider: address, cap: uint256) <<AccessControlLP>>    <<event>> TotalAssetsCap(cap: uint256) <<AccessControlLP>> -    <<event>> TranchePricesUpdated(prices: uint256[5]) <<MultiPriceARM>> +    <<event>> TranchePricesUpdated(discounts: uint16[5]) <<MultiPriceARM>>    <<event>> TrancheAllocationsUpdated(allocations: uint256[5]) <<MultiPriceARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> From 32ff7fac4d3d702b2b4d33e4fc8b6bf0a8de6832 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 19:47:38 +1000 Subject: [PATCH 031/196] Fixes to MultiPriceARM --- src/contracts/MultiPriceARM.sol | 8 +-- .../mainnet/LidoMultiPriceMultiLpARM.t.sol | 55 ++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol index 57ae552..94ab8a6 100644 --- a/src/contracts/MultiPriceARM.sol +++ b/src/contracts/MultiPriceARM.sol @@ -52,7 +52,7 @@ abstract contract MultiPriceARM is AbstractARM { uint16[15] memory tranchesMem = tranches; // Fill the tranches with the new liquidity from first to last - for (uint256 i = 0; i < tranchesMem.length; i + 3) { + for (uint256 i = 0; i < tranchesMem.length; i += 3) { unallocatedLiquidity = tranchesMem[i + LIQUIDITY_ALLOCATED_INDEX] - tranchesMem[i + LIQUIDITY_REMAINING_INDEX]; @@ -64,7 +64,7 @@ abstract contract MultiPriceARM is AbstractARM { remainingLiquidity -= liquidityToAdd; if (remainingLiquidity == 0) { - return; + break; } } @@ -80,7 +80,7 @@ abstract contract MultiPriceARM is AbstractARM { // Take liquidity from the tranches from last to first for (uint256 i = tranchesMem.length; i > 2;) { - i = i - 3; + i -= 3; liquidityToRemove = remainingLiquidity <= tranchesMem[i + LIQUIDITY_REMAINING_INDEX] ? remainingLiquidity : tranchesMem[i + LIQUIDITY_REMAINING_INDEX]; @@ -91,7 +91,7 @@ abstract contract MultiPriceARM is AbstractARM { remainingLiquidity -= liquidityToRemove; if (remainingLiquidity == 0) { - return; + break; } } diff --git a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol index e227f2c..23b311d 100644 --- a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol +++ b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol @@ -48,7 +48,9 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { // Set the tranche discounts for each tranche // 8 basis point discount (0.08%) would be 800 with a price of 0.9992 lidoARM.setTrancheDiscounts([uint16(200), 375, 800, 1400, 1800]); - lidoARM.setTrancheAllocations([uint256(8e18), 5e18, 3e18, 2e18, 1e18]); + lidoARM.setTrancheAllocations([uint256(80 ether), 50 ether, 30 ether, 20 ether, 10 ether]); + + lidoARM.setTotalAssetsCap(100 ether); // Only fuzz from this address. Big speedup on fork. targetSender(address(this)); @@ -63,6 +65,23 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { deal(address(weth), to, amount); } + /// @dev Check initial state + function test_initial_state() external { + assertEq(lidoARM.name(), "Lido ARM"); + assertEq(lidoARM.symbol(), "ARM-ST"); + assertEq(lidoARM.owner(), address(this)); + assertEq(lidoARM.operator(), operator); + assertEq(lidoARM.feeCollector(), feeCollector); + assertEq(lidoARM.fee(), performanceFee); + assertEq(lidoARM.lastTotalAssets(), 0); + assertEq(lidoARM.feesAccrued(), 0); + // the 20% performance fee is removed + assertEq(lidoARM.totalAssets(), 1e12 * 0.8); + assertEq(lidoARM.totalSupply(), 1e12); + assertEq(weth.balanceOf(address(lidoARM)), 1e12); + assertEq(lidoARM.totalAssetsCap(), 100 ether); + } + /// @dev ARM owner sets valid trance discounts ranging from 1 to MAX_DISCOUNT function test_setValidTrancheDiscounts() external { lidoARM.setTrancheDiscounts([uint16(1), 20, 300, 9999, 65535]); @@ -77,6 +96,40 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { // Revert when a tranche discount is greater than the MAX_DISCOUNT // Revert when non owner tries to set tranche discounts + // whitelisted LP adds WETH liquidity to the ARM + function test_depositAssets() external { + lidoARM.setLiquidityProviderCap(address(this), 20 ether); + + _dealWETH(address(this), 10 ether); + uint256 startWETH = weth.balanceOf(address(lidoARM)); + + lidoARM.deposit(10 ether); + + assertEq(weth.balanceOf(address(lidoARM)), startWETH + 10 ether); + } + // non whitelisted LP tries to add WETH liquidity to the ARM + + function test_redeemAssets() external { + lidoARM.setLiquidityProviderCap(address(this), 20 ether); + _dealWETH(address(this), 10 ether); + lidoARM.deposit(10 ether); + + lidoARM.requestRedeem(8 ether); + } + + // with enough liquidity in all tranches + //// swap stETH to WETH using just the first tranche + //// swap stETH to WETH using the first two tranches + //// swap stETH to WETH using all five tranches + //// fail to swap stETH to WETH with a swap larger than the available liquidity + // with all liquidity in the first tranche used + //// swap stETH to WETH using just the second tranche + //// swap stETH to WETH using the second and third tranches + //// swap stETH to WETH using the remaining four tranches + //// fail to swap stETH to WETH with a swap larger than the available liquidity + // with only liquidity in the fifth tranche + //// swap stETH to WETH using just the fifth tranche + // function test_realistic_swaps() external { // // vm.prank(operator); // // lidoARM.setPrices(997 * 1e33, 998 * 1e33); From 37558703d99d18261b3bc2439a72d2f13c5a16ef Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 22:30:36 +1000 Subject: [PATCH 032/196] More LidoMultiPriceMultiLpARM fork tests --- src/contracts/LidoMultiPriceMultiLpARM.sol | 2 + src/contracts/MultiPriceARM.sol | 24 +- .../mainnet/LidoMultiPriceMultiLpARM.t.sol | 281 +++++------------- 3 files changed, 100 insertions(+), 207 deletions(-) diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index 5b4364e..5abde6b 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.23; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AccessControlLP} from "./AccessControlLP.sol"; import {AbstractARM} from "./AbstractARM.sol"; @@ -51,6 +52,7 @@ contract LidoMultiPriceMultiLpARM is ) external initializer { _initOwnableOperable(_operator); _initMultiLP(_name, _symbol); + lastTotalAssets = SafeCast.toUint128(MIN_TOTAL_SUPPLY); _initPerformanceFee(_fee, _feeCollector); } diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol index 94ab8a6..b4edfa1 100644 --- a/src/contracts/MultiPriceARM.sol +++ b/src/contracts/MultiPriceARM.sol @@ -270,12 +270,6 @@ abstract contract MultiPriceARM is AbstractARM { emit TranchePricesUpdated(discounts); } - function getTrancheDiscounts() external view returns (uint16[5] memory discounts) { - for (uint256 i = 0; i < discounts.length; i++) { - discounts[i] = tranches[i * 3 + DISCOUNT_INDEX]; - } - } - function setTrancheAllocations(uint256[5] calldata allocations) external onlyOwner { uint16[15] memory tranchesMem = tranches; @@ -328,4 +322,22 @@ abstract contract MultiPriceARM is AbstractARM { // Write back the tranche data to storage once tranches = tranchesMem; } + + function getTrancheDiscounts() external view returns (uint16[5] memory discounts) { + for (uint256 i = 0; i < discounts.length; i++) { + discounts[i] = tranches[i * 3 + DISCOUNT_INDEX]; + } + } + + function getTrancheAllocations() external view returns (uint256[5] memory allocations) { + for (uint256 i = 0; i < allocations.length; i++) { + allocations[i] = tranches[i * 3 + LIQUIDITY_ALLOCATED_INDEX] * TRANCHE_AMOUNT_MULTIPLIER; + } + } + + function getTrancheRemaining() external view returns (uint256[5] memory remaining) { + for (uint256 i = 0; i < remaining.length; i++) { + remaining[i] = tranches[i * 3 + LIQUIDITY_REMAINING_INDEX] * TRANCHE_AMOUNT_MULTIPLIER; + } + } } diff --git a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol index 23b311d..5d06198 100644 --- a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol +++ b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol @@ -15,6 +15,74 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); uint256 performanceFee = 2000; // 20% address feeCollector = 0x000000000000000000000000000000Feec011ec1; + AssertData beforeData; + DeltaData noChangeDeltaData = DeltaData({ + totalAssets: 10, + totalSupply: 0, + totalAssetsCap: 0, + armWeth: 0, + armSteth: 0, + feesAccrued: 0, + tranchesDiscounts: [int16(0), 0, 0, 0, 0], + tranchesAllocations: [int256(0), 0, 0, 0, 0], + tranchesRemaining: [int256(0), 0, 0, 0, 0] + }); + + struct AssertData { + uint256 totalAssets; + uint256 totalSupply; + uint256 totalAssetsCap; + uint256 armWeth; + uint256 armSteth; + uint256 feesAccrued; + uint16[5] tranchesDiscounts; + uint256[5] tranchesAllocations; + uint256[5] tranchesRemaining; + } + + struct DeltaData { + int256 totalAssets; + int256 totalSupply; + int256 totalAssetsCap; + int256 armWeth; + int256 armSteth; + int256 feesAccrued; + int16[5] tranchesDiscounts; + int256[5] tranchesAllocations; + int256[5] tranchesRemaining; + } + + function _snapData() internal view returns (AssertData memory data) { + return AssertData({ + totalAssets: lidoARM.totalAssets(), + totalSupply: lidoARM.totalSupply(), + totalAssetsCap: lidoARM.totalAssetsCap(), + armWeth: weth.balanceOf(address(lidoARM)), + armSteth: steth.balanceOf(address(lidoARM)), + feesAccrued: lidoARM.feesAccrued(), + tranchesDiscounts: lidoARM.getTrancheDiscounts(), + tranchesAllocations: lidoARM.getTrancheAllocations(), + tranchesRemaining: lidoARM.getTrancheRemaining() + }); + } + + function assertData(AssertData memory before, DeltaData memory delta) internal view { + AssertData memory afterData = _snapData(); + + assertEq(int256(afterData.totalAssets), int256(before.totalAssets) + delta.totalAssets, "totalAssets"); + assertEq(int256(afterData.totalSupply), int256(before.totalSupply) + delta.totalSupply, "totalSupply"); + assertEq( + int256(afterData.totalAssetsCap), int256(before.totalAssetsCap) + delta.totalAssetsCap, "totalAssetsCap" + ); + assertEq(int256(afterData.feesAccrued), int256(before.feesAccrued) + delta.feesAccrued, "feesAccrued"); + assertEq(int256(afterData.armWeth), int256(before.armWeth) + delta.armWeth, "armWeth"); + assertEq(int256(afterData.armSteth), int256(before.armSteth) + delta.armSteth, "armSteth"); + // for (uint256 i = 0; i < 5; i++) { + // assertEq(afterData.tranchesDiscounts[i], before.tranchesDiscounts[i] + delta.tranchesDiscounts[i]); + // assertEq(afterData.tranchesAllocations[i], before.tranchesAllocations[i] + delta.tranchesAllocations[i]); + // assertEq(afterData.tranchesRemaining[i], before.tranchesRemaining[i] + delta.tranchesRemaining[i]); + // } + } // Account for stETH rounding errors. // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case @@ -75,7 +143,7 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { assertEq(lidoARM.fee(), performanceFee); assertEq(lidoARM.lastTotalAssets(), 0); assertEq(lidoARM.feesAccrued(), 0); - // the 20% performance fee is removed + // the 20% performance fee is removed on initialization assertEq(lidoARM.totalAssets(), 1e12 * 0.8); assertEq(lidoARM.totalSupply(), 1e12); assertEq(weth.balanceOf(address(lidoARM)), 1e12); @@ -101,11 +169,20 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { lidoARM.setLiquidityProviderCap(address(this), 20 ether); _dealWETH(address(this), 10 ether); - uint256 startWETH = weth.balanceOf(address(lidoARM)); + beforeData = _snapData(); lidoARM.deposit(10 ether); - assertEq(weth.balanceOf(address(lidoARM)), startWETH + 10 ether); + DeltaData memory delta = noChangeDeltaData; + delta.totalAssets = 10 ether; + delta.totalSupply = 10 ether; + delta.armWeth = 10 ether; + assertData(beforeData, delta); + + // assert whitelisted LP cap was decreased + // assert remaining liquidity in appropriate tranches increased + // assert last total assets was set with performance fee removed + // assert performance fee was accrued on asset increases but not the deposit } // non whitelisted LP tries to add WETH liquidity to the ARM @@ -129,202 +206,4 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { //// fail to swap stETH to WETH with a swap larger than the available liquidity // with only liquidity in the fifth tranche //// swap stETH to WETH using just the fifth tranche - - // function test_realistic_swaps() external { - // // vm.prank(operator); - // // lidoARM.setPrices(997 * 1e33, 998 * 1e33); - // _swapExactTokensForTokens(steth, weth, 10 ether, 9.97 ether); - // // _swapExactTokensForTokens(weth, steth, 10 ether, 10020040080160320641); - // } - - // function test_swapExactTokensForTokens_WETH_TO_STETH() external { - // _swapExactTokensForTokens(weth, steth, 10 ether, 6.25 ether); - // } - - // function test_swapExactTokensForTokens_STETH_TO_WETH() external { - // _swapExactTokensForTokens(steth, weth, 10 ether, 5 ether); - // } - - // function test_swapTokensForExactTokens_WETH_TO_STETH() external { - // _swapTokensForExactTokens(weth, steth, 10 ether, 6.25 ether); - // } - - // function test_swapTokensForExactTokens_STETH_TO_WETH() external { - // _swapTokensForExactTokens(steth, weth, 10 ether, 5 ether); - // } - - function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - if (inToken == weth) { - _dealWETH(address(this), amountIn + 1000); - } else { - _dealStETH(address(this), amountIn + 1000); - } - uint256 startIn = inToken.balanceOf(address(this)); - uint256 startOut = outToken.balanceOf(address(this)); - lidoARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); - assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); - assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); - assertGe(outToken.balanceOf(address(this)), startOut + expectedOut - ROUNDING, "Out actual"); - assertLe(outToken.balanceOf(address(this)), startOut + expectedOut + ROUNDING, "Out actual"); - } - - // function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - // internal - // { - // if (inToken == weth) { - // _dealWETH(address(this), amountIn + 1000); - // } else { - // _dealStETH(address(this), amountIn + 1000); - // } - // uint256 startIn = inToken.balanceOf(address(this)); - // lidoARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); - // assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); - // assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); - // assertGe(outToken.balanceOf(address(this)), expectedOut - ROUNDING, "Out actual"); - // assertLe(outToken.balanceOf(address(this)), expectedOut + ROUNDING, "Out actual"); - // } - - // function test_unauthorizedAccess() external { - // address RANDOM_ADDRESS = 0xfEEDBeef00000000000000000000000000000000; - // vm.startPrank(RANDOM_ADDRESS); - - // // Proxy's restricted methods. - // vm.expectRevert("ARM: Only owner can call this function."); - // proxy.setOwner(RANDOM_ADDRESS); - - // vm.expectRevert("ARM: Only owner can call this function."); - // proxy.initialize(address(this), address(this), ""); - - // vm.expectRevert("ARM: Only owner can call this function."); - // proxy.upgradeTo(address(this)); - - // vm.expectRevert("ARM: Only owner can call this function."); - // proxy.upgradeToAndCall(address(this), ""); - - // // Implementation's restricted methods. - // vm.expectRevert("ARM: Only owner can call this function."); - // lidoARM.setOwner(RANDOM_ADDRESS); - - // vm.expectRevert("ARM: Only operator or owner can call this function."); - // lidoARM.setPrices(123, 321); - // } - - // function test_wrongInTokenExactIn() external { - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapExactTokensForTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); - // } - - // function test_wrongOutTokenExactIn() external { - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); - // } - - // function test_wrongInTokenExactOut() external { - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); - // } - - // function test_wrongOutTokenExactOut() external { - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); - // vm.expectRevert("ARM: Invalid token"); - // lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); - // } - - // function test_collectTokens() external { - // lidoARM.transferToken(address(weth), address(this), weth.balanceOf(address(lidoARM))); - // assertGt(weth.balanceOf(address(this)), 50 ether); - // assertEq(weth.balanceOf(address(lidoARM)), 0); - - // lidoARM.transferToken(address(steth), address(this), steth.balanceOf(address(lidoARM))); - // assertGt(steth.balanceOf(address(this)), 50 ether); - // assertLt(steth.balanceOf(address(lidoARM)), 3); - // } - - // /* Operator Tests */ - - // function test_setOperator() external { - // lidoARM.setOperator(address(this)); - // assertEq(lidoARM.operator(), address(this)); - // } - - // function test_nonOwnerCannotSetOperator() external { - // vm.expectRevert("ARM: Only owner can call this function."); - // vm.prank(operator); - // lidoARM.setOperator(operator); - // } - - // function test_setMinimumFunds() external { - // lidoARM.setMinimumFunds(100 ether); - // assertEq(lidoARM.minimumFunds(), 100 ether); - // } - - // function test_setGoodCheckedTraderates() external { - // vm.prank(operator); - // lidoARM.setPrices(992 * 1e33, 2000 * 1e33); - // assertEq(lidoARM.traderate0(), 500 * 1e33); - // assertEq(lidoARM.traderate1(), 992 * 1e33); - // } - - // function test_setBadCheckedTraderates() external { - // vm.prank(operator); - // vm.expectRevert("ARM: Traderate too high"); - // lidoARM.setPrices(1010 * 1e33, 1020 * 1e33); - // vm.prank(operator); - // vm.expectRevert("ARM: Traderate too high"); - // lidoARM.setPrices(993 * 1e33, 994 * 1e33); - // } - - // function test_checkTraderateFailsMinimumFunds() external { - // uint256 currentFunds = - // lidoARM.token0().balanceOf(address(lidoARM)) + lidoARM.token1().balanceOf(address(lidoARM)); - // lidoARM.setMinimumFunds(currentFunds + 100); - - // vm.prank(operator); - // vm.expectRevert("ARM: Too much loss"); - // lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - // } - - // function test_checkTraderateWorksMinimumFunds() external { - // uint256 currentFunds = - // lidoARM.token0().balanceOf(address(lidoARM)) + lidoARM.token1().balanceOf(address(lidoARM)); - // lidoARM.setMinimumFunds(currentFunds - 100); - - // vm.prank(operator); - // lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - // } - - // // Slow on fork - // function invariant_nocrossed_trading_exact_eth() external { - // uint256 sumBefore = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); - // _dealWETH(address(this), 1 ether); - // lidoARM.swapExactTokensForTokens(weth, steth, weth.balanceOf(address(lidoARM)), 0, address(this)); - // lidoARM.swapExactTokensForTokens(steth, weth, steth.balanceOf(address(lidoARM)), 0, address(this)); - // uint256 sumAfter = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); - // assertGt(sumBefore, sumAfter, "Lost money swapping"); - // } } From 77e08ec0116188b100236265811cfce1af4ed539 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 22:48:47 +1000 Subject: [PATCH 033/196] Added whitelist controls to LidoFixedPriceMultiLpARM Started LidoFixedPriceMultiLpARM fork tests --- src/contracts/LidoFixedPriceMultiLpARM.sol | 37 ++++- src/contracts/LidoMultiPriceMultiLpARM.sol | 2 +- .../mainnet/LidoFixedPriceMultiLpARM.t.sol | 156 ++++++++++++++++++ .../mainnet/LidoMultiPriceMultiLpARM.t.sol | 4 +- 4 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index c318e14..3ac4500 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -2,17 +2,31 @@ pragma solidity ^0.8.23; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AbstractARM} from "./AbstractARM.sol"; +import {AccessControlLP} from "./AccessControlLP.sol"; import {FixedPriceARM} from "./FixedPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {MultiLP} from "./MultiLP.sol"; import {PerformanceFee} from "./PerformanceFee.sol"; -contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, PerformanceFee, FixedPriceARM, LidoLiquidityManager { +/** + * @title Lido (stETH) Application Redemption Manager (ARM) + * @dev This implementation supports multiple Liquidity Providers (LPs) with a single price. + * @author Origin Protocol Inc + */ +contract LidoFixedPriceMultiLpARM is + Initializable, + MultiLP, + PerformanceFee, + AccessControlLP, + FixedPriceARM, + LidoLiquidityManager +{ /// @param _stEth The address of the stETH token /// @param _weth The address of the WETH token - /// @param _lidoWithdrawalQueue The address of the stETH Withdrawal contract + /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract constructor(address _stEth, address _weth, address _lidoWithdrawalQueue) AbstractARM(_stEth, _weth) MultiLP(_weth) @@ -23,11 +37,11 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, PerformanceFee, Fix /// @notice Initialize the contract. /// @param _name The name of the liquidity provider (LP) token. /// @param _symbol The symbol of the liquidity provider (LP) token. - /// @param _operator The address of the account that can request and claim OETH withdrawals. + /// @param _operator The address of the account that can request and claim Lido withdrawals. /// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent). /// 10,000 = 100% performance fee /// 500 = 5% performance fee - /// @param _feeCollector The account that receives the performance fee as shares + /// @param _feeCollector The account that can collect the performance fee function initialize( string calldata _name, string calldata _symbol, @@ -37,6 +51,7 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, PerformanceFee, Fix ) external initializer { _initOwnableOperable(_operator); _initMultiLP(_name, _symbol); + lastTotalAssets = SafeCast.toUint128(MIN_TOTAL_SUPPLY); _initPerformanceFee(_fee, _feeCollector); } @@ -60,13 +75,19 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, PerformanceFee, Fix return LidoLiquidityManager._externalWithdrawQueue(); } - function _postDepositHook(uint256 assets) internal override(MultiLP, PerformanceFee) { - // Save the new total assets after the deposit and performance fee accrued + function _postDepositHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { + // Check the LP can deposit the assets + AccessControlLP._postDepositHook(assets); + + // Store the new total assets after the deposit and performance fee accrued PerformanceFee._postDepositHook(assets); } - function _postWithdrawHook(uint256 assets) internal override(MultiLP, PerformanceFee) { - // Save the new total assets after the withdrawal and performance fee accrued + function _postWithdrawHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { + // Update the LP's assets + AccessControlLP._postWithdrawHook(assets); + + // Store the new total assets after the withdrawal and performance fee accrued PerformanceFee._postWithdrawHook(assets); } diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index 5abde6b..89cd227 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.23; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {AccessControlLP} from "./AccessControlLP.sol"; import {AbstractARM} from "./AbstractARM.sol"; +import {AccessControlLP} from "./AccessControlLP.sol"; import {MultiPriceARM} from "./MultiPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {MultiLP} from "./MultiLP.sol"; diff --git a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol new file mode 100644 index 0000000..4500ed1 --- /dev/null +++ b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test, console2} from "forge-std/Test.sol"; + +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {Proxy} from "contracts/Proxy.sol"; + +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { + Proxy public lidoProxy; + LidoFixedPriceMultiLpARM public lidoARM; + IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); + uint256 performanceFee = 2000; // 20% + address feeCollector = 0x000000000000000000000000000000Feec011ec1; + AssertData beforeData; + DeltaData noChangeDeltaData = + DeltaData({totalAssets: 10, totalSupply: 0, totalAssetsCap: 0, armWeth: 0, armSteth: 0, feesAccrued: 0}); + + struct AssertData { + uint256 totalAssets; + uint256 totalSupply; + uint256 totalAssetsCap; + uint256 armWeth; + uint256 armSteth; + uint256 feesAccrued; + } + + struct DeltaData { + int256 totalAssets; + int256 totalSupply; + int256 totalAssetsCap; + int256 armWeth; + int256 armSteth; + int256 feesAccrued; + } + + function _snapData() internal view returns (AssertData memory data) { + return AssertData({ + totalAssets: lidoARM.totalAssets(), + totalSupply: lidoARM.totalSupply(), + totalAssetsCap: lidoARM.totalAssetsCap(), + armWeth: weth.balanceOf(address(lidoARM)), + armSteth: steth.balanceOf(address(lidoARM)), + feesAccrued: lidoARM.feesAccrued() + }); + } + + function assertData(AssertData memory before, DeltaData memory delta) internal view { + AssertData memory afterData = _snapData(); + + assertEq(int256(afterData.totalAssets), int256(before.totalAssets) + delta.totalAssets, "totalAssets"); + assertEq(int256(afterData.totalSupply), int256(before.totalSupply) + delta.totalSupply, "totalSupply"); + assertEq( + int256(afterData.totalAssetsCap), int256(before.totalAssetsCap) + delta.totalAssetsCap, "totalAssetsCap" + ); + assertEq(int256(afterData.feesAccrued), int256(before.feesAccrued) + delta.feesAccrued, "feesAccrued"); + assertEq(int256(afterData.armWeth), int256(before.armWeth) + delta.armWeth, "armWeth"); + assertEq(int256(afterData.armSteth), int256(before.armSteth) + delta.armSteth, "armSteth"); + } + + // Account for stETH rounding errors. + // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case + uint256 constant ROUNDING = 2; + + function setUp() public override { + super.setUp(); + + address lidoWithdrawal = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; + LidoFixedPriceMultiLpARM lidoImpl = new LidoFixedPriceMultiLpARM(address(steth), address(weth), lidoWithdrawal); + lidoProxy = new Proxy(); + + // The deployer needs a tiny amount of WETH to initialize the ARM + _dealWETH(address(this), 1e12); + weth.approve(address(lidoProxy), type(uint256).max); + steth.approve(address(lidoProxy), type(uint256).max); + + // Initialize Proxy with LidoFixedPriceMultiLpARM implementation. + bytes memory data = abi.encodeWithSignature( + "initialize(string,string,address,uint256,address)", + "Lido ARM", + "ARM-ST", + operator, + performanceFee, + feeCollector + ); + lidoProxy.initialize(address(lidoImpl), address(this), data); + + lidoARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); + + // TODO set price + + lidoARM.setTotalAssetsCap(100 ether); + + // Only fuzz from this address. Big speedup on fork. + targetSender(address(this)); + } + + function _dealStETH(address to, uint256 amount) internal { + vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); + steth.transfer(to, amount); + } + + function _dealWETH(address to, uint256 amount) internal { + deal(address(weth), to, amount); + } + + /// @dev Check initial state + function test_initial_state() external { + assertEq(lidoARM.name(), "Lido ARM"); + assertEq(lidoARM.symbol(), "ARM-ST"); + assertEq(lidoARM.owner(), address(this)); + assertEq(lidoARM.operator(), operator); + assertEq(lidoARM.feeCollector(), feeCollector); + assertEq(lidoARM.fee(), performanceFee); + assertEq(lidoARM.lastTotalAssets(), 1e12); + assertEq(lidoARM.feesAccrued(), 0); + // the 20% performance fee is removed on initialization + assertEq(lidoARM.totalAssets(), 1e12); + assertEq(lidoARM.totalSupply(), 1e12); + assertEq(weth.balanceOf(address(lidoARM)), 1e12); + assertEq(lidoARM.totalAssetsCap(), 100 ether); + } + + // whitelisted LP adds WETH liquidity to the ARM + function test_depositAssets() external { + lidoARM.setLiquidityProviderCap(address(this), 20 ether); + + _dealWETH(address(this), 10 ether); + beforeData = _snapData(); + + lidoARM.deposit(10 ether); + + DeltaData memory delta = noChangeDeltaData; + delta.totalAssets = 10 ether; + delta.totalSupply = 10 ether; + delta.armWeth = 10 ether; + assertData(beforeData, delta); + + // assert whitelisted LP cap was decreased + // assert remaining liquidity in appropriate tranches increased + // assert last total assets was set with performance fee removed + // assert performance fee was accrued on asset increases but not the deposit + } + // non whitelisted LP tries to add WETH liquidity to the ARM + + function test_redeemAssets() external { + lidoARM.setLiquidityProviderCap(address(this), 20 ether); + _dealWETH(address(this), 10 ether); + lidoARM.deposit(10 ether); + + lidoARM.requestRedeem(8 ether); + } +} diff --git a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol index 5d06198..080dc72 100644 --- a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol +++ b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol @@ -141,10 +141,10 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { assertEq(lidoARM.operator(), operator); assertEq(lidoARM.feeCollector(), feeCollector); assertEq(lidoARM.fee(), performanceFee); - assertEq(lidoARM.lastTotalAssets(), 0); + assertEq(lidoARM.lastTotalAssets(), 1e12); assertEq(lidoARM.feesAccrued(), 0); // the 20% performance fee is removed on initialization - assertEq(lidoARM.totalAssets(), 1e12 * 0.8); + assertEq(lidoARM.totalAssets(), 1e12); assertEq(lidoARM.totalSupply(), 1e12); assertEq(weth.balanceOf(address(lidoARM)), 1e12); assertEq(lidoARM.totalAssetsCap(), 100 ether); From 4487f131d74dae925e7bfdf4dc35eac6646a2205 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 22:59:35 +1000 Subject: [PATCH 034/196] Updated Natspec --- src/contracts/FixedPriceARM.sol | 4 ++++ src/contracts/LidoFixedPriceMultiLpARM.sol | 2 +- src/contracts/LidoLiquidityManager.sol | 4 ++++ src/contracts/LidoOwnerLpARM.sol | 5 +++++ src/contracts/MultiPriceARM.sol | 4 ++++ src/contracts/OethLiquidityManager.sol | 4 ++++ test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol | 3 ++- 7 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol index 0a2acdb..92ecfcd 100644 --- a/src/contracts/FixedPriceARM.sol +++ b/src/contracts/FixedPriceARM.sol @@ -4,6 +4,10 @@ pragma solidity ^0.8.23; import {AbstractARM} from "./AbstractARM.sol"; import {IERC20} from "./Interfaces.sol"; +/** + * @title Abstract support to an ARM with a single buy and sell price. + * @author Origin Protocol Inc + */ abstract contract FixedPriceARM is AbstractARM { /** * @notice For one `token0` from a Trader, how many `token1` does the pool send. diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index 3ac4500..9c9ce37 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -13,7 +13,7 @@ import {PerformanceFee} from "./PerformanceFee.sol"; /** * @title Lido (stETH) Application Redemption Manager (ARM) - * @dev This implementation supports multiple Liquidity Providers (LPs) with a single price. + * @dev This implementation supports multiple Liquidity Providers (LPs) with single buy and sell prices. * @author Origin Protocol Inc */ contract LidoFixedPriceMultiLpARM is diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 8cd8119..fac126d 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -4,6 +4,10 @@ pragma solidity ^0.8.23; import {OwnableOperable} from "./OwnableOperable.sol"; import {IERC20, IWETH, IStETHWithdrawal} from "./Interfaces.sol"; +/** + * @title Manages stETH liquidity against the Lido Withdrawal Queue. + * @author Origin Protocol Inc + */ abstract contract LidoLiquidityManager is OwnableOperable { IERC20 public immutable steth; IWETH public immutable weth; diff --git a/src/contracts/LidoOwnerLpARM.sol b/src/contracts/LidoOwnerLpARM.sol index 9fe19f3..8cc1c4a 100644 --- a/src/contracts/LidoOwnerLpARM.sol +++ b/src/contracts/LidoOwnerLpARM.sol @@ -8,6 +8,11 @@ import {FixedPriceARM} from "./FixedPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {OwnerLP} from "./OwnerLP.sol"; +/** + * @title Lido (stETH) Application Redemption Manager (ARM) + * @dev This implementation supports a single LP with single buy and sell prices. + * @author Origin Protocol Inc + */ contract LidoOwnerLpARM is Initializable, OwnerLP, FixedPriceARM, LidoLiquidityManager { /// @param _stEth The address of the stETH token /// @param _weth The address of the WETH token diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol index b4edfa1..993ec6a 100644 --- a/src/contracts/MultiPriceARM.sol +++ b/src/contracts/MultiPriceARM.sol @@ -5,6 +5,10 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AbstractARM} from "./AbstractARM.sol"; import {IERC20} from "./Interfaces.sol"; +/** + * @title Abstract support to an ARM with multiple liquidity tranches with different prices. + * @author Origin Protocol Inc + */ abstract contract MultiPriceARM is AbstractARM { /// @notice The token being bought by the ARM at a discount. eg stETH address private immutable discountToken; diff --git a/src/contracts/OethLiquidityManager.sol b/src/contracts/OethLiquidityManager.sol index 2f073a4..475625f 100644 --- a/src/contracts/OethLiquidityManager.sol +++ b/src/contracts/OethLiquidityManager.sol @@ -4,6 +4,10 @@ pragma solidity ^0.8.23; import {OwnableOperable} from "./OwnableOperable.sol"; import {IERC20, IOETHVault} from "./Interfaces.sol"; +/** + * @title Manages OETH liquidity against the OETH Vault. + * @author Origin Protocol Inc + */ contract OethLiquidityManager is OwnableOperable { address public immutable oeth; address public immutable oethVault; diff --git a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol index 4500ed1..0641999 100644 --- a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol +++ b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol @@ -90,7 +90,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { lidoARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); - // TODO set price + // set prices + lidoARM.setPrices(992 * 1e33, 1001 * 1e33); lidoARM.setTotalAssetsCap(100 ether); From bbb36e15bd36b054e0881b074c2e93be417b589c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 17 Sep 2024 22:59:50 +1000 Subject: [PATCH 035/196] Generated latest contract code --- docs/LidoFixedPriceMultiLpARMHierarchy.svg | 154 +++++++++------- docs/LidoFixedPriceMultiLpARMSquashed.svg | 197 +++++++++++---------- docs/LidoMultiPriceMultiLpARMSquashed.svg | 194 ++++++++++---------- 3 files changed, 287 insertions(+), 258 deletions(-) diff --git a/docs/LidoFixedPriceMultiLpARMHierarchy.svg b/docs/LidoFixedPriceMultiLpARMHierarchy.svg index 21e017a..265732c 100644 --- a/docs/LidoFixedPriceMultiLpARMHierarchy.svg +++ b/docs/LidoFixedPriceMultiLpARMHierarchy.svg @@ -4,34 +4,56 @@ - + UmlClassDiagram - + 0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol - + 24 - -OwnableOperable -../src/contracts/OwnableOperable.sol + +OwnableOperable +../src/contracts/OwnableOperable.sol 0->24 - - + + - + +1 + +<<Abstract>> +AccessControlLP +../src/contracts/AccessControlLP.sol + + + +17 + +<<Abstract>> +MultiLP +../src/contracts/MultiLP.sol + + + +1->17 + + + + + 3 <<Abstract>> @@ -39,96 +61,94 @@ ../src/contracts/FixedPriceARM.sol - + 3->0 - - + + - + 13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + + + +13->1 + + - + 13->3 - - + + - + 14 - -<<Abstract>> -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol + +<<Abstract>> +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol - + 13->14 - - - - - -17 - -<<Abstract>> -MultiLP -../src/contracts/MultiLP.sol + + - + 13->17 - - + + - + 27 - -<<Abstract>> -PerformanceFee -../src/contracts/PerformanceFee.sol + +<<Abstract>> +PerformanceFee +../src/contracts/PerformanceFee.sol - + 13->27 - - + + - + 14->24 - - + + - + 17->0 - - + + - + 23 - -Ownable -../src/contracts/Ownable.sol + +Ownable +../src/contracts/Ownable.sol - + 24->23 - - + + - + 27->17 - - + + diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index 1bbbcb8..12c8c15 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,105 +4,112 @@ - - + + UmlClassDiagram - + 13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<MultiLP>> -   _gap: uint256[50] <<PerformanceFee>> -   _gap: uint256[50] <<FixedPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityAsset: address <<MultiLP>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   MAX_FEE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _externalWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> -    _rawTotalAssets(): uint256 <<PerformanceFee>> -    _setFee(_fee: uint256) <<PerformanceFee>> -    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -    _postClaimHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> -    collectFees(): (fees: uint256) <<PerformanceFee>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> -    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> -    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<MultiLP>> +   _gap: uint256[50] <<PerformanceFee>> +   _gap: uint256[50] <<AccessControlLP>> +   _gap: uint256[50] <<FixedPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityAsset: address <<MultiLP>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   MAX_FEE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   totalAssetsCap: uint256 <<AccessControlLP>> +   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   minimumFunds: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _externalWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _rawTotalAssets(): uint256 <<PerformanceFee>> +    _setFee(_fee: uint256) <<PerformanceFee>> +    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +    _postClaimHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> +    collectFees(): (fees: uint256) <<PerformanceFee>> +    setLiquidityProviderCap(liquidityProvider: address, cap: uint256) <<onlyOwner>> <<AccessControlLP>> +    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> +    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> +    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> +    <<event>> LiquidityProviderCap(liquidityProvider: address, cap: uint256) <<AccessControlLP>> +    <<event>> TotalAssetsCap(cap: uint256) <<AccessControlLP>>    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index 2968288..979ad34 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -4,107 +4,109 @@ - - + + UmlClassDiagram - + 15 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<MultiLP>> -   _gap: uint256[50] <<PerformanceFee>> -   _gap: uint256[50] <<AccessControlLP>> -   discountToken: address <<MultiPriceARM>> -   liquidityToken: address <<MultiPriceARM>> -   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> -   tranches: uint16[15] <<MultiPriceARM>> -   _gap: uint256[50] <<MultiPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityAsset: address <<MultiLP>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   MAX_FEE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   totalAssetsCap: uint256 <<AccessControlLP>> -   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> -   PRICE_PRECISION: uint256 <<MultiPriceARM>> -   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _externalWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> -    _rawTotalAssets(): uint256 <<PerformanceFee>> -    _setFee(_fee: uint256) <<PerformanceFee>> -    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> -    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> -    _removeLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> -    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> -    _postClaimHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> -    collectFees(): (fees: uint256) <<PerformanceFee>> -    setLiquidityProviderCap(liquidityProvider: address, cap: uint256) <<onlyOwner>> <<AccessControlLP>> -    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>> -    setTrancheDiscounts(discounts: uint16[5]) <<onlyOwner>> <<MultiPriceARM>> + +LidoMultiPriceMultiLpARM +../src/contracts/LidoMultiPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<MultiLP>> +   _gap: uint256[50] <<PerformanceFee>> +   _gap: uint256[50] <<AccessControlLP>> +   discountToken: address <<MultiPriceARM>> +   liquidityToken: address <<MultiPriceARM>> +   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> +   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> +   tranches: uint16[15] <<MultiPriceARM>> +   _gap: uint256[50] <<MultiPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityAsset: address <<MultiLP>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   MAX_FEE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   totalAssetsCap: uint256 <<AccessControlLP>> +   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> +   PRICE_PRECISION: uint256 <<MultiPriceARM>> +   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _externalWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _rawTotalAssets(): uint256 <<PerformanceFee>> +    _setFee(_fee: uint256) <<PerformanceFee>> +    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> +    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> +    _removeLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> +    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> +    _postClaimHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> +    collectFees(): (fees: uint256) <<PerformanceFee>> +    setLiquidityProviderCap(liquidityProvider: address, cap: uint256) <<onlyOwner>> <<AccessControlLP>> +    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>> +    setTrancheDiscounts(discounts: uint16[5]) <<onlyOwner>> <<MultiPriceARM>> +    setTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> +    resetRemainingLiquidity() <<onlyOwner>> <<MultiPriceARM>>    getTrancheDiscounts(): (discounts: uint16[5]) <<MultiPriceARM>> -    setTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> -    resetRemainingLiquidity() <<onlyOwner>> <<MultiPriceARM>> +    getTrancheAllocations(): (allocations: uint256[5]) <<MultiPriceARM>> +    getTrancheRemaining(): (remaining: uint256[5]) <<MultiPriceARM>>    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> From b348276e6d46811b8c584a019778656832c83f27 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 18 Sep 2024 08:01:31 +1000 Subject: [PATCH 036/196] Removed minimumFunds logic for LP version --- src/contracts/FixedPriceARM.sol | 11 ----------- test/fork/mainnet/LidoOwnerLpARM.t.sol | 24 ------------------------ 2 files changed, 35 deletions(-) diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol index 92ecfcd..9139f0e 100644 --- a/src/contracts/FixedPriceARM.sol +++ b/src/contracts/FixedPriceARM.sol @@ -28,8 +28,6 @@ abstract contract FixedPriceARM is AbstractARM { /// @dev Maximum operator settable traderate. 1e36 uint256 internal constant MAX_OPERATOR_RATE = 1005 * 1e33; - /// @dev Minimum funds to allow operator to price changes - uint256 public minimumFunds; uint256[50] private _gap; @@ -103,21 +101,12 @@ abstract contract FixedPriceARM is AbstractARM { uint256 _traderate1 = buyT1; // token (t1) -> base (t0) // Limit funds and loss when called by operator if (msg.sender == operator) { - uint256 currentFunds = token0.balanceOf(address(this)) + token1.balanceOf(address(this)); - require(currentFunds > minimumFunds, "ARM: Too much loss"); require(_traderate0 <= MAX_OPERATOR_RATE, "ARM: Traderate too high"); require(_traderate1 <= MAX_OPERATOR_RATE, "ARM: Traderate too high"); } _setTraderates(_traderate0, _traderate1); } - /** - * @notice Sets the minimum funds to allow operator price changes - */ - function setMinimumFunds(uint256 _minimumFunds) external onlyOwner { - minimumFunds = _minimumFunds; - } - function _setTraderates(uint256 _traderate0, uint256 _traderate1) internal { require((1e72 / (_traderate0)) > _traderate1, "ARM: Price cross"); traderate0 = _traderate0; diff --git a/test/fork/mainnet/LidoOwnerLpARM.t.sol b/test/fork/mainnet/LidoOwnerLpARM.t.sol index 8c4da2a..5c46417 100644 --- a/test/fork/mainnet/LidoOwnerLpARM.t.sol +++ b/test/fork/mainnet/LidoOwnerLpARM.t.sol @@ -215,11 +215,6 @@ contract Fork_Concrete_LidoARM_Test is Fork_Shared_Test_ { lidoARM.setOperator(operator); } - function test_setMinimumFunds() external { - lidoARM.setMinimumFunds(100 ether); - assertEq(lidoARM.minimumFunds(), 100 ether); - } - function test_setGoodCheckedTraderates() external { vm.prank(operator); lidoARM.setPrices(992 * 1e33, 2000 * 1e33); @@ -236,25 +231,6 @@ contract Fork_Concrete_LidoARM_Test is Fork_Shared_Test_ { lidoARM.setPrices(993 * 1e33, 994 * 1e33); } - function test_checkTraderateFailsMinimumFunds() external { - uint256 currentFunds = - lidoARM.token0().balanceOf(address(lidoARM)) + lidoARM.token1().balanceOf(address(lidoARM)); - lidoARM.setMinimumFunds(currentFunds + 100); - - vm.prank(operator); - vm.expectRevert("ARM: Too much loss"); - lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - } - - function test_checkTraderateWorksMinimumFunds() external { - uint256 currentFunds = - lidoARM.token0().balanceOf(address(lidoARM)) + lidoARM.token1().balanceOf(address(lidoARM)); - lidoARM.setMinimumFunds(currentFunds - 100); - - vm.prank(operator); - lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - } - // // Slow on fork // function invariant_nocrossed_trading_exact_eth() external { // uint256 sumBefore = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); From 74de0bb3630867db64f69f59447b0e8fca5923de Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 18 Sep 2024 08:08:52 +1000 Subject: [PATCH 037/196] Fix Napspec --- src/contracts/MultiLP.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 54eea63..561f5e3 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -15,7 +15,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { uint256 public constant CLAIM_DELAY = 10 minutes; /// @dev The amount of shares that are minted to a dead address on initalization uint256 internal constant MIN_TOTAL_SUPPLY = 1e12; - /// @dev The address with no know private key that the initial shares are minted to + /// @dev The address with no known private key that the initial shares are minted to address internal constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD; /// @notice The address of the asset that is used to add and remove liquidity. eg WETH From 07d024272f0f3bc5450768151700e0f231a085dd Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 18 Sep 2024 08:27:08 +1000 Subject: [PATCH 038/196] changed setLiquidityProviderCaps to bulk set lp caps Removed increasing the LP's cap on withdrawals --- src/contracts/AccessControlLP.sol | 14 +++++--------- src/contracts/LidoFixedPriceMultiLpARM.sol | 5 +---- src/contracts/LidoMultiPriceMultiLpARM.sol | 5 +---- test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol | 9 +++++---- test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol | 9 +++++---- 5 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/contracts/AccessControlLP.sol b/src/contracts/AccessControlLP.sol index c756e0f..dbb9fd7 100644 --- a/src/contracts/AccessControlLP.sol +++ b/src/contracts/AccessControlLP.sol @@ -21,16 +21,12 @@ abstract contract AccessControlLP is MultiLP { liquidityProviderCaps[msg.sender] -= assets; } - /// @dev Adds assets to the liquidity provider's cap when withdrawing assets or redeeming shares. - /// Will not revert if the total assets cap is less than the total assets. - function _postWithdrawHook(uint256 assets) internal virtual override { - liquidityProviderCaps[msg.sender] += assets; - } - - function setLiquidityProviderCap(address liquidityProvider, uint256 cap) external onlyOwner { - liquidityProviderCaps[liquidityProvider] = cap; + function setLiquidityProviderCaps(address[] calldata _liquidityProviders, uint256 cap) external onlyOwner { + for (uint256 i = 0; i < _liquidityProviders.length; i++) { + liquidityProviderCaps[_liquidityProviders[i]] = cap; - emit LiquidityProviderCap(liquidityProvider, cap); + emit LiquidityProviderCap(_liquidityProviders[i], cap); + } } /// @notice Set the total assets cap for a liquidity provider. diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index 9c9ce37..1feb161 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -83,10 +83,7 @@ contract LidoFixedPriceMultiLpARM is PerformanceFee._postDepositHook(assets); } - function _postWithdrawHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { - // Update the LP's assets - AccessControlLP._postWithdrawHook(assets); - + function _postWithdrawHook(uint256 assets) internal override(MultiLP, PerformanceFee) { // Store the new total assets after the withdrawal and performance fee accrued PerformanceFee._postWithdrawHook(assets); } diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol index 89cd227..03fce4c 100644 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ b/src/contracts/LidoMultiPriceMultiLpARM.sol @@ -87,13 +87,10 @@ contract LidoMultiPriceMultiLpARM is PerformanceFee._postDepositHook(assets); } - function _postWithdrawHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { + function _postWithdrawHook(uint256 assets) internal override(MultiLP, PerformanceFee) { // Remove assets from the liquidity tranches MultiPriceARM._removeLiquidity(assets); - // Update the LP's assets - AccessControlLP._postWithdrawHook(assets); - // Store the new total assets after the withdrawal and performance fee accrued PerformanceFee._postWithdrawHook(assets); } diff --git a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol index 0641999..876cb27 100644 --- a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol +++ b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol @@ -95,6 +95,10 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { lidoARM.setTotalAssetsCap(100 ether); + address[] memory liquidityProviders = new address[](1); + liquidityProviders[0] = address(this); + lidoARM.setLiquidityProviderCaps(liquidityProviders, 20 ether); + // Only fuzz from this address. Big speedup on fork. targetSender(address(this)); } @@ -109,7 +113,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { } /// @dev Check initial state - function test_initial_state() external { + function test_initial_state() external view { assertEq(lidoARM.name(), "Lido ARM"); assertEq(lidoARM.symbol(), "ARM-ST"); assertEq(lidoARM.owner(), address(this)); @@ -127,8 +131,6 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { // whitelisted LP adds WETH liquidity to the ARM function test_depositAssets() external { - lidoARM.setLiquidityProviderCap(address(this), 20 ether); - _dealWETH(address(this), 10 ether); beforeData = _snapData(); @@ -148,7 +150,6 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { // non whitelisted LP tries to add WETH liquidity to the ARM function test_redeemAssets() external { - lidoARM.setLiquidityProviderCap(address(this), 20 ether); _dealWETH(address(this), 10 ether); lidoARM.deposit(10 ether); diff --git a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol index 080dc72..6e869a4 100644 --- a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol +++ b/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol @@ -120,6 +120,10 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { lidoARM.setTotalAssetsCap(100 ether); + address[] memory liquidityProviders = new address[](1); + liquidityProviders[0] = address(this); + lidoARM.setLiquidityProviderCaps(liquidityProviders, 20 ether); + // Only fuzz from this address. Big speedup on fork. targetSender(address(this)); } @@ -134,7 +138,7 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { } /// @dev Check initial state - function test_initial_state() external { + function test_initial_state() external view { assertEq(lidoARM.name(), "Lido ARM"); assertEq(lidoARM.symbol(), "ARM-ST"); assertEq(lidoARM.owner(), address(this)); @@ -166,8 +170,6 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { // whitelisted LP adds WETH liquidity to the ARM function test_depositAssets() external { - lidoARM.setLiquidityProviderCap(address(this), 20 ether); - _dealWETH(address(this), 10 ether); beforeData = _snapData(); @@ -187,7 +189,6 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { // non whitelisted LP tries to add WETH liquidity to the ARM function test_redeemAssets() external { - lidoARM.setLiquidityProviderCap(address(this), 20 ether); _dealWETH(address(this), 10 ether); lidoARM.deposit(10 ether); From 8bf0599ecd526cfdca8ea63c7317950e146f28cc Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 18 Sep 2024 08:37:09 +1000 Subject: [PATCH 039/196] Added LiquidityProviderCap event when decrementing the lp cap --- src/contracts/AccessControlLP.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/contracts/AccessControlLP.sol b/src/contracts/AccessControlLP.sol index dbb9fd7..b0eeced 100644 --- a/src/contracts/AccessControlLP.sol +++ b/src/contracts/AccessControlLP.sol @@ -13,12 +13,17 @@ abstract contract AccessControlLP is MultiLP { event TotalAssetsCap(uint256 cap); function _postDepositHook(uint256 assets) internal virtual override { - require(liquidityProviderCaps[msg.sender] >= assets, "ARM: LP cap exceeded"); + uint256 oldCap = liquidityProviderCaps[msg.sender]; + require(oldCap >= assets, "ARM: LP cap exceeded"); // total assets has already been updated with the new assets require(totalAssetsCap >= totalAssets(), "ARM: Total assets cap exceeded"); + uint256 newCap = oldCap - assets; + // Save the new LP cap to storage - liquidityProviderCaps[msg.sender] -= assets; + liquidityProviderCaps[msg.sender] = newCap; + + emit LiquidityProviderCap(msg.sender, newCap); } function setLiquidityProviderCaps(address[] calldata _liquidityProviders, uint256 cap) external onlyOwner { From dfb26f41e35142467df698eb0ad35e8b19ea4747 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 18 Sep 2024 20:51:05 +1000 Subject: [PATCH 040/196] Moved the Liquidity Provider Controller out of the ARM --- docs/LidoFixedPriceMultiLpARMHierarchy.svg | 208 +++++++++--------- docs/LidoFixedPriceMultiLpARMSquashed.svg | 204 +++++++++-------- src/contracts/Interfaces.sol | 17 ++ src/contracts/LidoFixedPriceMultiLpARM.sol | 16 +- src/contracts/LiquidityProviderController.sol | 63 ++++++ .../LiquidityProviderControllerARM.sol | 40 ++++ .../mainnet/LidoFixedPriceMultiLpARM.t.sol | 34 ++- 7 files changed, 357 insertions(+), 225 deletions(-) create mode 100644 src/contracts/LiquidityProviderController.sol create mode 100644 src/contracts/LiquidityProviderControllerARM.sol diff --git a/docs/LidoFixedPriceMultiLpARMHierarchy.svg b/docs/LidoFixedPriceMultiLpARMHierarchy.svg index 265732c..40070a2 100644 --- a/docs/LidoFixedPriceMultiLpARMHierarchy.svg +++ b/docs/LidoFixedPriceMultiLpARMHierarchy.svg @@ -4,56 +4,34 @@ - + UmlClassDiagram - + 0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol - + -24 - -OwnableOperable -../src/contracts/OwnableOperable.sol +28 + +OwnableOperable +../src/contracts/OwnableOperable.sol - + -0->24 - - - - - -1 - -<<Abstract>> -AccessControlLP -../src/contracts/AccessControlLP.sol - - - -17 - -<<Abstract>> -MultiLP -../src/contracts/MultiLP.sol - - - -1->17 - - +0->28 + + - + 3 <<Abstract>> @@ -61,94 +39,116 @@ ../src/contracts/FixedPriceARM.sol - + 3->0 - - + + - - -13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol + + +15 + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol - + -13->1 - - +15->3 + + + + + +16 + +<<Abstract>> +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol - + -13->3 - - +15->16 + + - + -14 - -<<Abstract>> -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol +20 + +<<Abstract>> +LiquidityProviderControllerARM +../src/contracts/LiquidityProviderControllerARM.sol - - -13->14 - - + + +15->20 + + - - -13->17 - - + + +21 + +<<Abstract>> +MultiLP +../src/contracts/MultiLP.sol - + + +15->21 + + + + -27 - -<<Abstract>> -PerformanceFee -../src/contracts/PerformanceFee.sol +31 + +<<Abstract>> +PerformanceFee +../src/contracts/PerformanceFee.sol - - -13->27 - - + + +15->31 + + - + + +16->28 + + + + -14->24 - - +20->21 + + - + -17->0 - - +21->0 + + - + -23 - -Ownable -../src/contracts/Ownable.sol +27 + +Ownable +../src/contracts/Ownable.sol - + -24->23 - - +28->27 + + - + -27->17 - - +31->21 + + diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index 12c8c15..60369eb 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,112 +4,108 @@ - - + + UmlClassDiagram - - + + -13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<MultiLP>> -   _gap: uint256[50] <<PerformanceFee>> -   _gap: uint256[50] <<AccessControlLP>> -   _gap: uint256[50] <<FixedPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityAsset: address <<MultiLP>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   MAX_FEE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   totalAssetsCap: uint256 <<AccessControlLP>> -   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _externalWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> -    _rawTotalAssets(): uint256 <<PerformanceFee>> -    _setFee(_fee: uint256) <<PerformanceFee>> -    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -    _postClaimHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> -    collectFees(): (fees: uint256) <<PerformanceFee>> -    setLiquidityProviderCap(liquidityProvider: address, cap: uint256) <<onlyOwner>> <<AccessControlLP>> -    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> -    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> -    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> -    <<event>> LiquidityProviderCap(liquidityProvider: address, cap: uint256) <<AccessControlLP>> -    <<event>> TotalAssetsCap(cap: uint256) <<AccessControlLP>> +15 + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<MultiLP>> +   _gap: uint256[50] <<PerformanceFee>> +   _gap: uint256[49] <<LiquidityProviderControllerARM>> +   _gap: uint256[50] <<FixedPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityAsset: address <<MultiLP>> +   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   MAX_FEE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   liquidityProviderController: address <<LiquidityProviderControllerARM>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> +    _externalWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _rawTotalAssets(): uint256 <<PerformanceFee>> +    _setFee(_fee: uint256) <<PerformanceFee>> +    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> +    _initLPControllerARM(_liquidityProviderController: address) <<LiquidityProviderControllerARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +    _postClaimHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> +    collectFees(): (fees: uint256) <<PerformanceFee>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<LiquidityProviderControllerARM>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> +    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> +    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<LiquidityProviderControllerARM>>    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index a8af55c..4ebe543 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -113,6 +113,23 @@ interface IOethARM { function claimWithdrawals(uint256[] calldata requestIds) external; } +interface ILiquidityProviderARM { + function previewDeposit(uint256 assets) external returns (uint256 shares); + function deposit(uint256 assets) external returns (uint256 shares); + + function previewRedeem(uint256 shares) external returns (uint256 assets); + function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets); + function claimRedeem(uint256 requestId) external returns (uint256 assets); + + function totalAssets() external returns (uint256 assets); + function convertToShares(uint256 assets) external returns (uint256 shares); + function convertToAssets(uint256 shares) external returns (uint256 assets); +} + +interface ILiquidityProviderController { + function postDepositHook(address liquidityProvider, uint256 assets) external; +} + interface IOETHVault { function mint(address _asset, uint256 _amount, uint256 _minimumOusdAmount) external; diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index 1feb161..6569eb9 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -5,7 +5,7 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AbstractARM} from "./AbstractARM.sol"; -import {AccessControlLP} from "./AccessControlLP.sol"; +import {LiquidityProviderControllerARM} from "./LiquidityProviderControllerARM.sol"; import {FixedPriceARM} from "./FixedPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; import {MultiLP} from "./MultiLP.sol"; @@ -20,7 +20,7 @@ contract LidoFixedPriceMultiLpARM is Initializable, MultiLP, PerformanceFee, - AccessControlLP, + LiquidityProviderControllerARM, FixedPriceARM, LidoLiquidityManager { @@ -42,17 +42,20 @@ contract LidoFixedPriceMultiLpARM is /// 10,000 = 100% performance fee /// 500 = 5% performance fee /// @param _feeCollector The account that can collect the performance fee + /// @param _liquidityProviderController The address of the Liquidity Provider Controller function initialize( string calldata _name, string calldata _symbol, address _operator, uint256 _fee, - address _feeCollector + address _feeCollector, + address _liquidityProviderController ) external initializer { _initOwnableOperable(_operator); _initMultiLP(_name, _symbol); lastTotalAssets = SafeCast.toUint128(MIN_TOTAL_SUPPLY); _initPerformanceFee(_fee, _feeCollector); + _initLPControllerARM(_liquidityProviderController); } /** @@ -75,9 +78,12 @@ contract LidoFixedPriceMultiLpARM is return LidoLiquidityManager._externalWithdrawQueue(); } - function _postDepositHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { + function _postDepositHook(uint256 assets) + internal + override(MultiLP, LiquidityProviderControllerARM, PerformanceFee) + { // Check the LP can deposit the assets - AccessControlLP._postDepositHook(assets); + LiquidityProviderControllerARM._postDepositHook(assets); // Store the new total assets after the deposit and performance fee accrued PerformanceFee._postDepositHook(assets); diff --git a/src/contracts/LiquidityProviderController.sol b/src/contracts/LiquidityProviderController.sol new file mode 100644 index 0000000..a51cd3c --- /dev/null +++ b/src/contracts/LiquidityProviderController.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Ownable} from "./Ownable.sol"; +import {ILiquidityProviderARM} from "./Interfaces.sol"; + +/** + * @title Controller of ARM liquidity providers. + * @author Origin Protocol Inc + */ +contract LiquidityProviderController is Ownable { + /// @notice The address of the linked Application Redemption Manager (ARM). + address public immutable arm; + + /// @notice The ARM's maximum allowed total assets. + uint256 public totalAssetsCap; + /// @notice The maximum allowed assets for each liquidity provider. + /// This is effectively a whitelist of liquidity providers as a zero amount prevents any deposits. + mapping(address liquidityProvider => uint256 cap) public liquidityProviderCaps; + + uint256[48] private _gap; + + event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); + event TotalAssetsCap(uint256 cap); + + constructor(address _arm) { + arm = _arm; + } + + function postDepositHook(address liquidityProvider, uint256 assets) external { + require(msg.sender == arm, "LPC: Caller is not ARM"); + + uint256 oldCap = liquidityProviderCaps[liquidityProvider]; + require(oldCap >= assets, "LPC: LP cap exceeded"); + + // total assets has already been updated with the new assets + require(totalAssetsCap >= ILiquidityProviderARM(arm).totalAssets(), "LPC: Total assets cap exceeded"); + + uint256 newCap = oldCap - assets; + + // Save the new LP cap to storage + liquidityProviderCaps[liquidityProvider] = newCap; + + emit LiquidityProviderCap(liquidityProvider, newCap); + } + + function setLiquidityProviderCaps(address[] calldata _liquidityProviders, uint256 cap) external onlyOwner { + for (uint256 i = 0; i < _liquidityProviders.length; i++) { + liquidityProviderCaps[_liquidityProviders[i]] = cap; + + emit LiquidityProviderCap(_liquidityProviders[i], cap); + } + } + + /// @notice Set the ARM's maximum total assets. + /// Setting to zero will prevent any further deposits. + /// The liquidity provider can still withdraw assets. + function setTotalAssetsCap(uint256 _totalAssetsCap) external onlyOwner { + totalAssetsCap = _totalAssetsCap; + + emit TotalAssetsCap(_totalAssetsCap); + } +} diff --git a/src/contracts/LiquidityProviderControllerARM.sol b/src/contracts/LiquidityProviderControllerARM.sol new file mode 100644 index 0000000..07aefa9 --- /dev/null +++ b/src/contracts/LiquidityProviderControllerARM.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {MultiLP} from "./MultiLP.sol"; +import {ILiquidityProviderController} from "./Interfaces.sol"; + +/** + * @title ARM integration to the Liquidity Provider Controller that whitelists liquidity providers + * and enforces a total assets cap. + * @author Origin Protocol Inc + */ +abstract contract LiquidityProviderControllerARM is MultiLP { + address public liquidityProviderController; + + uint256[49] private _gap; + + event LiquidityProviderControllerUpdated(address indexed liquidityProviderController); + + /// @dev called in the ARM's initialize function to set the Liquidity Provider Controller + function _initLPControllerARM(address _liquidityProviderController) internal { + liquidityProviderController = _liquidityProviderController; + + emit LiquidityProviderControllerUpdated(_liquidityProviderController); + } + + /// @dev calls the liquidity provider controller if one is configured to check the liquidity provider and total assets caps + function _postDepositHook(uint256 assets) internal virtual override { + if (liquidityProviderController != address(0)) { + ILiquidityProviderController(liquidityProviderController).postDepositHook(msg.sender, assets); + } + } + + /// @notice Set the Liquidity Provider Controller contract address. + /// Set to a zero address to disable the controller. + function setLiquidityProviderController(address _liquidityProviderController) external onlyOwner { + liquidityProviderController = _liquidityProviderController; + + emit LiquidityProviderControllerUpdated(_liquidityProviderController); + } +} diff --git a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol index 876cb27..343dad1 100644 --- a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol +++ b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol @@ -4,14 +4,17 @@ pragma solidity ^0.8.23; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "contracts/Interfaces.sol"; +import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; import {Proxy} from "contracts/Proxy.sol"; import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { - Proxy public lidoProxy; + Proxy public armProxy; + Proxy public lpcProxy; LidoFixedPriceMultiLpARM public lidoARM; + LiquidityProviderController public liquidityProviderController; IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); uint256 performanceFee = 2000; // 20% address feeCollector = 0x000000000000000000000000000000Feec011ec1; @@ -41,7 +44,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { return AssertData({ totalAssets: lidoARM.totalAssets(), totalSupply: lidoARM.totalSupply(), - totalAssetsCap: lidoARM.totalAssetsCap(), + totalAssetsCap: liquidityProviderController.totalAssetsCap(), armWeth: weth.balanceOf(address(lidoARM)), armSteth: steth.balanceOf(address(lidoARM)), feesAccrued: lidoARM.feesAccrued() @@ -68,36 +71,43 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { function setUp() public override { super.setUp(); + lpcProxy = new Proxy(); + armProxy = new Proxy(); + + LiquidityProviderController lpcImpl = new LiquidityProviderController(address(armProxy)); + lpcProxy.initialize(address(lpcImpl), address(this), ""); + liquidityProviderController = LiquidityProviderController(payable(address(lpcProxy))); + address lidoWithdrawal = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; LidoFixedPriceMultiLpARM lidoImpl = new LidoFixedPriceMultiLpARM(address(steth), address(weth), lidoWithdrawal); - lidoProxy = new Proxy(); // The deployer needs a tiny amount of WETH to initialize the ARM _dealWETH(address(this), 1e12); - weth.approve(address(lidoProxy), type(uint256).max); - steth.approve(address(lidoProxy), type(uint256).max); + weth.approve(address(armProxy), type(uint256).max); + steth.approve(address(armProxy), type(uint256).max); // Initialize Proxy with LidoFixedPriceMultiLpARM implementation. bytes memory data = abi.encodeWithSignature( - "initialize(string,string,address,uint256,address)", + "initialize(string,string,address,uint256,address,address)", "Lido ARM", "ARM-ST", operator, performanceFee, - feeCollector + feeCollector, + address(liquidityProviderController) ); - lidoProxy.initialize(address(lidoImpl), address(this), data); + armProxy.initialize(address(lidoImpl), address(this), data); - lidoARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); + lidoARM = LidoFixedPriceMultiLpARM(payable(address(armProxy))); // set prices lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - lidoARM.setTotalAssetsCap(100 ether); + liquidityProviderController.setTotalAssetsCap(100 ether); address[] memory liquidityProviders = new address[](1); liquidityProviders[0] = address(this); - lidoARM.setLiquidityProviderCaps(liquidityProviders, 20 ether); + liquidityProviderController.setLiquidityProviderCaps(liquidityProviders, 20 ether); // Only fuzz from this address. Big speedup on fork. targetSender(address(this)); @@ -126,7 +136,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { assertEq(lidoARM.totalAssets(), 1e12); assertEq(lidoARM.totalSupply(), 1e12); assertEq(weth.balanceOf(address(lidoARM)), 1e12); - assertEq(lidoARM.totalAssetsCap(), 100 ether); + assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); } // whitelisted LP adds WETH liquidity to the ARM From d61a3d632d6dfc5bd893d514029783ae8c83067a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Thu, 19 Sep 2024 04:46:00 +0200 Subject: [PATCH 041/196] Refactor test structure (#16) * refactor test structur. * feat: adjust tests for liquidityProviderController. * forge fmt * feat: add asOwner modifier * bump forge version. * feat: use random address. --- lib/forge-std | 2 +- src/contracts/utils/Addresses.sol | 5 + test/Base.sol | 12 + .../Constructor.t.sol | 33 +++ .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 80 ++++++ .../RequestRedeem.t.sol | 28 ++ .../LidoMultiPriceMultiLpARM.t.sol | 3 +- test/fork/LidoOwnerLpARM/Setters.t.sol | 66 +++++ .../SwapExactTokensForTokens.t copy.sol | 89 +++++++ .../SwapTokensForExactTokens.t.sol | 82 ++++++ test/fork/LidoOwnerLpARM/TransferToken.t.sol | 40 +++ test/fork/{mainnet => OethARM}/Ownable.t.sol | 0 test/fork/{mainnet => OethARM}/Proxy.t.sol | 0 .../SwapExactTokensForTokens.t.sol | 1 - .../SwapTokensForExactTokens.t.sol | 1 - test/fork/{mainnet => OethARM}/Transfer.t.sol | 0 test/fork/{mainnet => OethARM}/Withdraw.t.sol | 0 .../mainnet/LidoFixedPriceMultiLpARM.t.sol | 168 ------------ test/fork/mainnet/LidoOwnerLpARM.t.sol | 243 ------------------ test/fork/shared/Shared.sol | 79 +++++- test/fork/utils/Helpers.sol | 7 + test/fork/utils/Modifiers.sol | 37 +++ 22 files changed, 560 insertions(+), 416 deletions(-) create mode 100644 test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol create mode 100644 test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol create mode 100644 test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol rename test/fork/{mainnet => LidoMultiPriceMultiLpARM}/LidoMultiPriceMultiLpARM.t.sol (99%) create mode 100644 test/fork/LidoOwnerLpARM/Setters.t.sol create mode 100644 test/fork/LidoOwnerLpARM/SwapExactTokensForTokens.t copy.sol create mode 100644 test/fork/LidoOwnerLpARM/SwapTokensForExactTokens.t.sol create mode 100644 test/fork/LidoOwnerLpARM/TransferToken.t.sol rename test/fork/{mainnet => OethARM}/Ownable.t.sol (100%) rename test/fork/{mainnet => OethARM}/Proxy.t.sol (100%) rename test/fork/{mainnet => OethARM}/SwapExactTokensForTokens.t.sol (99%) rename test/fork/{mainnet => OethARM}/SwapTokensForExactTokens.t.sol (99%) rename test/fork/{mainnet => OethARM}/Transfer.t.sol (100%) rename test/fork/{mainnet => OethARM}/Withdraw.t.sol (100%) delete mode 100644 test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol delete mode 100644 test/fork/mainnet/LidoOwnerLpARM.t.sol diff --git a/lib/forge-std b/lib/forge-std index 978ac6f..beb836e 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801 +Subproject commit beb836e33f9a207f4927abb7cd09ad0afe4b3f9f diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index 40a2820..299f4b3 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -22,10 +22,14 @@ library Mainnet { address public constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + address public constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; // Contracts address public constant OETH_VAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; address public constant OETH_ARM = 0x6bac785889A4127dB0e0CeFEE88E0a9F1Aaf3cC7; + + // Lido + address public constant LIDO_WITHDRAWAL = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; } library Holesky { @@ -66,6 +70,7 @@ contract AddressResolver { resolver[MAINNET]["OETH"] = Mainnet.OETH; resolver[MAINNET]["WETH"] = Mainnet.WETH; resolver[MAINNET]["STETH"] = Mainnet.STETH; + resolver[MAINNET]["WSTETH"] = Mainnet.WSTETH; // Contracts resolver[MAINNET]["OETH_VAULT"] = Mainnet.OETH_VAULT; diff --git a/test/Base.sol b/test/Base.sol index fcf33ab..2668c5e 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -7,6 +7,9 @@ import {Test} from "forge-std/Test.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; +import {LidoOwnerLpARM} from "contracts/LidoOwnerLpARM.sol"; +import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; @@ -29,11 +32,18 @@ abstract contract Base_Test_ is Test { /// --- CONTRACTS ////////////////////////////////////////////////////// Proxy public proxy; + Proxy public lpcProxy; + Proxy public lidoProxy; + Proxy public lidoOwnerProxy; OethARM public oethARM; + LidoOwnerLpARM public lidoOwnerLpARM; + LidoFixedPriceMultiLpARM public lidoARM; + LiquidityProviderController public liquidityProviderController; IERC20 public oeth; IERC20 public weth; IERC20 public steth; + IERC20 public wsteth; IOETHVault public vault; ////////////////////////////////////////////////////// @@ -44,11 +54,13 @@ abstract contract Base_Test_ is Test { address public governor; address public operator; address public oethWhale; + address public feeCollector; ////////////////////////////////////////////////////// /// --- DEFAULT VALUES ////////////////////////////////////////////////////// uint256 public constant DEFAULT_AMOUNT = 1 ether; + uint256 public constant STETH_ERROR_ROUNDING = 2; ////////////////////////////////////////////////////// /// --- SETUP diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol new file mode 100644 index 0000000..13b5542 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_Constructor_Test is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_Initial_State() public { + assertEq(lidoARM.name(), "Lido ARM"); + assertEq(lidoARM.symbol(), "ARM-ST"); + assertEq(lidoARM.owner(), address(this)); + assertEq(lidoARM.operator(), operator); + assertEq(lidoARM.feeCollector(), feeCollector); + assertEq(lidoARM.fee(), 2000); + assertEq(lidoARM.lastTotalAssets(), 1e12); + assertEq(lidoARM.feesAccrued(), 0); + // the 20% performance fee is removed on initialization + assertEq(lidoARM.totalAssets(), 1e12); + assertEq(lidoARM.totalSupply(), 1e12); + assertEq(weth.balanceOf(address(lidoARM)), 1e12); + assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol new file mode 100644 index 0000000..727f1bb --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Test_ { + AssertData beforeData; + DeltaData noChangeDeltaData = + DeltaData({totalAssets: 10, totalSupply: 0, totalAssetsCap: 0, armWeth: 0, armSteth: 0, feesAccrued: 0}); + + struct AssertData { + uint256 totalAssets; + uint256 totalSupply; + uint256 totalAssetsCap; + uint256 armWeth; + uint256 armSteth; + uint256 feesAccrued; + } + + struct DeltaData { + int256 totalAssets; + int256 totalSupply; + int256 totalAssetsCap; + int256 armWeth; + int256 armSteth; + int256 feesAccrued; + } + + function _snapData() internal view returns (AssertData memory data) { + return AssertData({ + totalAssets: lidoARM.totalAssets(), + totalSupply: lidoARM.totalSupply(), + totalAssetsCap: liquidityProviderController.totalAssetsCap(), + armWeth: weth.balanceOf(address(lidoARM)), + armSteth: steth.balanceOf(address(lidoARM)), + feesAccrued: lidoARM.feesAccrued() + }); + } + + function assertData(AssertData memory before, DeltaData memory delta) internal view { + AssertData memory afterData = _snapData(); + + assertEq(int256(afterData.totalAssets), int256(before.totalAssets) + delta.totalAssets, "totalAssets"); + assertEq(int256(afterData.totalSupply), int256(before.totalSupply) + delta.totalSupply, "totalSupply"); + assertEq( + int256(afterData.totalAssetsCap), int256(before.totalAssetsCap) + delta.totalAssetsCap, "totalAssetsCap" + ); + assertEq(int256(afterData.feesAccrued), int256(before.feesAccrued) + delta.feesAccrued, "feesAccrued"); + assertEq(int256(afterData.armWeth), int256(before.armWeth) + delta.armWeth, "armWeth"); + assertEq(int256(afterData.armSteth), int256(before.armSteth) + delta.armSteth, "armSteth"); + } + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_Deposit_SimpleCase() + public + asLidoFixedPriceMultiLpARMOwner + setLiquidityProviderCap(address(this), 20 ether) + { + deal(address(weth), address(this), 10 ether); + beforeData = _snapData(); + + lidoARM.deposit(10 ether); + + DeltaData memory delta = noChangeDeltaData; + delta.totalAssets = 10 ether; + delta.totalSupply = 10 ether; + delta.armWeth = 10 ether; + assertData(beforeData, delta); + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol new file mode 100644 index 0000000..7d4b9a3 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_RequestRedeem_SimpleCase() + public + asLidoFixedPriceMultiLpARMOwner + setLiquidityProviderCap(address(this), 20 ether) + { + deal(address(weth), address(this), 10 ether); + + lidoARM.deposit(10 ether); + lidoARM.requestRedeem(8 ether); + } +} diff --git a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol b/test/fork/LidoMultiPriceMultiLpARM/LidoMultiPriceMultiLpARM.t.sol similarity index 99% rename from test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol rename to test/fork/LidoMultiPriceMultiLpARM/LidoMultiPriceMultiLpARM.t.sol index 6e869a4..a16018a 100644 --- a/test/fork/mainnet/LidoMultiPriceMultiLpARM.t.sol +++ b/test/fork/LidoMultiPriceMultiLpARM/LidoMultiPriceMultiLpARM.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; - +/* import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "contracts/Interfaces.sol"; @@ -208,3 +208,4 @@ contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { // with only liquidity in the fifth tranche //// swap stETH to WETH using just the fifth tranche } +*/ diff --git a/test/fork/LidoOwnerLpARM/Setters.t.sol b/test/fork/LidoOwnerLpARM/Setters.t.sol new file mode 100644 index 0000000..3bcd360 --- /dev/null +++ b/test/fork/LidoOwnerLpARM/Setters.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +contract Fork_Concrete_LidoOwnerLpARM_Setters_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_SetPrices_Because_PriceCross() public { + vm.expectRevert("ARM: Price cross"); + lidoOwnerLpARM.setPrices(90 * 1e33, 89 * 1e33); + vm.expectRevert("ARM: Price cross"); + lidoOwnerLpARM.setPrices(72, 70); + vm.expectRevert("ARM: Price cross"); + lidoOwnerLpARM.setPrices(1005 * 1e33, 1000 * 1e33); + } + + function test_RevertWhen_SetPrices_Because_TraderateTooHigh() public { + //vm.expectRevert("ARM: Traderates too high"); + //lidoOwnerLpARM.setPrices(1010 * 1e33, 1020 * 1e33); + //vm.expectRevert("ARM: Traderates too high"); + //lidoOwnerLpARM.setPrices(993 * 1e33, 994 * 1e33); + } + + function test_RevertWhen_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { + vm.expectRevert("ARM: Only operator or owner can call this function."); + lidoOwnerLpARM.setPrices(0, 0); + } + + function test_RevertWhen_SetOwner_Because_NotOwner() public asRandomAddress { + vm.expectRevert("ARM: Only owner can call this function."); + lidoOwnerLpARM.setOwner(address(0)); + } + + function test_RevertWhen_SetOperator_Because_NotOwner() public asRandomAddress { + vm.expectRevert("ARM: Only owner can call this function."); + lidoOwnerLpARM.setOperator(address(0)); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SetPrices() public asLidoOwnerLpARMOperator { + lidoOwnerLpARM.setPrices(992 * 1e33, 1001 * 1e33); + lidoOwnerLpARM.setPrices(1001 * 1e33, 1004 * 1e33); + lidoOwnerLpARM.setPrices(992 * 1e33, 2000 * 1e33); + + // Check the traderates + assertEq(lidoOwnerLpARM.traderate0(), 500 * 1e33); + assertEq(lidoOwnerLpARM.traderate1(), 992 * 1e33); + } + + function test_SetOperator() public asLidoOwnerLpARMOwner { + lidoOwnerLpARM.setOperator(address(this)); + assertEq(lidoOwnerLpARM.operator(), address(this)); + } +} diff --git a/test/fork/LidoOwnerLpARM/SwapExactTokensForTokens.t copy.sol b/test/fork/LidoOwnerLpARM/SwapExactTokensForTokens.t copy.sol new file mode 100644 index 0000000..89f8166 --- /dev/null +++ b/test/fork/LidoOwnerLpARM/SwapExactTokensForTokens.t copy.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_LidoOwnerLpARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Test_ { + // Account for stETH rounding errors. + // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case + uint256 constant ROUNDING = STETH_ERROR_ROUNDING; + + IERC20 BAD_TOKEN; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + deal(address(weth), address(lidoOwnerLpARM), 1_000_000 ether); + deal(address(steth), address(lidoOwnerLpARM), 1_000_000 ether); + + BAD_TOKEN = IERC20(makeAddr("bad token")); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenIn() public { + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapExactTokensForTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); + } + + function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut() public { + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapExactTokensForTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapExactTokensForTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SwapExactTokensForTokens_WETH_TO_STETH() public { + _swapExactTokensForTokens(weth, steth, 10 ether, 6.25 ether); + } + + function test_SwapExactTokensForTokens_STETH_TO_WETH() public { + _swapExactTokensForTokens(steth, weth, 10 ether, 5 ether); + } + + function test_RealisticSwap() public { + lidoOwnerLpARM.setPrices(997 * 1e33, 998 * 1e33); + _swapExactTokensForTokens(steth, weth, 10 ether, 9.97 ether); + _swapExactTokensForTokens(weth, steth, 10 ether, 10020040080160320641); + } + + ////////////////////////////////////////////////////// + /// --- HELPERS + ////////////////////////////////////////////////////// + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) + internal + { + if (inToken == weth) { + deal(address(weth), address(this), amountIn + 1000); + } else { + deal(address(steth), address(this), amountIn + 1000); + } + uint256 startIn = inToken.balanceOf(address(this)); + uint256 startOut = outToken.balanceOf(address(this)); + lidoOwnerLpARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); + assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); + assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); + assertGe(outToken.balanceOf(address(this)), startOut + expectedOut - ROUNDING, "Out actual"); + assertLe(outToken.balanceOf(address(this)), startOut + expectedOut + ROUNDING, "Out actual"); + } +} diff --git a/test/fork/LidoOwnerLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoOwnerLpARM/SwapTokensForExactTokens.t.sol new file mode 100644 index 0000000..edeef47 --- /dev/null +++ b/test/fork/LidoOwnerLpARM/SwapTokensForExactTokens.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_LidoOwnerLpARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Test_ { + // Account for stETH rounding errors. + // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case + uint256 constant ROUNDING = STETH_ERROR_ROUNDING; + + IERC20 BAD_TOKEN; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + deal(address(weth), address(lidoOwnerLpARM), 1_000_000 ether); + deal(address(steth), address(lidoOwnerLpARM), 1_000_000 ether); + + BAD_TOKEN = IERC20(makeAddr("bad token")); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenIn() public { + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapTokensForExactTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); + } + + function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut() public { + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoOwnerLpARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SwapTokensForExactTokens_WETH_TO_STETH() public { + _swapTokensForExactTokens(weth, steth, 10 ether, 6.25 ether); + } + + function test_SwapTokensForExactTokens_STETH_TO_WETH() public { + _swapTokensForExactTokens(steth, weth, 10 ether, 5 ether); + } + + ////////////////////////////////////////////////////// + /// --- HELPERS + ////////////////////////////////////////////////////// + function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) + internal + { + if (inToken == weth) { + deal(address(weth), address(this), amountIn + 1000); + } else { + deal(address(steth), address(this), amountIn + 1000); + } + uint256 startIn = inToken.balanceOf(address(this)); + lidoOwnerLpARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); + assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); + assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); + assertGe(outToken.balanceOf(address(this)), expectedOut - ROUNDING, "Out actual"); + assertLe(outToken.balanceOf(address(this)), expectedOut + ROUNDING, "Out actual"); + } +} diff --git a/test/fork/LidoOwnerLpARM/TransferToken.t.sol b/test/fork/LidoOwnerLpARM/TransferToken.t.sol new file mode 100644 index 0000000..4e35bfb --- /dev/null +++ b/test/fork/LidoOwnerLpARM/TransferToken.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +contract Fork_Concrete_LidoOwnerLpARM_TransferToken_Test_ is Fork_Shared_Test_ { + uint256 public constant ROUNDING = STETH_ERROR_ROUNDING; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + deal(address(weth), address(lidoOwnerLpARM), 1_000 ether); + deal(address(steth), address(lidoOwnerLpARM), 1_000 ether); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_TransferToken_WETH() public asLidoOwnerLpARMOwner { + uint256 balanceARMBeforeWETH = weth.balanceOf(address(lidoOwnerLpARM)); + uint256 balanceThisBeforeWETH = weth.balanceOf(address(this)); + lidoOwnerLpARM.transferToken(address(weth), address(this), balanceARMBeforeWETH); + + assertEq(weth.balanceOf(address(this)), balanceThisBeforeWETH + balanceARMBeforeWETH); + assertEq(weth.balanceOf(address(lidoOwnerLpARM)), 0); + } + + function test_TransferToken_STETH() public asLidoOwnerLpARMOwner { + uint256 balanceARMBeforeSTETH = steth.balanceOf(address(lidoOwnerLpARM)); + uint256 balanceThisBeforeSTETH = steth.balanceOf(address(this)); + lidoOwnerLpARM.transferToken(address(steth), address(this), balanceARMBeforeSTETH); + + assertApproxEqAbs(steth.balanceOf(address(this)), balanceThisBeforeSTETH + balanceARMBeforeSTETH, ROUNDING); + assertApproxEqAbs(steth.balanceOf(address(lidoOwnerLpARM)), 0, ROUNDING); + } +} diff --git a/test/fork/mainnet/Ownable.t.sol b/test/fork/OethARM/Ownable.t.sol similarity index 100% rename from test/fork/mainnet/Ownable.t.sol rename to test/fork/OethARM/Ownable.t.sol diff --git a/test/fork/mainnet/Proxy.t.sol b/test/fork/OethARM/Proxy.t.sol similarity index 100% rename from test/fork/mainnet/Proxy.t.sol rename to test/fork/OethARM/Proxy.t.sol diff --git a/test/fork/mainnet/SwapExactTokensForTokens.t.sol b/test/fork/OethARM/SwapExactTokensForTokens.t.sol similarity index 99% rename from test/fork/mainnet/SwapExactTokensForTokens.t.sol rename to test/fork/OethARM/SwapExactTokensForTokens.t.sol index fe183de..111afa2 100644 --- a/test/fork/mainnet/SwapExactTokensForTokens.t.sol +++ b/test/fork/OethARM/SwapExactTokensForTokens.t.sol @@ -33,7 +33,6 @@ contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Tes ////////////////////////////////////////////////////// /// --- REVERTING TESTS ////////////////////////////////////////////////////// - function test_RevertWhen_SwapExactTokensForTokens_Simple_Because_InsufficientOutputAmount() public { vm.expectRevert("ARM: Insufficient output amount"); oethARM.swapExactTokensForTokens(oeth, weth, 10 ether, 11 ether, address(this)); diff --git a/test/fork/mainnet/SwapTokensForExactTokens.t.sol b/test/fork/OethARM/SwapTokensForExactTokens.t.sol similarity index 99% rename from test/fork/mainnet/SwapTokensForExactTokens.t.sol rename to test/fork/OethARM/SwapTokensForExactTokens.t.sol index ebfb4c0..3e01237 100644 --- a/test/fork/mainnet/SwapTokensForExactTokens.t.sol +++ b/test/fork/OethARM/SwapTokensForExactTokens.t.sol @@ -33,7 +33,6 @@ contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Tes ////////////////////////////////////////////////////// /// --- REVERTING TESTS ////////////////////////////////////////////////////// - function test_RevertWhen_SwapTokensForExactTokens_Simple_Because_InsufficientOutputAmount() public { vm.expectRevert("ARM: Excess input amount"); oethARM.swapTokensForExactTokens(oeth, weth, 10 ether, 9 ether, address(this)); diff --git a/test/fork/mainnet/Transfer.t.sol b/test/fork/OethARM/Transfer.t.sol similarity index 100% rename from test/fork/mainnet/Transfer.t.sol rename to test/fork/OethARM/Transfer.t.sol diff --git a/test/fork/mainnet/Withdraw.t.sol b/test/fork/OethARM/Withdraw.t.sol similarity index 100% rename from test/fork/mainnet/Withdraw.t.sol rename to test/fork/OethARM/Withdraw.t.sol diff --git a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol b/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol deleted file mode 100644 index 343dad1..0000000 --- a/test/fork/mainnet/LidoFixedPriceMultiLpARM.t.sol +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Test, console2} from "forge-std/Test.sol"; - -import {IERC20} from "contracts/Interfaces.sol"; -import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; -import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; -import {Proxy} from "contracts/Proxy.sol"; - -import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; - -contract Fork_Concrete_LidoFixedPriceMultiLpARM_Test is Fork_Shared_Test_ { - Proxy public armProxy; - Proxy public lpcProxy; - LidoFixedPriceMultiLpARM public lidoARM; - LiquidityProviderController public liquidityProviderController; - IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); - uint256 performanceFee = 2000; // 20% - address feeCollector = 0x000000000000000000000000000000Feec011ec1; - AssertData beforeData; - DeltaData noChangeDeltaData = - DeltaData({totalAssets: 10, totalSupply: 0, totalAssetsCap: 0, armWeth: 0, armSteth: 0, feesAccrued: 0}); - - struct AssertData { - uint256 totalAssets; - uint256 totalSupply; - uint256 totalAssetsCap; - uint256 armWeth; - uint256 armSteth; - uint256 feesAccrued; - } - - struct DeltaData { - int256 totalAssets; - int256 totalSupply; - int256 totalAssetsCap; - int256 armWeth; - int256 armSteth; - int256 feesAccrued; - } - - function _snapData() internal view returns (AssertData memory data) { - return AssertData({ - totalAssets: lidoARM.totalAssets(), - totalSupply: lidoARM.totalSupply(), - totalAssetsCap: liquidityProviderController.totalAssetsCap(), - armWeth: weth.balanceOf(address(lidoARM)), - armSteth: steth.balanceOf(address(lidoARM)), - feesAccrued: lidoARM.feesAccrued() - }); - } - - function assertData(AssertData memory before, DeltaData memory delta) internal view { - AssertData memory afterData = _snapData(); - - assertEq(int256(afterData.totalAssets), int256(before.totalAssets) + delta.totalAssets, "totalAssets"); - assertEq(int256(afterData.totalSupply), int256(before.totalSupply) + delta.totalSupply, "totalSupply"); - assertEq( - int256(afterData.totalAssetsCap), int256(before.totalAssetsCap) + delta.totalAssetsCap, "totalAssetsCap" - ); - assertEq(int256(afterData.feesAccrued), int256(before.feesAccrued) + delta.feesAccrued, "feesAccrued"); - assertEq(int256(afterData.armWeth), int256(before.armWeth) + delta.armWeth, "armWeth"); - assertEq(int256(afterData.armSteth), int256(before.armSteth) + delta.armSteth, "armSteth"); - } - - // Account for stETH rounding errors. - // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case - uint256 constant ROUNDING = 2; - - function setUp() public override { - super.setUp(); - - lpcProxy = new Proxy(); - armProxy = new Proxy(); - - LiquidityProviderController lpcImpl = new LiquidityProviderController(address(armProxy)); - lpcProxy.initialize(address(lpcImpl), address(this), ""); - liquidityProviderController = LiquidityProviderController(payable(address(lpcProxy))); - - address lidoWithdrawal = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; - LidoFixedPriceMultiLpARM lidoImpl = new LidoFixedPriceMultiLpARM(address(steth), address(weth), lidoWithdrawal); - - // The deployer needs a tiny amount of WETH to initialize the ARM - _dealWETH(address(this), 1e12); - weth.approve(address(armProxy), type(uint256).max); - steth.approve(address(armProxy), type(uint256).max); - - // Initialize Proxy with LidoFixedPriceMultiLpARM implementation. - bytes memory data = abi.encodeWithSignature( - "initialize(string,string,address,uint256,address,address)", - "Lido ARM", - "ARM-ST", - operator, - performanceFee, - feeCollector, - address(liquidityProviderController) - ); - armProxy.initialize(address(lidoImpl), address(this), data); - - lidoARM = LidoFixedPriceMultiLpARM(payable(address(armProxy))); - - // set prices - lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - - liquidityProviderController.setTotalAssetsCap(100 ether); - - address[] memory liquidityProviders = new address[](1); - liquidityProviders[0] = address(this); - liquidityProviderController.setLiquidityProviderCaps(liquidityProviders, 20 ether); - - // Only fuzz from this address. Big speedup on fork. - targetSender(address(this)); - } - - function _dealStETH(address to, uint256 amount) internal { - vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); - steth.transfer(to, amount); - } - - function _dealWETH(address to, uint256 amount) internal { - deal(address(weth), to, amount); - } - - /// @dev Check initial state - function test_initial_state() external view { - assertEq(lidoARM.name(), "Lido ARM"); - assertEq(lidoARM.symbol(), "ARM-ST"); - assertEq(lidoARM.owner(), address(this)); - assertEq(lidoARM.operator(), operator); - assertEq(lidoARM.feeCollector(), feeCollector); - assertEq(lidoARM.fee(), performanceFee); - assertEq(lidoARM.lastTotalAssets(), 1e12); - assertEq(lidoARM.feesAccrued(), 0); - // the 20% performance fee is removed on initialization - assertEq(lidoARM.totalAssets(), 1e12); - assertEq(lidoARM.totalSupply(), 1e12); - assertEq(weth.balanceOf(address(lidoARM)), 1e12); - assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); - } - - // whitelisted LP adds WETH liquidity to the ARM - function test_depositAssets() external { - _dealWETH(address(this), 10 ether); - beforeData = _snapData(); - - lidoARM.deposit(10 ether); - - DeltaData memory delta = noChangeDeltaData; - delta.totalAssets = 10 ether; - delta.totalSupply = 10 ether; - delta.armWeth = 10 ether; - assertData(beforeData, delta); - - // assert whitelisted LP cap was decreased - // assert remaining liquidity in appropriate tranches increased - // assert last total assets was set with performance fee removed - // assert performance fee was accrued on asset increases but not the deposit - } - // non whitelisted LP tries to add WETH liquidity to the ARM - - function test_redeemAssets() external { - _dealWETH(address(this), 10 ether); - lidoARM.deposit(10 ether); - - lidoARM.requestRedeem(8 ether); - } -} diff --git a/test/fork/mainnet/LidoOwnerLpARM.t.sol b/test/fork/mainnet/LidoOwnerLpARM.t.sol deleted file mode 100644 index 5c46417..0000000 --- a/test/fork/mainnet/LidoOwnerLpARM.t.sol +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Test, console2} from "forge-std/Test.sol"; - -import {IERC20} from "contracts/Interfaces.sol"; -import {LidoOwnerLpARM} from "contracts/LidoOwnerLpARM.sol"; -import {Proxy} from "contracts/Proxy.sol"; - -import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; - -contract Fork_Concrete_LidoARM_Test is Fork_Shared_Test_ { - Proxy public lidoProxy; - LidoOwnerLpARM public lidoARM; - IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); - - // Account for stETH rounding errors. - // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case - uint256 constant ROUNDING = 2; - - function setUp() public override { - super.setUp(); - - address lidoWithdrawal = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; - LidoOwnerLpARM lidoImpl = new LidoOwnerLpARM(address(weth), address(steth), lidoWithdrawal); - lidoProxy = new Proxy(); - // Initialize Proxy with LidoOwnerLpARM implementation. - bytes memory data = abi.encodeWithSignature("initialize(address)", operator); - lidoProxy.initialize(address(lidoImpl), address(this), data); - - lidoARM = LidoOwnerLpARM(payable(address(lidoProxy))); - - _dealWETH(address(lidoARM), 100 ether); - _dealStETH(address(lidoARM), 100 ether); - // Contract will trade - // give us 1 WETH, get 0.625 stETH - // give us 1 stETH, get 0.5 WETH - lidoARM.setPrices(500 * 1e33, 1600000000000000000000000000000000000); - - weth.approve(address(lidoARM), type(uint256).max); - steth.approve(address(lidoARM), type(uint256).max); - - // Only fuzz from this address. Big speedup on fork. - targetSender(address(this)); - } - - function test_goodPriceSet() external { - lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - lidoARM.setPrices(1001 * 1e33, 1004 * 1e33); - } - - function test_badPriceSet() external { - vm.expectRevert(bytes("ARM: Price cross")); - lidoARM.setPrices(90 * 1e33, 89 * 1e33); - vm.expectRevert(bytes("ARM: Price cross")); - lidoARM.setPrices(72, 70); - vm.expectRevert(bytes("ARM: Price cross")); - lidoARM.setPrices(1005 * 1e33, 1000 * 1e33); - } - - function test_realistic_swaps() external { - vm.prank(operator); - lidoARM.setPrices(997 * 1e33, 998 * 1e33); - _swapExactTokensForTokens(steth, weth, 10 ether, 9.97 ether); - _swapExactTokensForTokens(weth, steth, 10 ether, 10020040080160320641); - } - - function test_swapExactTokensForTokens_WETH_TO_STETH() external { - _swapExactTokensForTokens(weth, steth, 10 ether, 6.25 ether); - } - - function test_swapExactTokensForTokens_STETH_TO_WETH() external { - _swapExactTokensForTokens(steth, weth, 10 ether, 5 ether); - } - - function test_swapTokensForExactTokens_WETH_TO_STETH() external { - _swapTokensForExactTokens(weth, steth, 10 ether, 6.25 ether); - } - - function test_swapTokensForExactTokens_STETH_TO_WETH() external { - _swapTokensForExactTokens(steth, weth, 10 ether, 5 ether); - } - - function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - if (inToken == weth) { - _dealWETH(address(this), amountIn + 1000); - } else { - _dealStETH(address(this), amountIn + 1000); - } - uint256 startIn = inToken.balanceOf(address(this)); - uint256 startOut = outToken.balanceOf(address(this)); - lidoARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); - assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); - assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); - assertGe(outToken.balanceOf(address(this)), startOut + expectedOut - ROUNDING, "Out actual"); - assertLe(outToken.balanceOf(address(this)), startOut + expectedOut + ROUNDING, "Out actual"); - } - - function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - if (inToken == weth) { - _dealWETH(address(this), amountIn + 1000); - } else { - _dealStETH(address(this), amountIn + 1000); - } - uint256 startIn = inToken.balanceOf(address(this)); - lidoARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); - assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); - assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); - assertGe(outToken.balanceOf(address(this)), expectedOut - ROUNDING, "Out actual"); - assertLe(outToken.balanceOf(address(this)), expectedOut + ROUNDING, "Out actual"); - } - - function test_unauthorizedAccess() external { - address RANDOM_ADDRESS = 0xfEEDBeef00000000000000000000000000000000; - vm.startPrank(RANDOM_ADDRESS); - - // Proxy's restricted methods. - vm.expectRevert("ARM: Only owner can call this function."); - proxy.setOwner(RANDOM_ADDRESS); - - vm.expectRevert("ARM: Only owner can call this function."); - proxy.initialize(address(this), address(this), ""); - - vm.expectRevert("ARM: Only owner can call this function."); - proxy.upgradeTo(address(this)); - - vm.expectRevert("ARM: Only owner can call this function."); - proxy.upgradeToAndCall(address(this), ""); - - // Implementation's restricted methods. - vm.expectRevert("ARM: Only owner can call this function."); - lidoARM.setOwner(RANDOM_ADDRESS); - - vm.expectRevert("ARM: Only operator or owner can call this function."); - lidoARM.setPrices(123, 321); - } - - function test_wrongInTokenExactIn() external { - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapExactTokensForTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); - } - - function test_wrongOutTokenExactIn() external { - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); - } - - function test_wrongInTokenExactOut() external { - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); - } - - function test_wrongOutTokenExactOut() external { - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); - } - - function test_collectTokens() external { - lidoARM.transferToken(address(weth), address(this), weth.balanceOf(address(lidoARM))); - assertGt(weth.balanceOf(address(this)), 50 ether); - assertEq(weth.balanceOf(address(lidoARM)), 0); - - lidoARM.transferToken(address(steth), address(this), steth.balanceOf(address(lidoARM))); - assertGt(steth.balanceOf(address(this)), 50 ether); - assertLt(steth.balanceOf(address(lidoARM)), 3); - } - - function _dealStETH(address to, uint256 amount) internal { - vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); - steth.transfer(to, amount); - } - - function _dealWETH(address to, uint256 amount) internal { - deal(address(weth), to, amount); - } - - /* Operator Tests */ - - function test_setOperator() external { - lidoARM.setOperator(address(this)); - assertEq(lidoARM.operator(), address(this)); - } - - function test_nonOwnerCannotSetOperator() external { - vm.expectRevert("ARM: Only owner can call this function."); - vm.prank(operator); - lidoARM.setOperator(operator); - } - - function test_setGoodCheckedTraderates() external { - vm.prank(operator); - lidoARM.setPrices(992 * 1e33, 2000 * 1e33); - assertEq(lidoARM.traderate0(), 500 * 1e33); - assertEq(lidoARM.traderate1(), 992 * 1e33); - } - - function test_setBadCheckedTraderates() external { - vm.prank(operator); - vm.expectRevert("ARM: Traderate too high"); - lidoARM.setPrices(1010 * 1e33, 1020 * 1e33); - vm.prank(operator); - vm.expectRevert("ARM: Traderate too high"); - lidoARM.setPrices(993 * 1e33, 994 * 1e33); - } - - // // Slow on fork - // function invariant_nocrossed_trading_exact_eth() external { - // uint256 sumBefore = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); - // _dealWETH(address(this), 1 ether); - // lidoARM.swapExactTokensForTokens(weth, steth, weth.balanceOf(address(lidoARM)), 0, address(this)); - // lidoARM.swapExactTokensForTokens(steth, weth, steth.balanceOf(address(lidoARM)), 0, address(this)); - // uint256 sumAfter = weth.balanceOf(address(lidoARM)) + steth.balanceOf(address(lidoARM)); - // assertGt(sumBefore, sumAfter, "Lost money swapping"); - // } -} diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index c82d616..0939b3e 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -10,12 +10,16 @@ import {Modifiers} from "test/fork/utils/Modifiers.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; +import {LidoOwnerLpARM} from "contracts/LidoOwnerLpARM.sol"; +import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; import {IOETHVault} from "contracts/Interfaces.sol"; // Utils +import {Mainnet} from "contracts/utils/Addresses.sol"; import {AddressResolver} from "contracts/utils/Addresses.sol"; /// @notice This contract should inherit (directly or indirectly) from `Base_Test_`. @@ -81,6 +85,8 @@ abstract contract Fork_Shared_Test_ is Modifiers { // Users and multisigs alice = makeAddr("alice"); deployer = makeAddr("deployer"); + feeCollector = makeAddr("fee collector"); + operator = resolver.resolve("OPERATOR"); governor = resolver.resolve("GOVERNOR"); oethWhale = resolver.resolve("WHALE_OETH"); @@ -89,13 +95,18 @@ abstract contract Fork_Shared_Test_ is Modifiers { oeth = IERC20(resolver.resolve("OETH")); weth = IERC20(resolver.resolve("WETH")); steth = IERC20(resolver.resolve("STETH")); + wsteth = IERC20(resolver.resolve("WSTETH")); vault = IOETHVault(resolver.resolve("OETH_VAULT")); } function _deployContracts() internal { - // Deploy Proxy. + // --- Deploy all proxies --- proxy = new Proxy(); + lpcProxy = new Proxy(); + lidoProxy = new Proxy(); + lidoOwnerProxy = new Proxy(); + // --- Deploy OethARM implementation --- // Deploy OethARM implementation. address implementation = address(new OethARM(address(oeth), address(weth), address(vault))); vm.label(implementation, "OETH ARM IMPLEMENTATION"); @@ -106,6 +117,68 @@ abstract contract Fork_Shared_Test_ is Modifiers { // Set the Proxy as the OethARM. oethARM = OethARM(address(proxy)); + + // --- Deploy LiquidityProviderController implementation --- + // Deploy LiquidityProviderController implementation. + LiquidityProviderController lpcImpl = new LiquidityProviderController(address(lidoProxy)); + + // Initialize Proxy with LiquidityProviderController implementation. + lpcProxy.initialize(address(lpcImpl), address(this), ""); + + // Set the Proxy as the LiquidityProviderController. + liquidityProviderController = LiquidityProviderController(payable(address(lpcProxy))); + + liquidityProviderController.setTotalAssetsCap(100 ether); + + address[] memory liquidityProviders = new address[](1); + liquidityProviders[0] = address(this); + liquidityProviderController.setLiquidityProviderCaps(liquidityProviders, 20 ether); + liquidityProviderController.setTotalAssetsCap(100 ether); + + // --- Deploy LidoFixedPriceMultiLpARM implementation --- + // Deploy LidoARM implementation. + LidoFixedPriceMultiLpARM lidoImpl = + new LidoFixedPriceMultiLpARM(address(steth), address(weth), Mainnet.LIDO_WITHDRAWAL); + + // Deployer will need WETH to initialize the ARM. + deal(address(weth), address(this), 1e12); + weth.approve(address(lidoProxy), type(uint256).max); + steth.approve(address(lidoProxy), type(uint256).max); + + // Initialize Proxy with LidoFixedPriceMultiLpARM implementation. + data = abi.encodeWithSignature( + "initialize(string,string,address,uint256,address,address)", + "Lido ARM", + "ARM-ST", + operator, + 2000, + feeCollector, + address(lpcProxy) + ); + lidoProxy.initialize(address(lidoImpl), address(this), data); + + // Set the Proxy as the LidoARM. + lidoARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); + + // set prices + lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + + // --- Deploy LidoOwnerLpARM implementation --- + // Deploy LidoOwnerLpARM implementation. + LidoOwnerLpARM lidoOwnerImpl = new LidoOwnerLpARM(address(weth), address(steth), Mainnet.LIDO_WITHDRAWAL); + + // Initialize Proxy with LidoOwnerLpARM implementation. + data = abi.encodeWithSignature("initialize(address)", operator); + lidoOwnerProxy.initialize(address(lidoOwnerImpl), address(this), data); + + // Set the Proxy as the LidoOwnerARM. + lidoOwnerLpARM = LidoOwnerLpARM(payable(address(lidoOwnerProxy))); + + // Set Prices + lidoOwnerLpARM.setPrices(500 * 1e33, 1600000000000000000000000000000000000); + + weth.approve(address(lidoOwnerLpARM), type(uint256).max); + steth.approve(address(lidoOwnerLpARM), type(uint256).max); } function _label() internal { @@ -115,6 +188,10 @@ abstract contract Fork_Shared_Test_ is Modifiers { vm.label(address(vault), "OETH VAULT"); vm.label(address(oethARM), "OETH ARM"); vm.label(address(proxy), "OETH ARM PROXY"); + vm.label(address(lidoARM), "LIDO ARM"); + vm.label(address(lidoProxy), "LIDO ARM PROXY"); + vm.label(address(lidoOwnerLpARM), "LIDO OWNER LP ARM"); + vm.label(address(lidoOwnerProxy), "LIDO OWNER LP ARM PROXY"); vm.label(operator, "OPERATOR"); vm.label(oethWhale, "WHALE OETH"); vm.label(governor, "GOVERNOR"); diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index 29989ed..db2efa6 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -15,6 +15,13 @@ abstract contract Helpers is Base_Test_ { // Transfer OETH from WHALE_OETH to the user. vm.prank(oethWhale); oeth.transfer(to, amount); + } else if (token == address(steth)) { + // Check than whale as enough stETH. Whale is wsteth contract. + require(steth.balanceOf(address(wsteth)) >= amount, "Fork_Shared_Test_: Not enough stETH in WHALE_stETH"); + + // Transfer stETH from WHALE_stETH to the user. + vm.prank(address(wsteth)); + steth.transfer(to, amount); } else { super.deal(token, to, amount); } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index acc8825..44ea756 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -20,9 +20,46 @@ abstract contract Modifiers is Helpers { vm.stopPrank(); } + /// @notice Impersonate the owner of LidoOwnerLpARM contract. + modifier asLidoOwnerLpARMOwner() { + vm.startPrank(lidoOwnerLpARM.owner()); + _; + vm.stopPrank(); + } + + /// @notice Impersonate the operator of LidoOwnerLpARM contract. + modifier asLidoOwnerLpARMOperator() { + vm.startPrank(lidoOwnerLpARM.operator()); + _; + vm.stopPrank(); + } + + /// @notice Impersonate the owner of LidoFixedPriceMultiLpARM contract. + modifier asLidoFixedPriceMultiLpARMOwner() { + vm.startPrank(lidoARM.owner()); + _; + vm.stopPrank(); + } + + /// @notice Impersonate a random address + modifier asRandomAddress() { + vm.startPrank(vm.randomAddress()); + _; + vm.stopPrank(); + } + /// @notice Mock the call to the dripper's `collect` function, bypass it and return `true`. modifier mockCallDripperCollect() { MockCall.mockCallDripperCollect(vault.dripper()); _; } + + /// @notice Set the liquidity provider cap for a given provider on the LidoFixedPriceMultiLpARM contract. + modifier setLiquidityProviderCap(address provider, uint256 cap) { + address[] memory providers = new address[](1); + providers[0] = provider; + + liquidityProviderController.setLiquidityProviderCaps(providers, cap); + _; + } } From b607e6f192cbc05abaea86f0603739d0ff661293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:41:51 +0200 Subject: [PATCH 042/196] Switch from gitmodules to Soldeer. (#17) * chore: switch from gitmodules to soldeer. * chore: update Makefile. * try to fix CI * chore: use rpc_endpoints for forking. --- .github/workflows/main.yml | 3 ++ .gitignore | 2 ++ .gitmodules | 9 ------ Makefile | 9 +++++- foundry.toml | 41 ++++++++++++++++++-------- lib/forge-std | 1 - lib/openzeppelin-contracts | 1 - lib/openzeppelin-contracts-upgradeable | 1 - src/contracts/utils/Addresses.sol | 2 +- src/contracts/utils/GovSixHelper.sol | 2 +- test/fork/shared/Shared.sol | 2 +- 11 files changed, 45 insertions(+), 28 deletions(-) delete mode 100644 .gitmodules delete mode 160000 lib/forge-std delete mode 160000 lib/openzeppelin-contracts delete mode 160000 lib/openzeppelin-contracts-upgradeable diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 532168b..9f64538 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,5 +40,8 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + - name: Install Dependencies + run: forge soldeer install + - name: Run fork tests run: forge test -vvv --summary --detailed diff --git a/.gitignore b/.gitignore index 4b3bc4a..e6764a8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ artifacts # Forge .gas-snapshot +dependencies/ +soldeer.lock # Defender Actions dist diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 9296efd..0000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/openzeppelin-contracts-upgradeable"] - path = lib/openzeppelin-contracts-upgradeable - url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/Makefile b/Makefile index 69f8604..d9e213b 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,14 @@ default: # Always keep Forge up to date install: foundryup - forge install + forge soldeer install + yarn install + +clean: + @rm -rf broadcast cache out + +clean-all: + @rm -rf broadcast cache out dependencies node_modules soldeer.lock gas: @forge test --gas-report diff --git a/foundry.toml b/foundry.toml index e76dc03..9b53e7b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,24 +1,41 @@ [profile.default] src = "src/contracts" out = "out" -libs = ["lib"] +libs = ["dependencies"] verbosity = 3 sender = "0x0165C55EF814dEFdd658532A48Bd17B2c8356322" tx_origin = "0x0165C55EF814dEFdd658532A48Bd17B2c8356322" -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options - -gas_reports = ["OEthARM", "Proxy" ] - +auto_detect_remappings = false +gas_reports = ["OEthARM", "Proxy"] +fs_permissions = [{ access = "read-write", path = "./build" }] +extra_output_files = ["metadata"] remappings = [ "contracts/=./src/contracts", - "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", - "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", "script/=./script", "test/=./test", - "utils/=./src/contracts/utils" + "utils/=./src/contracts/utils", + "forge-std/=dependencies/forge-std-1.9.2/src/", + "@openzeppelin/contracts/=dependencies/@openzeppelin-contracts-5.0.2/", + "@openzeppelin/contracts-upgradeable/=dependencies/@openzeppelin-contracts-upgradeable-5.0.2/", ] -fs_permissions = [{ access = "read-write", path = "./build"}] -extra_output_files = [ - "metadata" -] \ No newline at end of file +[dependencies] +forge-std = "1.9.2" +"@openzeppelin-contracts" = "5.0.2" +"@openzeppelin-contracts-upgradeable" = "5.0.2" + +[soldeer] +recursive_deps = false +remappings_version = false +remappings_generate = false +remappings_regenerate = false +remappings_prefix = "@" +remappings_location = "config" + +[rpc_endpoints] +mainnet = "${PROVIDER_URL}" + +[etherscan] +mainnet = { key = "${ETHERSCAN_API_KEY}", url = "https://etherscan.io/", chain = "mainnet" } + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index beb836e..0000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit beb836e33f9a207f4927abb7cd09ad0afe4b3f9f diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts deleted file mode 160000 index dbb6104..0000000 --- a/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable deleted file mode 160000 index 723f8ca..0000000 --- a/lib/openzeppelin-contracts-upgradeable +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index 299f4b3..f72b167 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; library Common { address public constant ZERO = address(0); diff --git a/src/contracts/utils/GovSixHelper.sol b/src/contracts/utils/GovSixHelper.sol index 0b964a0..cfa1761 100644 --- a/src/contracts/utils/GovSixHelper.sol +++ b/src/contracts/utils/GovSixHelper.sol @@ -5,7 +5,7 @@ import {AddressResolver} from "./Addresses.sol"; import {IGovernance} from "../Interfaces.sol"; import {Vm} from "forge-std/Vm.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; struct GovAction { address target; diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 0939b3e..2a68c3d 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -78,7 +78,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { require(vm.envExists("PROVIDER_URL"), "PROVIDER_URL not set"); // Create and select a fork. - forkId = vm.createSelectFork(vm.envString("PROVIDER_URL")); + forkId = vm.createSelectFork("mainnet"); } function _generateAddresses() internal { From 9b7788c621c0179c28a35e34643e6b8371943ede Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 19 Sep 2024 21:55:39 +1000 Subject: [PATCH 043/196] Added Lido ARM deploy script --- script/deploy/AbstractDeployScript.sol | 9 +- script/deploy/DeployManager.sol | 2 + .../deploy/mainnet/001_DeployCoreScript.sol | 6 +- .../mainnet/003_UpgradeLidoARMScript.sol | 96 +++++++++++++++++++ src/contracts/utils/Addresses.sol | 11 ++- test/smoke/OethARMSmokeTest.t.sol | 2 +- 6 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 script/deploy/mainnet/003_UpgradeLidoARMScript.sol diff --git a/script/deploy/AbstractDeployScript.sol b/script/deploy/AbstractDeployScript.sol index 79a16e9..cf2c5af 100644 --- a/script/deploy/AbstractDeployScript.sol +++ b/script/deploy/AbstractDeployScript.sol @@ -12,6 +12,7 @@ import {GovProposal, GovSixHelper} from "contracts/utils/GovSixHelper.sol"; abstract contract AbstractDeployScript is Script { using GovSixHelper for GovProposal; + address deployer; uint256 public deployBlockNum = type(uint256).max; // DeployerRecord stuff to be extracted as well @@ -53,12 +54,12 @@ abstract contract AbstractDeployScript is Script { } if (this.isForked()) { - address impersonator = Mainnet.INITIAL_DEPLOYER; - console.log("Running script on mainnet fork impersonating: %s", impersonator); - vm.startPrank(impersonator); + deployer = Mainnet.INITIAL_DEPLOYER; + console.log("Running script on mainnet fork impersonating: %s", deployer); + vm.startPrank(deployer); } else { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); - address deployer = vm.rememberKey(deployerPrivateKey); + deployer = vm.rememberKey(deployerPrivateKey); vm.startBroadcast(deployer); console.log("Deploying on mainnet with deployer: %s", deployer); } diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol index 63ca209..dd8780d 100644 --- a/script/deploy/DeployManager.sol +++ b/script/deploy/DeployManager.sol @@ -7,6 +7,7 @@ import {VmSafe} from "forge-std/Vm.sol"; import {AbstractDeployScript} from "./AbstractDeployScript.sol"; import {DeployCoreMainnetScript} from "./mainnet/001_DeployCoreScript.sol"; import {UpgradeMainnetScript} from "./mainnet/002_UpgradeScript.sol"; +import {UpgradeLidoARMMainnetScript} from "./mainnet/003_UpgradeLidoARMScript.sol"; import {DeployCoreHoleskyScript} from "./holesky/001_DeployCoreScript.sol"; import {UpgradeHoleskyScript} from "./holesky/002_UpgradeScript.sol"; @@ -58,6 +59,7 @@ contract DeployManager is Script { // TODO: Use vm.readDir to recursively build this? _runDeployFile(new DeployCoreMainnetScript()); _runDeployFile(new UpgradeMainnetScript(this)); + _runDeployFile(new UpgradeLidoARMMainnetScript()); } else if (block.chainid == 17000) { _runDeployFile(new DeployCoreHoleskyScript()); _runDeployFile(new UpgradeHoleskyScript(this)); diff --git a/script/deploy/mainnet/001_DeployCoreScript.sol b/script/deploy/mainnet/001_DeployCoreScript.sol index 362c1bf..99661cc 100644 --- a/script/deploy/mainnet/001_DeployCoreScript.sol +++ b/script/deploy/mainnet/001_DeployCoreScript.sol @@ -31,8 +31,8 @@ contract DeployCoreMainnetScript is AbstractDeployScript { OethARM implementation = new OethARM(Mainnet.OETH, Mainnet.WETH, Mainnet.OETH_VAULT); _recordDeploy("OETH_ARM_IMPL", address(implementation)); - // 3. Initialize proxy, set the owner to TIMELOCK, set the operator to RELAYER and approve the OETH Vault to transfer OETH - bytes memory data = abi.encodeWithSignature("initialize(address)", Mainnet.RELAYER); + // 3. Initialize proxy, set the owner to TIMELOCK, set the operator to the OETH Relayer and approve the OETH Vault to transfer OETH + bytes memory data = abi.encodeWithSignature("initialize(address)", Mainnet.OETH_RELAYER); proxy.initialize(address(implementation), Mainnet.TIMELOCK, data); } @@ -43,7 +43,7 @@ contract DeployCoreMainnetScript is AbstractDeployScript { // but doing this here to test governance flow. // Set operator - // govProposal.action(deployedContracts["OETH_ARM"], "initialize(address)", abi.encode(Mainnet.RELAYER)); + // govProposal.action(deployedContracts["OETH_ARM"], "initialize(address)", abi.encode(Mainnet.OETH_RELAYER)); } function _fork() internal override { diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol new file mode 100644 index 0000000..2654006 --- /dev/null +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import "forge-std/console.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20, IWETH} from "contracts/Interfaces.sol"; +import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; +import {Proxy} from "contracts/Proxy.sol"; +import {Mainnet} from "contracts/utils/Addresses.sol"; +import {GovProposal, GovSixHelper} from "contracts/utils/GovSixHelper.sol"; +import {AbstractDeployScript} from "../AbstractDeployScript.sol"; + +contract UpgradeLidoARMMainnetScript is AbstractDeployScript { + using GovSixHelper for GovProposal; + + GovProposal public govProposal; + + string public constant override DEPLOY_NAME = "003_UpgradeLidoARMScript"; + bool public constant override proposalExecuted = false; + + Proxy lidoARMProxy; + Proxy lpcProxy; + LidoFixedPriceMultiLpARM lidoARMImpl; + + function _execute() internal override { + console.log("Deploy:", DEPLOY_NAME); + console.log("------------"); + + // 1. Record the proxy address used for AMM v1 + _recordDeploy("LIDO_ARM", Mainnet.LIDO_ARM); + lidoARMProxy = Proxy(Mainnet.LIDO_ARM); + + // 2. Deploy proxy for the Liquidity Provider Controller + lpcProxy = new Proxy(); + _recordDeploy("LIDO_ARM_LPC", address(lpcProxy)); + + // 3. Deploy Liquidity Provider Controller implementation + LiquidityProviderController lpcImpl = new LiquidityProviderController(address(lidoARMProxy)); + _recordDeploy("LIDO_ARM_LPC_IMPL", address(lpcImpl)); + + // 4. Initialize Proxy with LiquidityProviderController implementation and set the owner to the deployer for now + lpcProxy.initialize(address(lpcImpl), deployer, ""); + LiquidityProviderController liquidityProviderController = LiquidityProviderController(address(lpcProxy)); + + // 5. Set the liquidity Provider caps + liquidityProviderController.setTotalAssetsCap(10 ether); + address[] memory liquidityProviders = new address[](1); + liquidityProviders[0] = Mainnet.TREASURY; + liquidityProviderController.setLiquidityProviderCaps(liquidityProviders, 10 ether); + + // 6. Deploy Lido implementation + lidoARMImpl = new LidoFixedPriceMultiLpARM(Mainnet.STETH, Mainnet.WETH, Mainnet.LIDO_WITHDRAWAL); + _recordDeploy("LIDO_ARM_IMPL", address(lidoARMImpl)); + + // 7. Transfer ownership of LiquidityProviderController to the 2/5 ARM multisig + lpcProxy.setOwner(Mainnet.ARM_MULTISIG); + + // Post deploy + // 1. The Lido ARM multisig needs to upgrade and call initialize on the Lido ARM + // 2. the Relayer needs to set the swap prices + } + + function _buildGovernanceProposal() internal override {} + + function _fork() internal override { + // Initialize Lido ARM proxy and implementation contract + bytes memory data = abi.encodeWithSignature( + "initialize(string,string,address,uint256,address,address)", + "Lido ARM", + "ARM-ST", + Mainnet.ARM_RELAYER, + 1500, // 15% performance fee + Mainnet.ARM_BUYBACK, + address(lpcProxy) + ); + + vm.startPrank(Mainnet.ARM_MULTISIG); + uint256 tinyMintAmount = 1e12; + // Get some WETH + vm.deal(Mainnet.ARM_MULTISIG, tinyMintAmount); + IWETH(Mainnet.WETH).deposit{value: tinyMintAmount}(); + // Approve the Lido ARM proxy to spend WETH + IERC20(Mainnet.WETH).approve(address(lidoARMProxy), tinyMintAmount); + + // upgrade and initialize the Lido ARM + lidoARMProxy.upgradeToAndCall(address(lidoARMImpl), data); + + vm.stopPrank(); + + // vm.prank(Mainnet.ARM_RELAYER); + // Set the swap prices + } +} diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index 299f4b3..a1a8b4c 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -12,11 +12,15 @@ library Mainnet { address public constant TIMELOCK = 0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F; address public constant GOVERNOR_FIVE = 0x3cdD07c16614059e66344a7b579DAB4f9516C0b6; address public constant GOVERNOR_SIX = 0x1D3Fbd4d129Ddd2372EA85c5Fa00b2682081c9EC; + address public constant STRATEGIST = 0xF14BBdf064E3F67f51cd9BD646aE3716aD938FDC; + address public constant TREASURY = 0x0000000000000000000000000000000000000001; // Multisig and EOAs address public constant INITIAL_DEPLOYER = address(0x1001); address public constant GOV_MULTISIG = 0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899; - address public constant RELAYER = 0x4b91827516f79d6F6a1F292eD99671663b09169a; + address public constant ARM_MULTISIG = 0xC8F2cF4742C86295653f893214725813B16f7410; + address public constant OETH_RELAYER = 0x4b91827516f79d6F6a1F292eD99671663b09169a; + address public constant ARM_RELAYER = 0x39878253374355DBcc15C86458F084fb6f2d6DE7; // Tokens address public constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; @@ -27,6 +31,9 @@ library Mainnet { // Contracts address public constant OETH_VAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; address public constant OETH_ARM = 0x6bac785889A4127dB0e0CeFEE88E0a9F1Aaf3cC7; + address public constant LIDO_ARM = 0x85B78AcA6Deae198fBF201c82DAF6Ca21942acc6; + // TODO add once deployed from the Origin Dollar repo + address public constant ARM_BUYBACK = 0x0000000000000000000000000000000000000002; // Lido address public constant LIDO_WITHDRAWAL = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; @@ -64,7 +71,7 @@ contract AddressResolver { resolver[MAINNET]["GOVERNOR"] = Mainnet.TIMELOCK; resolver[MAINNET]["GOVERNANCE"] = Mainnet.GOVERNOR_SIX; resolver[MAINNET]["GOV_MULTISIG"] = Mainnet.GOV_MULTISIG; - resolver[MAINNET]["OPERATOR"] = Mainnet.RELAYER; + resolver[MAINNET]["OPERATOR"] = Mainnet.OETH_RELAYER; // Tokens resolver[MAINNET]["OETH"] = Mainnet.OETH; diff --git a/test/smoke/OethARMSmokeTest.t.sol b/test/smoke/OethARMSmokeTest.t.sol index 3f01057..8483403 100644 --- a/test/smoke/OethARMSmokeTest.t.sol +++ b/test/smoke/OethARMSmokeTest.t.sol @@ -10,7 +10,7 @@ import {OethARM} from "contracts/OethARM.sol"; import {Proxy} from "contracts/Proxy.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; -contract OethARMSmokeTest is AbstractSmokeTest { +contract Fork_OethARM_Smoke_Test is AbstractSmokeTest { IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); IERC20 weth; From b7e7d246d2a2ad7c24ade5501d29a050aead1665 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 19 Sep 2024 22:36:06 +1000 Subject: [PATCH 044/196] Changed the Lido ARM owner to be the mainnet 5/8 multisig --- .../deploy/mainnet/003_UpgradeLidoARMScript.sol | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 2654006..a6894a5 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -55,17 +55,22 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { lidoARMImpl = new LidoFixedPriceMultiLpARM(Mainnet.STETH, Mainnet.WETH, Mainnet.LIDO_WITHDRAWAL); _recordDeploy("LIDO_ARM_IMPL", address(lidoARMImpl)); - // 7. Transfer ownership of LiquidityProviderController to the 2/5 ARM multisig - lpcProxy.setOwner(Mainnet.ARM_MULTISIG); + // 7. Transfer ownership of LiquidityProviderController to the mainnet 5/8 multisig + lpcProxy.setOwner(Mainnet.GOV_MULTISIG); // Post deploy - // 1. The Lido ARM multisig needs to upgrade and call initialize on the Lido ARM + // 1. The Lido ARM multisig needs to set the owner to the mainnet 5/8 multisig + // 1. The mainnet 5/8 multisig needs to upgrade and call initialize on the Lido ARM // 2. the Relayer needs to set the swap prices } function _buildGovernanceProposal() internal override {} function _fork() internal override { + // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig + vm.prank(Mainnet.ARM_MULTISIG); + lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); + // Initialize Lido ARM proxy and implementation contract bytes memory data = abi.encodeWithSignature( "initialize(string,string,address,uint256,address,address)", @@ -77,10 +82,10 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { address(lpcProxy) ); - vm.startPrank(Mainnet.ARM_MULTISIG); + vm.startPrank(Mainnet.GOV_MULTISIG); uint256 tinyMintAmount = 1e12; // Get some WETH - vm.deal(Mainnet.ARM_MULTISIG, tinyMintAmount); + vm.deal(Mainnet.GOV_MULTISIG, tinyMintAmount); IWETH(Mainnet.WETH).deposit{value: tinyMintAmount}(); // Approve the Lido ARM proxy to spend WETH IERC20(Mainnet.WETH).approve(address(lidoARMProxy), tinyMintAmount); From 9d958196d4f0df7acc49a28c258a11620016ca65 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 19 Sep 2024 22:40:45 +1000 Subject: [PATCH 045/196] Anyone can now call collectFees --- src/contracts/PerformanceFee.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index f805f2c..b69b842 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -135,8 +135,6 @@ abstract contract PerformanceFee is MultiLP { /// @notice Transfer accrued performance fees to the fee collector /// This requires enough liquidity assets in the ARM to cover the accrued fees. function collectFees() external returns (uint256 fees) { - require(msg.sender == feeCollector, "ARM: not fee collector"); - // Accrued all fees up to this point _calcFee(); From cda3c0e325a9e7e8db96d066fb80b7cb6e55f14d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 19 Sep 2024 22:40:59 +1000 Subject: [PATCH 046/196] Updated README --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c492bc..571b645 100644 --- a/README.md +++ b/README.md @@ -116,14 +116,18 @@ function swapTokensForExactTokens( ### Install ``` -foundryup -forge install -forge compile +make install cp .env.example .env ``` In the `.env` file, set the environment variables as needed. eg `PROVIDER_URL` for the RPC endpoint. +### Format and Compile + +``` +make +``` + ### Running tests Fork and Unit tests run with the same command, as the fork is initiated on the test file itself if needed. From 991e10df1e3a63c4a6f39fcc2162702cf77802d2 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 19 Sep 2024 22:49:45 +1000 Subject: [PATCH 047/196] Added operator to LiquidityProviderController which will be a Defender Relayer --- .../deploy/mainnet/003_UpgradeLidoARMScript.sol | 3 ++- src/contracts/LiquidityProviderController.sol | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index a6894a5..46f54d7 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -42,7 +42,8 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { _recordDeploy("LIDO_ARM_LPC_IMPL", address(lpcImpl)); // 4. Initialize Proxy with LiquidityProviderController implementation and set the owner to the deployer for now - lpcProxy.initialize(address(lpcImpl), deployer, ""); + bytes memory data = abi.encodeWithSignature("initialize(address)", Mainnet.ARM_RELAYER); + lpcProxy.initialize(address(lpcImpl), deployer, data); LiquidityProviderController liquidityProviderController = LiquidityProviderController(address(lpcProxy)); // 5. Set the liquidity Provider caps diff --git a/src/contracts/LiquidityProviderController.sol b/src/contracts/LiquidityProviderController.sol index a51cd3c..633d153 100644 --- a/src/contracts/LiquidityProviderController.sol +++ b/src/contracts/LiquidityProviderController.sol @@ -1,14 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {Ownable} from "./Ownable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import {OwnableOperable} from "./OwnableOperable.sol"; import {ILiquidityProviderARM} from "./Interfaces.sol"; /** * @title Controller of ARM liquidity providers. * @author Origin Protocol Inc */ -contract LiquidityProviderController is Ownable { +contract LiquidityProviderController is Initializable, OwnableOperable { /// @notice The address of the linked Application Redemption Manager (ARM). address public immutable arm; @@ -27,6 +29,10 @@ contract LiquidityProviderController is Ownable { arm = _arm; } + function initialize(address _operator) external initializer { + _initOwnableOperable(_operator); + } + function postDepositHook(address liquidityProvider, uint256 assets) external { require(msg.sender == arm, "LPC: Caller is not ARM"); @@ -44,7 +50,10 @@ contract LiquidityProviderController is Ownable { emit LiquidityProviderCap(liquidityProvider, newCap); } - function setLiquidityProviderCaps(address[] calldata _liquidityProviders, uint256 cap) external onlyOwner { + function setLiquidityProviderCaps(address[] calldata _liquidityProviders, uint256 cap) + external + onlyOperatorOrOwner + { for (uint256 i = 0; i < _liquidityProviders.length; i++) { liquidityProviderCaps[_liquidityProviders[i]] = cap; @@ -55,7 +64,7 @@ contract LiquidityProviderController is Ownable { /// @notice Set the ARM's maximum total assets. /// Setting to zero will prevent any further deposits. /// The liquidity provider can still withdraw assets. - function setTotalAssetsCap(uint256 _totalAssetsCap) external onlyOwner { + function setTotalAssetsCap(uint256 _totalAssetsCap) external onlyOperatorOrOwner { totalAssetsCap = _totalAssetsCap; emit TotalAssetsCap(_totalAssetsCap); From 74c36a07f042470a00b44d7e13f35c646c974ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Fri, 20 Sep 2024 04:19:20 +0200 Subject: [PATCH 048/196] Add test for Swaps in LidoFixedPriceMultiLpARM.sol. (#18) * fix: rename `lidoARM` in `lidoFixedPriceMulltiLpARM` for tests. * feat: add badToken. * [WIP] test: add tests for `SwapExactTokensForTokens`. * test: finalize `swapExactTokensForTokens()`. * chore: set fuzz runs in config. * fix: transfer to address dead steth when dealing 0. * test: add tests for `swapTokensForExactTokens()`. * fix: adjust `swapExactTokensForTokens()` tests. --- foundry.toml | 3 + test/Base.sol | 3 +- .../Constructor.t.sol | 22 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 12 +- .../RequestRedeem.t.sol | 4 +- .../SwapExactTokensForTokens.t.sol | 373 +++++++++++++++++ .../SwapTokensForExactTokens.t.sol | 374 ++++++++++++++++++ test/fork/shared/Shared.sol | 14 +- test/fork/utils/Helpers.sol | 11 +- test/fork/utils/Modifiers.sol | 2 +- 10 files changed, 790 insertions(+), 28 deletions(-) create mode 100644 test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol create mode 100644 test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol diff --git a/foundry.toml b/foundry.toml index 9b53e7b..7be56ab 100644 --- a/foundry.toml +++ b/foundry.toml @@ -19,6 +19,9 @@ remappings = [ "@openzeppelin/contracts-upgradeable/=dependencies/@openzeppelin-contracts-upgradeable-5.0.2/", ] +[fuzz] +runs = 1_000 + [dependencies] forge-std = "1.9.2" "@openzeppelin-contracts" = "5.0.2" diff --git a/test/Base.sol b/test/Base.sol index 2668c5e..1cb4349 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -37,13 +37,14 @@ abstract contract Base_Test_ is Test { Proxy public lidoOwnerProxy; OethARM public oethARM; LidoOwnerLpARM public lidoOwnerLpARM; - LidoFixedPriceMultiLpARM public lidoARM; + LidoFixedPriceMultiLpARM public lidoFixedPriceMulltiLpARM; LiquidityProviderController public liquidityProviderController; IERC20 public oeth; IERC20 public weth; IERC20 public steth; IERC20 public wsteth; + IERC20 public badToken; IOETHVault public vault; ////////////////////////////////////////////////////// diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol index 13b5542..51d52aa 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -16,18 +16,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Constructor_Test is Fork_Shared_ /// --- PASSING TESTS ////////////////////////////////////////////////////// function test_Initial_State() public { - assertEq(lidoARM.name(), "Lido ARM"); - assertEq(lidoARM.symbol(), "ARM-ST"); - assertEq(lidoARM.owner(), address(this)); - assertEq(lidoARM.operator(), operator); - assertEq(lidoARM.feeCollector(), feeCollector); - assertEq(lidoARM.fee(), 2000); - assertEq(lidoARM.lastTotalAssets(), 1e12); - assertEq(lidoARM.feesAccrued(), 0); + assertEq(lidoFixedPriceMulltiLpARM.name(), "Lido ARM"); + assertEq(lidoFixedPriceMulltiLpARM.symbol(), "ARM-ST"); + assertEq(lidoFixedPriceMulltiLpARM.owner(), address(this)); + assertEq(lidoFixedPriceMulltiLpARM.operator(), operator); + assertEq(lidoFixedPriceMulltiLpARM.feeCollector(), feeCollector); + assertEq(lidoFixedPriceMulltiLpARM.fee(), 2000); + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), 1e12); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // the 20% performance fee is removed on initialization - assertEq(lidoARM.totalAssets(), 1e12); - assertEq(lidoARM.totalSupply(), 1e12); - assertEq(weth.balanceOf(address(lidoARM)), 1e12); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), 1e12); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), 1e12); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 1e12); assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 727f1bb..3a9c159 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -29,12 +29,12 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes function _snapData() internal view returns (AssertData memory data) { return AssertData({ - totalAssets: lidoARM.totalAssets(), - totalSupply: lidoARM.totalSupply(), + totalAssets: lidoFixedPriceMulltiLpARM.totalAssets(), + totalSupply: lidoFixedPriceMulltiLpARM.totalSupply(), totalAssetsCap: liquidityProviderController.totalAssetsCap(), - armWeth: weth.balanceOf(address(lidoARM)), - armSteth: steth.balanceOf(address(lidoARM)), - feesAccrued: lidoARM.feesAccrued() + armWeth: weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), + armSteth: steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), + feesAccrued: lidoFixedPriceMulltiLpARM.feesAccrued() }); } @@ -69,7 +69,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes deal(address(weth), address(this), 10 ether); beforeData = _snapData(); - lidoARM.deposit(10 ether); + lidoFixedPriceMulltiLpARM.deposit(10 ether); DeltaData memory delta = noChangeDeltaData; delta.totalAssets = 10 ether; diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 7d4b9a3..59fc2c0 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -22,7 +22,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar { deal(address(weth), address(this), 10 ether); - lidoARM.deposit(10 ether); - lidoARM.requestRedeem(8 ether); + lidoFixedPriceMulltiLpARM.deposit(10 ether); + lidoFixedPriceMulltiLpARM.requestRedeem(8 ether); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol new file mode 100644 index 0000000..fc38630 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- CONSTANTS + ////////////////////////////////////////////////////// + uint256 private constant MIN_PRICE0 = 980e33; // 0.98 + uint256 private constant MAX_PRICE0 = 1_000e33; // 1.00 + uint256 private constant MIN_PRICE1 = 1_000e33; // 1.00 + uint256 private constant MAX_PRICE1 = 1_020e33; // 1.02 + uint256 private constant MAX_WETH_RESERVE = 1_000_000 ether; // 1M WETH, no limit, but need to be consistent. + uint256 private constant MAX_STETH_RESERVE = 2_000_000 ether; // 2M stETH, limited by wsteth balance of steth. + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + deal(address(weth), address(this), 1_000 ether); + deal(address(steth), address(this), 1_000 ether); + + deal(address(weth), address(lidoFixedPriceMulltiLpARM), 1_000 ether); + deal(address(steth), address(lidoFixedPriceMulltiLpARM), 1_000 ether); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut1() public { + lidoFixedPriceMulltiLpARM.token0(); + vm.expectRevert("ARM: Invalid token"); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + steth, // inToken + badToken, // outToken + 1, // amountIn + 1, // amountOutMin + address(this) // to + ); + } + + function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut0() public { + vm.expectRevert("ARM: Invalid token"); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + weth, // inToken + badToken, // outToken + 1, // amountIn + 1, // amountOutMin + address(this) // to + ); + } + + function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenIn() public { + vm.expectRevert("ARM: Invalid token"); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + badToken, // inToken + steth, // outToken + 1, // amountIn + 1, // amountOutMin + address(this) // to + ); + } + + function test_RevertWhen_SwapExactTokensForTokens_Because_NotEnoughTokenIn() public { + uint256 initialBalance = weth.balanceOf(address(this)); + + vm.expectRevert(); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + weth, // inToken + steth, // outToken + initialBalance + 1, // amountIn + 0, // amountOutMin + address(this) // to + ); + + initialBalance = steth.balanceOf(address(this)); + vm.expectRevert("BALANCE_EXCEEDED"); // Lido error + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + steth, // inToken + weth, // outToken + initialBalance + 3, // amountIn + 0, // amountOutMin + address(this) // to + ); + } + + function test_RevertWhen_SwapExactTokensForTokens_Because_NotEnoughTokenOut() public { + uint256 initialBalance = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + deal(address(weth), address(this), initialBalance * 2); + + vm.expectRevert("BALANCE_EXCEEDED"); // Lido error + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + weth, // inToken + steth, // outToken + initialBalance * 2, // amountIn + 0, // amountOutMin + address(this) // to + ); + + initialBalance = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + deal(address(steth), address(this), initialBalance * 2); + vm.expectRevert(); // Lido error + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + steth, // inToken + weth, // outToken + initialBalance * 2, // amountIn + 0, // amountOutMin + address(this) // to + ); + } + + function test_RevertWhen_SwapExactTokensForTokens_Because_InsufficientOutputAmount() public { + deal(address(steth), address(lidoFixedPriceMulltiLpARM), 100 wei); + + // Test for this function signature: swapExactTokensForTokens(IERC20,IERC20,uint56,uint256,address) + vm.expectRevert("ARM: Insufficient output amount"); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + weth, // inToken + steth, // outToken + 1, // amountIn + 1_000 ether, // amountOutMin + address(this) // to + ); + + // Test for this function signature: swapExactTokensForTokens(uint256,uint256,address[],address,uint256) + address[] memory path = new address[](2); + path[0] = address(weth); + path[1] = address(steth); + vm.expectRevert("ARM: Insufficient output amount"); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + 1, // amountIn + 1_000 ether, // amountOutMin + path, // path + address(this), // to + block.timestamp // deadline + ); + } + + function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidePathLength() public { + vm.expectRevert("ARM: Invalid path length"); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + 1, // amountIn + 1, // amountOutMin + new address[](3), // path + address(this), // to + 0 // deadline + ); + } + + function test_RevertWhen_SwapExactTokensForTokens_Because_DeadlineExpired() public { + vm.expectRevert("ARM: Deadline expired"); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + 1, // amountIn + 1, // amountOutMin + new address[](2), // path + address(this), // to + block.timestamp - 1 // deadline + ); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SwapExactTokensForTokens_WithDeadLine_Weth_To_Steth() public { + uint256 amountIn = 1 ether; + address[] memory path = new address[](2); + path[0] = address(weth); + path[1] = address(steth); + + // State before + uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); + uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Get minimum amount of STETH to receive + uint256 traderates1 = lidoFixedPriceMulltiLpARM.traderate1(); + uint256 minAmount = amountIn * traderates1 / 1e36; + + // Expected events: Already checked in fuzz tests + + uint256[] memory outputs = new uint256[](2); + // Main call + outputs = lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + amountIn, // amountIn + minAmount, // amountOutMin + path, // path + address(this), // to + block.timestamp // deadline + ); + + // State after + uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); + uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Assertions + assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); + assertApproxEqAbs(balanceSTETHBeforeThis + minAmount, balanceSTETHAfterThis, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM); + assertApproxEqAbs(balanceSTETHBeforeARM, balanceSTETHAfterARM + minAmount, STETH_ERROR_ROUNDING); + assertEq(outputs[0], amountIn); + assertEq(outputs[1], minAmount); + } + + function test_SwapExactTokensForTokens_WithDeadLine_Steth_To_Weth() public { + uint256 amountIn = 1 ether; + address[] memory path = new address[](2); + path[0] = address(steth); + path[1] = address(weth); + + // State before + uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); + uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Get minimum amount of WETH to receive + uint256 traderates0 = lidoFixedPriceMulltiLpARM.traderate0(); + uint256 minAmount = amountIn * traderates0 / 1e36; + + // Expected events: Already checked in fuzz tests + + uint256[] memory outputs = new uint256[](2); + // Main call + outputs = lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + amountIn, // amountIn + minAmount, // amountOutMin + path, // path + address(this), // to + block.timestamp // deadline + ); + + // State after + uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); + uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Assertions + assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis); + assertApproxEqAbs(balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + minAmount); + assertApproxEqAbs(balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING); + assertEq(outputs[0], amountIn); + assertEq(outputs[1], minAmount); + } + + ////////////////////////////////////////////////////// + /// --- FUZZING TESTS + ////////////////////////////////////////////////////// + /// @notice Fuzz test for swapExactTokensForTokens(IERC20,IERC20,uint256,uint256,address), with WETH to stETH. + /// @param amountIn Amount of WETH to swap. Fuzzed between 0 and steth in the ARM. + /// @param stethReserve Amount of stETH in the ARM. Fuzzed between 0 and MAX_STETH_RESERVE. + /// @param price Price of the stETH in WETH. Fuzzed between 0.98 and 1. + function test_SwapExactTokensForTokens_Weth_To_Steth(uint256 amountIn, uint256 stethReserve, uint256 price) + public + { + // Use random price between 0.98 and 1 for traderate1, + // Traderate0 value doesn't matter as it is not used in this test. + price = _bound(price, MIN_PRICE0, MAX_PRICE0); + lidoFixedPriceMulltiLpARM.setPrices(price, MAX_PRICE1); + + // Set random amount of stETH in the ARM + stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); + deal(address(steth), address(lidoFixedPriceMulltiLpARM), stethReserve); + + // Calculate maximum amount of WETH to swap + // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. + amountIn = _bound(amountIn, 0, stethReserve); + deal(address(weth), address(this), amountIn); + + // State before + uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); + uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Get minimum amount of STETH to receive + uint256 traderates1 = lidoFixedPriceMulltiLpARM.traderate1(); + uint256 minAmount = amountIn * traderates1 / 1e36; + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amountIn); + vm.expectEmit({emitter: address(steth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), minAmount + STETH_ERROR_ROUNDING); + // Main call + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + weth, // inToken + steth, // outToken + amountIn, // amountIn + minAmount, // amountOutMin + address(this) // to + ); + + // State after + uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); + uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Assertions + assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); + assertApproxEqAbs(balanceSTETHBeforeThis + minAmount, balanceSTETHAfterThis, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM); + assertApproxEqAbs(balanceSTETHBeforeARM, balanceSTETHAfterARM + minAmount, STETH_ERROR_ROUNDING); + } + + /// @notice Fuzz test for swapExactTokensForTokens(IERC20,IERC20,uint256,uint256,address), with stETH to WETH. + /// @param amountIn Amount of stETH to swap. Fuzzed between 0 and weth in the ARM. + /// @param wethReserve Amount of WETH in the ARM. Fuzzed between 0 and MAX_WETH_RESERVE. + /// @param price Price of the stETH in WETH. Fuzzed between 1 and 1.02. + function test_SwapExactTokensForTokens_Steth_To_Weth(uint256 amountIn, uint256 wethReserve, uint256 price) public { + // Use random price between MIN_PRICE1 and MAX_PRICE1 for traderate1, + // Traderate0 value doesn't matter as it is not used in this test. + price = _bound(price, MIN_PRICE1, MAX_PRICE1); + lidoFixedPriceMulltiLpARM.setPrices(MIN_PRICE0, price); + + // Set random amount of WETH in the ARM + wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); + deal(address(weth), address(lidoFixedPriceMulltiLpARM), wethReserve); + + // Calculate maximum amount of stETH to swap + // As the price is below 1, we can take 100% of the balance of WETH of the ARM. + amountIn = _bound(amountIn, 0, wethReserve); + deal(address(steth), address(this), amountIn); + + // State before + uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); + uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Get minimum amount of WETH to receive + uint256 traderates0 = lidoFixedPriceMulltiLpARM.traderate0(); + uint256 minAmount = amountIn * traderates0 / 1e36; + + // Expected events + vm.expectEmit({emitter: address(steth)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amountIn); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), minAmount); + + // Main call + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + steth, // inToken + weth, // outToken + amountIn, // amountIn + minAmount, // amountOutMin + address(this) // to + ); + + // State after + uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); + uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Assertions + assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis); + assertApproxEqAbs(balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + minAmount); + assertApproxEqAbs(balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING); + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol new file mode 100644 index 0000000..e3f8e17 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- CONSTANTS + ////////////////////////////////////////////////////// + uint256 private constant MIN_PRICE0 = 980e33; // 0.98 + uint256 private constant MAX_PRICE0 = 1_000e33; // 1.00 + uint256 private constant MIN_PRICE1 = 1_000e33; // 1.00 + uint256 private constant MAX_PRICE1 = 1_020e33; // 1.02 + uint256 private constant MAX_WETH_RESERVE = 1_000_000 ether; // 1M WETH, no limit, but need to be consistent. + uint256 private constant MAX_STETH_RESERVE = 2_000_000 ether; // 2M stETH, limited by wsteth balance of steth. + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + deal(address(weth), address(this), 1_000 ether); + deal(address(steth), address(this), 1_000 ether); + + deal(address(weth), address(lidoFixedPriceMulltiLpARM), 1_000 ether); + deal(address(steth), address(lidoFixedPriceMulltiLpARM), 1_000 ether); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut1() public { + lidoFixedPriceMulltiLpARM.token0(); + vm.expectRevert("ARM: Invalid token"); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + steth, // inToken + badToken, // outToken + 1, // amountOut + 1, // amountOutMax + address(this) // to + ); + } + + function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut0() public { + vm.expectRevert("ARM: Invalid token"); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + weth, // inToken + badToken, // outToken + 1, // amountOut + 1, // amountOutMax + address(this) // to + ); + } + + function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenIn() public { + vm.expectRevert("ARM: Invalid token"); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + badToken, // inToken + steth, // outToken + 1, // amountOut + 1, // amountOutMax + address(this) // to + ); + } + + function test_RevertWhen_SwapTokensForExactTokens_Because_NotEnoughTokenIn() public { + deal(address(weth), address(this), 0); + + vm.expectRevert(); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + weth, // inToken + steth, // outToken + 1, // amountOut + type(uint256).max, // amountOutMax + address(this) // to + ); + + deal(address(steth), address(this), 0); + vm.expectRevert(); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + steth, // inToken + weth, // outToken + STETH_ERROR_ROUNDING + 1, // amountOut * + type(uint256).max, // amountOutMax + address(this) // to + ); + // Note*: As deal can sometimes leave STETH_ERROR_ROUNDING to `to`, we need to try to transfer more. + } + + function test_RevertWhen_SwapTokensForExactTokens_Because_NotEnoughTokenOut() public { + deal(address(weth), address(this), 0); + + vm.expectRevert(); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + weth, // inToken + steth, // outToken + 1 ether, // amountOut + type(uint256).max, // amountInMax + address(this) // to + ); + + deal(address(steth), address(this), 0); + vm.expectRevert("BALANCE_EXCEEDED"); // Lido error + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + steth, // inToken + weth, // outToken + 1 ether, // amountOut + type(uint256).max, // amountInMax + address(this) // to + ); + } + + function test_RevertWhen_SwapTokensForExactTokens_Because_InsufficientOutputAmount() public { + deal(address(steth), address(lidoFixedPriceMulltiLpARM), 100 wei); + + // Test for this function signature: swapTokensForExactTokens(IERC20,IERC20,uint56,uint256,address) + vm.expectRevert("ARM: Excess input amount"); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + weth, // inToken + steth, // outToken + 1, // amountOut + 0, // amountInMax + address(this) // to + ); + + // Test for this function signature: swapTokensForExactTokens(uint256,uint256,address[],address,uint256) + address[] memory path = new address[](2); + path[0] = address(weth); + path[1] = address(steth); + vm.expectRevert("ARM: Excess input amount"); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + 1, // amountOut + 0, // amountInMax + path, // path + address(this), // to + block.timestamp // deadline + ); + } + + function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidePathLength() public { + vm.expectRevert("ARM: Invalid path length"); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + 1, // amountOut + 1, // amountInMax + new address[](3), // path + address(this), // to + 0 // deadline + ); + } + + function test_RevertWhen_SwapTokensForExactTokens_Because_DeadlineExpired() public { + vm.expectRevert("ARM: Deadline expired"); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + 1, // amountOut + 1, // amountInMax + new address[](2), // path + address(this), // to + block.timestamp - 1 // deadline + ); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SwapTokensForExactTokens_WithDeadLine_Weth_To_Steth() public { + uint256 amountOut = 1 ether; + address[] memory path = new address[](2); + path[0] = address(weth); + path[1] = address(steth); + + // State before + uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); + uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Get minimum amount of STETH to receive + uint256 traderates1 = lidoFixedPriceMulltiLpARM.traderate1(); + uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; + + // Expected events: Already checked in fuzz tests + + uint256[] memory outputs = new uint256[](2); + // Main call + outputs = lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + amountOut, // amountOut + amountIn, // amountInMax + path, // path + address(this), // to + block.timestamp // deadline + ); + + // State after + uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); + uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Assertions + assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); + assertApproxEqAbs(balanceSTETHBeforeThis + amountOut, balanceSTETHAfterThis, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM); + assertApproxEqAbs(balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOut, STETH_ERROR_ROUNDING); + assertEq(outputs[0], amountIn); + assertEq(outputs[1], amountOut); + } + + function test_SwapTokensForExactTokens_WithDeadLine_Steth_To_Weth() public { + uint256 amountOut = 1 ether; + address[] memory path = new address[](2); + path[0] = address(steth); + path[1] = address(weth); + + // State before + uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); + uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Get minimum amount of WETH to receive + uint256 traderates0 = lidoFixedPriceMulltiLpARM.traderate0(); + uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; + + // Expected events: Already checked in fuzz tests + + uint256[] memory outputs = new uint256[](2); + // Main call + outputs = lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + amountOut, // amountOut + amountIn, // amountInMax + path, // path + address(this), // to + block.timestamp // deadline + ); + + // State after + uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); + uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Assertions + assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis); + assertApproxEqAbs(balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + amountOut); + assertApproxEqAbs(balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING); + assertEq(outputs[0], amountIn); + assertEq(outputs[1], amountOut); + } + + ////////////////////////////////////////////////////// + /// --- FUZZING TESTS + ////////////////////////////////////////////////////// + /// @notice Fuzz test for swapTokensForExactTokens(IERC20,IERC20,uint256,uint256,address), with WETH to stETH. + /// @param amountOut Amount of WETH to swap. Fuzzed between 0 and steth in the ARM. + /// @param stethReserve Amount of stETH in the ARM. Fuzzed between 0 and MAX_STETH_RESERVE. + /// @param price Price of the stETH in WETH. Fuzzed between 0.98 and 1. + function test_SwapTokensForExactTokens_Weth_To_Steth(uint256 amountOut, uint256 stethReserve, uint256 price) + public + { + // Use random price between 0.98 and 1 for traderate1, + // Traderate0 value doesn't matter as it is not used in this test. + price = _bound(price, MIN_PRICE0, MAX_PRICE0); + lidoFixedPriceMulltiLpARM.setPrices(price, MAX_PRICE1); + + // Set random amount of stETH in the ARM + stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); + deal(address(steth), address(lidoFixedPriceMulltiLpARM), stethReserve); + + // Calculate maximum amount of WETH to swap + // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. + amountOut = _bound(amountOut, 0, stethReserve); + deal(address(weth), address(this), amountOut * 2 + 1); // // Deal more as AmountIn is greater than AmountOut + + // State before + uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); + uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Get minimum amount of STETH to receive + uint256 traderates1 = lidoFixedPriceMulltiLpARM.traderate1(); + uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amountIn); + vm.expectEmit({emitter: address(steth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), amountOut + STETH_ERROR_ROUNDING); + // Main call + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + weth, // inToken + steth, // outToken + amountOut, // amountOut + amountIn, // amountInMax + address(this) // to + ); + + // State after + uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); + uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Assertions + assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); + assertApproxEqAbs(balanceSTETHBeforeThis + amountOut, balanceSTETHAfterThis, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM); + assertApproxEqAbs(balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOut, STETH_ERROR_ROUNDING); + } + + /// @notice Fuzz test for swapTokensForExactTokens(IERC20,IERC20,uint256,uint256,address), with stETH to WETH. + /// @param amountOut Amount of stETH to swap. Fuzzed between 0 and weth in the ARM. + /// @param wethReserve Amount of WETH in the ARM. Fuzzed between 0 and MAX_WETH_RESERVE. + /// @param price Price of the stETH in WETH. Fuzzed between 1 and 1.02. + function test_SwapTokensForExactTokens_Steth_To_Weth(uint256 amountOut, uint256 wethReserve, uint256 price) + public + { + // Use random price between MIN_PRICE1 and MAX_PRICE1 for traderate1, + // Traderate0 value doesn't matter as it is not used in this test. + price = _bound(price, MIN_PRICE1, MAX_PRICE1); + lidoFixedPriceMulltiLpARM.setPrices(MIN_PRICE0, price); + + // Set random amount of WETH in the ARM + wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); + deal(address(weth), address(lidoFixedPriceMulltiLpARM), wethReserve); + + // Calculate maximum amount of stETH to swap + // As the price is below 1, we can take 100% of the balance of WETH of the ARM. + amountOut = _bound(amountOut, 0, wethReserve); + deal(address(steth), address(this), amountOut * 2 + 1); // Deal more as AmountIn is greater than AmountOut + + // State before + uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); + uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Get minimum amount of WETH to receive + uint256 traderates0 = lidoFixedPriceMulltiLpARM.traderate0(); + uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; + + // Expected events + vm.expectEmit({emitter: address(steth)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amountIn); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), amountOut); + + // Main call + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + steth, // inToken + weth, // outToken + amountOut, // amountOut + amountIn, // amountInMax + address(this) // to + ); + + // State after + uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); + uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + + // Assertions + assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis); + assertApproxEqAbs(balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + amountOut); + assertApproxEqAbs(balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING); + } +} diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 2a68c3d..6797ecb 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -78,7 +78,11 @@ abstract contract Fork_Shared_Test_ is Modifiers { require(vm.envExists("PROVIDER_URL"), "PROVIDER_URL not set"); // Create and select a fork. - forkId = vm.createSelectFork("mainnet"); + if (vm.envExists("FORK_BLOCK_NUMBER_MAINNET")) { + forkId = vm.createSelectFork("mainnet", vm.envUint("FORK_BLOCK_NUMBER_MAINNET")); + } else { + forkId = vm.createSelectFork("mainnet"); + } } function _generateAddresses() internal { @@ -97,6 +101,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { steth = IERC20(resolver.resolve("STETH")); wsteth = IERC20(resolver.resolve("WSTETH")); vault = IOETHVault(resolver.resolve("OETH_VAULT")); + badToken = IERC20(vm.randomAddress()); } function _deployContracts() internal { @@ -158,10 +163,10 @@ abstract contract Fork_Shared_Test_ is Modifiers { lidoProxy.initialize(address(lidoImpl), address(this), data); // Set the Proxy as the LidoARM. - lidoARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); + lidoFixedPriceMulltiLpARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); // set prices - lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + lidoFixedPriceMulltiLpARM.setPrices(992 * 1e33, 1001 * 1e33); // --- Deploy LidoOwnerLpARM implementation --- // Deploy LidoOwnerLpARM implementation. @@ -185,10 +190,11 @@ abstract contract Fork_Shared_Test_ is Modifiers { vm.label(address(oeth), "OETH"); vm.label(address(weth), "WETH"); vm.label(address(steth), "stETH"); + vm.label(address(badToken), "BAD TOKEN"); vm.label(address(vault), "OETH VAULT"); vm.label(address(oethARM), "OETH ARM"); vm.label(address(proxy), "OETH ARM PROXY"); - vm.label(address(lidoARM), "LIDO ARM"); + vm.label(address(lidoFixedPriceMulltiLpARM), "LIDO ARM"); vm.label(address(lidoProxy), "LIDO ARM PROXY"); vm.label(address(lidoOwnerLpARM), "LIDO OWNER LP ARM"); vm.label(address(lidoOwnerProxy), "LIDO OWNER LP ARM PROXY"); diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index db2efa6..2b4a66f 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -19,9 +19,14 @@ abstract contract Helpers is Base_Test_ { // Check than whale as enough stETH. Whale is wsteth contract. require(steth.balanceOf(address(wsteth)) >= amount, "Fork_Shared_Test_: Not enough stETH in WHALE_stETH"); - // Transfer stETH from WHALE_stETH to the user. - vm.prank(address(wsteth)); - steth.transfer(to, amount); + if (amount == 0) { + vm.prank(to); + steth.transfer(address(0x1), steth.balanceOf(to)); + } else { + // Transfer stETH from WHALE_stETH to the user. + vm.prank(address(wsteth)); + steth.transfer(to, amount); + } } else { super.deal(token, to, amount); } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index 44ea756..c582860 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -36,7 +36,7 @@ abstract contract Modifiers is Helpers { /// @notice Impersonate the owner of LidoFixedPriceMultiLpARM contract. modifier asLidoFixedPriceMultiLpARMOwner() { - vm.startPrank(lidoARM.owner()); + vm.startPrank(lidoFixedPriceMulltiLpARM.owner()); _; vm.stopPrank(); } From 12dbafe3f2672b9a96bc188b62117e5345ec5db6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Sep 2024 14:25:07 +1000 Subject: [PATCH 049/196] Refactor setPrices for FixedPriceARM --- .../mainnet/003_UpgradeLidoARMScript.sol | 6 +-- src/contracts/FixedPriceARM.sol | 30 ++++++++------ test/fork/LidoOwnerLpARM/Setters.t.sol | 40 ++++++++++++++++--- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 46f54d7..666cff8 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -94,9 +94,9 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // upgrade and initialize the Lido ARM lidoARMProxy.upgradeToAndCall(address(lidoARMImpl), data); - vm.stopPrank(); + // Set the buy price with a 8 basis point discount. The sell price is 1.0 + LidoFixedPriceMultiLpARM(payable(Mainnet.LIDO_ARM)).setPrices(9994e32, 1e36); - // vm.prank(Mainnet.ARM_RELAYER); - // Set the swap prices + vm.stopPrank(); } } diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol index 9139f0e..fc4803c 100644 --- a/src/contracts/FixedPriceARM.sol +++ b/src/contracts/FixedPriceARM.sol @@ -26,8 +26,11 @@ abstract contract FixedPriceARM is AbstractARM { */ uint256 public traderate1; - /// @dev Maximum operator settable traderate. 1e36 - uint256 internal constant MAX_OPERATOR_RATE = 1005 * 1e33; + /// @notice Maximum amount the Operator can set the price from 1 scaled to 36 decimals. + /// 1e33 is a 0.1% deviation, or 10 basis points. + uint256 public constant MAX_PRICE_DEVIATION = 1e33; + /// @notice Scale of the prices. + uint256 public constant PRICE_SCALE = 1e36; uint256[50] private _gap; @@ -48,7 +51,7 @@ abstract contract FixedPriceARM is AbstractARM { } else { revert("ARM: Invalid token"); } - amountOut = amountIn * price / 1e36; + amountOut = amountIn * price / PRICE_SCALE; // Transfer the input tokens from the caller to this ARM contract inToken.transferFrom(msg.sender, address(this), amountIn); @@ -73,7 +76,7 @@ abstract contract FixedPriceARM is AbstractARM { } else { revert("ARM: Invalid token"); } - amountIn = ((amountOut * 1e36) / price) + 1; // +1 to always round in our favor + amountIn = ((amountOut * PRICE_SCALE) / price) + 1; // +1 to always round in our favor // Transfer the input tokens from the caller to this ARM contract inToken.transferFrom(msg.sender, address(this), amountIn); @@ -92,18 +95,21 @@ abstract contract FixedPriceARM is AbstractARM { } /** - * @notice Set exchange rates from an operator account - * @param buyT1 The buy price of Token 1 (t0 -> t1), denominated in Token 0. 1e36 - * @param sellT1 The sell price of Token 1 (t1 -> t0), denominated in Token 0. 1e36 + * @notice Set exchange rates from an operator account from the ARM's perspective. + * If token 0 is WETH and token 1 is stETH, then both prices will be set using the stETH/WETH price. + * @param buyT1 The price the ARM buys Token 1 from the Trader, denominated in Token 0, scaled to 36 decimals. + * From the Trader's perspective, this is the sell price. + * @param sellT1 The price the ARM sells Token 1 to the Trader, denominated in Token 0, scaled to 36 decimals. + * From the Trader's perspective, this is the buy price. */ function setPrices(uint256 buyT1, uint256 sellT1) external onlyOperatorOrOwner { - uint256 _traderate0 = 1e72 / sellT1; // base (t0) -> token (t1) - uint256 _traderate1 = buyT1; // token (t1) -> base (t0) - // Limit funds and loss when called by operator + // Limit funds and loss when called by the Operator if (msg.sender == operator) { - require(_traderate0 <= MAX_OPERATOR_RATE, "ARM: Traderate too high"); - require(_traderate1 <= MAX_OPERATOR_RATE, "ARM: Traderate too high"); + require(sellT1 >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: sell price too low"); + require(buyT1 <= PRICE_SCALE + MAX_PRICE_DEVIATION, "ARM: buy price too high"); } + uint256 _traderate0 = 1e72 / sellT1; // base (t0) -> token (t1) + uint256 _traderate1 = buyT1; // token (t1) -> base (t0) _setTraderates(_traderate0, _traderate1); } diff --git a/test/fork/LidoOwnerLpARM/Setters.t.sol b/test/fork/LidoOwnerLpARM/Setters.t.sol index 3bcd360..c54364a 100644 --- a/test/fork/LidoOwnerLpARM/Setters.t.sol +++ b/test/fork/LidoOwnerLpARM/Setters.t.sol @@ -18,17 +18,30 @@ contract Fork_Concrete_LidoOwnerLpARM_Setters_Test_ is Fork_Shared_Test_ { function test_RevertWhen_SetPrices_Because_PriceCross() public { vm.expectRevert("ARM: Price cross"); lidoOwnerLpARM.setPrices(90 * 1e33, 89 * 1e33); + vm.expectRevert("ARM: Price cross"); lidoOwnerLpARM.setPrices(72, 70); + vm.expectRevert("ARM: Price cross"); lidoOwnerLpARM.setPrices(1005 * 1e33, 1000 * 1e33); + + // Both set to 1.0 + vm.expectRevert("ARM: Price cross"); + lidoOwnerLpARM.setPrices(1e36, 1e36); } - function test_RevertWhen_SetPrices_Because_TraderateTooHigh() public { - //vm.expectRevert("ARM: Traderates too high"); - //lidoOwnerLpARM.setPrices(1010 * 1e33, 1020 * 1e33); - //vm.expectRevert("ARM: Traderates too high"); - //lidoOwnerLpARM.setPrices(993 * 1e33, 994 * 1e33); + function test_RevertWhen_SetPrices_Because_PriceRange() public asLidoOwnerLpARMOperator { + // buy price 11 basis points higher than 1.0 + vm.expectRevert("ARM: buy price too high"); + lidoOwnerLpARM.setPrices(10011e32, 10020e32); + + // sell price 11 basis points lower than 1.0 + vm.expectRevert("ARM: sell price too low"); + lidoOwnerLpARM.setPrices(9980e32, 9989e32); + + // Forgot to scale up to 36 decimals + vm.expectRevert("ARM: sell price too low"); + lidoOwnerLpARM.setPrices(1e18, 1e18); } function test_RevertWhen_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { @@ -49,7 +62,14 @@ contract Fork_Concrete_LidoOwnerLpARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_SetPrices() public asLidoOwnerLpARMOperator { + function test_SetPrices_Operator() public asLidoOwnerLpARMOperator { + // buy price 10 basis points higher than 1.0 + lidoOwnerLpARM.setPrices(1001e33, 1002e33); + // sell price 10 basis points lower than 1.0 + lidoOwnerLpARM.setPrices(9980e32, 9991e32); + // 2% of one basis point spread + lidoOwnerLpARM.setPrices(999999e30, 1000001e30); + lidoOwnerLpARM.setPrices(992 * 1e33, 1001 * 1e33); lidoOwnerLpARM.setPrices(1001 * 1e33, 1004 * 1e33); lidoOwnerLpARM.setPrices(992 * 1e33, 2000 * 1e33); @@ -59,6 +79,14 @@ contract Fork_Concrete_LidoOwnerLpARM_Setters_Test_ is Fork_Shared_Test_ { assertEq(lidoOwnerLpARM.traderate1(), 992 * 1e33); } + function test_SetPrices_Owner() public { + // buy price 11 basis points higher than 1.0 + lidoOwnerLpARM.setPrices(10011e32, 10020e32); + + // sell price 11 basis points lower than 1.0 + lidoOwnerLpARM.setPrices(9980e32, 9989e32); + } + function test_SetOperator() public asLidoOwnerLpARMOwner { lidoOwnerLpARM.setOperator(address(this)); assertEq(lidoOwnerLpARM.operator(), address(this)); From 2bc1c2e5f3de329e10e398c294f2c1e851a48193 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Sep 2024 16:39:10 +1000 Subject: [PATCH 050/196] Renamed MAX_FEE to FEE_SCALE --- docs/LidoFixedPriceMultiLpARMSquashed.svg | 75 ++++++++--------- docs/LidoMultiPriceMultiLpARMHierarchy.svg | 76 ++++++++--------- docs/LidoMultiPriceMultiLpARMSquashed.svg | 8 +- docs/LidoOwnerLpARMHierarchy.svg | 48 +++++------ docs/LidoOwnerLpARMSquashed.svg | 97 +++++++++++----------- src/contracts/PerformanceFee.sol | 10 +-- 6 files changed, 157 insertions(+), 157 deletions(-) diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index 60369eb..b70e631 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,47 +4,48 @@ - - + + UmlClassDiagram - + 15 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<MultiLP>> -   _gap: uint256[50] <<PerformanceFee>> -   _gap: uint256[49] <<LiquidityProviderControllerARM>> -   _gap: uint256[50] <<FixedPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityAsset: address <<MultiLP>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   MAX_FEE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   liquidityProviderController: address <<LiquidityProviderControllerARM>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<MultiLP>> +   _gap: uint256[50] <<PerformanceFee>> +   _gap: uint256[49] <<LiquidityProviderControllerARM>> +   _gap: uint256[50] <<FixedPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityAsset: address <<MultiLP>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   FEE_SCALE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   liquidityProviderController: address <<LiquidityProviderControllerARM>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   MAX_PRICE_DEVIATION: uint256 <<FixedPriceARM>> +   PRICE_SCALE: uint256 <<FixedPriceARM>>   steth: IERC20 <<LidoLiquidityManager>>   weth: IWETH <<LidoLiquidityManager>>   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> diff --git a/docs/LidoMultiPriceMultiLpARMHierarchy.svg b/docs/LidoMultiPriceMultiLpARMHierarchy.svg index 60447b1..61c98ee 100644 --- a/docs/LidoMultiPriceMultiLpARMHierarchy.svg +++ b/docs/LidoMultiPriceMultiLpARMHierarchy.svg @@ -17,16 +17,16 @@ AbstractARM ../src/contracts/AbstractARM.sol - + -24 +28 OwnableOperable ../src/contracts/OwnableOperable.sol - + -0->24 +0->28 @@ -38,115 +38,115 @@ AccessControlLP ../src/contracts/AccessControlLP.sol - + -17 +21 <<Abstract>> MultiLP ../src/contracts/MultiLP.sol - + -1->17 +1->21 - + -14 +16 <<Abstract>> LidoLiquidityManager ../src/contracts/LidoLiquidityManager.sol - + -14->24 +16->28 - + -15 +17 LidoMultiPriceMultiLpARM ../src/contracts/LidoMultiPriceMultiLpARM.sol - + -15->1 +17->1 - + -15->14 +17->16 - + -15->17 +17->21 - + -20 +24 <<Abstract>> MultiPriceARM ../src/contracts/MultiPriceARM.sol - + -15->20 +17->24 - + -27 +31 <<Abstract>> PerformanceFee ../src/contracts/PerformanceFee.sol - + -15->27 +17->31 - + -17->0 +21->0 - + -20->0 +24->0 - + -23 +27 Ownable ../src/contracts/Ownable.sol - + -24->23 +28->27 - + -27->17 +31->21 diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index 979ad34..4bd20cc 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -9,9 +9,9 @@ UmlClassDiagram - + -15 +17 LidoMultiPriceMultiLpARM ../src/contracts/LidoMultiPriceMultiLpARM.sol @@ -42,7 +42,7 @@   CLAIM_DELAY: uint256 <<MultiLP>>   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>>   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   MAX_FEE: uint256 <<PerformanceFee>> +   FEE_SCALE: uint256 <<PerformanceFee>>   feeCollector: address <<PerformanceFee>>   fee: uint16 <<PerformanceFee>>   feesAccrued: uint112 <<PerformanceFee>> @@ -99,7 +99,7 @@    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>>    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>>    collectFees(): (fees: uint256) <<PerformanceFee>> -    setLiquidityProviderCap(liquidityProvider: address, cap: uint256) <<onlyOwner>> <<AccessControlLP>> +    setLiquidityProviderCaps(_liquidityProviders: address[], cap: uint256) <<onlyOwner>> <<AccessControlLP>>    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>>    setTrancheDiscounts(discounts: uint16[5]) <<onlyOwner>> <<MultiPriceARM>>    setTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> diff --git a/docs/LidoOwnerLpARMHierarchy.svg b/docs/LidoOwnerLpARMHierarchy.svg index 542284d..2761596 100644 --- a/docs/LidoOwnerLpARMHierarchy.svg +++ b/docs/LidoOwnerLpARMHierarchy.svg @@ -17,16 +17,16 @@ AbstractARM ../src/contracts/AbstractARM.sol - + -24 +28 OwnableOperable ../src/contracts/OwnableOperable.sol - + -0->24 +0->28 @@ -44,69 +44,69 @@ - + -14 +16 <<Abstract>> LidoLiquidityManager ../src/contracts/LidoLiquidityManager.sol - + -14->24 +16->28 - + -16 +18 LidoOwnerLpARM ../src/contracts/LidoOwnerLpARM.sol - + -16->3 +18->3 - + -16->14 +18->16 - + -25 +29 <<Abstract>> OwnerLP ../src/contracts/OwnerLP.sol - + -16->25 +18->29 - + -23 +27 Ownable ../src/contracts/Ownable.sol - + -24->23 +28->27 - + -25->23 +29->27 diff --git a/docs/LidoOwnerLpARMSquashed.svg b/docs/LidoOwnerLpARMSquashed.svg index abfdfb3..5227bd2 100644 --- a/docs/LidoOwnerLpARMSquashed.svg +++ b/docs/LidoOwnerLpARMSquashed.svg @@ -4,63 +4,62 @@ - - + + UmlClassDiagram - - + + -16 - -LidoOwnerLpARM -../src/contracts/LidoOwnerLpARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<FixedPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MAX_OPERATOR_RATE: uint256 <<FixedPriceARM>> +18 + +LidoOwnerLpARM +../src/contracts/LidoOwnerLpARM.sol + +Private: +   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[50] <<FixedPriceARM>> +   _gap: uint256[50] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> Public:   operator: address <<OwnableOperable>>   token0: IERC20 <<AbstractARM>>   token1: IERC20 <<AbstractARM>>   traderate0: uint256 <<FixedPriceARM>>   traderate1: uint256 <<FixedPriceARM>> -   minimumFunds: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoOwnerLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -    _postClaimHook(assets: uint256) <<LidoOwnerLpARM>> -    _externalWithdrawQueue(): (assets: uint256) <<LidoLiquidityManager>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    owner(): address <<Ownable>> -    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> -    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>> -    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>> <<FixedPriceARM>> -    setMinimumFunds(_minimumFunds: uint256) <<onlyOwner>> <<FixedPriceARM>> +   MAX_PRICE_DEVIATION: uint256 <<FixedPriceARM>> +   PRICE_SCALE: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoOwnerLpARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> +    _postClaimHook(assets: uint256) <<LidoOwnerLpARM>> +    _externalWithdrawQueue(): (assets: uint256) <<LidoLiquidityManager>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    owner(): address <<Ownable>> +    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>> +    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>> <<FixedPriceARM>>    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index b69b842..8237499 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -11,9 +11,9 @@ import {MultiLP} from "./MultiLP.sol"; * @author Origin Protocol Inc */ abstract contract PerformanceFee is MultiLP { - /// @notice The maximum performance fee that can be set + /// @notice The scale of the performance fee /// 10,000 = 100% performance fee - uint256 public constant MAX_FEE = 10000; + uint256 public constant FEE_SCALE = 10000; /// @notice The account that can collect the performance fee address public feeCollector; @@ -72,7 +72,7 @@ abstract contract PerformanceFee is MultiLP { if (newTotalAssets <= lastTotalAssets) return; uint256 assetIncrease = newTotalAssets - lastTotalAssets; - uint256 newFeesAccrued = (assetIncrease * fee) / MAX_FEE; + uint256 newFeesAccrued = (assetIncrease * fee) / FEE_SCALE; // Save the new accrued fees back to storage feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); @@ -90,7 +90,7 @@ abstract contract PerformanceFee is MultiLP { uint256 assetIncrease = totalAssetsBeforeFees - lastTotalAssets; // Calculate the performance fee and remove from the total assets before new fees are removed - return totalAssetsBeforeFees - ((assetIncrease * fee) / MAX_FEE); + return totalAssetsBeforeFees - ((assetIncrease * fee) / FEE_SCALE); } /// @dev Calculate the total assets in the ARM, external withdrawal queue, @@ -114,7 +114,7 @@ abstract contract PerformanceFee is MultiLP { } function _setFee(uint256 _fee) internal { - require(_fee <= MAX_FEE, "ARM: fee too high"); + require(_fee <= FEE_SCALE, "ARM: fee too high"); // Calculate fees up to this point using the old fee _calcFee(); From 2371dfbd0a9e3e5beb836dd65215f9d82c4a5cf2 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Sep 2024 16:39:49 +1000 Subject: [PATCH 051/196] Removed old Make upgrade actions --- Makefile | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Makefile b/Makefile index d9e213b..b8f7787 100644 --- a/Makefile +++ b/Makefile @@ -61,15 +61,5 @@ deploy-testnet: deploy-holesky: @forge script script/deploy/DeployManager.sol --rpc-url $(HOLESKY_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvv -# Upgrade scripts -upgrade: - @forge script script/002_Upgrade.s.sol --rpc-url $(PROVIDER_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvv - -upgrade-testnet: - @forge script script/002_Upgrade.s.sol --rpc-url $(TESTNET_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow -vvvv - -upgrade-holesky: - @forge script script/002_Upgrade.s.sol --rpc-url $(HOLESKY_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvv - # Override default `test` and `coverage` targets .PHONY: test coverage From 9a31216ffc500de150421a814cb568c0190bac0d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Sep 2024 16:48:00 +1000 Subject: [PATCH 052/196] Changed the _gap's so each contract uses 50 slots --- docs/LidoFixedPriceMultiLpARMSquashed.svg | 10 +++++----- docs/LidoMultiPriceMultiLpARMSquashed.svg | 12 ++++++------ docs/LidoOwnerLpARMSquashed.svg | 6 +++--- src/contracts/AccessControlLP.sol | 2 +- src/contracts/AccessControlShares.sol | 2 +- src/contracts/FixedPriceARM.sol | 2 +- src/contracts/LidoLiquidityManager.sol | 2 +- src/contracts/MultiLP.sol | 2 +- src/contracts/MultiPriceARM.sol | 2 +- src/contracts/OwnableOperable.sol | 2 +- src/contracts/PerformanceFee.sol | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index b70e631..b67edf2 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -17,13 +17,13 @@ ../src/contracts/LidoFixedPriceMultiLpARM.sol Private: -   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[49] <<OwnableOperable>>   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<MultiLP>> -   _gap: uint256[50] <<PerformanceFee>> +   _gap: uint256[47] <<MultiLP>> +   _gap: uint256[48] <<PerformanceFee>>   _gap: uint256[49] <<LiquidityProviderControllerARM>> -   _gap: uint256[50] <<FixedPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> +   _gap: uint256[48] <<FixedPriceARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> Internal:   OWNER_SLOT: bytes32 <<Ownable>>   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg index 4bd20cc..aec813b 100644 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ b/docs/LidoMultiPriceMultiLpARMSquashed.svg @@ -17,19 +17,19 @@ ../src/contracts/LidoMultiPriceMultiLpARM.sol Private: -   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[49] <<OwnableOperable>>   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<MultiLP>> -   _gap: uint256[50] <<PerformanceFee>> -   _gap: uint256[50] <<AccessControlLP>> +   _gap: uint256[47] <<MultiLP>> +   _gap: uint256[48] <<PerformanceFee>> +   _gap: uint256[48] <<AccessControlLP>>   discountToken: address <<MultiPriceARM>>   liquidityToken: address <<MultiPriceARM>>   DISCOUNT_INDEX: uint256 <<MultiPriceARM>>   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>>   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>>   tranches: uint16[15] <<MultiPriceARM>> -   _gap: uint256[50] <<MultiPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> +   _gap: uint256[49] <<MultiPriceARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> Internal:   OWNER_SLOT: bytes32 <<Ownable>>   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> diff --git a/docs/LidoOwnerLpARMSquashed.svg b/docs/LidoOwnerLpARMSquashed.svg index 5227bd2..7e4e0aa 100644 --- a/docs/LidoOwnerLpARMSquashed.svg +++ b/docs/LidoOwnerLpARMSquashed.svg @@ -17,10 +17,10 @@ ../src/contracts/LidoOwnerLpARM.sol Private: -   _gap: uint256[50] <<OwnableOperable>> +   _gap: uint256[49] <<OwnableOperable>>   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[50] <<FixedPriceARM>> -   _gap: uint256[50] <<LidoLiquidityManager>> +   _gap: uint256[48] <<FixedPriceARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> Internal:   OWNER_SLOT: bytes32 <<Ownable>> Public: diff --git a/src/contracts/AccessControlLP.sol b/src/contracts/AccessControlLP.sol index b0eeced..ff54335 100644 --- a/src/contracts/AccessControlLP.sol +++ b/src/contracts/AccessControlLP.sol @@ -7,7 +7,7 @@ abstract contract AccessControlLP is MultiLP { uint256 public totalAssetsCap; mapping(address lp => uint256 cap) public liquidityProviderCaps; - uint256[50] private _gap; + uint256[48] private _gap; event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); event TotalAssetsCap(uint256 cap); diff --git a/src/contracts/AccessControlShares.sol b/src/contracts/AccessControlShares.sol index 07ef4d7..14977e2 100644 --- a/src/contracts/AccessControlShares.sol +++ b/src/contracts/AccessControlShares.sol @@ -7,7 +7,7 @@ abstract contract AccessControlShares is MultiLP { uint256 public totalSupplyCap; mapping(address lp => uint256 shares) public liquidityProviderCaps; - uint256[50] private _gap; + uint256[48] private _gap; event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); event TotalSupplyCap(uint256 shares); diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol index fc4803c..c203b71 100644 --- a/src/contracts/FixedPriceARM.sol +++ b/src/contracts/FixedPriceARM.sol @@ -32,7 +32,7 @@ abstract contract FixedPriceARM is AbstractARM { /// @notice Scale of the prices. uint256 public constant PRICE_SCALE = 1e36; - uint256[50] private _gap; + uint256[48] private _gap; event TraderateChanged(uint256 traderate0, uint256 traderate1); diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index fac126d..e353a0a 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -15,7 +15,7 @@ abstract contract LidoLiquidityManager is OwnableOperable { uint256 public outstandingEther; - uint256[50] private _gap; + uint256[49] private _gap; constructor(address _steth, address _weth, address _lidoWithdrawalQueue) { steth = IERC20(_steth); diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 561f5e3..d1a253f 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -55,7 +55,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { /// @notice Mapping of withdrawal request indices to the user withdrawal request data mapping(uint256 requestId => WithdrawalRequest) public withdrawalRequests; - uint256[50] private _gap; + uint256[47] private _gap; event RedeemRequested( address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol index 993ec6a..1a47cbd 100644 --- a/src/contracts/MultiPriceARM.sol +++ b/src/contracts/MultiPriceARM.sol @@ -37,7 +37,7 @@ abstract contract MultiPriceARM is AbstractARM { /// @dev Five tranches are used as they fit in a single storage slot uint16[15] private tranches; - uint256[50] private _gap; + uint256[49] private _gap; event TranchePricesUpdated(uint16[5] discounts); event TrancheAllocationsUpdated(uint256[5] allocations); diff --git a/src/contracts/OwnableOperable.sol b/src/contracts/OwnableOperable.sol index 4a371d4..e2e0363 100644 --- a/src/contracts/OwnableOperable.sol +++ b/src/contracts/OwnableOperable.sol @@ -7,7 +7,7 @@ contract OwnableOperable is Ownable { /// @notice The account that can request and claim withdrawals. address public operator; - uint256[50] private _gap; + uint256[49] private _gap; event OperatorChanged(address newAdmin); diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index 8237499..b0ffe53 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -29,7 +29,7 @@ abstract contract PerformanceFee is MultiLP { /// This can only go up so is a high watermark. uint128 public lastTotalAssets; - uint256[50] private _gap; + uint256[48] private _gap; event FeeCalculated(uint256 newFeesAccrued, uint256 assetIncrease); event FeeCollected(address indexed feeCollector, uint256 fee); From 73dacff95de8588dca2bb7ef9833ebb90537b2f3 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Sep 2024 18:52:44 +1000 Subject: [PATCH 053/196] Updated Lido ARM deploy script --- script/deploy/mainnet/001_DeployCoreScript.sol | 2 +- script/deploy/mainnet/002_UpgradeScript.sol | 2 +- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 12 +++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/script/deploy/mainnet/001_DeployCoreScript.sol b/script/deploy/mainnet/001_DeployCoreScript.sol index 99661cc..2b97c61 100644 --- a/script/deploy/mainnet/001_DeployCoreScript.sol +++ b/script/deploy/mainnet/001_DeployCoreScript.sol @@ -17,7 +17,7 @@ contract DeployCoreMainnetScript is AbstractDeployScript { GovProposal public govProposal; string public constant override DEPLOY_NAME = "001_CoreMainnet"; - bool public constant override proposalExecuted = false; + bool public constant override proposalExecuted = true; function _execute() internal override { console.log("Deploy:", DEPLOY_NAME); diff --git a/script/deploy/mainnet/002_UpgradeScript.sol b/script/deploy/mainnet/002_UpgradeScript.sol index d042cf7..53a1fd8 100644 --- a/script/deploy/mainnet/002_UpgradeScript.sol +++ b/script/deploy/mainnet/002_UpgradeScript.sol @@ -13,7 +13,7 @@ import {DeployManager} from "../DeployManager.sol"; contract UpgradeMainnetScript is AbstractDeployScript { string public constant override DEPLOY_NAME = "002_UpgradeMainnet"; - bool public constant override proposalExecuted = false; + bool public constant override proposalExecuted = true; address newImpl; DeployManager internal deployManager; diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 666cff8..7b156e8 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -68,9 +68,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { function _buildGovernanceProposal() internal override {} function _fork() internal override { - // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig - vm.prank(Mainnet.ARM_MULTISIG); - lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); + vm.startPrank(Mainnet.ARM_MULTISIG); // Initialize Lido ARM proxy and implementation contract bytes memory data = abi.encodeWithSignature( @@ -82,11 +80,12 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { Mainnet.ARM_BUYBACK, address(lpcProxy) ); + console.log("lidoARM initialize data:"); + console.logBytes(data); - vm.startPrank(Mainnet.GOV_MULTISIG); uint256 tinyMintAmount = 1e12; // Get some WETH - vm.deal(Mainnet.GOV_MULTISIG, tinyMintAmount); + vm.deal(Mainnet.ARM_MULTISIG, tinyMintAmount); IWETH(Mainnet.WETH).deposit{value: tinyMintAmount}(); // Approve the Lido ARM proxy to spend WETH IERC20(Mainnet.WETH).approve(address(lidoARMProxy), tinyMintAmount); @@ -97,6 +96,9 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // Set the buy price with a 8 basis point discount. The sell price is 1.0 LidoFixedPriceMultiLpARM(payable(Mainnet.LIDO_ARM)).setPrices(9994e32, 1e36); + // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig + lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); + vm.stopPrank(); } } From d57fe5de825d75cbad71da4fa9351511e98d024e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Sep 2024 20:25:29 +1000 Subject: [PATCH 054/196] Updated deploy script --- .../mainnet/003_UpgradeLidoARMScript.sol | 23 +++++++++++++++---- src/contracts/Interfaces.sol | 10 ++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 7b156e8..9cfc25d 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.23; import "forge-std/console.sol"; import {Vm} from "forge-std/Vm.sol"; -import {IERC20, IWETH} from "contracts/Interfaces.sol"; +import {IERC20, IWETH, LegacyAMM} from "contracts/Interfaces.sol"; import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; import {Proxy} from "contracts/Proxy.sol"; @@ -70,6 +70,19 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { function _fork() internal override { vm.startPrank(Mainnet.ARM_MULTISIG); + // remove all liquidity from the old AMM v1 contract + uint256 wethLegacyBalance = IERC20(Mainnet.WETH).balanceOf(Mainnet.LIDO_ARM); + if (wethLegacyBalance > 0) { + console.log("Withdrawing WETH from legacy Lido ARM"); + LegacyAMM(Mainnet.LIDO_ARM).transferToken(Mainnet.WETH, Mainnet.ARM_MULTISIG, wethLegacyBalance); + } + uint256 stethLegacyBalance = IERC20(Mainnet.STETH).balanceOf(Mainnet.LIDO_ARM); + if (stethLegacyBalance > 0) { + console.log("Withdrawing stETH from legacy Lido ARM"); + LegacyAMM(Mainnet.LIDO_ARM).transferToken(Mainnet.STETH, Mainnet.ARM_MULTISIG, stethLegacyBalance); + } + // TODO need to also remove anything in the Lido withdrawal queue + // Initialize Lido ARM proxy and implementation contract bytes memory data = abi.encodeWithSignature( "initialize(string,string,address,uint256,address,address)", @@ -84,9 +97,11 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.logBytes(data); uint256 tinyMintAmount = 1e12; - // Get some WETH - vm.deal(Mainnet.ARM_MULTISIG, tinyMintAmount); - IWETH(Mainnet.WETH).deposit{value: tinyMintAmount}(); + + // Get some WETH which has already been done on mainnet + // vm.deal(Mainnet.ARM_MULTISIG, tinyMintAmount); + // IWETH(Mainnet.WETH).deposit{value: tinyMintAmount}(); + // Approve the Lido ARM proxy to spend WETH IERC20(Mainnet.WETH).approve(address(lidoARMProxy), tinyMintAmount); diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 4ebe543..7e7e4a4 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -13,12 +13,6 @@ interface IERC20 { event Transfer(address indexed from, address indexed to, uint256 value); } -interface IERC20Metadata is IERC20 { - function name() external view returns (string memory); - function symbol() external view returns (string memory); - function decimals() external view returns (uint8); -} - interface IOethARM { function token0() external returns (address); function token1() external returns (address); @@ -130,6 +124,10 @@ interface ILiquidityProviderController { function postDepositHook(address liquidityProvider, uint256 assets) external; } +interface LegacyAMM { + function transferToken(address tokenOut, address to, uint256 amount) external; +} + interface IOETHVault { function mint(address _asset, uint256 _amount, uint256 _minimumOusdAmount) external; From 5acf34bd440eddb5a943e4d60c7a3a25f69bcbfb Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Sep 2024 22:00:14 +1000 Subject: [PATCH 055/196] Added postDeploy Hardhat task --- src/contracts/utils/Addresses.sol | 1 + src/js/tasks/tasks.js | 69 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index 57e6d88..7b9e7b9 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -82,6 +82,7 @@ contract AddressResolver { // Contracts resolver[MAINNET]["OETH_VAULT"] = Mainnet.OETH_VAULT; resolver[MAINNET]["OETH_ARM"] = Mainnet.OETH_ARM; + resolver[MAINNET]["LIDO_ARM"] = Mainnet.LIDO_ARM; // Test accounts resolver[MAINNET]["INITIAL_DEPLOYER"] = address(0x1001); diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 122b575..895bef6 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -1,4 +1,5 @@ const { subtask, task, types } = require("hardhat/config"); +const { formatUnits, parseUnits } = require("ethers"); const { parseAddress } = require("../utils/addressParser"); const { setAutotaskVars } = require("./autotask"); @@ -447,3 +448,71 @@ subtask( task("setActionVars").setAction(async (_, __, runSuper) => { return runSuper(); }); + +subtask( + "postDeploy", + "Used for Testnets after running the Lido deploy script" +).setAction(async () => { + const signer = await getSigner(); + + const wethAddress = await parseAddress("WETH"); + const stethAddress = await parseAddress("STETH"); + const lidoArmAddress = await parseAddress("LIDO_ARM"); + const relayerAddress = await parseAddress("ARM_RELAYER"); + + const weth = await ethers.getContractAt("IWETH", wethAddress); + const steth = await ethers.getContractAt("IWETH", stethAddress); + const legacyAMM = await ethers.getContractAt("LegacyAMM", lidoArmAddress); + const lidoARM = await ethers.getContractAt( + "LidoFixedPriceMultiLpARM", + lidoArmAddress + ); + const lidoProxy = await ethers.getContractAt("Proxy", lidoArmAddress); + + const wethBalance = await weth.balanceOf(lidoArmAddress); + console.log( + `Amount to transfer ${formatUnits(wethBalance)} WETH out of the LidoARM` + ); + await legacyAMM + .connect(signer) + .transferToken(wethAddress, await signer.getAddress(), wethBalance); + + const stethBalance = await steth.balanceOf(lidoArmAddress); + console.log( + `Amount to transfer ${formatUnits(stethBalance)} stETH out of the LidoARM` + ); + await legacyAMM + .connect(signer) + .transferToken(stethAddress, await signer.getAddress(), stethBalance); + + console.log(`Amount to approve the Lido ARM`); + await weth.connect(signer).approve(lidoArmAddress, "1000000000000"); + + const initData = lidoARM.interface.encodeFunctionData( + "initialize(string,string,address,uint256,address,address)", + [ + "Lido ARM", + "ARM-ST", + relayerAddress, + 1500, // 15% performance fee + // TODO set to Buyback contract + relayerAddress, + "0x187FfF686a5f42ACaaF56469FcCF8e6Feca18248", + ] + ); + + const lidoArmImpl = "0x3d724176c8f1F965eF4020CB5DA5ad1a891BEEf1"; + console.log(`Amount to upgradeToAndCall the Lido ARM`); + await lidoProxy.connect(signer).upgradeToAndCall(lidoArmImpl, initData); + + console.log(`Amount to setPrices on the Lido ARM`); + await lidoARM + .connect(signer) + .setPrices(parseUnits("9994", 32), parseUnits("1", 36)); + + console.log(`Amount to setOwner on the Lido ARM`); + await lidoProxy.connect(signer).setOwner(await parseAddress("GOV_MULTISIG")); +}); +task("postDeploy").setAction(async (_, __, runSuper) => { + return runSuper(); +}); From 0b6e950da17614584c2ed8c87c659ccd5439a779 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Mon, 23 Sep 2024 12:35:20 +1000 Subject: [PATCH 056/196] Merge latest tests (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [WIP] test: add reverting test for `deposit()`. * fix: update `lastTotalAssets` before checking caps. * [WIP] test: add more test for deposit. * fix: cleanup tests. * test: add failing test. * test: add failling test. --------- Co-authored-by: Clément --- src/contracts/Interfaces.sol | 1 + src/contracts/LidoFixedPriceMultiLpARM.sol | 6 +- test/Base.sol | 1 + .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 316 +++++++++++++++--- .../RequestRedeem.t.sol | 28 ++ test/fork/shared/Shared.sol | 2 +- test/fork/utils/Helpers.sol | 17 +- test/fork/utils/Modifiers.sol | 41 ++- 8 files changed, 352 insertions(+), 60 deletions(-) diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 7e7e4a4..c91657b 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -118,6 +118,7 @@ interface ILiquidityProviderARM { function totalAssets() external returns (uint256 assets); function convertToShares(uint256 assets) external returns (uint256 shares); function convertToAssets(uint256 shares) external returns (uint256 assets); + function lastTotalAssets() external returns (uint256 assets); } interface ILiquidityProviderController { diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index 6569eb9..d258f3a 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -82,11 +82,11 @@ contract LidoFixedPriceMultiLpARM is internal override(MultiLP, LiquidityProviderControllerARM, PerformanceFee) { - // Check the LP can deposit the assets - LiquidityProviderControllerARM._postDepositHook(assets); - // Store the new total assets after the deposit and performance fee accrued PerformanceFee._postDepositHook(assets); + + // Check the LP can deposit the assets + LiquidityProviderControllerARM._postDepositHook(assets); } function _postWithdrawHook(uint256 assets) internal override(MultiLP, PerformanceFee) { diff --git a/test/Base.sol b/test/Base.sol index 1cb4349..834a46e 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -61,6 +61,7 @@ abstract contract Base_Test_ is Test { /// --- DEFAULT VALUES ////////////////////////////////////////////////////// uint256 public constant DEFAULT_AMOUNT = 1 ether; + uint256 public constant MIN_TOTAL_SUPPLY = 1e12; uint256 public constant STETH_ERROR_ROUNDING = 2; ////////////////////////////////////////////////////// diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 3a9c159..49fdf8b 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -4,77 +4,285 @@ pragma solidity 0.8.23; // Test imports import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {MultiLP} from "contracts/MultiLP.sol"; +import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; + contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Test_ { - AssertData beforeData; - DeltaData noChangeDeltaData = - DeltaData({totalAssets: 10, totalSupply: 0, totalAssetsCap: 0, armWeth: 0, armSteth: 0, feesAccrued: 0}); - - struct AssertData { - uint256 totalAssets; - uint256 totalSupply; - uint256 totalAssetsCap; - uint256 armWeth; - uint256 armSteth; - uint256 feesAccrued; + /** + * As Deposit is complex function due to the entaglement of virtual and override functions in inheritance. + * This is a small recap of the functions that are called in the deposit function. + * 1. ML: _preDepositHook() -> PF: _calcFee() -> PF: _rawTotalAssets() -> ML: _totalAssets() + * 2. ML: convertToShares() -> ML: _totalAssets() -> ARM: totalAssets() -> PF : totalAssets() -> + * -> PF: _rawTotalAssets() -> ML: _totalAssets() + * 3. ML: _postDepositHook() -> ARM: _postDepositHook() => + * | -> LCPARM: postDepositHook() -> LPC: postDepositHook() -> ARM: totalAssets() -> + * -> PF : totalAssets() -> PF: _rawTotalAssets() -> ML: _totalAssets() + * | -> PF: _postDepositHook() -> PF: _rawTotalAssets() -> ML: _totalAssets() + * + * ML = MultiLP + * PF = PerformanceFee + * ARM = LidoFixedPriceMultiLpARM + * LPC = LiquidityProviderController + * LCPARM = LiquidityProviderControllerARM + */ + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + deal(address(weth), address(this), 1_000 ether); + + // Alice + deal(address(weth), alice, 1_000 ether); + vm.prank(alice); + weth.approve(address(lidoFixedPriceMulltiLpARM), type(uint256).max); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapNull() + public + setLiquidityProviderCap(address(this), 0) + { + vm.expectRevert("LPC: LP cap exceeded"); + lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT); } - struct DeltaData { - int256 totalAssets; - int256 totalSupply; - int256 totalAssetsCap; - int256 armWeth; - int256 armSteth; - int256 feesAccrued; + function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapNotNull() + public + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + { + vm.expectRevert("LPC: LP cap exceeded"); + lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT + 1); } - function _snapData() internal view returns (AssertData memory data) { - return AssertData({ - totalAssets: lidoFixedPriceMulltiLpARM.totalAssets(), - totalSupply: lidoFixedPriceMulltiLpARM.totalSupply(), - totalAssetsCap: liquidityProviderController.totalAssetsCap(), - armWeth: weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), - armSteth: steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), - feesAccrued: lidoFixedPriceMulltiLpARM.feesAccrued() - }); + function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapReached() + public + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + { + // Initial deposit + lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT / 2); + + // Cap is now 0.5 ether + vm.expectRevert("LPC: LP cap exceeded"); + lidoFixedPriceMulltiLpARM.deposit((DEFAULT_AMOUNT / 2) + 1); } - function assertData(AssertData memory before, DeltaData memory delta) internal view { - AssertData memory afterData = _snapData(); - - assertEq(int256(afterData.totalAssets), int256(before.totalAssets) + delta.totalAssets, "totalAssets"); - assertEq(int256(afterData.totalSupply), int256(before.totalSupply) + delta.totalSupply, "totalSupply"); - assertEq( - int256(afterData.totalAssetsCap), int256(before.totalAssetsCap) + delta.totalAssetsCap, "totalAssetsCap" - ); - assertEq(int256(afterData.feesAccrued), int256(before.feesAccrued) + delta.feesAccrued, "feesAccrued"); - assertEq(int256(afterData.armWeth), int256(before.armWeth) + delta.armWeth, "armWeth"); - assertEq(int256(afterData.armSteth), int256(before.armSteth) + delta.armSteth, "armSteth"); + function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapNull() + public + setTotalAssetsCap(0) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT + 1) + { + vm.expectRevert("LPC: Total assets cap exceeded"); + lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT); } - ////////////////////////////////////////////////////// - /// --- SETUP - ////////////////////////////////////////////////////// - function setUp() public override { - super.setUp(); + function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapNotNull() + public + setTotalAssetsCap(DEFAULT_AMOUNT) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + { + vm.expectRevert("LPC: Total assets cap exceeded"); + lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT - MIN_TOTAL_SUPPLY + 1); + } + + function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapReached() + public + setTotalAssetsCap(DEFAULT_AMOUNT) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + { + lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT / 2); + vm.expectRevert("LPC: Total assets cap exceeded"); + lidoFixedPriceMulltiLpARM.deposit((DEFAULT_AMOUNT / 2) - MIN_TOTAL_SUPPLY + 1); // This should revert! } ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_Deposit_SimpleCase() + /// @notice Test the simplest case of depositing into the ARM, first deposit of first user. + /// @dev No fees accrued, no withdrawals queued, and no performance fees generated + function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_FirstDeposit_NoPerfs() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + { + uint256 amount = DEFAULT_AMOUNT; + // Assertions Before + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); // Ensure no shares before + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); + assertEqQueueMetadata(0, 0, 0, 0); + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amount); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); + + // Main call + uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + + // Assertions After + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEqQueueMetadata(0, 0, 0, 0); + assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether + } + + /// @notice Test a simple case of depositing into the ARM, second deposit of first user. + /// @dev No fees accrued, no withdrawals queued, and no performance fees generated + function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_SecondDepositSameUser_NoPerfs() + public + setTotalAssetsCap(DEFAULT_AMOUNT * 2 + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT * 2) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + uint256 amount = DEFAULT_AMOUNT; + // Assertions Before + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), amount); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); + assertEqQueueMetadata(0, 0, 0, 0); + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amount); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); + + // Main call + uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + + // Assertions After + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares * 2); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEqQueueMetadata(0, 0, 0, 0); + assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether + } + + /// @notice Test a simple case of depositing into the ARM, first deposit of second user. + /// @dev No fees accrued, no withdrawals queued, and no performance fees generated + function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_SecondDepositDiffUser_NoPerfs() + public + setTotalAssetsCap(DEFAULT_AMOUNT * 2 + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + setLiquidityProviderCap(alice, DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + uint256 amount = DEFAULT_AMOUNT; + // Assertions Before + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(liquidityProviderController.liquidityProviderCaps(alice), amount); + assertEqQueueMetadata(0, 0, 0, 0); + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(alice, address(lidoFixedPriceMulltiLpARM), amount); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(0), alice, amount); // shares == amount here + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.LiquidityProviderCap(alice, 0); + + vm.prank(alice); + // Main call + uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + + // Assertions After + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), shares); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(liquidityProviderController.liquidityProviderCaps(alice), 0); // All the caps are used + assertEqQueueMetadata(0, 0, 0, 0); + assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether + } + + function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_FirstDeposit_WithPerfs() public - asLidoFixedPriceMultiLpARMOwner - setLiquidityProviderCap(address(this), 20 ether) + setTotalAssetsCap(type(uint256).max) // No need to restrict it for this test. + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { - deal(address(weth), address(this), 10 ether); - beforeData = _snapData(); + // simulate profit + uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 profit = DEFAULT_AMOUNT; + deal(address(weth), address(lidoFixedPriceMulltiLpARM), balanceBefore + profit); + + // Assertions Before + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + profit); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); // Ensure no shares before + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); + assertEqQueueMetadata(0, 0, 0, 0); + + uint256 feesAccrued = profit * lidoFixedPriceMulltiLpARM.fee() / lidoFixedPriceMulltiLpARM.FEE_SCALE(); + uint256 rawTotalAsset = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - feesAccrued; // No steth and no externalWithdrawQueue + uint256 amount = DEFAULT_AMOUNT; + + uint256 expectedShares = amount * MIN_TOTAL_SUPPLY / (rawTotalAsset); + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amount); + //vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + //emit IERC20.Transfer(address(0), address(this), expectedShares); + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); - lidoFixedPriceMulltiLpARM.deposit(10 ether); + // Main call + uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + assertEq(shares, expectedShares, "Wrong shares calculation"); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether - DeltaData memory delta = noChangeDeltaData; - delta.totalAssets = 10 ether; - delta.totalSupply = 10 ether; - delta.armWeth = 10 ether; - assertData(beforeData, delta); + // Assertions After + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + profit + amount); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feesAccrued); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + profit + amount); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEqQueueMetadata(0, 0, 0, 0); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 59fc2c0..3c90dee 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -10,6 +10,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar ////////////////////////////////////////////////////// function setUp() public override { super.setUp(); + + deal(address(weth), address(this), 1_000 ether); } ////////////////////////////////////////////////////// @@ -25,4 +27,30 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar lidoFixedPriceMulltiLpARM.deposit(10 ether); lidoFixedPriceMulltiLpARM.requestRedeem(8 ether); } + + /// @notice Test the `requestRedeem` function when there are no profits and the first deposit is made. + function test_RequestWithdraw_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Assertions Before + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEqQueueMetadata(0, 0, 0, 0); + + // Main Call + (uint256 requestId, uint256 assets) = lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT); + + assertEq(requestId, 0); // First request + assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); // One request in the queue + assertEq(assets, DEFAULT_AMOUNT, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited + } } diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 6797ecb..ca32114 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -197,7 +197,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { vm.label(address(lidoFixedPriceMulltiLpARM), "LIDO ARM"); vm.label(address(lidoProxy), "LIDO ARM PROXY"); vm.label(address(lidoOwnerLpARM), "LIDO OWNER LP ARM"); - vm.label(address(lidoOwnerProxy), "LIDO OWNER LP ARM PROXY"); + vm.label(address(liquidityProviderController), "LIQUIDITY PROVIDER CONTROLLER"); vm.label(operator, "OPERATOR"); vm.label(oethWhale, "WHALE OETH"); vm.label(governor, "GOVERNOR"); diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index 2b4a66f..f05c7b5 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.23; import {Base_Test_} from "test/Base.sol"; abstract contract Helpers is Base_Test_ { - /// @notice Override `deal()` function to handle OETH special case. + /// @notice Override `deal()` function to handle OETH and STETH special case. function deal(address token, address to, uint256 amount) internal override { // Handle OETH special case, as rebasing tokens are not supported by the VM. if (token == address(oeth)) { @@ -31,4 +31,19 @@ abstract contract Helpers is Base_Test_ { super.deal(token, to, amount); } } + + /// @notice Asserts the equality bewteen value of `withdrawalQueueMetadata()` and the expected values. + function assertEqQueueMetadata( + uint256 expectedQueued, + uint256 expectedClaimable, + uint256 expectedClaimed, + uint256 expectedNextIndex + ) public view { + (uint256 queued, uint256 claimable, uint256 claimed, uint256 nextWithdrawalIndex) = + lidoFixedPriceMulltiLpARM.withdrawalQueueMetadata(); + assertEq(queued, expectedQueued); + assertEq(claimable, expectedClaimable); + assertEq(claimed, expectedClaimed); + assertEq(nextWithdrawalIndex, expectedNextIndex); + } } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index c582860..0485ab6 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -1,11 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +// Foundry +import {VmSafe} from "forge-std/Vm.sol"; + // Test imports import {Helpers} from "test/fork/utils/Helpers.sol"; import {MockCall} from "test/fork/utils/MockCall.sol"; abstract contract Modifiers is Helpers { + /// @notice Impersonate Alice. + modifier asAlice() { + vm.startPrank(alice); + _; + vm.stopPrank(); + } + /// @notice Impersonate the owner of the contract. modifier asOwner() { vm.startPrank(oethARM.owner()); @@ -54,7 +64,7 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Set the liquidity provider cap for a given provider on the LidoFixedPriceMultiLpARM contract. + /// @notice Set the liquidity provider cap for a given provider on the LiquidityProviderController contract. modifier setLiquidityProviderCap(address provider, uint256 cap) { address[] memory providers = new address[](1); providers[0] = provider; @@ -62,4 +72,33 @@ abstract contract Modifiers is Helpers { liquidityProviderController.setLiquidityProviderCaps(providers, cap); _; } + + /// @notice Set the total assets cap on the LiquidityProviderController contract. + modifier setTotalAssetsCap(uint256 cap) { + liquidityProviderController.setTotalAssetsCap(cap); + _; + } + + modifier depositInLidoFixedPriceMultiLpARM(address user, uint256 amount) { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + // Check current balance + uint256 balance = weth.balanceOf(user); + + // Deal amount as "extra" to user + deal(address(weth), user, amount + balance); + vm.startPrank(user); + weth.approve(address(lidoFixedPriceMulltiLpARM), type(uint256).max); + lidoFixedPriceMulltiLpARM.deposit(amount); + vm.stopPrank(); + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } + _; + } } From 5a9f1f60daea3e721802573617ef5c702e692897 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 12:43:35 +1000 Subject: [PATCH 057/196] Added address of ARMBuyback --- src/contracts/utils/Addresses.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index 7b9e7b9..4d45187 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -32,8 +32,7 @@ library Mainnet { address public constant OETH_VAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; address public constant OETH_ARM = 0x6bac785889A4127dB0e0CeFEE88E0a9F1Aaf3cC7; address public constant LIDO_ARM = 0x85B78AcA6Deae198fBF201c82DAF6Ca21942acc6; - // TODO add once deployed from the Origin Dollar repo - address public constant ARM_BUYBACK = 0x0000000000000000000000000000000000000002; + address public constant ARM_BUYBACK = 0xBa0E6d6ea72cDc0D6f9fCdcc04147c671BA83dB5; // Lido address public constant LIDO_WITHDRAWAL = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; From a9de6f012a944a09dde2d5875c4dc5b1746b3cd0 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 16:03:38 +1000 Subject: [PATCH 058/196] Moved initializing lastTotalAssets to _initPerformanceFee --- src/contracts/LidoFixedPriceMultiLpARM.sol | 1 - src/contracts/PerformanceFee.sol | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index d258f3a..be7fd25 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -53,7 +53,6 @@ contract LidoFixedPriceMultiLpARM is ) external initializer { _initOwnableOperable(_operator); _initMultiLP(_name, _symbol); - lastTotalAssets = SafeCast.toUint128(MIN_TOTAL_SUPPLY); _initPerformanceFee(_fee, _feeCollector); _initLPControllerARM(_liquidityProviderController); } diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index b0ffe53..e2a2a54 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -37,6 +37,9 @@ abstract contract PerformanceFee is MultiLP { event FeeCollectorUpdated(address indexed newFeeCollector); function _initPerformanceFee(uint256 _fee, address _feeCollector) internal { + // Initialize the last total assets to the current total assets + // This ensures no performance fee is accrued when the performance fee is calculated when the fee is set + lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); _setFee(_fee); _setFeeCollector(_feeCollector); } From 1ff7b1cdbd3bcb6bb0050980a13fc73e7bfa81ae Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 16:04:50 +1000 Subject: [PATCH 059/196] Moved burn of shares down in requestRedeem --- src/contracts/MultiLP.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index d1a253f..91f8ded 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -121,9 +121,6 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { _preWithdrawHook(); - // burn redeemer's shares - _burn(msg.sender, shares); - // Calculate the amount of assets to transfer to the redeemer assets = previewRedeem(shares); @@ -142,6 +139,9 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { queued: SafeCast.toUint128(queued) }); + // burn redeemer's shares + _burn(msg.sender, shares); + _postWithdrawHook(assets); emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); From 2de950acf8d8cd86953ea7413e03755b4665eaa4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 17:03:20 +1000 Subject: [PATCH 060/196] Fixed applying the performance fee twice on deposits --- src/contracts/PerformanceFee.sol | 4 + .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 73 ++++++++++++------- test/fork/shared/Shared.sol | 2 +- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index e2a2a54..398ddc5 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -79,6 +79,10 @@ abstract contract PerformanceFee is MultiLP { // Save the new accrued fees back to storage feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); + // Save the new total assets back to storage + // This is be updated again in the post deposit and post withdraw hooks to include + // the assets deposited or withdrawn + lastTotalAssets = SafeCast.toUint128(newTotalAssets); emit FeeCalculated(newFeesAccrued, assetIncrease); } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 49fdf8b..484159b 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -11,7 +11,7 @@ import {LiquidityProviderController} from "contracts/LiquidityProviderController contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Test_ { /** - * As Deposit is complex function due to the entaglement of virtual and override functions in inheritance. + * As Deposit is complex function due to the entanglement of virtual and override functions in inheritance. * This is a small recap of the functions that are called in the deposit function. * 1. ML: _preDepositHook() -> PF: _calcFee() -> PF: _rawTotalAssets() -> ML: _totalAssets() * 2. ML: convertToShares() -> ML: _totalAssets() -> ARM: totalAssets() -> PF : totalAssets() -> @@ -119,6 +119,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0, 0); @@ -141,6 +142,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether @@ -163,6 +165,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), amount); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0, 0); @@ -185,6 +188,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares * 2); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -208,6 +212,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), 0); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(alice), amount); assertEqQueueMetadata(0, 0, 0, 0); @@ -231,6 +236,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), shares); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(alice), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -241,48 +247,59 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setTotalAssetsCap(type(uint256).max) // No need to restrict it for this test. setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { - // simulate profit + // simulate asset gain uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 profit = DEFAULT_AMOUNT; - deal(address(weth), address(lidoFixedPriceMulltiLpARM), balanceBefore + profit); + uint256 assetGain = DEFAULT_AMOUNT; + deal(address(weth), address(lidoFixedPriceMulltiLpARM), balanceBefore + assetGain); // Assertions Before assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + profit); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + assetGain); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether before"); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy + // 80% of the asset gain goes to the total assets + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), balanceBefore + assetGain * 80 / 100, "Total assets before"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT, "lp cap before"); assertEqQueueMetadata(0, 0, 0, 0); - uint256 feesAccrued = profit * lidoFixedPriceMulltiLpARM.fee() / lidoFixedPriceMulltiLpARM.FEE_SCALE(); + // 20% of the asset gain goes to the performance fees + uint256 feesAccrued = assetGain * 20 / 100; uint256 rawTotalAsset = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - feesAccrued; // No steth and no externalWithdrawQueue - uint256 amount = DEFAULT_AMOUNT; + uint256 depositedAssets = DEFAULT_AMOUNT; - uint256 expectedShares = amount * MIN_TOTAL_SUPPLY / (rawTotalAsset); + uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / rawTotalAsset; // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amount); - //vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); - //emit IERC20.Transfer(address(0), address(this), expectedShares); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), depositedAssets); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(0), address(this), expectedShares); vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); - // Main call - uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); - assertEq(shares, expectedShares, "Wrong shares calculation"); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether + // deposit assets + uint256 shares = lidoFixedPriceMulltiLpARM.deposit(depositedAssets); + + assertEq(shares, expectedShares, "minted shares"); + // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + profit + amount); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feesAccrued); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + profit + amount); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0, "stETH balance after"); + assertEq( + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), + MIN_TOTAL_SUPPLY + assetGain + depositedAssets, + "WETH balance after" + ); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether after"); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees + // assertEq( + // lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "last total assets after" + // ); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares, "user shares after"); + // assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); } } diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index ca32114..190a68d 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -156,7 +156,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { "Lido ARM", "ARM-ST", operator, - 2000, + 2000, // 20% performance fee feeCollector, address(lpcProxy) ); From 568c97478c573f6b2fd1c184d1217e5a3aa7edb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 23 Sep 2024 10:13:43 +0200 Subject: [PATCH 061/196] fix: increase global queued amount when requestRedeem. --- src/contracts/MultiLP.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 91f8ded..56232f8 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -130,6 +130,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // Store the next withdrawal request withdrawalQueueMetadata.nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); + // Store the new queued amount + withdrawalQueueMetadata.queued = SafeCast.toUint128(queued); // Store requests withdrawalRequests[requestId] = WithdrawalRequest({ withdrawer: msg.sender, From aa61f9c4be3d4e1d71d5fa2988eda104511316f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 23 Sep 2024 10:14:17 +0200 Subject: [PATCH 062/196] [WIP] test: add tests for `requestRedeem()`. --- .../RequestRedeem.t.sol | 79 ++++++++++++++++--- test/fork/utils/Helpers.sol | 18 +++++ test/fork/utils/Modifiers.sol | 18 +++++ 3 files changed, 103 insertions(+), 12 deletions(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 3c90dee..7b4cd64 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.23; // Test imports import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; +import {MultiLP} from "contracts/MultiLP.sol"; +import {IERC20} from "contracts/Interfaces.sol"; + contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP @@ -17,19 +20,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_RequestRedeem_SimpleCase() - public - asLidoFixedPriceMultiLpARMOwner - setLiquidityProviderCap(address(this), 20 ether) - { - deal(address(weth), address(this), 10 ether); - - lidoFixedPriceMulltiLpARM.deposit(10 ether); - lidoFixedPriceMulltiLpARM.requestRedeem(8 ether); - } - /// @notice Test the `requestRedeem` function when there are no profits and the first deposit is made. - function test_RequestWithdraw_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue() + function test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue() public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) @@ -46,11 +38,74 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(0, 0, 0, 0); + uint256 delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit MultiLP.RedeemRequested(address(this), 0, DEFAULT_AMOUNT, DEFAULT_AMOUNT, block.timestamp + delay); // Main Call (uint256 requestId, uint256 assets) = lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT); + // Assertions After assertEq(requestId, 0); // First request assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); // One request in the queue + assertEqUserRequest(0, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT, DEFAULT_AMOUNT); // Requested the full amount assertEq(assets, DEFAULT_AMOUNT, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + } + + /// @notice Test the `requestRedeem` function when there are no profits and the first deposit is made. + function test_RequestRedeem_AfterFirstDeposit_NoPerfs_NonEmptyWithdrawQueue_SecondRedeem() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT / 4) + { + // Assertions Before + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only + assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 0, 1); + + uint256 delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT / 2); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit MultiLP.RedeemRequested( + address(this), 1, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT * 3 / 4, block.timestamp + delay + ); + // Main Call + (uint256 requestId, uint256 assets) = lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT / 2); + + // Assertions After + assertEq(requestId, 1); // Second request + assertEqQueueMetadata(DEFAULT_AMOUNT * 3 / 4, 0, 0, 2); // Two requests in the queue + assertEqUserRequest( + 1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT * 3 / 4 + ); + assertEq(assets, DEFAULT_AMOUNT / 2, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only } } diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index f05c7b5..27f5d75 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -46,4 +46,22 @@ abstract contract Helpers is Base_Test_ { assertEq(claimed, expectedClaimed); assertEq(nextWithdrawalIndex, expectedNextIndex); } + + /// @notice Asserts the equality bewteen value of `withdrawalRequests()` and the expected values. + function assertEqUserRequest( + uint256 requestId, + address withdrawer, + bool claimed, + uint256 claimTimestamp, + uint256 assets, + uint256 queued + ) public view { + (address _withdrawer, bool _claimed, uint40 _claimTimestamp, uint128 _assets, uint128 _queued) = + lidoFixedPriceMulltiLpARM.withdrawalRequests(requestId); + assertEq(_withdrawer, withdrawer, "Wrong withdrawer"); + assertEq(_claimed, claimed, "Wrong claimed"); + assertEq(_claimTimestamp, claimTimestamp, "Wrong claimTimestamp"); + assertEq(_assets, assets, "Wrong assets"); + assertEq(_queued, queued, "Wrong queued"); + } } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index 0485ab6..ae89194 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -101,4 +101,22 @@ abstract contract Modifiers is Helpers { } _; } + + /// @notice Request redeem from LidoFixedPriceMultiLpARM contract. + modifier requestRedeemFromLidoFixedPriceMultiLpARM(address user, uint256 amount) { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + vm.startPrank(user); + lidoFixedPriceMulltiLpARM.requestRedeem(amount); + vm.stopPrank(); + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } + _; + } } From 7ce329e604543144b4a8c7740dd79975299ab4ab Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 17:03:20 +1000 Subject: [PATCH 063/196] Fixed applying the performance fee twice on deposits --- src/contracts/PerformanceFee.sol | 4 + .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 73 ++++++++++++------- test/fork/shared/Shared.sol | 2 +- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index e2a2a54..398ddc5 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -79,6 +79,10 @@ abstract contract PerformanceFee is MultiLP { // Save the new accrued fees back to storage feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); + // Save the new total assets back to storage + // This is be updated again in the post deposit and post withdraw hooks to include + // the assets deposited or withdrawn + lastTotalAssets = SafeCast.toUint128(newTotalAssets); emit FeeCalculated(newFeesAccrued, assetIncrease); } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 49fdf8b..484159b 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -11,7 +11,7 @@ import {LiquidityProviderController} from "contracts/LiquidityProviderController contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Test_ { /** - * As Deposit is complex function due to the entaglement of virtual and override functions in inheritance. + * As Deposit is complex function due to the entanglement of virtual and override functions in inheritance. * This is a small recap of the functions that are called in the deposit function. * 1. ML: _preDepositHook() -> PF: _calcFee() -> PF: _rawTotalAssets() -> ML: _totalAssets() * 2. ML: convertToShares() -> ML: _totalAssets() -> ARM: totalAssets() -> PF : totalAssets() -> @@ -119,6 +119,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0, 0); @@ -141,6 +142,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether @@ -163,6 +165,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), amount); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0, 0); @@ -185,6 +188,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares * 2); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -208,6 +212,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), 0); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(alice), amount); assertEqQueueMetadata(0, 0, 0, 0); @@ -231,6 +236,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), shares); assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(alice), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -241,48 +247,59 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setTotalAssetsCap(type(uint256).max) // No need to restrict it for this test. setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { - // simulate profit + // simulate asset gain uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 profit = DEFAULT_AMOUNT; - deal(address(weth), address(lidoFixedPriceMulltiLpARM), balanceBefore + profit); + uint256 assetGain = DEFAULT_AMOUNT; + deal(address(weth), address(lidoFixedPriceMulltiLpARM), balanceBefore + assetGain); // Assertions Before assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + profit); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + assetGain); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether before"); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy + // 80% of the asset gain goes to the total assets + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), balanceBefore + assetGain * 80 / 100, "Total assets before"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT, "lp cap before"); assertEqQueueMetadata(0, 0, 0, 0); - uint256 feesAccrued = profit * lidoFixedPriceMulltiLpARM.fee() / lidoFixedPriceMulltiLpARM.FEE_SCALE(); + // 20% of the asset gain goes to the performance fees + uint256 feesAccrued = assetGain * 20 / 100; uint256 rawTotalAsset = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - feesAccrued; // No steth and no externalWithdrawQueue - uint256 amount = DEFAULT_AMOUNT; + uint256 depositedAssets = DEFAULT_AMOUNT; - uint256 expectedShares = amount * MIN_TOTAL_SUPPLY / (rawTotalAsset); + uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / rawTotalAsset; // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amount); - //vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); - //emit IERC20.Transfer(address(0), address(this), expectedShares); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), depositedAssets); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(0), address(this), expectedShares); vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); - // Main call - uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); - assertEq(shares, expectedShares, "Wrong shares calculation"); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether + // deposit assets + uint256 shares = lidoFixedPriceMulltiLpARM.deposit(depositedAssets); + + assertEq(shares, expectedShares, "minted shares"); + // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + profit + amount); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feesAccrued); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + profit + amount); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0, "stETH balance after"); + assertEq( + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), + MIN_TOTAL_SUPPLY + assetGain + depositedAssets, + "WETH balance after" + ); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether after"); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees + // assertEq( + // lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "last total assets after" + // ); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares, "user shares after"); + // assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); } } diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index ca32114..190a68d 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -156,7 +156,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { "Lido ARM", "ARM-ST", operator, - 2000, + 2000, // 20% performance fee feeCollector, address(lpcProxy) ); From 541c50ec4a562f0fdf606badff82a598292519a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 23 Sep 2024 11:37:42 +0200 Subject: [PATCH 064/196] test: add tests for `requestRedeem()` when ARM profit/loss. --- .../RequestRedeem.t.sol | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 7b4cd64..6e0c127 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -4,8 +4,10 @@ pragma solidity 0.8.23; // Test imports import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; -import {MultiLP} from "contracts/MultiLP.sol"; +// Contracts import {IERC20} from "contracts/Interfaces.sol"; +import {MultiLP} from "contracts/MultiLP.sol"; +import {PerformanceFee} from "contracts/PerformanceFee.sol"; contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// @@ -108,4 +110,92 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only } + + /// @notice Test the `requestRedeem` function when there are profits and the first deposit is already made. + function test_RequestRedeem_AfterFirstDeposit_WithPerfs_EmptyWithdrawQueue() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Assertions Before + // Not needed as the same as in `test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue` + + // Simulate assets gain in ARM + uint256 assetsGain = DEFAULT_AMOUNT; + deal( + address(weth), + address(lidoFixedPriceMulltiLpARM), + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) + assetsGain + ); + + // Calculate expected values + uint256 feeAccrued = assetsGain * 20 / 100; // 20% fee + uint256 totalAsset = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - feeAccrued; + uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit PerformanceFee.FeeCalculated(feeAccrued, assetsGain); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + // Main call + lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT); + + uint256 delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + // Assertions After + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); // +perfs + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feeAccrued); + assertApproxEqAbs(lidoFixedPriceMulltiLpARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEqQueueMetadata(expectedAssets, 0, 0, 1); + assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); + } + + /// @notice Test the `requestRedeem` function when ARM lost a bit of money before the request. + function test_RequestRedeem_AfterFirstDeposit_WhenLosingFunds() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Assertions Before + // Not needed as the same as in `test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue` + + // Simulate assets loss in ARM + uint256 assetsLoss = DEFAULT_AMOUNT / 10; // 0.1 ether of loss + deal( + address(weth), + address(lidoFixedPriceMulltiLpARM), + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - assetsLoss + ); + + // Calculate expected values + uint256 feeAccrued = 0; // No profits + uint256 totalAsset = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + // Main call + lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT); + + uint256 delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + // Assertions After + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feeAccrued); + assertApproxEqAbs(lidoFixedPriceMulltiLpARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEqQueueMetadata(expectedAssets, 0, 0, 1); + assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); + } } From ca3af6453f8a8a8cd45bc0ebf2e6c8206e7b695b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 19:46:46 +1000 Subject: [PATCH 065/196] Updated deposit assertions --- test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 484159b..29aa0a5 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -294,11 +294,13 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes ); assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether after"); assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees - // assertEq( - // lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "last total assets after" - // ); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares, "user shares after"); - // assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); + assertEq( + lidoFixedPriceMulltiLpARM.lastTotalAssets(), + MIN_TOTAL_SUPPLY + (assetGain * 80 / 100) + depositedAssets, + "last total assets after" + ); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), expectedShares, "user shares after"); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); } From bc0403e9116d05d4cabd07ccbb94f279fd86e5a0 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 19:47:28 +1000 Subject: [PATCH 066/196] Fix requestRedeem which now stored the updated queued amount --- src/contracts/MultiLP.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 91f8ded..f6833b8 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -130,6 +130,8 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // Store the next withdrawal request withdrawalQueueMetadata.nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); + // Store the updated queued amount which reserves WETH in the withdrawal queue + withdrawalQueueMetadata.queued = SafeCast.toUint128(queued); // Store requests withdrawalRequests[requestId] = WithdrawalRequest({ withdrawer: msg.sender, From 796686084a72677bc8b5466811f97bc72bb63848 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 20:29:22 +1000 Subject: [PATCH 067/196] Updated deploy script --- build/deployments-1.json | 7 +++++-- script/deploy/AbstractDeployScript.sol | 2 +- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/build/deployments-1.json b/build/deployments-1.json index 50a7d9c..09445e3 100644 --- a/build/deployments-1.json +++ b/build/deployments-1.json @@ -1,7 +1,10 @@ { - "executions": { "001_CoreMainnet": 1723685111 }, + "executions": { + "001_CoreMainnet": 1723685111, + "002_UpgradeMainnet": 1726812322 + }, "contracts": { "OETH_ARM": "0x6bac785889A4127dB0e0CeFEE88E0a9F1Aaf3cC7", - "OETH_ARM_IMPL": "0xd8fF298eAed581f74ab845Af62C48aCF85B2f05e" + "OETH_ARM_IMPL": "0x187FfF686a5f42ACaaF56469FcCF8e6Feca18248" } } diff --git a/script/deploy/AbstractDeployScript.sol b/script/deploy/AbstractDeployScript.sol index cf2c5af..4fa9a6b 100644 --- a/script/deploy/AbstractDeployScript.sol +++ b/script/deploy/AbstractDeployScript.sol @@ -95,6 +95,6 @@ abstract contract AbstractDeployScript is Script { } _buildGovernanceProposal(); - _fork(); + // _fork(); } } diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 9cfc25d..62b3d51 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -70,6 +70,10 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { function _fork() internal override { vm.startPrank(Mainnet.ARM_MULTISIG); + if (lidoARMProxy == Proxy(0x0000000000000000000000000000000000000000)) { + revert("Lido ARM proxy not found"); + } + // remove all liquidity from the old AMM v1 contract uint256 wethLegacyBalance = IERC20(Mainnet.WETH).balanceOf(Mainnet.LIDO_ARM); if (wethLegacyBalance > 0) { From a56e90ce4516607e128c0fbbdf55ea9118a4bdc6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 23 Sep 2024 22:10:10 +1000 Subject: [PATCH 068/196] Added Lido smoke test that uses the deploy script --- test/smoke/LidoARMSmokeTest.t.sol | 209 ++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 test/smoke/LidoARMSmokeTest.t.sol diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol new file mode 100644 index 0000000..8be184c --- /dev/null +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test, console2} from "forge-std/Test.sol"; + +import {AbstractSmokeTest} from "./AbstractSmokeTest.sol"; + +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {Proxy} from "contracts/Proxy.sol"; +import {Mainnet} from "contracts/utils/Addresses.sol"; +import {console} from "forge-std/console.sol"; + +contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { + IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); + + IERC20 weth; + IERC20 steth; + Proxy proxy; + LidoFixedPriceMultiLpARM lidoARM; + address operator; + + function setUp() public { + weth = IERC20(resolver.resolve("WETH")); + steth = IERC20(resolver.resolve("STETH")); + operator = resolver.resolve("OPERATOR"); + + vm.label(address(weth), "WETH"); + vm.label(address(steth), "stETH"); + vm.label(address(operator), "OPERATOR"); + + proxy = Proxy(deployManager.getDeployment("LIDO_ARM")); + lidoARM = LidoFixedPriceMultiLpARM(payable(deployManager.getDeployment("LIDO_ARM"))); + + // Only fuzz from this address. Big speedup on fork. + targetSender(address(this)); + } + + function test_initialConfig() external { + assertEq(lidoARM.name(), "Lido ARM", "Name"); + assertEq(lidoARM.symbol(), "ARM-ST", "Symbol"); + assertEq(lidoARM.owner(), Mainnet.GOV_MULTISIG, "Owner"); + assertEq(lidoARM.operator(), Mainnet.ARM_RELAYER, "Operator"); + assertEq(lidoARM.feeCollector(), Mainnet.ARM_BUYBACK, "Fee collector"); + assertEq((100 * uint256(lidoARM.fee())) / lidoARM.FEE_SCALE(), 15, "Performance fee as a percentage"); + assertEq(lidoARM.feesAccrued(), 0, "Fees accrued"); + // Some dust stETH is left in AMM v1 when stETH is transferred to the Treasury. + assertEq(lidoARM.totalAssets(), 1e12 + 1, "Total assets"); + assertEq(lidoARM.lastTotalAssets(), 1e12 + 1, "Last total assets"); + assertEq(lidoARM.totalSupply(), 1e12, "Total supply"); + assertEq(weth.balanceOf(address(lidoARM)), 1e12, "WETH balance"); + } + + function test_swapExactTokensForTokens() external { + _swapExactTokensForTokens(steth, weth, 10 ether, 10 ether); + } + + function test_swapTokensForExactTokens() external { + _swapTokensForExactTokens(steth, weth, 10 ether, 10 ether); + } + + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) + internal + { + _dealWETH(address(lidoARM), 100 ether); + _dealStETH(address(lidoARM), 100 ether); + if (inToken == weth) { + _dealWETH(address(this), amountIn + 1000); + } else { + _dealStETH(address(this), amountIn + 1000); + } + // Approve the ARM to transfer the input token of the swap. + inToken.approve(address(lidoARM), amountIn); + + uint256 startIn = inToken.balanceOf(address(this)); + uint256 startOut = outToken.balanceOf(address(this)); + + lidoARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); + + assertEq(inToken.balanceOf(address(this)), startIn - amountIn, "In actual"); + assertEq(outToken.balanceOf(address(this)), startOut + expectedOut, "Out actual"); + } + + function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) + internal + { + _dealWETH(address(lidoARM), 100 ether); + _dealStETH(address(lidoARM), 100 ether); + if (inToken == weth) { + _dealWETH(address(this), amountIn + 1000); + } else { + _dealStETH(address(this), amountIn + 1000); + } + // Approve the ARM to transfer the input token of the swap. + inToken.approve(address(lidoARM), amountIn + 10000); + console.log("Approved Lido ARM to spend %d", inToken.allowance(address(this), address(lidoARM))); + console.log("In token balance: %d", inToken.balanceOf(address(this))); + + uint256 startIn = inToken.balanceOf(address(this)); + + lidoARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); + + assertEq(inToken.balanceOf(address(this)), startIn - amountIn - 1, "In actual"); + assertEq(outToken.balanceOf(address(this)), expectedOut, "Out actual"); + } + + function test_proxy_unauthorizedAccess() external { + address RANDOM_ADDRESS = 0xfEEDBeef00000000000000000000000000000000; + vm.startPrank(RANDOM_ADDRESS); + + // Proxy's restricted methods. + vm.expectRevert("OSwap: Only owner can call this function."); + proxy.setOwner(RANDOM_ADDRESS); + + vm.expectRevert("OSwap: Only owner can call this function."); + proxy.initialize(address(this), address(this), ""); + + vm.expectRevert("OSwap: Only owner can call this function."); + proxy.upgradeTo(address(this)); + + vm.expectRevert("OSwap: Only owner can call this function."); + proxy.upgradeToAndCall(address(this), ""); + + // Implementation's restricted methods. + vm.expectRevert("OSwap: Only owner can call this function."); + lidoARM.setOwner(RANDOM_ADDRESS); + } + + function test_wrongInTokenExactIn() external { + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapExactTokensForTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); + } + + function test_wrongOutTokenExactIn() external { + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 10 ether, address(this)); + } + + function test_wrongInTokenExactOut() external { + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(BAD_TOKEN, steth, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 10 ether, address(this)); + } + + function test_wrongOutTokenExactOut() external { + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); + vm.expectRevert("ARM: Invalid token"); + lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 10 ether, address(this)); + } + + // function test_collectTokens() external { + // vm.startPrank(Mainnet.TIMELOCK); + + // lidoARM.transferToken(address(weth), address(this), weth.balanceOf(address(lidoARM))); + // assertGt(weth.balanceOf(address(this)), 50 ether); + // assertEq(weth.balanceOf(address(lidoARM)), 0); + + // lidoARM.transferToken(address(steth), address(this), steth.balanceOf(address(lidoARM))); + // assertGt(steth.balanceOf(address(this)), 50 ether); + // assertLt(steth.balanceOf(address(lidoARM)), 3); + + // vm.stopPrank(); + // } + + function _dealStETH(address to, uint256 amount) internal { + vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); + steth.transfer(to, amount + 2); + } + + function _dealWETH(address to, uint256 amount) internal { + deal(address(weth), to, amount); + } + + /* Operator Tests */ + + function test_setOperator() external { + vm.prank(Mainnet.GOV_MULTISIG); + lidoARM.setOperator(address(this)); + assertEq(lidoARM.operator(), address(this)); + } + + function test_nonOwnerCannotSetOperator() external { + vm.expectRevert("ARM: Only owner can call this function."); + vm.prank(operator); + lidoARM.setOperator(operator); + } +} From 68109528a64d45cdf403a9affc8deefe5b94a70e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 24 Sep 2024 04:41:13 +1000 Subject: [PATCH 069/196] Removed experimentation code --- src/contracts/AccessControlLP.sol | 44 --- src/contracts/AccessControlShares.sol | 36 -- src/contracts/LidoMultiPriceMultiLpARM.sol | 107 ------ src/contracts/LidoOwnerLpARM.sol | 51 --- src/contracts/MultiPriceARM.sol | 347 ------------------ test/Base.sol | 2 - .../Constructor.t.sol | 2 +- .../Setters.t.sol | 51 ++- .../LidoMultiPriceMultiLpARM.t.sol | 211 ----------- .../SwapExactTokensForTokens.t copy.sol | 89 ----- .../SwapTokensForExactTokens.t.sol | 82 ----- test/fork/LidoOwnerLpARM/TransferToken.t.sol | 40 -- test/fork/shared/Shared.sol | 19 - test/fork/utils/Modifiers.sol | 11 +- test/smoke/LidoARMSmokeTest.t.sol | 6 +- 15 files changed, 29 insertions(+), 1069 deletions(-) delete mode 100644 src/contracts/AccessControlLP.sol delete mode 100644 src/contracts/AccessControlShares.sol delete mode 100644 src/contracts/LidoMultiPriceMultiLpARM.sol delete mode 100644 src/contracts/LidoOwnerLpARM.sol delete mode 100644 src/contracts/MultiPriceARM.sol rename test/fork/{LidoOwnerLpARM => LidoFixedPriceMultiLpARM}/Setters.t.sol (59%) delete mode 100644 test/fork/LidoMultiPriceMultiLpARM/LidoMultiPriceMultiLpARM.t.sol delete mode 100644 test/fork/LidoOwnerLpARM/SwapExactTokensForTokens.t copy.sol delete mode 100644 test/fork/LidoOwnerLpARM/SwapTokensForExactTokens.t.sol delete mode 100644 test/fork/LidoOwnerLpARM/TransferToken.t.sol diff --git a/src/contracts/AccessControlLP.sol b/src/contracts/AccessControlLP.sol deleted file mode 100644 index ff54335..0000000 --- a/src/contracts/AccessControlLP.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {MultiLP} from "./MultiLP.sol"; - -abstract contract AccessControlLP is MultiLP { - uint256 public totalAssetsCap; - mapping(address lp => uint256 cap) public liquidityProviderCaps; - - uint256[48] private _gap; - - event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); - event TotalAssetsCap(uint256 cap); - - function _postDepositHook(uint256 assets) internal virtual override { - uint256 oldCap = liquidityProviderCaps[msg.sender]; - require(oldCap >= assets, "ARM: LP cap exceeded"); - // total assets has already been updated with the new assets - require(totalAssetsCap >= totalAssets(), "ARM: Total assets cap exceeded"); - - uint256 newCap = oldCap - assets; - - // Save the new LP cap to storage - liquidityProviderCaps[msg.sender] = newCap; - - emit LiquidityProviderCap(msg.sender, newCap); - } - - function setLiquidityProviderCaps(address[] calldata _liquidityProviders, uint256 cap) external onlyOwner { - for (uint256 i = 0; i < _liquidityProviders.length; i++) { - liquidityProviderCaps[_liquidityProviders[i]] = cap; - - emit LiquidityProviderCap(_liquidityProviders[i], cap); - } - } - - /// @notice Set the total assets cap for a liquidity provider. - /// Setting to zero will prevent any further deposits. The lp can still withdraw assets. - function setTotalAssetsCap(uint256 _totalAssetsCap) external onlyOwner { - totalAssetsCap = _totalAssetsCap; - - emit TotalAssetsCap(_totalAssetsCap); - } -} diff --git a/src/contracts/AccessControlShares.sol b/src/contracts/AccessControlShares.sol deleted file mode 100644 index 14977e2..0000000 --- a/src/contracts/AccessControlShares.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {MultiLP} from "./MultiLP.sol"; - -abstract contract AccessControlShares is MultiLP { - uint256 public totalSupplyCap; - mapping(address lp => uint256 shares) public liquidityProviderCaps; - - uint256[48] private _gap; - - event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); - event TotalSupplyCap(uint256 shares); - - function _postDepositHook(uint256) internal virtual override { - require(liquidityProviderCaps[msg.sender] >= balanceOf(msg.sender), "ARM: LP cap exceeded"); - // total supply has already been updated - require(totalSupplyCap >= totalSupply(), "ARM: Supply cap exceeded"); - } - - function _postWithdrawHook(uint256) internal virtual override { - // Do nothing - } - - function setLiquidityProviderCap(address liquidityProvider, uint256 cap) external onlyOwner { - liquidityProviderCaps[liquidityProvider] = cap; - - emit LiquidityProviderCap(liquidityProvider, cap); - } - - function setTotalSupplyCap(uint256 _totalSupplyCap) external onlyOwner { - totalSupplyCap = _totalSupplyCap; - - emit TotalSupplyCap(_totalSupplyCap); - } -} diff --git a/src/contracts/LidoMultiPriceMultiLpARM.sol b/src/contracts/LidoMultiPriceMultiLpARM.sol deleted file mode 100644 index 03fce4c..0000000 --- a/src/contracts/LidoMultiPriceMultiLpARM.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {AbstractARM} from "./AbstractARM.sol"; -import {AccessControlLP} from "./AccessControlLP.sol"; -import {MultiPriceARM} from "./MultiPriceARM.sol"; -import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; -import {MultiLP} from "./MultiLP.sol"; -import {PerformanceFee} from "./PerformanceFee.sol"; - -/** - * @title Lido (stETH) Application Redemption Manager (ARM) - * @dev This implementation supports multiple Liquidity Providers (LPs) and multiple liquidity tranches - * with different prices. - * @author Origin Protocol Inc - */ -contract LidoMultiPriceMultiLpARM is - Initializable, - MultiLP, - PerformanceFee, - AccessControlLP, - MultiPriceARM, - LidoLiquidityManager -{ - /// @param _stEth The address of Lido's stETH token - /// @param _weth The address of the WETH token - /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract - constructor(address _stEth, address _weth, address _lidoWithdrawalQueue) - AbstractARM(_stEth, _weth) - MultiLP(_weth) - MultiPriceARM(_stEth, _weth) - LidoLiquidityManager(_stEth, _weth, _lidoWithdrawalQueue) - {} - - /// @notice Initialize the contract. - /// @param _name The name of the liquidity provider (LP) token. - /// @param _symbol The symbol of the liquidity provider (LP) token. - /// @param _operator The address of the account that can request and claim Lido withdrawals. - /// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent). - /// 10,000 = 100% performance fee - /// 500 = 5% performance fee - /// @param _feeCollector The account that can collect the performance fee - function initialize( - string calldata _name, - string calldata _symbol, - address _operator, - uint256 _fee, - address _feeCollector - ) external initializer { - _initOwnableOperable(_operator); - _initMultiLP(_name, _symbol); - lastTotalAssets = SafeCast.toUint128(MIN_TOTAL_SUPPLY); - _initPerformanceFee(_fee, _feeCollector); - } - - /** - * @notice Calculate transfer amount for outToken. - * 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. - */ - function _calcTransferAmount(address outToken, uint256 amount) - internal - view - override - returns (uint256 transferAmount) - { - // Add 2 wei if transferring stETH - transferAmount = outToken == address(token0) ? amount + 2 : amount; - } - - function _externalWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { - return LidoLiquidityManager._externalWithdrawQueue(); - } - - function _postDepositHook(uint256 assets) internal override(MultiLP, AccessControlLP, PerformanceFee) { - // Add assets to the liquidity tranches - MultiPriceARM._addLiquidity(assets); - - // Check the LP can deposit the assets - AccessControlLP._postDepositHook(assets); - - // Store the new total assets after the deposit and performance fee accrued - PerformanceFee._postDepositHook(assets); - } - - function _postWithdrawHook(uint256 assets) internal override(MultiLP, PerformanceFee) { - // Remove assets from the liquidity tranches - MultiPriceARM._removeLiquidity(assets); - - // Store the new total assets after the withdrawal and performance fee accrued - PerformanceFee._postWithdrawHook(assets); - } - - function _postClaimHook(uint256 assets) internal override { - // Add assets to the liquidity tranches - MultiPriceARM._addLiquidity(assets); - } - - function totalAssets() public view override(MultiLP, PerformanceFee) returns (uint256) { - // Return the total assets less the collected performance fee - return PerformanceFee.totalAssets(); - } -} diff --git a/src/contracts/LidoOwnerLpARM.sol b/src/contracts/LidoOwnerLpARM.sol deleted file mode 100644 index 8cc1c4a..0000000 --- a/src/contracts/LidoOwnerLpARM.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - -import {AbstractARM} from "./AbstractARM.sol"; -import {FixedPriceARM} from "./FixedPriceARM.sol"; -import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; -import {OwnerLP} from "./OwnerLP.sol"; - -/** - * @title Lido (stETH) Application Redemption Manager (ARM) - * @dev This implementation supports a single LP with single buy and sell prices. - * @author Origin Protocol Inc - */ -contract LidoOwnerLpARM is Initializable, OwnerLP, FixedPriceARM, LidoLiquidityManager { - /// @param _stEth The address of the stETH token - /// @param _weth The address of the WETH token - /// @param _lidoWithdrawalQueue The address of the Lido Withdrawal Queue contract - constructor(address _stEth, address _weth, address _lidoWithdrawalQueue) - AbstractARM(_stEth, _weth) - FixedPriceARM() - LidoLiquidityManager(_stEth, _weth, _lidoWithdrawalQueue) - {} - - /// @notice Initialize the contract. - /// @param _operator The address of the account that can request and claim OETH withdrawals. - function initialize(address _operator) external initializer { - _setOperator(_operator); - } - - /** - * @notice Calculate transfer amount for outToken. - * 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. - */ - function _calcTransferAmount(address outToken, uint256 amount) - internal - view - override - returns (uint256 transferAmount) - { - // Add 2 wei if transferring stETH - transferAmount = outToken == address(token0) ? amount + 2 : amount; - } - - function _postClaimHook(uint256 assets) internal override { - // do nothing - } -} diff --git a/src/contracts/MultiPriceARM.sol b/src/contracts/MultiPriceARM.sol deleted file mode 100644 index 1a47cbd..0000000 --- a/src/contracts/MultiPriceARM.sol +++ /dev/null @@ -1,347 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {AbstractARM} from "./AbstractARM.sol"; -import {IERC20} from "./Interfaces.sol"; - -/** - * @title Abstract support to an ARM with multiple liquidity tranches with different prices. - * @author Origin Protocol Inc - */ -abstract contract MultiPriceARM is AbstractARM { - /// @notice The token being bought by the ARM at a discount. eg stETH - address private immutable discountToken; - /// @notice The token being sold by the ARM. eg WETH - address private immutable liquidityToken; - - uint256 public constant PRICE_PRECISION = 1e18; - uint256 public constant DISCOUNT_MULTIPLIER = 1e13; - /// @notice The amount of tranche allocation and remaining amounts are multiplied by to get the actual amount. - /// min amount is 0.1 Ether, Max amount is 6,553.6 Ether - uint256 public constant TRANCHE_AMOUNT_MULTIPLIER = 1e17; - - uint256 private constant DISCOUNT_INDEX = 0; - uint256 private constant LIQUIDITY_ALLOCATED_INDEX = 1; - uint256 private constant LIQUIDITY_REMAINING_INDEX = 2; - - /// @notice The five liquidity tranches of the ARM - /// Each tranche is represented by three uint16 values: - /// 0 - the discount from the liquidity token scaled to 1e5 - // eg a 8 basis point discount (0.08%) would be 800 with a price of 0.9992 - // eg a 1.25 basis point discount (0.0125%) would be 125 with a price of 0.999875 - /// 1 - the amount of liquidity allocated to this tranche. 1 = 0.1 Ether - /// 2 - the amount of liquidity remaining in this tranche. 1 = 0.1 Ether - /// The three tranche values are repeated five times in the array as follows: - /// [discount, allocated, remaining, discount, allocated, remaining, ...] - /// @dev Five tranches are used as they fit in a single storage slot - uint16[15] private tranches; - - uint256[49] private _gap; - - event TranchePricesUpdated(uint16[5] discounts); - event TrancheAllocationsUpdated(uint256[5] allocations); - - constructor(address _discountToken, address _liquidityToken) { - discountToken = _discountToken; - liquidityToken = _liquidityToken; - } - - function _addLiquidity(uint256 liquidityAmount) internal virtual { - uint256 remainingLiquidity = liquidityAmount; - uint256 unallocatedLiquidity; - uint256 liquidityToAdd; - - // Read the tranches from storage into memory - uint16[15] memory tranchesMem = tranches; - - // Fill the tranches with the new liquidity from first to last - for (uint256 i = 0; i < tranchesMem.length; i += 3) { - unallocatedLiquidity = - tranchesMem[i + LIQUIDITY_ALLOCATED_INDEX] - tranchesMem[i + LIQUIDITY_REMAINING_INDEX]; - - liquidityToAdd = remainingLiquidity <= unallocatedLiquidity ? remainingLiquidity : unallocatedLiquidity; - - // Update the liquidity remaining in memory - tranchesMem[i + LIQUIDITY_REMAINING_INDEX] += SafeCast.toUint16(liquidityToAdd / TRANCHE_AMOUNT_MULTIPLIER); - - remainingLiquidity -= liquidityToAdd; - - if (remainingLiquidity == 0) { - break; - } - } - - // Write back the tranche data to storage once - tranches = tranchesMem; - } - - function _removeLiquidity(uint256 liquidityAmount) internal virtual { - uint256 remainingLiquidity = liquidityAmount; - uint256 liquidityToRemove; - - uint16[15] memory tranchesMem = tranches; - - // Take liquidity from the tranches from last to first - for (uint256 i = tranchesMem.length; i > 2;) { - i -= 3; - liquidityToRemove = remainingLiquidity <= tranchesMem[i + LIQUIDITY_REMAINING_INDEX] - ? remainingLiquidity - : tranchesMem[i + LIQUIDITY_REMAINING_INDEX]; - - tranchesMem[i + LIQUIDITY_REMAINING_INDEX] -= - SafeCast.toUint16(liquidityToRemove / TRANCHE_AMOUNT_MULTIPLIER); - - remainingLiquidity -= liquidityToRemove; - - if (remainingLiquidity == 0) { - break; - } - } - - // Write back the tranche data to storage once - tranches = tranchesMem; - } - - function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to) - internal - override - returns (uint256 amountOut) - { - uint256 price; - if (address(inToken) == discountToken) { - require(address(outToken) == liquidityToken, "ARM: Invalid token"); - price = _calcPriceFromDiscount(amountIn); - } else if (address(inToken) == liquidityToken) { - require(address(outToken) == discountToken, "ARM: Invalid token"); - price = _calcPriceFromLiquidity(amountIn); - } else { - revert("ARM: Invalid token"); - } - - amountOut = amountIn * price / 1e36; - - // Transfer the input tokens from the caller to this ARM contract - inToken.transferFrom(msg.sender, address(this), amountIn); - - // Transfer the output tokens to the recipient - uint256 transferAmountOut = _calcTransferAmount(address(outToken), amountOut); - outToken.transfer(to, transferAmountOut); - } - - function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to) - internal - override - returns (uint256 amountIn) - { - uint256 price; - if (address(inToken) == discountToken) { - require(address(outToken) == liquidityToken, "ARM: Invalid token"); - price = _calcPriceFromLiquidity(amountOut); - } else if (address(inToken) == liquidityToken) { - require(address(outToken) == discountToken, "ARM: Invalid token"); - price = _calcPriceFromDiscount(amountOut); - } else { - revert("ARM: Invalid token"); - } - amountIn = ((amountOut * 1e36) / price) + 1; // +1 to always round in our favor - - // Transfer the input tokens from the caller to this ARM contract - inToken.transferFrom(msg.sender, address(this), amountIn); - - // Transfer the output tokens to the recipient - uint256 transferAmountOut = _calcTransferAmount(address(outToken), amountOut); - outToken.transfer(to, transferAmountOut); - } - - /// @dev Calculate the volume weighted price from the available liquidity amount. eg WETH amount - function _calcPriceFromLiquidity(uint256 liquiditySwapAmount) internal returns (uint256 price) { - uint16[15] memory tranchesMem = tranches; - - uint256 trancheVolume; - uint256 totalPriceVolume; - uint256 remainingSwapVolume = liquiditySwapAmount; - - // For each tranche - for (uint256 i = 0; i < tranchesMem.length; i + 3) { - uint256 actualLiquidityRemainingInTranche = - tranchesMem[i + LIQUIDITY_REMAINING_INDEX] * TRANCHE_AMOUNT_MULTIPLIER; - trancheVolume = remainingSwapVolume <= actualLiquidityRemainingInTranche - ? remainingSwapVolume - : actualLiquidityRemainingInTranche; - - // Update the liquidity remaining in memory - tranchesMem[i + LIQUIDITY_REMAINING_INDEX] = - SafeCast.toUint16((actualLiquidityRemainingInTranche - trancheVolume) / TRANCHE_AMOUNT_MULTIPLIER); - - // If there is no liquidity in the tranche then move to the next tranche - if (trancheVolume == 0) { - continue; - } - - uint256 actualPrice = PRICE_PRECISION - (tranchesMem[i + DISCOUNT_INDEX] * DISCOUNT_MULTIPLIER); - totalPriceVolume += actualPrice * trancheVolume; - remainingSwapVolume -= trancheVolume; - - // Break from the loop if we have enough liquidity - if (remainingSwapVolume == 0) { - break; - } - } - - // If there is not enough liquidity in all the tranches then revert - require(remainingSwapVolume == 0, "ARM: Not enough liquidity"); - - // Write back the tranche data to storage once - tranches = tranchesMem; - - // Calculate the volume weighted average price which is returned - return totalPriceVolume / liquiditySwapAmount; - } - - /// @dev Calculate the volume weighted price from the available discount amount. eg stETH amount - function _calcPriceFromDiscount(uint256 discountSwapAmount) internal returns (uint256 price) { - uint16[15] memory tranchesMem = tranches; - - uint256 discountTrancheVolume; - uint256 totalDiscountPriceVolume; - uint256 remainingDiscountSwapVolume = discountSwapAmount; - - // For each tranche - for (uint256 i = 0; i < tranchesMem.length; i + 3) { - uint256 tranchePrice = (PRICE_PRECISION - (tranchesMem[i + DISCOUNT_INDEX] * DISCOUNT_MULTIPLIER)); - // Convert the tranche liquidity to the discount token - uint256 actualDiscountRemainingInTranche = - tranchesMem[i + LIQUIDITY_REMAINING_INDEX] * TRANCHE_AMOUNT_MULTIPLIER * PRICE_PRECISION / tranchePrice; - discountTrancheVolume = remainingDiscountSwapVolume <= actualDiscountRemainingInTranche - ? remainingDiscountSwapVolume - : actualDiscountRemainingInTranche; - - // Update the liquidity remaining in memory - uint256 liquidityTrancheVolume = discountTrancheVolume * tranchePrice / PRICE_PRECISION; - tranchesMem[i + LIQUIDITY_REMAINING_INDEX] = tranchesMem[i + LIQUIDITY_REMAINING_INDEX] - - SafeCast.toUint16(liquidityTrancheVolume / TRANCHE_AMOUNT_MULTIPLIER); - - // If there is no liquidity in the tranche then move to the next tranche - if (discountTrancheVolume == 0) { - continue; - } - - totalDiscountPriceVolume += discountTrancheVolume * PRICE_PRECISION * PRICE_PRECISION / tranchePrice; - remainingDiscountSwapVolume -= discountTrancheVolume; - - // Break from the loop if we have enough liquidity - if (remainingDiscountSwapVolume == 0) { - break; - } - } - - // If there is not enough liquidity in all the tranches then revert - require(remainingDiscountSwapVolume == 0, "ARM: Not enough liquidity"); - - // Write back the tranche data to storage once - tranches = tranchesMem; - - // Calculate the volume weighted average price - uint256 discountPrice = totalDiscountPriceVolume / discountSwapAmount; - // Convert back to a liquidity price. - return PRICE_PRECISION * PRICE_PRECISION / discountPrice; - } - - /** - * @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; - } - - /// @notice sets the tranche discounts from the liquidity asset scaled to 1e5. - /// For example: - /// a 8 basis point discount (0.08%) would be 800 with a price of 0.9992 - /// a 1.25 basis point discount (0.0125%) would be 125 with a price of 0.999875 - function setTrancheDiscounts(uint16[5] calldata discounts) external onlyOwner { - uint16[15] memory tranchesMem = tranches; - - for (uint256 i = 0; i < discounts.length; i++) { - require(discounts[i] > 0, "ARM: zero discount"); - tranchesMem[i * 3 + DISCOUNT_INDEX] = discounts[i]; - } - - // Write back the tranche data to storage once - tranches = tranchesMem; - - emit TranchePricesUpdated(discounts); - } - - function setTrancheAllocations(uint256[5] calldata allocations) external onlyOwner { - uint16[15] memory tranchesMem = tranches; - - for (uint256 i = 0; i < allocations.length; ++i) { - uint256 trancheIndex = i * 3; - // TODO add amount safely checks - tranchesMem[trancheIndex + LIQUIDITY_ALLOCATED_INDEX] = - SafeCast.toUint16(allocations[i] / TRANCHE_AMOUNT_MULTIPLIER); - - // If the allocation is smaller than the remaining liquidity then set the remaining liquidity to the allocation - if ( - tranchesMem[trancheIndex + LIQUIDITY_ALLOCATED_INDEX] - < tranchesMem[trancheIndex + LIQUIDITY_REMAINING_INDEX] - ) { - tranchesMem[trancheIndex + LIQUIDITY_REMAINING_INDEX] = - tranchesMem[trancheIndex + LIQUIDITY_ALLOCATED_INDEX]; - } - } - - // Write back the tranche data to storage once - tranches = tranchesMem; - - emit TrancheAllocationsUpdated(allocations); - } - - function resetRemainingLiquidity() external onlyOwner { - uint256 remainingLiquidity = IERC20(liquidityToken).balanceOf(address(this)); - uint256 allocatedLiquidity; - uint256 trancheLiquidity; - - // Read the tranches from storage into memory - uint16[15] memory tranchesMem = tranches; - - // Fill the tranches with the new liquidity from first to last - for (uint256 i = 0; i < tranchesMem.length; i + 3) { - allocatedLiquidity = tranchesMem[i + LIQUIDITY_ALLOCATED_INDEX]; - - trancheLiquidity = remainingLiquidity <= allocatedLiquidity ? remainingLiquidity : allocatedLiquidity; - - // Update the liquidity remaining in memory - tranchesMem[i + LIQUIDITY_REMAINING_INDEX] = SafeCast.toUint16(trancheLiquidity / TRANCHE_AMOUNT_MULTIPLIER); - - remainingLiquidity -= trancheLiquidity; - - if (remainingLiquidity == 0) { - return; - } - } - - // Write back the tranche data to storage once - tranches = tranchesMem; - } - - function getTrancheDiscounts() external view returns (uint16[5] memory discounts) { - for (uint256 i = 0; i < discounts.length; i++) { - discounts[i] = tranches[i * 3 + DISCOUNT_INDEX]; - } - } - - function getTrancheAllocations() external view returns (uint256[5] memory allocations) { - for (uint256 i = 0; i < allocations.length; i++) { - allocations[i] = tranches[i * 3 + LIQUIDITY_ALLOCATED_INDEX] * TRANCHE_AMOUNT_MULTIPLIER; - } - } - - function getTrancheRemaining() external view returns (uint256[5] memory remaining) { - for (uint256 i = 0; i < remaining.length; i++) { - remaining[i] = tranches[i * 3 + LIQUIDITY_REMAINING_INDEX] * TRANCHE_AMOUNT_MULTIPLIER; - } - } -} diff --git a/test/Base.sol b/test/Base.sol index 834a46e..0abcdd6 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -7,7 +7,6 @@ import {Test} from "forge-std/Test.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; -import {LidoOwnerLpARM} from "contracts/LidoOwnerLpARM.sol"; import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; @@ -36,7 +35,6 @@ abstract contract Base_Test_ is Test { Proxy public lidoProxy; Proxy public lidoOwnerProxy; OethARM public oethARM; - LidoOwnerLpARM public lidoOwnerLpARM; LidoFixedPriceMultiLpARM public lidoFixedPriceMulltiLpARM; LiquidityProviderController public liquidityProviderController; diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol index 51d52aa..8dfe37e 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -15,7 +15,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Constructor_Test is Fork_Shared_ ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_Initial_State() public { + function test_Initial_State() public view { assertEq(lidoFixedPriceMulltiLpARM.name(), "Lido ARM"); assertEq(lidoFixedPriceMulltiLpARM.symbol(), "ARM-ST"); assertEq(lidoFixedPriceMulltiLpARM.owner(), address(this)); diff --git a/test/fork/LidoOwnerLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol similarity index 59% rename from test/fork/LidoOwnerLpARM/Setters.t.sol rename to test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index c54364a..53e3e58 100644 --- a/test/fork/LidoOwnerLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.23; // Test imports import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; -contract Fork_Concrete_LidoOwnerLpARM_Setters_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -17,78 +17,73 @@ contract Fork_Concrete_LidoOwnerLpARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// function test_RevertWhen_SetPrices_Because_PriceCross() public { vm.expectRevert("ARM: Price cross"); - lidoOwnerLpARM.setPrices(90 * 1e33, 89 * 1e33); + lidoFixedPriceMulltiLpARM.setPrices(90 * 1e33, 89 * 1e33); vm.expectRevert("ARM: Price cross"); - lidoOwnerLpARM.setPrices(72, 70); + lidoFixedPriceMulltiLpARM.setPrices(72, 70); vm.expectRevert("ARM: Price cross"); - lidoOwnerLpARM.setPrices(1005 * 1e33, 1000 * 1e33); + lidoFixedPriceMulltiLpARM.setPrices(1005 * 1e33, 1000 * 1e33); // Both set to 1.0 vm.expectRevert("ARM: Price cross"); - lidoOwnerLpARM.setPrices(1e36, 1e36); + lidoFixedPriceMulltiLpARM.setPrices(1e36, 1e36); } - function test_RevertWhen_SetPrices_Because_PriceRange() public asLidoOwnerLpARMOperator { + function test_RevertWhen_SetPrices_Because_PriceRange() public asLidoFixedPriceMulltiLpARMOperator { // buy price 11 basis points higher than 1.0 vm.expectRevert("ARM: buy price too high"); - lidoOwnerLpARM.setPrices(10011e32, 10020e32); + lidoFixedPriceMulltiLpARM.setPrices(10011e32, 10020e32); // sell price 11 basis points lower than 1.0 vm.expectRevert("ARM: sell price too low"); - lidoOwnerLpARM.setPrices(9980e32, 9989e32); + lidoFixedPriceMulltiLpARM.setPrices(9980e32, 9989e32); // Forgot to scale up to 36 decimals vm.expectRevert("ARM: sell price too low"); - lidoOwnerLpARM.setPrices(1e18, 1e18); + lidoFixedPriceMulltiLpARM.setPrices(1e18, 1e18); } function test_RevertWhen_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); - lidoOwnerLpARM.setPrices(0, 0); + lidoFixedPriceMulltiLpARM.setPrices(0, 0); } function test_RevertWhen_SetOwner_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoOwnerLpARM.setOwner(address(0)); + lidoFixedPriceMulltiLpARM.setOwner(address(0)); } function test_RevertWhen_SetOperator_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoOwnerLpARM.setOperator(address(0)); + lidoFixedPriceMulltiLpARM.setOperator(address(0)); } ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_SetPrices_Operator() public asLidoOwnerLpARMOperator { + function test_SetPrices_Operator() public asLidoFixedPriceMulltiLpARMOperator { // buy price 10 basis points higher than 1.0 - lidoOwnerLpARM.setPrices(1001e33, 1002e33); + lidoFixedPriceMulltiLpARM.setPrices(1001e33, 1002e33); // sell price 10 basis points lower than 1.0 - lidoOwnerLpARM.setPrices(9980e32, 9991e32); + lidoFixedPriceMulltiLpARM.setPrices(9980e32, 9991e32); // 2% of one basis point spread - lidoOwnerLpARM.setPrices(999999e30, 1000001e30); + lidoFixedPriceMulltiLpARM.setPrices(999999e30, 1000001e30); - lidoOwnerLpARM.setPrices(992 * 1e33, 1001 * 1e33); - lidoOwnerLpARM.setPrices(1001 * 1e33, 1004 * 1e33); - lidoOwnerLpARM.setPrices(992 * 1e33, 2000 * 1e33); + lidoFixedPriceMulltiLpARM.setPrices(992 * 1e33, 1001 * 1e33); + lidoFixedPriceMulltiLpARM.setPrices(1001 * 1e33, 1004 * 1e33); + lidoFixedPriceMulltiLpARM.setPrices(992 * 1e33, 2000 * 1e33); // Check the traderates - assertEq(lidoOwnerLpARM.traderate0(), 500 * 1e33); - assertEq(lidoOwnerLpARM.traderate1(), 992 * 1e33); + assertEq(lidoFixedPriceMulltiLpARM.traderate0(), 500 * 1e33); + assertEq(lidoFixedPriceMulltiLpARM.traderate1(), 992 * 1e33); } function test_SetPrices_Owner() public { // buy price 11 basis points higher than 1.0 - lidoOwnerLpARM.setPrices(10011e32, 10020e32); + lidoFixedPriceMulltiLpARM.setPrices(10011e32, 10020e32); // sell price 11 basis points lower than 1.0 - lidoOwnerLpARM.setPrices(9980e32, 9989e32); - } - - function test_SetOperator() public asLidoOwnerLpARMOwner { - lidoOwnerLpARM.setOperator(address(this)); - assertEq(lidoOwnerLpARM.operator(), address(this)); + lidoFixedPriceMulltiLpARM.setPrices(9980e32, 9989e32); } } diff --git a/test/fork/LidoMultiPriceMultiLpARM/LidoMultiPriceMultiLpARM.t.sol b/test/fork/LidoMultiPriceMultiLpARM/LidoMultiPriceMultiLpARM.t.sol deleted file mode 100644 index a16018a..0000000 --- a/test/fork/LidoMultiPriceMultiLpARM/LidoMultiPriceMultiLpARM.t.sol +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; -/* -import {Test, console2} from "forge-std/Test.sol"; - -import {IERC20} from "contracts/Interfaces.sol"; -import {LidoMultiPriceMultiLpARM} from "contracts/LidoMultiPriceMultiLpARM.sol"; -import {Proxy} from "contracts/Proxy.sol"; - -import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; - -contract Fork_Concrete_LidoMultiPriceMultiLpARM_Test is Fork_Shared_Test_ { - Proxy public lidoProxy; - LidoMultiPriceMultiLpARM public lidoARM; - IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); - uint256 performanceFee = 2000; // 20% - address feeCollector = 0x000000000000000000000000000000Feec011ec1; - AssertData beforeData; - DeltaData noChangeDeltaData = DeltaData({ - totalAssets: 10, - totalSupply: 0, - totalAssetsCap: 0, - armWeth: 0, - armSteth: 0, - feesAccrued: 0, - tranchesDiscounts: [int16(0), 0, 0, 0, 0], - tranchesAllocations: [int256(0), 0, 0, 0, 0], - tranchesRemaining: [int256(0), 0, 0, 0, 0] - }); - - struct AssertData { - uint256 totalAssets; - uint256 totalSupply; - uint256 totalAssetsCap; - uint256 armWeth; - uint256 armSteth; - uint256 feesAccrued; - uint16[5] tranchesDiscounts; - uint256[5] tranchesAllocations; - uint256[5] tranchesRemaining; - } - - struct DeltaData { - int256 totalAssets; - int256 totalSupply; - int256 totalAssetsCap; - int256 armWeth; - int256 armSteth; - int256 feesAccrued; - int16[5] tranchesDiscounts; - int256[5] tranchesAllocations; - int256[5] tranchesRemaining; - } - - function _snapData() internal view returns (AssertData memory data) { - return AssertData({ - totalAssets: lidoARM.totalAssets(), - totalSupply: lidoARM.totalSupply(), - totalAssetsCap: lidoARM.totalAssetsCap(), - armWeth: weth.balanceOf(address(lidoARM)), - armSteth: steth.balanceOf(address(lidoARM)), - feesAccrued: lidoARM.feesAccrued(), - tranchesDiscounts: lidoARM.getTrancheDiscounts(), - tranchesAllocations: lidoARM.getTrancheAllocations(), - tranchesRemaining: lidoARM.getTrancheRemaining() - }); - } - - function assertData(AssertData memory before, DeltaData memory delta) internal view { - AssertData memory afterData = _snapData(); - - assertEq(int256(afterData.totalAssets), int256(before.totalAssets) + delta.totalAssets, "totalAssets"); - assertEq(int256(afterData.totalSupply), int256(before.totalSupply) + delta.totalSupply, "totalSupply"); - assertEq( - int256(afterData.totalAssetsCap), int256(before.totalAssetsCap) + delta.totalAssetsCap, "totalAssetsCap" - ); - assertEq(int256(afterData.feesAccrued), int256(before.feesAccrued) + delta.feesAccrued, "feesAccrued"); - assertEq(int256(afterData.armWeth), int256(before.armWeth) + delta.armWeth, "armWeth"); - assertEq(int256(afterData.armSteth), int256(before.armSteth) + delta.armSteth, "armSteth"); - // for (uint256 i = 0; i < 5; i++) { - // assertEq(afterData.tranchesDiscounts[i], before.tranchesDiscounts[i] + delta.tranchesDiscounts[i]); - // assertEq(afterData.tranchesAllocations[i], before.tranchesAllocations[i] + delta.tranchesAllocations[i]); - // assertEq(afterData.tranchesRemaining[i], before.tranchesRemaining[i] + delta.tranchesRemaining[i]); - // } - } - - // Account for stETH rounding errors. - // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case - uint256 constant ROUNDING = 2; - - function setUp() public override { - super.setUp(); - - address lidoWithdrawal = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; - LidoMultiPriceMultiLpARM lidoImpl = new LidoMultiPriceMultiLpARM(address(steth), address(weth), lidoWithdrawal); - lidoProxy = new Proxy(); - - // The deployer needs a tiny amount of WETH to initialize the ARM - _dealWETH(address(this), 1e12); - weth.approve(address(lidoProxy), type(uint256).max); - steth.approve(address(lidoProxy), type(uint256).max); - - // Initialize Proxy with LidoMultiPriceMultiLpARM implementation. - bytes memory data = abi.encodeWithSignature( - "initialize(string,string,address,uint256,address)", - "Lido ARM", - "ARM-ST", - operator, - performanceFee, - feeCollector - ); - lidoProxy.initialize(address(lidoImpl), address(this), data); - - lidoARM = LidoMultiPriceMultiLpARM(payable(address(lidoProxy))); - - // Set the tranche discounts for each tranche - // 8 basis point discount (0.08%) would be 800 with a price of 0.9992 - lidoARM.setTrancheDiscounts([uint16(200), 375, 800, 1400, 1800]); - lidoARM.setTrancheAllocations([uint256(80 ether), 50 ether, 30 ether, 20 ether, 10 ether]); - - lidoARM.setTotalAssetsCap(100 ether); - - address[] memory liquidityProviders = new address[](1); - liquidityProviders[0] = address(this); - lidoARM.setLiquidityProviderCaps(liquidityProviders, 20 ether); - - // Only fuzz from this address. Big speedup on fork. - targetSender(address(this)); - } - - function _dealStETH(address to, uint256 amount) internal { - vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); - steth.transfer(to, amount); - } - - function _dealWETH(address to, uint256 amount) internal { - deal(address(weth), to, amount); - } - - /// @dev Check initial state - function test_initial_state() external view { - assertEq(lidoARM.name(), "Lido ARM"); - assertEq(lidoARM.symbol(), "ARM-ST"); - assertEq(lidoARM.owner(), address(this)); - assertEq(lidoARM.operator(), operator); - assertEq(lidoARM.feeCollector(), feeCollector); - assertEq(lidoARM.fee(), performanceFee); - assertEq(lidoARM.lastTotalAssets(), 1e12); - assertEq(lidoARM.feesAccrued(), 0); - // the 20% performance fee is removed on initialization - assertEq(lidoARM.totalAssets(), 1e12); - assertEq(lidoARM.totalSupply(), 1e12); - assertEq(weth.balanceOf(address(lidoARM)), 1e12); - assertEq(lidoARM.totalAssetsCap(), 100 ether); - } - - /// @dev ARM owner sets valid trance discounts ranging from 1 to MAX_DISCOUNT - function test_setValidTrancheDiscounts() external { - lidoARM.setTrancheDiscounts([uint16(1), 20, 300, 9999, 65535]); - uint16[5] memory discounts = lidoARM.getTrancheDiscounts(); - assertEq(discounts[0], 1); - assertEq(discounts[1], 20); - assertEq(discounts[2], 300); - assertEq(discounts[3], 9999); - assertEq(discounts[4], 65535); - } - // Revert when a tranche discount is zero - // Revert when a tranche discount is greater than the MAX_DISCOUNT - // Revert when non owner tries to set tranche discounts - - // whitelisted LP adds WETH liquidity to the ARM - function test_depositAssets() external { - _dealWETH(address(this), 10 ether); - beforeData = _snapData(); - - lidoARM.deposit(10 ether); - - DeltaData memory delta = noChangeDeltaData; - delta.totalAssets = 10 ether; - delta.totalSupply = 10 ether; - delta.armWeth = 10 ether; - assertData(beforeData, delta); - - // assert whitelisted LP cap was decreased - // assert remaining liquidity in appropriate tranches increased - // assert last total assets was set with performance fee removed - // assert performance fee was accrued on asset increases but not the deposit - } - // non whitelisted LP tries to add WETH liquidity to the ARM - - function test_redeemAssets() external { - _dealWETH(address(this), 10 ether); - lidoARM.deposit(10 ether); - - lidoARM.requestRedeem(8 ether); - } - - // with enough liquidity in all tranches - //// swap stETH to WETH using just the first tranche - //// swap stETH to WETH using the first two tranches - //// swap stETH to WETH using all five tranches - //// fail to swap stETH to WETH with a swap larger than the available liquidity - // with all liquidity in the first tranche used - //// swap stETH to WETH using just the second tranche - //// swap stETH to WETH using the second and third tranches - //// swap stETH to WETH using the remaining four tranches - //// fail to swap stETH to WETH with a swap larger than the available liquidity - // with only liquidity in the fifth tranche - //// swap stETH to WETH using just the fifth tranche -} -*/ diff --git a/test/fork/LidoOwnerLpARM/SwapExactTokensForTokens.t copy.sol b/test/fork/LidoOwnerLpARM/SwapExactTokensForTokens.t copy.sol deleted file mode 100644 index 89f8166..0000000 --- a/test/fork/LidoOwnerLpARM/SwapExactTokensForTokens.t copy.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -// Test imports -import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; - -import {IERC20} from "contracts/Interfaces.sol"; - -contract Fork_Concrete_LidoOwnerLpARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Test_ { - // Account for stETH rounding errors. - // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case - uint256 constant ROUNDING = STETH_ERROR_ROUNDING; - - IERC20 BAD_TOKEN; - - ////////////////////////////////////////////////////// - /// --- SETUP - ////////////////////////////////////////////////////// - function setUp() public override { - super.setUp(); - - deal(address(weth), address(lidoOwnerLpARM), 1_000_000 ether); - deal(address(steth), address(lidoOwnerLpARM), 1_000_000 ether); - - BAD_TOKEN = IERC20(makeAddr("bad token")); - } - - ////////////////////////////////////////////////////// - /// --- REVERTING TESTS - ////////////////////////////////////////////////////// - function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenIn() public { - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapExactTokensForTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); - } - - function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut() public { - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapExactTokensForTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapExactTokensForTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); - } - - ////////////////////////////////////////////////////// - /// --- PASSING TESTS - ////////////////////////////////////////////////////// - function test_SwapExactTokensForTokens_WETH_TO_STETH() public { - _swapExactTokensForTokens(weth, steth, 10 ether, 6.25 ether); - } - - function test_SwapExactTokensForTokens_STETH_TO_WETH() public { - _swapExactTokensForTokens(steth, weth, 10 ether, 5 ether); - } - - function test_RealisticSwap() public { - lidoOwnerLpARM.setPrices(997 * 1e33, 998 * 1e33); - _swapExactTokensForTokens(steth, weth, 10 ether, 9.97 ether); - _swapExactTokensForTokens(weth, steth, 10 ether, 10020040080160320641); - } - - ////////////////////////////////////////////////////// - /// --- HELPERS - ////////////////////////////////////////////////////// - function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - if (inToken == weth) { - deal(address(weth), address(this), amountIn + 1000); - } else { - deal(address(steth), address(this), amountIn + 1000); - } - uint256 startIn = inToken.balanceOf(address(this)); - uint256 startOut = outToken.balanceOf(address(this)); - lidoOwnerLpARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); - assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); - assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); - assertGe(outToken.balanceOf(address(this)), startOut + expectedOut - ROUNDING, "Out actual"); - assertLe(outToken.balanceOf(address(this)), startOut + expectedOut + ROUNDING, "Out actual"); - } -} diff --git a/test/fork/LidoOwnerLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoOwnerLpARM/SwapTokensForExactTokens.t.sol deleted file mode 100644 index edeef47..0000000 --- a/test/fork/LidoOwnerLpARM/SwapTokensForExactTokens.t.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -// Test imports -import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; - -import {IERC20} from "contracts/Interfaces.sol"; - -contract Fork_Concrete_LidoOwnerLpARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Test_ { - // Account for stETH rounding errors. - // See https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case - uint256 constant ROUNDING = STETH_ERROR_ROUNDING; - - IERC20 BAD_TOKEN; - - ////////////////////////////////////////////////////// - /// --- SETUP - ////////////////////////////////////////////////////// - function setUp() public override { - super.setUp(); - - deal(address(weth), address(lidoOwnerLpARM), 1_000_000 ether); - deal(address(steth), address(lidoOwnerLpARM), 1_000_000 ether); - - BAD_TOKEN = IERC20(makeAddr("bad token")); - } - - ////////////////////////////////////////////////////// - /// --- REVERTING TESTS - ////////////////////////////////////////////////////// - function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenIn() public { - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapTokensForExactTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); - } - - function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut() public { - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapTokensForExactTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoOwnerLpARM.swapTokensForExactTokens(steth, steth, 10 ether, 0, address(this)); - } - - ////////////////////////////////////////////////////// - /// --- PASSING TESTS - ////////////////////////////////////////////////////// - function test_SwapTokensForExactTokens_WETH_TO_STETH() public { - _swapTokensForExactTokens(weth, steth, 10 ether, 6.25 ether); - } - - function test_SwapTokensForExactTokens_STETH_TO_WETH() public { - _swapTokensForExactTokens(steth, weth, 10 ether, 5 ether); - } - - ////////////////////////////////////////////////////// - /// --- HELPERS - ////////////////////////////////////////////////////// - function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - if (inToken == weth) { - deal(address(weth), address(this), amountIn + 1000); - } else { - deal(address(steth), address(this), amountIn + 1000); - } - uint256 startIn = inToken.balanceOf(address(this)); - lidoOwnerLpARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); - assertGt(inToken.balanceOf(address(this)), (startIn - amountIn) - ROUNDING, "In actual"); - assertLt(inToken.balanceOf(address(this)), (startIn - amountIn) + ROUNDING, "In actual"); - assertGe(outToken.balanceOf(address(this)), expectedOut - ROUNDING, "Out actual"); - assertLe(outToken.balanceOf(address(this)), expectedOut + ROUNDING, "Out actual"); - } -} diff --git a/test/fork/LidoOwnerLpARM/TransferToken.t.sol b/test/fork/LidoOwnerLpARM/TransferToken.t.sol deleted file mode 100644 index 4e35bfb..0000000 --- a/test/fork/LidoOwnerLpARM/TransferToken.t.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -// Test imports -import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; - -contract Fork_Concrete_LidoOwnerLpARM_TransferToken_Test_ is Fork_Shared_Test_ { - uint256 public constant ROUNDING = STETH_ERROR_ROUNDING; - - ////////////////////////////////////////////////////// - /// --- SETUP - ////////////////////////////////////////////////////// - function setUp() public override { - super.setUp(); - - deal(address(weth), address(lidoOwnerLpARM), 1_000 ether); - deal(address(steth), address(lidoOwnerLpARM), 1_000 ether); - } - - ////////////////////////////////////////////////////// - /// --- PASSING TESTS - ////////////////////////////////////////////////////// - function test_TransferToken_WETH() public asLidoOwnerLpARMOwner { - uint256 balanceARMBeforeWETH = weth.balanceOf(address(lidoOwnerLpARM)); - uint256 balanceThisBeforeWETH = weth.balanceOf(address(this)); - lidoOwnerLpARM.transferToken(address(weth), address(this), balanceARMBeforeWETH); - - assertEq(weth.balanceOf(address(this)), balanceThisBeforeWETH + balanceARMBeforeWETH); - assertEq(weth.balanceOf(address(lidoOwnerLpARM)), 0); - } - - function test_TransferToken_STETH() public asLidoOwnerLpARMOwner { - uint256 balanceARMBeforeSTETH = steth.balanceOf(address(lidoOwnerLpARM)); - uint256 balanceThisBeforeSTETH = steth.balanceOf(address(this)); - lidoOwnerLpARM.transferToken(address(steth), address(this), balanceARMBeforeSTETH); - - assertApproxEqAbs(steth.balanceOf(address(this)), balanceThisBeforeSTETH + balanceARMBeforeSTETH, ROUNDING); - assertApproxEqAbs(steth.balanceOf(address(lidoOwnerLpARM)), 0, ROUNDING); - } -} diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 190a68d..96ad079 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -10,7 +10,6 @@ import {Modifiers} from "test/fork/utils/Modifiers.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; -import {LidoOwnerLpARM} from "contracts/LidoOwnerLpARM.sol"; import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; @@ -167,23 +166,6 @@ abstract contract Fork_Shared_Test_ is Modifiers { // set prices lidoFixedPriceMulltiLpARM.setPrices(992 * 1e33, 1001 * 1e33); - - // --- Deploy LidoOwnerLpARM implementation --- - // Deploy LidoOwnerLpARM implementation. - LidoOwnerLpARM lidoOwnerImpl = new LidoOwnerLpARM(address(weth), address(steth), Mainnet.LIDO_WITHDRAWAL); - - // Initialize Proxy with LidoOwnerLpARM implementation. - data = abi.encodeWithSignature("initialize(address)", operator); - lidoOwnerProxy.initialize(address(lidoOwnerImpl), address(this), data); - - // Set the Proxy as the LidoOwnerARM. - lidoOwnerLpARM = LidoOwnerLpARM(payable(address(lidoOwnerProxy))); - - // Set Prices - lidoOwnerLpARM.setPrices(500 * 1e33, 1600000000000000000000000000000000000); - - weth.approve(address(lidoOwnerLpARM), type(uint256).max); - steth.approve(address(lidoOwnerLpARM), type(uint256).max); } function _label() internal { @@ -196,7 +178,6 @@ abstract contract Fork_Shared_Test_ is Modifiers { vm.label(address(proxy), "OETH ARM PROXY"); vm.label(address(lidoFixedPriceMulltiLpARM), "LIDO ARM"); vm.label(address(lidoProxy), "LIDO ARM PROXY"); - vm.label(address(lidoOwnerLpARM), "LIDO OWNER LP ARM"); vm.label(address(liquidityProviderController), "LIQUIDITY PROVIDER CONTROLLER"); vm.label(operator, "OPERATOR"); vm.label(oethWhale, "WHALE OETH"); diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index ae89194..659fd9d 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -30,16 +30,9 @@ abstract contract Modifiers is Helpers { vm.stopPrank(); } - /// @notice Impersonate the owner of LidoOwnerLpARM contract. - modifier asLidoOwnerLpARMOwner() { - vm.startPrank(lidoOwnerLpARM.owner()); - _; - vm.stopPrank(); - } - /// @notice Impersonate the operator of LidoOwnerLpARM contract. - modifier asLidoOwnerLpARMOperator() { - vm.startPrank(lidoOwnerLpARM.operator()); + modifier asLidoFixedPriceMulltiLpARMOperator() { + vm.startPrank(lidoFixedPriceMulltiLpARM.operator()); _; vm.stopPrank(); } diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 8be184c..2e0ad96 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -36,7 +36,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { targetSender(address(this)); } - function test_initialConfig() external { + function test_initialConfig() external view { assertEq(lidoARM.name(), "Lido ARM", "Name"); assertEq(lidoARM.symbol(), "ARM-ST", "Symbol"); assertEq(lidoARM.owner(), Mainnet.GOV_MULTISIG, "Owner"); @@ -77,7 +77,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { lidoARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); - assertEq(inToken.balanceOf(address(this)), startIn - amountIn, "In actual"); + assertApproxEqAbs(inToken.balanceOf(address(this)), startIn - amountIn, 2, "In actual"); assertEq(outToken.balanceOf(address(this)), startOut + expectedOut, "Out actual"); } @@ -100,7 +100,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { lidoARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); - assertEq(inToken.balanceOf(address(this)), startIn - amountIn - 1, "In actual"); + assertApproxEqAbs(inToken.balanceOf(address(this)), startIn - amountIn, 2, "In actual"); assertEq(outToken.balanceOf(address(this)), expectedOut, "Out actual"); } From c5d23b5cfea02e2ad3ebd033f1099650703f5eaa Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 24 Sep 2024 04:44:59 +1000 Subject: [PATCH 070/196] generated contract docs --- docs/LidoFixedPriceMultiLpARMHierarchy.svg | 80 +++++------ docs/LidoFixedPriceMultiLpARMSquashed.svg | 4 +- docs/LidoMultiPriceMultiLpARMHierarchy.svg | 154 --------------------- docs/LidoMultiPriceMultiLpARMSquashed.svg | 140 ------------------- docs/LidoOwnerLpARMHierarchy.svg | 114 --------------- docs/LidoOwnerLpARMSquashed.svg | 78 ----------- docs/generate.sh | 15 +- src/contracts/README.md | 8 +- 8 files changed, 47 insertions(+), 546 deletions(-) delete mode 100644 docs/LidoMultiPriceMultiLpARMHierarchy.svg delete mode 100644 docs/LidoMultiPriceMultiLpARMSquashed.svg delete mode 100644 docs/LidoOwnerLpARMHierarchy.svg delete mode 100644 docs/LidoOwnerLpARMSquashed.svg diff --git a/docs/LidoFixedPriceMultiLpARMHierarchy.svg b/docs/LidoFixedPriceMultiLpARMHierarchy.svg index 40070a2..ffb8bb9 100644 --- a/docs/LidoFixedPriceMultiLpARMHierarchy.svg +++ b/docs/LidoFixedPriceMultiLpARMHierarchy.svg @@ -17,136 +17,136 @@ AbstractARM ../src/contracts/AbstractARM.sol - + -28 +23 OwnableOperable ../src/contracts/OwnableOperable.sol - + -0->28 +0->23 - + -3 +1 <<Abstract>> FixedPriceARM ../src/contracts/FixedPriceARM.sol - + -3->0 +1->0 - + -15 +13 LidoFixedPriceMultiLpARM ../src/contracts/LidoFixedPriceMultiLpARM.sol - + -15->3 +13->1 - + -16 +14 <<Abstract>> LidoLiquidityManager ../src/contracts/LidoLiquidityManager.sol - + -15->16 +13->14 - + -20 +16 <<Abstract>> LiquidityProviderControllerARM ../src/contracts/LiquidityProviderControllerARM.sol - + -15->20 +13->16 - + -21 +17 <<Abstract>> MultiLP ../src/contracts/MultiLP.sol - + -15->21 +13->17 - + -31 +26 <<Abstract>> PerformanceFee ../src/contracts/PerformanceFee.sol - + -15->31 +13->26 - + -16->28 +14->23 - + -20->21 +16->17 - + -21->0 +17->0 - + -27 +22 Ownable ../src/contracts/Ownable.sol - + -28->27 +23->22 - + -31->21 +26->17 diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index b67edf2..55bf342 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -9,9 +9,9 @@ UmlClassDiagram - + -15 +13 LidoFixedPriceMultiLpARM ../src/contracts/LidoFixedPriceMultiLpARM.sol diff --git a/docs/LidoMultiPriceMultiLpARMHierarchy.svg b/docs/LidoMultiPriceMultiLpARMHierarchy.svg deleted file mode 100644 index 61c98ee..0000000 --- a/docs/LidoMultiPriceMultiLpARMHierarchy.svg +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - -UmlClassDiagram - - - -0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol - - - -28 - -OwnableOperable -../src/contracts/OwnableOperable.sol - - - -0->28 - - - - - -1 - -<<Abstract>> -AccessControlLP -../src/contracts/AccessControlLP.sol - - - -21 - -<<Abstract>> -MultiLP -../src/contracts/MultiLP.sol - - - -1->21 - - - - - -16 - -<<Abstract>> -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol - - - -16->28 - - - - - -17 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - - - -17->1 - - - - - -17->16 - - - - - -17->21 - - - - - -24 - -<<Abstract>> -MultiPriceARM -../src/contracts/MultiPriceARM.sol - - - -17->24 - - - - - -31 - -<<Abstract>> -PerformanceFee -../src/contracts/PerformanceFee.sol - - - -17->31 - - - - - -21->0 - - - - - -24->0 - - - - - -27 - -Ownable -../src/contracts/Ownable.sol - - - -28->27 - - - - - -31->21 - - - - - diff --git a/docs/LidoMultiPriceMultiLpARMSquashed.svg b/docs/LidoMultiPriceMultiLpARMSquashed.svg deleted file mode 100644 index aec813b..0000000 --- a/docs/LidoMultiPriceMultiLpARMSquashed.svg +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - -UmlClassDiagram - - - -17 - -LidoMultiPriceMultiLpARM -../src/contracts/LidoMultiPriceMultiLpARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[47] <<MultiLP>> -   _gap: uint256[48] <<PerformanceFee>> -   _gap: uint256[48] <<AccessControlLP>> -   discountToken: address <<MultiPriceARM>> -   liquidityToken: address <<MultiPriceARM>> -   DISCOUNT_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_ALLOCATED_INDEX: uint256 <<MultiPriceARM>> -   LIQUIDITY_REMAINING_INDEX: uint256 <<MultiPriceARM>> -   tranches: uint16[15] <<MultiPriceARM>> -   _gap: uint256[49] <<MultiPriceARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityAsset: address <<MultiLP>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   FEE_SCALE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   totalAssetsCap: uint256 <<AccessControlLP>> -   liquidityProviderCaps: mapping(address=>uint256) <<AccessControlLP>> -   PRICE_PRECISION: uint256 <<MultiPriceARM>> -   DISCOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   TRANCHE_AMOUNT_MULTIPLIER: uint256 <<MultiPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<MultiPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<MultiPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postWithdrawHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _externalWithdrawQueue(): uint256 <<LidoMultiPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> -    _rawTotalAssets(): uint256 <<PerformanceFee>> -    _setFee(_fee: uint256) <<PerformanceFee>> -    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> -    _addLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> -    _removeLiquidity(liquidityAmount: uint256) <<MultiPriceARM>> -    _calcPriceFromLiquidity(liquiditySwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcPriceFromDiscount(discountSwapAmount: uint256): (price: uint256) <<MultiPriceARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoMultiPriceMultiLpARM>> -    _postClaimHook(assets: uint256) <<LidoMultiPriceMultiLpARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> -    collectFees(): (fees: uint256) <<PerformanceFee>> -    setLiquidityProviderCaps(_liquidityProviders: address[], cap: uint256) <<onlyOwner>> <<AccessControlLP>> -    setTotalAssetsCap(_totalAssetsCap: uint256) <<onlyOwner>> <<AccessControlLP>> -    setTrancheDiscounts(discounts: uint16[5]) <<onlyOwner>> <<MultiPriceARM>> -    setTrancheAllocations(allocations: uint256[5]) <<onlyOwner>> <<MultiPriceARM>> -    resetRemainingLiquidity() <<onlyOwner>> <<MultiPriceARM>> -    getTrancheDiscounts(): (discounts: uint16[5]) <<MultiPriceARM>> -    getTrancheAllocations(): (allocations: uint256[5]) <<MultiPriceARM>> -    getTrancheRemaining(): (remaining: uint256[5]) <<MultiPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address) <<initializer>> <<LidoMultiPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> -    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> -    <<event>> LiquidityProviderCap(liquidityProvider: address, cap: uint256) <<AccessControlLP>> -    <<event>> TotalAssetsCap(cap: uint256) <<AccessControlLP>> -    <<event>> TranchePricesUpdated(discounts: uint16[5]) <<MultiPriceARM>> -    <<event>> TrancheAllocationsUpdated(allocations: uint256[5]) <<MultiPriceARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_discountToken: address, _liquidityToken: address) <<MultiPriceARM>> -    constructor(_liquidityAsset: address) <<MultiLP>> -    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> -    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> -    totalAssets(): uint256 <<LidoMultiPriceMultiLpARM>> -    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>> -    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>> -    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoMultiPriceMultiLpARM>> - - - diff --git a/docs/LidoOwnerLpARMHierarchy.svg b/docs/LidoOwnerLpARMHierarchy.svg deleted file mode 100644 index 2761596..0000000 --- a/docs/LidoOwnerLpARMHierarchy.svg +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - -UmlClassDiagram - - - -0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol - - - -28 - -OwnableOperable -../src/contracts/OwnableOperable.sol - - - -0->28 - - - - - -3 - -<<Abstract>> -FixedPriceARM -../src/contracts/FixedPriceARM.sol - - - -3->0 - - - - - -16 - -<<Abstract>> -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol - - - -16->28 - - - - - -18 - -LidoOwnerLpARM -../src/contracts/LidoOwnerLpARM.sol - - - -18->3 - - - - - -18->16 - - - - - -29 - -<<Abstract>> -OwnerLP -../src/contracts/OwnerLP.sol - - - -18->29 - - - - - -27 - -Ownable -../src/contracts/Ownable.sol - - - -28->27 - - - - - -29->27 - - - - - diff --git a/docs/LidoOwnerLpARMSquashed.svg b/docs/LidoOwnerLpARMSquashed.svg deleted file mode 100644 index 7e4e0aa..0000000 --- a/docs/LidoOwnerLpARMSquashed.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - -UmlClassDiagram - - - -18 - -LidoOwnerLpARM -../src/contracts/LidoOwnerLpARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[48] <<FixedPriceARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   MAX_PRICE_DEVIATION: uint256 <<FixedPriceARM>> -   PRICE_SCALE: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoOwnerLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -    _postClaimHook(assets: uint256) <<LidoOwnerLpARM>> -    _externalWithdrawQueue(): (assets: uint256) <<LidoLiquidityManager>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    owner(): address <<Ownable>> -    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> -    transferToken(token: address, to: address, amount: uint256) <<onlyOwner>> <<OwnerLP>> -    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>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_operator: address) <<initializer>> <<LidoOwnerLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> -    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoOwnerLpARM>> - - - diff --git a/docs/generate.sh b/docs/generate.sh index a7126c4..aa0c8b5 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -13,23 +13,10 @@ sol2uml storage ../src/contracts -c OethARM -o OethARMStorage.svg \ -st address,address \ --hideExpand gap,_gap -sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoOwnerLpARM -o LidoOwnerLpARMHierarchy.svg -sol2uml ../src/contracts -s -d 0 -b LidoOwnerLpARM -o LidoOwnerLpARMSquashed.svg -sol2uml storage ../src/contracts,../lib -c LidoOwnerLpARM -o LidoOwnerLpARMStorage.svg \ - -sn eip1967.proxy.implementation,eip1967.proxy.admin \ - -st address,address \ - --hideExpand gap,_gap - sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMHierarchy.svg sol2uml ../src/contracts -s -d 0 -b LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMSquashed.svg +sol2uml ../src/contracts -hp -s -d 0 -b LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMPublicSquashed.svg sol2uml storage ../src/contracts,../lib -c LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMStorage.svg \ -sn eip1967.proxy.implementation,eip1967.proxy.admin \ -st address,address \ --hideExpand gap,_gap - -sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoMultiPriceMultiLpARM -o LidoMultiPriceMultiLpARMHierarchy.svg -sol2uml ../src/contracts -s -d 0 -b LidoMultiPriceMultiLpARM -o LidoMultiPriceMultiLpARMSquashed.svg -sol2uml storage ../src/contracts,../lib -c LidoMultiPriceMultiLpARM -o LidoMultiPriceMultiLpARMStorage.svg \ - -sn eip1967.proxy.implementation,eip1967.proxy.admin \ - -st address,address \ - --hideExpand gap,_gap diff --git a/src/contracts/README.md b/src/contracts/README.md index 020ee09..494648e 100644 --- a/src/contracts/README.md +++ b/src/contracts/README.md @@ -30,12 +30,12 @@ ### Hierarchy -![Lido ARM Hierarchy](../../docs/LidoMultiLpARMHierarchy.svg) +![Lido ARM Hierarchy](../../docs/LidoFixedPriceMultiLpARMHierarchy.svg) ## OETH ARM Squashed -![Lido ARM Squashed](../../docs/LidoMultiLpARMSquashed.svg) +![Lido ARM Squashed](../../docs/LidoFixedPriceMultiLpARMSquashed.svg) -## OETH ARM Storage + From 4c33338b3b4f23bfd27e87fdfa3b2e211d24262e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Tue, 24 Sep 2024 03:09:38 +0200 Subject: [PATCH 071/196] Add more tests. (#21) * chore: add coverage files to gitignore. * test: add tests for claimRedeem and TotalAssets. * test: add more tests for setters. * forge fmt --- .gitignore | 3 + .../ClaimRedeem.t.sol | 246 ++++++++++++++++++ .../LidoFixedPriceMultiLpARM/Setters.t.sol | 96 +++++-- .../TotalAssets.t.sol | 136 ++++++++++ test/fork/utils/Helpers.sol | 3 +- test/fork/utils/Modifiers.sol | 22 ++ 6 files changed, 489 insertions(+), 17 deletions(-) create mode 100644 test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol create mode 100644 test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol diff --git a/.gitignore b/.gitignore index e6764a8..9b792cf 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,9 @@ artifacts dependencies/ soldeer.lock +# Coverage +lock.info* + # Defender Actions dist diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol new file mode 100644 index 0000000..fbd76b7 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {MultiLP} from "contracts/MultiLP.sol"; +import {PerformanceFee} from "contracts/PerformanceFee.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { + uint256 private delay; + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + + function setUp() public override { + super.setUp(); + + delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + + deal(address(weth), address(this), 1_000 ether); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_ClaimRequest_Because_ClaimDelayNotMet() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + skip(delay - 1); + vm.expectRevert("Claim delay not met"); + lidoFixedPriceMulltiLpARM.claimRedeem(0); + } + + function test_RevertWhen_ClaimRequest_Because_QueuePendingLiquidity_NoLiquidity() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Remove all weth liquidity from ARM + deal(address(weth), address(lidoFixedPriceMulltiLpARM), 0); + + // Time jump claim delay + skip(delay); + + // Expect revert + vm.expectRevert("Queue pending liquidity"); + lidoFixedPriceMulltiLpARM.claimRedeem(0); + } + + function test_RevertWhen_ClaimRequest_Because_QueuePendingLiquidity_NoEnoughLiquidity() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Remove half of weth liquidity from ARM + uint256 halfAmount = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) / 2; + deal(address(weth), address(lidoFixedPriceMulltiLpARM), halfAmount); + + // Time jump claim delay + skip(delay); + + // Expect revert + vm.expectRevert("Queue pending liquidity"); + lidoFixedPriceMulltiLpARM.claimRedeem(0); + } + + function test_RevertWhen_ClaimRequest_Because_NotRequester() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Time jump claim delay + skip(delay); + + // Expect revert + vm.startPrank(vm.randomAddress()); + vm.expectRevert("Not requester"); + lidoFixedPriceMulltiLpARM.claimRedeem(0); + } + + function test_RevertWhen_ClaimRequest_Because_AlreadyClaimed() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + skipTime(delay) + claimRequestOnLidoFixedPriceMultiLpARM(address(this), 0) + { + // Expect revert + vm.expectRevert("Already claimed"); + lidoFixedPriceMulltiLpARM.claimRedeem(0); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + + function test_ClaimRequest_MoreThanEnoughLiquidity_() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + skipTime(delay) + { + // Assertions before + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); + assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); + + // Expected events + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit MultiLP.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), DEFAULT_AMOUNT); + + // Main call + (uint256 assets) = lidoFixedPriceMulltiLpARM.claimRedeem(0); + + // Assertions after + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); + assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); + assertEq(assets, DEFAULT_AMOUNT); + } + + function test_ClaimRequest_JustEnoughLiquidity_() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + skipTime(delay) + { + // Assertions before + // Same situation as above + + // Swap MIN_TOTAL_SUPPLY from WETH in STETH + deal(address(weth), address(lidoFixedPriceMulltiLpARM), DEFAULT_AMOUNT); + deal(address(steth), address(lidoFixedPriceMulltiLpARM), MIN_TOTAL_SUPPLY); + + // Handle lido rounding issue to ensure that balance is exactly MIN_TOTAL_SUPPLY + if (steth.balanceOf(address(lidoFixedPriceMulltiLpARM)) == MIN_TOTAL_SUPPLY - 1) { + deal(address(steth), address(lidoFixedPriceMulltiLpARM), 0); + deal(address(steth), address(lidoFixedPriceMulltiLpARM), MIN_TOTAL_SUPPLY + 1); + } + + // Expected events + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit MultiLP.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), DEFAULT_AMOUNT); + + // Main call + (uint256 assets) = lidoFixedPriceMulltiLpARM.claimRedeem(0); + + // Assertions after + assertApproxEqAbs(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY, 1); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); + assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); + assertEq(assets, DEFAULT_AMOUNT); + } + + function test_ClaimRequest_SecondClaim() + public + setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT / 2) + skipTime(delay) + claimRequestOnLidoFixedPriceMultiLpARM(address(this), 0) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT / 2) + { + // Assertions before + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2, 2); + assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); + assertEqUserRequest(1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); + + // Expected events + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit MultiLP.RedeemClaimed(address(this), 1, DEFAULT_AMOUNT / 2); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), DEFAULT_AMOUNT / 2); + + // Main call + skip(delay); + (uint256 assets) = lidoFixedPriceMulltiLpARM.claimRedeem(1); + + // Assertions after + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); + assertEqUserRequest(0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); + assertEqUserRequest(1, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); + assertEq(assets, DEFAULT_AMOUNT / 2); + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 53e3e58..cca60cb 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -4,6 +4,11 @@ pragma solidity 0.8.23; // Test imports import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {MultiLP} from "contracts/MultiLP.sol"; +import {PerformanceFee} from "contracts/PerformanceFee.sol"; + contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP @@ -13,7 +18,63 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te } ////////////////////////////////////////////////////// - /// --- REVERTING TESTS + /// --- PERFORMANCE FEE - REVERTING TEST + ////////////////////////////////////////////////////// + function test_RevertWhen_PerformanceFee_SetFee_Because_NotOwner() public asRandomAddress { + vm.expectRevert("ARM: Only owner can call this function."); + lidoFixedPriceMulltiLpARM.setFee(0); + } + + function test_RevertWhen_PerformanceFee_SetFee_Because_FeeIsTooHigh() public asLidoFixedPriceMultiLpARMOwner { + uint256 max = lidoFixedPriceMulltiLpARM.FEE_SCALE(); + vm.expectRevert("ARM: fee too high"); + lidoFixedPriceMulltiLpARM.setFee(max + 1); + } + + function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_NotOwner() public asRandomAddress { + vm.expectRevert("ARM: Only owner can call this function."); + lidoFixedPriceMulltiLpARM.setFeeCollector(address(0)); + } + + function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_FeeCollectorIsZero() + public + asLidoFixedPriceMultiLpARMOwner + { + vm.expectRevert("ARM: invalid fee collector"); + lidoFixedPriceMulltiLpARM.setFeeCollector(address(0)); + } + + ////////////////////////////////////////////////////// + /// --- PERFORMANCE FEE - PASSING TEST + ////////////////////////////////////////////////////// + function test_PerformanceFee_SetFee_() public asLidoFixedPriceMultiLpARMOwner { + uint256 feeBefore = lidoFixedPriceMulltiLpARM.fee(); + + uint256 newFee = _bound(vm.randomUint(), 0, lidoFixedPriceMulltiLpARM.FEE_SCALE()); + + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit PerformanceFee.FeeUpdated(newFee); + lidoFixedPriceMulltiLpARM.setFee(newFee); + + assertEq(lidoFixedPriceMulltiLpARM.fee(), newFee); + assertNotEq(feeBefore, lidoFixedPriceMulltiLpARM.fee()); + } + + function test_PerformanceFee_SetFeeCollector() public asLidoFixedPriceMultiLpARMOwner { + address feeCollectorBefore = lidoFixedPriceMulltiLpARM.feeCollector(); + + address newFeeCollector = vm.randomAddress(); + + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit PerformanceFee.FeeCollectorUpdated(newFeeCollector); + lidoFixedPriceMulltiLpARM.setFeeCollector(newFeeCollector); + + assertEq(lidoFixedPriceMulltiLpARM.feeCollector(), newFeeCollector); + assertNotEq(feeCollectorBefore, lidoFixedPriceMulltiLpARM.feeCollector()); + } + + ////////////////////////////////////////////////////// + /// --- FIXED PRICE ARM - REVERTING TESTS ////////////////////////////////////////////////////// function test_RevertWhen_SetPrices_Because_PriceCross() public { vm.expectRevert("ARM: Price cross"); @@ -30,7 +91,7 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te lidoFixedPriceMulltiLpARM.setPrices(1e36, 1e36); } - function test_RevertWhen_SetPrices_Because_PriceRange() public asLidoFixedPriceMulltiLpARMOperator { + function test_RevertWhen_FixedPriceARM_SetPrices_Because_PriceRange() public asLidoFixedPriceMulltiLpARMOperator { // buy price 11 basis points higher than 1.0 vm.expectRevert("ARM: buy price too high"); lidoFixedPriceMulltiLpARM.setPrices(10011e32, 10020e32); @@ -44,25 +105,15 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te lidoFixedPriceMulltiLpARM.setPrices(1e18, 1e18); } - function test_RevertWhen_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { + function test_RevertWhen_FixedPriceARM_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); lidoFixedPriceMulltiLpARM.setPrices(0, 0); } - function test_RevertWhen_SetOwner_Because_NotOwner() public asRandomAddress { - vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMulltiLpARM.setOwner(address(0)); - } - - function test_RevertWhen_SetOperator_Because_NotOwner() public asRandomAddress { - vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMulltiLpARM.setOperator(address(0)); - } - ////////////////////////////////////////////////////// - /// --- PASSING TESTS + /// --- FIXED PRICE ARM - PASSING TESTS ////////////////////////////////////////////////////// - function test_SetPrices_Operator() public asLidoFixedPriceMulltiLpARMOperator { + function test_FixedPriceARM_SetPrices_Operator() public asLidoFixedPriceMulltiLpARMOperator { // buy price 10 basis points higher than 1.0 lidoFixedPriceMulltiLpARM.setPrices(1001e33, 1002e33); // sell price 10 basis points lower than 1.0 @@ -79,11 +130,24 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te assertEq(lidoFixedPriceMulltiLpARM.traderate1(), 992 * 1e33); } - function test_SetPrices_Owner() public { + function test_FixedPriceARM_SetPrices_Owner() public { // buy price 11 basis points higher than 1.0 lidoFixedPriceMulltiLpARM.setPrices(10011e32, 10020e32); // sell price 11 basis points lower than 1.0 lidoFixedPriceMulltiLpARM.setPrices(9980e32, 9989e32); } + + ////////////////////////////////////////////////////// + /// --- OWNABLE - REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_Ownable_SetOwner_Because_NotOwner() public asRandomAddress { + vm.expectRevert("ARM: Only owner can call this function."); + lidoFixedPriceMulltiLpARM.setOwner(address(0)); + } + + function test_RevertWhen_Ownable_SetOperator_Because_NotOwner() public asRandomAddress { + vm.expectRevert("ARM: Only owner can call this function."); + lidoFixedPriceMulltiLpARM.setOperator(address(0)); + } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol new file mode 100644 index 0000000..a92e2a4 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {MultiLP} from "contracts/MultiLP.sol"; +import {PerformanceFee} from "contracts/PerformanceFee.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + // Set Cap to max, as not to interfere with the tests + address[] memory providers = new address[](1); + providers[0] = address(this); + liquidityProviderController.setLiquidityProviderCaps(providers, type(uint256).max); + liquidityProviderController.setTotalAssetsCap(type(uint256).max); + + // Approve STETH for Lido + lidoFixedPriceMulltiLpARM.approveStETH(); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TEST + ////////////////////////////////////////////////////// + function test_TotalAssets_AfterInitialization() public { + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); + } + + function test_TotalAssets_AfterDeposit_NoAssetGainOrLoss() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + } + + function test_TotalAssets_AfterDeposit_WithAssetGain_InWETH() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Simulate asset gain + uint256 assetGain = DEFAULT_AMOUNT / 2; + deal( + address(weth), + address(lidoFixedPriceMulltiLpARM), + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) + assetGain + ); + + // Calculate Fees + uint256 fee = assetGain * 20 / 100; // 20% fee + + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee); + } + + function test_TotalAssets_AfterDeposit_WithAssetGain_InSTETH() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + // Simulate asset gain + uint256 assetGain = DEFAULT_AMOUNT / 2 + 1; + // We are sure that steth balance is empty, so we can deal directly final amount. + deal(address(steth), address(lidoFixedPriceMulltiLpARM), assetGain); + + // Calculate Fees + uint256 fee = assetGain * 20 / 100; // 20% fee + + assertApproxEqAbs( + lidoFixedPriceMulltiLpARM.totalAssets(), + MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee, + STETH_ERROR_ROUNDING + ); + } + + function test_TotalAssets_AfterDeposit_WithAssetLoss_InWETH() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Simulate asset loss + uint256 assetLoss = DEFAULT_AMOUNT / 2; + deal( + address(weth), + address(lidoFixedPriceMulltiLpARM), + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - assetLoss + ); + + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss); + } + + function test_TotalAssets_AfterDeposit_WithAssetLoss_InSTETH() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Simulate Swap at 1:1 between WETH and stETH + uint256 swapAmount = DEFAULT_AMOUNT / 2; + deal( + address(weth), + address(lidoFixedPriceMulltiLpARM), + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - swapAmount + ); + // Then simulate a loss on stETH, do all in the same deal + uint256 assetLoss = swapAmount / 2; + deal(address(steth), address(lidoFixedPriceMulltiLpARM), swapAmount / 2); + + assertApproxEqAbs( + lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss, STETH_ERROR_ROUNDING + ); + } + + function test_TotalAssets_After_WithdrawingFromLido() public { + // Simulate a Swap at 1:1 between WETH and stETH using initial liquidity + uint256 swapAmount = MIN_TOTAL_SUPPLY / 2; + deal( + address(weth), + address(lidoFixedPriceMulltiLpARM), + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - swapAmount + ); + deal(address(steth), address(lidoFixedPriceMulltiLpARM), swapAmount); // Empty stETH balance, so we can deal directly + + uint256 totalAssetsBefore = lidoFixedPriceMulltiLpARM.totalAssets(); + + // Request a redeem on Lido + uint256[] memory amounts = new uint256[](1); + amounts[0] = swapAmount; + lidoFixedPriceMulltiLpARM.requestStETHWithdrawalForETH(amounts); + + // Check total assets after withdrawal is the same as before + assertApproxEqAbs(lidoFixedPriceMulltiLpARM.totalAssets(), totalAssetsBefore, STETH_ERROR_ROUNDING); + } +} diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index 27f5d75..dae80e0 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -20,8 +20,9 @@ abstract contract Helpers is Base_Test_ { require(steth.balanceOf(address(wsteth)) >= amount, "Fork_Shared_Test_: Not enough stETH in WHALE_stETH"); if (amount == 0) { - vm.prank(to); + vm.startPrank(to); steth.transfer(address(0x1), steth.balanceOf(to)); + vm.stopPrank(); } else { // Transfer stETH from WHALE_stETH to the user. vm.prank(address(wsteth)); diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index 659fd9d..ae2570c 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -112,4 +112,26 @@ abstract contract Modifiers is Helpers { } _; } + + modifier claimRequestOnLidoFixedPriceMultiLpARM(address user, uint256 requestId) { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + vm.startPrank(user); + lidoFixedPriceMulltiLpARM.claimRedeem(requestId); + vm.stopPrank(); + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } + _; + } + + modifier skipTime(uint256 delay) { + skip(delay); + _; + } } From 40af28fee4252b32ea678960ceb2694ab522bc5f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 24 Sep 2024 21:10:26 +1000 Subject: [PATCH 072/196] Deposit now adds to the withdrawal queue Added test of deposit with outstanding withdrawal request --- src/contracts/MultiLP.sol | 9 ++- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 76 ++++++++++++++++++- test/fork/utils/Helpers.sol | 8 +- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index f6833b8..9bc40c8 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -101,6 +101,9 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // mint shares _mint(msg.sender, shares); + // Add WETH to the withdrawal queue if the queue has a shortfall + _addWithdrawalQueueLiquidity(); + _postDepositHook(assets); } @@ -156,10 +159,8 @@ abstract contract MultiLP is AbstractARM, 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) { - if (withdrawalRequests[requestId].queued > withdrawalQueueMetadata.claimable) { - // Add any WETH from the Dripper to the withdrawal queue - _addWithdrawalQueueLiquidity(); - } + // Update the ARM's withdrawal queue details + _addWithdrawalQueueLiquidity(); // Load the structs from storage into memory WithdrawalRequest memory request = withdrawalRequests[requestId]; diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 29aa0a5..13d0bbf 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -103,7 +103,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - /// @notice Test the simplest case of depositing into the ARM, first deposit of first user. + + /// @notice Depositing into the ARM, first deposit of first user. /// @dev No fees accrued, no withdrawals queued, and no performance fees generated function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_FirstDeposit_NoPerfs() public @@ -148,7 +149,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether } - /// @notice Test a simple case of depositing into the ARM, second deposit of first user. + /// @notice Depositing into the ARM, second deposit of first user. /// @dev No fees accrued, no withdrawals queued, and no performance fees generated function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_SecondDepositSameUser_NoPerfs() public @@ -194,7 +195,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } - /// @notice Test a simple case of depositing into the ARM, first deposit of second user. + /// @notice Depositing into the ARM, first deposit of second user. /// @dev No fees accrued, no withdrawals queued, and no performance fees generated function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_SecondDepositDiffUser_NoPerfs() public @@ -242,6 +243,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } + /// @notice Depositing into the ARM, first deposit of user with performance fees. + /// @dev No fees accrued yet, no withdrawals queued, and performance fee taken function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_FirstDeposit_WithPerfs() public setTotalAssetsCap(type(uint256).max) // No need to restrict it for this test. @@ -304,4 +307,71 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); } + + /// @notice Depositing into the ARM reserves WETH for the withdrawal queue. + /// @dev No fees accrued, withdrawal queue shortfall, and no performance fees generated + function test_Deposit_NoFeesAccrued_WithdrawalRequestsOutstanding_SecondDepositDiffUser_NoPerfs() + public + setTotalAssetsCap(DEFAULT_AMOUNT * 3 + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + setLiquidityProviderCap(alice, DEFAULT_AMOUNT * 5) + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // set stETH/WETH buy price to 1 + lidoFixedPriceMulltiLpARM.setPrices(1e36, 1e36 + 1); + + // User Swap stETH for 3/4 of WETH in the ARM + deal(address(steth), address(this), DEFAULT_AMOUNT); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + steth, weth, 3 * DEFAULT_AMOUNT / 4, DEFAULT_AMOUNT, address(this) + ); + + // First user requests a full withdrawal + uint256 firstUserShares = lidoFixedPriceMulltiLpARM.balanceOf(address(this)); + lidoFixedPriceMulltiLpARM.requestRedeem(firstUserShares); + + // Assertions Before + uint256 stethBalanceBefore = 3 * DEFAULT_AMOUNT / 4; + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), stethBalanceBefore, "stETH ARM balance before"); + uint256 wethBalanceBefore = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - 3 * DEFAULT_AMOUNT / 4; + assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), wethBalanceBefore, "WETH ARM balance before"); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether before"); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0, "Fees accrued before"); + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), 0, "alice shares before"); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); + assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before"); + assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); + + uint256 amount = DEFAULT_AMOUNT * 2; + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(alice, address(lidoFixedPriceMulltiLpARM), amount); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(0), alice, amount); // shares == amount here + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); + + vm.prank(alice); + // Main call + uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + + // Assertions After + assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), stethBalanceBefore, "stETH ARM balance after"); + assertEq( + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), wethBalanceBefore + amount, "WETH ARM balance after" + ); + assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether after"); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees + assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount, "last total assets after"); + assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), shares, "alice shares after"); + assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); + assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used + // withdrawal request is now claimable + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 0, 1); + assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether + } } diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index dae80e0..29c6bb4 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -42,10 +42,10 @@ abstract contract Helpers is Base_Test_ { ) public view { (uint256 queued, uint256 claimable, uint256 claimed, uint256 nextWithdrawalIndex) = lidoFixedPriceMulltiLpARM.withdrawalQueueMetadata(); - assertEq(queued, expectedQueued); - assertEq(claimable, expectedClaimable); - assertEq(claimed, expectedClaimed); - assertEq(nextWithdrawalIndex, expectedNextIndex); + assertEq(queued, expectedQueued, "metadata queued"); + assertEq(claimable, expectedClaimable, "metadata claimable"); + assertEq(claimed, expectedClaimed, "metadata claimed"); + assertEq(nextWithdrawalIndex, expectedNextIndex, "metadata nextWithdrawalIndex"); } /// @notice Asserts the equality bewteen value of `withdrawalRequests()` and the expected values. From f1c9ee4870872ce03a63706543c42b5a1d6f0b5a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 24 Sep 2024 21:17:11 +1000 Subject: [PATCH 073/196] Changed requestRedeem to use convertToAssets instead of previewRedeem --- src/contracts/MultiLP.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 9bc40c8..9f7b1a8 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -125,7 +125,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { _preWithdrawHook(); // Calculate the amount of assets to transfer to the redeemer - assets = previewRedeem(shares); + assets = convertToAssets(shares); requestId = withdrawalQueueMetadata.nextWithdrawalIndex; uint256 queued = withdrawalQueueMetadata.queued + assets; From 5130d6c89bb83d6e1c0a59a008fc15b36c67acd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Wed, 25 Sep 2024 02:30:05 +0200 Subject: [PATCH 074/196] Add more tests. (#22) * test: add more test for `totalAssets()`. * feat: add modifiers to simulate gain or loss assets in ARM. * fix: use boolean instead of int256. * fix: use Foundry stdError lib for expectRevert. * test: add setter test for LCP. * chore: fix lcov name in .gitignore * test: add tests for collectFees(). --- .gitignore | 2 +- .../CollectFees.t.sol | 67 +++++++++++++++++++ .../LidoFixedPriceMultiLpARM/Setters.t.sol | 25 +++++++ .../TotalAssets.t.sol | 52 ++++++++++++++ test/fork/utils/Modifiers.sol | 40 +++++++++++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol diff --git a/.gitignore b/.gitignore index 9b792cf..8a07b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ dependencies/ soldeer.lock # Coverage -lock.info* +lcov.info* # Defender Actions dist diff --git a/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol b/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol new file mode 100644 index 0000000..e84b8d6 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {PerformanceFee} from "contracts/PerformanceFee.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_CollectFees_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + /// @notice This test is expected to revert as almost all the liquidity is in stETH + function test_RevertWhen_CollectFees_Because_InsufficientLiquidity() + public + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(steth), true) + { + vm.expectRevert("ARM: insufficient liquidity"); + lidoFixedPriceMulltiLpARM.collectFees(); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_CollectFees_Once() + public + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + { + address feeCollector = lidoFixedPriceMulltiLpARM.feeCollector(); + uint256 fee = DEFAULT_AMOUNT * 20 / 100; + + // Expected Events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), feeCollector, fee); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit PerformanceFee.FeeCollected(feeCollector, fee); + + // Main call + uint256 claimedFee = lidoFixedPriceMulltiLpARM.collectFees(); + + // Assertions after + assertEq(claimedFee, fee); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); + } + + function test_CollectFees_Twice() + public + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + collectFeesOnLidoFixedPriceMultiLpARM + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + { + // Main call + uint256 claimedFee = lidoFixedPriceMulltiLpARM.collectFees(); + + // Assertions after + assertEq(claimedFee, DEFAULT_AMOUNT * 20 / 100); // This test should pass! + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index cca60cb..34d19c6 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -8,6 +8,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; import {IERC20} from "contracts/Interfaces.sol"; import {MultiLP} from "contracts/MultiLP.sol"; import {PerformanceFee} from "contracts/PerformanceFee.sol"; +import {LiquidityProviderControllerARM} from "contracts/LiquidityProviderControllerARM.sol"; contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// @@ -150,4 +151,28 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te vm.expectRevert("ARM: Only owner can call this function."); lidoFixedPriceMulltiLpARM.setOperator(address(0)); } + + ////////////////////////////////////////////////////// + /// --- LIQUIIDITY PROVIDER CONTROLLER - REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_LiquidityProviderController_SetLiquidityProvider_Because_NotOwner() + public + asRandomAddress + { + vm.expectRevert("ARM: Only owner can call this function."); + lidoFixedPriceMulltiLpARM.setLiquidityProviderController(address(0)); + } + + ////////////////////////////////////////////////////// + /// --- LIQUIIDITY PROVIDER CONTROLLER - PASSING TESTS + ////////////////////////////////////////////////////// + function test_LiquidityProviderController_SetLiquidityProvider() public asLidoFixedPriceMultiLpARMOwner { + address newLiquidityProviderController = vm.randomAddress(); + + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit LiquidityProviderControllerARM.LiquidityProviderControllerUpdated(newLiquidityProviderController); + lidoFixedPriceMulltiLpARM.setLiquidityProviderController(newLiquidityProviderController); + + assertEq(lidoFixedPriceMulltiLpARM.liquidityProviderController(), newLiquidityProviderController); + } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index a92e2a4..bfeb97c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +// Foundry +import {stdError} from "forge-std/StdError.sol"; + // Test imports import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; @@ -24,6 +27,23 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared // Approve STETH for Lido lidoFixedPriceMulltiLpARM.approveStETH(); + + deal(address(weth), address(this), 1_000 ether); + weth.approve(address(lidoFixedPriceMulltiLpARM), type(uint256).max); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TEST + ////////////////////////////////////////////////////// + function test_RevertWhen_TotalAssets_Because_MathError() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT * 2, address(weth), false) + { + vm.expectRevert(stdError.arithmeticError); + lidoFixedPriceMulltiLpARM.totalAssets(); } ////////////////////////////////////////////////////// @@ -133,4 +153,36 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared // Check total assets after withdrawal is the same as before assertApproxEqAbs(lidoFixedPriceMulltiLpARM.totalAssets(), totalAssetsBefore, STETH_ERROR_ROUNDING); } + + function test_TotalAssets_With_FeeAccrued_NotNull() public { + uint256 assetGain = DEFAULT_AMOUNT; + // Simulate asset gain + deal( + address(weth), + address(lidoFixedPriceMulltiLpARM), + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) + assetGain + ); + + // User deposit, this will trigger a fee calculation + lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT); + + // Assert fee accrued is not null + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), assetGain * 20 / 100); + + assertEq( + lidoFixedPriceMulltiLpARM.totalAssets(), + MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - assetGain * 20 / 100 + ); + } + + function test_TotalAssets_When_ARMIsInsolvent() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Simulate a loss of assets + deal(address(weth), address(lidoFixedPriceMulltiLpARM), DEFAULT_AMOUNT - 1); + + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), 0); + } } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index ae2570c..c5d7464 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -8,6 +8,9 @@ import {VmSafe} from "forge-std/Vm.sol"; import {Helpers} from "test/fork/utils/Helpers.sol"; import {MockCall} from "test/fork/utils/MockCall.sol"; +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; + abstract contract Modifiers is Helpers { /// @notice Impersonate Alice. modifier asAlice() { @@ -72,6 +75,7 @@ abstract contract Modifiers is Helpers { _; } + /// @notice Deposit WETH into the LidoFixedPriceMultiLpARM contract. modifier depositInLidoFixedPriceMultiLpARM(address user, uint256 amount) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); @@ -113,6 +117,7 @@ abstract contract Modifiers is Helpers { _; } + /// @notice Claim redeem from LidoFixedPriceMultiLpARM contract. modifier claimRequestOnLidoFixedPriceMultiLpARM(address user, uint256 requestId) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); @@ -130,6 +135,41 @@ abstract contract Modifiers is Helpers { _; } + /// @notice Simulate asset gain or loss in LidoFixedPriceMultiLpARM contract. + modifier simulateAssetGainInLidoFixedPriceMultiLpARM(uint256 assetGain, address token, bool gain) { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + if (gain) { + deal( + token, + address(lidoFixedPriceMulltiLpARM), + IERC20(token).balanceOf(address(lidoFixedPriceMulltiLpARM)) + uint256(assetGain) + ); + } else { + deal( + token, + address(lidoFixedPriceMulltiLpARM), + IERC20(token).balanceOf(address(lidoFixedPriceMulltiLpARM)) - uint256(assetGain) + ); + } + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } + _; + } + + /// @notice Collect fees on LidoFixedPriceMultiLpARM contract. + modifier collectFeesOnLidoFixedPriceMultiLpARM() { + lidoFixedPriceMulltiLpARM.collectFees(); + _; + } + + /// @notice Skip time by a given delay. modifier skipTime(uint256 delay) { skip(delay); _; From c0997d581a7ee020a0376f34796847d5cca05f8c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 25 Sep 2024 11:15:44 +1000 Subject: [PATCH 075/196] Fix performance fee calc not removing new perf fee from lastTotalAssets --- src/contracts/PerformanceFee.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index 398ddc5..351ddc1 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -79,10 +79,10 @@ abstract contract PerformanceFee is MultiLP { // Save the new accrued fees back to storage feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); - // Save the new total assets back to storage + // Save the new total assets back to storage less the new accrued fees. // This is be updated again in the post deposit and post withdraw hooks to include // the assets deposited or withdrawn - lastTotalAssets = SafeCast.toUint128(newTotalAssets); + lastTotalAssets = SafeCast.toUint128(newTotalAssets - newFeesAccrued); emit FeeCalculated(newFeesAccrued, assetIncrease); } From 79eaf5c86b3e847b3b5f606c2947b42cc57f5d64 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 25 Sep 2024 11:39:00 +1000 Subject: [PATCH 076/196] Write to storage once in requestStETHWithdrawalForETH --- src/contracts/LidoLiquidityManager.sol | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index e353a0a..76235b0 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -42,10 +42,14 @@ abstract contract LidoLiquidityManager is OwnableOperable { { requestIds = withdrawalQueue.requestWithdrawals(amounts, address(this)); - // Increase the Ether outstanding from the Lido Withdrawal Queue + // Sum the total amount of stETH being withdraw + uint256 totalAmountRequested = 0; for (uint256 i = 0; i < amounts.length; i++) { - outstandingEther += amounts[i]; + totalAmountRequested += amounts[i]; } + + // Increase the Ether outstanding from the Lido Withdrawal Queue + outstandingEther += totalAmountRequested; } /** @@ -67,7 +71,7 @@ abstract contract LidoLiquidityManager is OwnableOperable { // Wrap all the received ETH to WETH. (bool success,) = address(weth).call{value: etherAfter}(new bytes(0)); - require(success, "OSwap: ETH transfer failed"); + require(success, "ARM: ETH transfer failed"); _postClaimHook(etherAfter); } From 9b981107babaf1640ab7e77c07d0b1d89c6ee1a6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 25 Sep 2024 11:53:02 +1000 Subject: [PATCH 077/196] Changed the preview functions to be external now that are no longer being used internally --- src/contracts/MultiLP.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 9f7b1a8..69a9fe2 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -82,7 +82,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { /// @notice Preview the amount of shares that would be minted for a given amount of assets /// @param assets The amount of liquidity assets to deposit /// @return shares The amount of shares that would be minted - function previewDeposit(uint256 assets) public view returns (uint256 shares) { + function previewDeposit(uint256 assets) external view returns (uint256 shares) { shares = convertToShares(assets); } @@ -113,7 +113,7 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { /// @notice Preview the amount of assets that would be received for burning a given amount of shares /// @param shares The amount of shares to burn /// @return assets The amount of liquidity assets that would be received - function previewRedeem(uint256 shares) public view returns (uint256 assets) { + function previewRedeem(uint256 shares) external view returns (uint256 assets) { assets = convertToAssets(shares); } From 202915febe85c48c3d8f1bba35da8aa0233cb6b2 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 25 Sep 2024 11:53:59 +1000 Subject: [PATCH 078/196] Generated latest contract diagram --- ...LidoFixedPriceMultiLpARMPublicSquashed.svg | 86 +++++++++++++++++++ docs/LidoFixedPriceMultiLpARMSquashed.svg | 60 ++++++------- 2 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 docs/LidoFixedPriceMultiLpARMPublicSquashed.svg diff --git a/docs/LidoFixedPriceMultiLpARMPublicSquashed.svg b/docs/LidoFixedPriceMultiLpARMPublicSquashed.svg new file mode 100644 index 0000000..dea5511 --- /dev/null +++ b/docs/LidoFixedPriceMultiLpARMPublicSquashed.svg @@ -0,0 +1,86 @@ + + + + + + +UmlClassDiagram + + + +15 + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   FEE_SCALE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   liquidityProviderController: address <<LiquidityProviderControllerARM>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   MAX_PRICE_DEVIATION: uint256 <<FixedPriceARM>> +   PRICE_SCALE: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> +    collectFees(): (fees: uint256) <<PerformanceFee>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<LiquidityProviderControllerARM>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> +    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> +    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<LiquidityProviderControllerARM>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> +    constructor(_liquidityAsset: address) <<MultiLP>> +    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> +    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> +    totalAssets(): uint256 <<LidoFixedPriceMultiLpARM>> +    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>> +    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>> +    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoFixedPriceMultiLpARM>> + + + diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index 55bf342..eab636e 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -85,36 +85,36 @@    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> -    collectFees(): (fees: uint256) <<PerformanceFee>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<LiquidityProviderControllerARM>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> -    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> -    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<LiquidityProviderControllerARM>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> -    constructor(_liquidityAsset: address) <<MultiLP>> -    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> -    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> +    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> +    deposit(assets: uint256): (shares: uint256) <<MultiLP>> +    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> +    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> +    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> +    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> +    collectFees(): (fees: uint256) <<PerformanceFee>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<LiquidityProviderControllerARM>> +    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> +    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> +    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> +    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> +    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<LiquidityProviderControllerARM>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> +    constructor(_liquidityAsset: address) <<MultiLP>>    totalAssets(): uint256 <<LidoFixedPriceMultiLpARM>>    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>>    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>> From 9d255ffcdd54c839bd045c2bafe961df2ca93750 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 25 Sep 2024 12:31:23 +1000 Subject: [PATCH 079/196] Added more detail to the invalid token error --- src/contracts/FixedPriceARM.sol | 12 ++-- .../SwapExactTokensForTokens.t.sol | 17 +++++- .../SwapTokensForExactTokens.t.sol | 17 +++++- test/smoke/LidoARMSmokeTest.t.sol | 58 ------------------- 4 files changed, 34 insertions(+), 70 deletions(-) diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol index c203b71..1e9e60f 100644 --- a/src/contracts/FixedPriceARM.sol +++ b/src/contracts/FixedPriceARM.sol @@ -43,13 +43,13 @@ abstract contract FixedPriceARM is AbstractARM { { uint256 price; if (inToken == token0) { - require(outToken == token1, "ARM: Invalid token"); + require(outToken == token1, "ARM: Invalid out token"); price = traderate0; } else if (inToken == token1) { - require(outToken == token0, "ARM: Invalid token"); + require(outToken == token0, "ARM: Invalid out token"); price = traderate1; } else { - revert("ARM: Invalid token"); + revert("ARM: Invalid in token"); } amountOut = amountIn * price / PRICE_SCALE; @@ -68,13 +68,13 @@ abstract contract FixedPriceARM is AbstractARM { { uint256 price; if (inToken == token0) { - require(outToken == token1, "ARM: Invalid token"); + require(outToken == token1, "ARM: Invalid out token"); price = traderate0; } else if (inToken == token1) { - require(outToken == token0, "ARM: Invalid token"); + require(outToken == token0, "ARM: Invalid out token"); price = traderate1; } else { - revert("ARM: Invalid token"); + revert("ARM: Invalid in token"); } amountIn = ((amountOut * PRICE_SCALE) / price) + 1; // +1 to always round in our favor diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index fc38630..c2fd61e 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -35,7 +35,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is ////////////////////////////////////////////////////// function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut1() public { lidoFixedPriceMulltiLpARM.token0(); - vm.expectRevert("ARM: Invalid token"); + vm.expectRevert("ARM: Invalid out token"); lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( steth, // inToken badToken, // outToken @@ -46,7 +46,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is } function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut0() public { - vm.expectRevert("ARM: Invalid token"); + vm.expectRevert("ARM: Invalid out token"); lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( weth, // inToken badToken, // outToken @@ -57,7 +57,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is } function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenIn() public { - vm.expectRevert("ARM: Invalid token"); + vm.expectRevert("ARM: Invalid in token"); lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( badToken, // inToken steth, // outToken @@ -67,6 +67,17 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is ); } + function test_RevertWhen_SwapExactTokensForTokens_Because_BothInvalidTokens() public { + vm.expectRevert("ARM: Invalid in token"); + lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + badToken, // inToken + badToken, // outToken + 1, // amountIn + 1, // amountOutMin + address(this) // to + ); + } + function test_RevertWhen_SwapExactTokensForTokens_Because_NotEnoughTokenIn() public { uint256 initialBalance = weth.balanceOf(address(this)); diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol index e3f8e17..edfa5ff 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -35,7 +35,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is ////////////////////////////////////////////////////// function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut1() public { lidoFixedPriceMulltiLpARM.token0(); - vm.expectRevert("ARM: Invalid token"); + vm.expectRevert("ARM: Invalid out token"); lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( steth, // inToken badToken, // outToken @@ -46,7 +46,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is } function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut0() public { - vm.expectRevert("ARM: Invalid token"); + vm.expectRevert("ARM: Invalid out token"); lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( weth, // inToken badToken, // outToken @@ -57,7 +57,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is } function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenIn() public { - vm.expectRevert("ARM: Invalid token"); + vm.expectRevert("ARM: Invalid in token"); lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( badToken, // inToken steth, // outToken @@ -67,6 +67,17 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is ); } + function test_RevertWhen_SwapTokensForExactTokens_Because_BothInvalidTokens() public { + vm.expectRevert("ARM: Invalid in token"); + lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + badToken, // inToken + badToken, // outToken + 1, // amountOut + 1, // amountOutMax + address(this) // to + ); + } + function test_RevertWhen_SwapTokensForExactTokens_Because_NotEnoughTokenIn() public { deal(address(weth), address(this), 0); diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 2e0ad96..081f8e1 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -126,64 +126,6 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { lidoARM.setOwner(RANDOM_ADDRESS); } - function test_wrongInTokenExactIn() external { - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapExactTokensForTokens(BAD_TOKEN, steth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapExactTokensForTokens(steth, steth, 10 ether, 0, address(this)); - } - - function test_wrongOutTokenExactIn() external { - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 10 ether, address(this)); - } - - function test_wrongInTokenExactOut() external { - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(BAD_TOKEN, steth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 10 ether, address(this)); - } - - function test_wrongOutTokenExactOut() external { - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, BAD_TOKEN, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid token"); - lidoARM.swapTokensForExactTokens(steth, steth, 10 ether, 10 ether, address(this)); - } - - // function test_collectTokens() external { - // vm.startPrank(Mainnet.TIMELOCK); - - // lidoARM.transferToken(address(weth), address(this), weth.balanceOf(address(lidoARM))); - // assertGt(weth.balanceOf(address(this)), 50 ether); - // assertEq(weth.balanceOf(address(lidoARM)), 0); - - // lidoARM.transferToken(address(steth), address(this), steth.balanceOf(address(lidoARM))); - // assertGt(steth.balanceOf(address(this)), 50 ether); - // assertLt(steth.balanceOf(address(lidoARM)), 3); - - // vm.stopPrank(); - // } - function _dealStETH(address to, uint256 amount) internal { vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); steth.transfer(to, amount + 2); From 4c58acd70b1af35bc5fbc62ae76f414ec1ee0cc4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 25 Sep 2024 14:05:07 +1000 Subject: [PATCH 080/196] Fix compile warning in test --- test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index bfeb97c..fe3286e 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -49,7 +49,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared ////////////////////////////////////////////////////// /// --- PASSING TEST ////////////////////////////////////////////////////// - function test_TotalAssets_AfterInitialization() public { + function test_TotalAssets_AfterInitialization() public view { assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); } From ad1445d6040e799c333f202fbec33a4b061e475e Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Wed, 25 Sep 2024 22:21:35 +1000 Subject: [PATCH 081/196] Ensure swaps don't use withdrawal queue liquidity (#23) * Ensure swaps don't use WETH reserved for the withdrawal queue * Fund the withdrawal queue when WETH is swapped into the ARM * More refactoring of MultiLP deposit and swap not longer add to the withdrawal queue. Its only need on claim * Refactor withdrawal queue storage to optimize swap gas usage * Generated latest contract diagrams * Removed _postClaimHook as it's no longer needed * Added more Natspec * Add more Natspec * Changed the _postWithdrawHook to _postRequestRedeemHook * Generated latest contract diagrams * Minor change to constructor param --- docs/LidoFixedPriceMultiLpARMHierarchy.svg | 32 ++-- docs/LidoFixedPriceMultiLpARMSquashed.svg | 142 +++++++++--------- docs/OEthARMHierarchy.svg | 56 +++---- docs/OEthARMSquashed.svg | 66 ++++---- src/contracts/AbstractARM.sol | 10 ++ src/contracts/FixedPriceARM.sol | 18 +-- src/contracts/LidoFixedPriceMultiLpARM.sol | 52 ++++--- src/contracts/LidoLiquidityManager.sol | 4 - src/contracts/MultiLP.sol | 114 ++++++++------ src/contracts/PerformanceFee.sol | 6 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 2 +- .../SwapExactTokensForTokens.t.sol | 2 +- test/fork/utils/Helpers.sol | 12 +- 13 files changed, 272 insertions(+), 244 deletions(-) diff --git a/docs/LidoFixedPriceMultiLpARMHierarchy.svg b/docs/LidoFixedPriceMultiLpARMHierarchy.svg index ffb8bb9..1c83217 100644 --- a/docs/LidoFixedPriceMultiLpARMHierarchy.svg +++ b/docs/LidoFixedPriceMultiLpARMHierarchy.svg @@ -17,16 +17,16 @@ AbstractARM ../src/contracts/AbstractARM.sol - + -23 +22 OwnableOperable ../src/contracts/OwnableOperable.sol - + -0->23 +0->22 @@ -99,23 +99,23 @@ - + -26 +25 <<Abstract>> PerformanceFee ../src/contracts/PerformanceFee.sol - + -13->26 +13->25 - + -14->23 +14->22 @@ -131,22 +131,22 @@ - + -22 +21 Ownable ../src/contracts/Ownable.sol - + -23->22 +22->21 - + -26->17 +25->17 diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg index eab636e..525a2dc 100644 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ b/docs/LidoFixedPriceMultiLpARMSquashed.svg @@ -4,78 +4,82 @@ - - + + UmlClassDiagram - + 13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[47] <<MultiLP>> -   _gap: uint256[48] <<PerformanceFee>> -   _gap: uint256[49] <<LiquidityProviderControllerARM>> -   _gap: uint256[48] <<FixedPriceARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityAsset: address <<MultiLP>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   FEE_SCALE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   liquidityProviderController: address <<LiquidityProviderControllerARM>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   MAX_PRICE_DEVIATION: uint256 <<FixedPriceARM>> -   PRICE_SCALE: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postWithdrawHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<MultiLP>> -    _externalWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> -    _rawTotalAssets(): uint256 <<PerformanceFee>> -    _setFee(_fee: uint256) <<PerformanceFee>> -    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> -    _initLPControllerARM(_liquidityProviderController: address) <<LiquidityProviderControllerARM>> -    _calcTransferAmount(outToken: address, amount: uint256): (transferAmount: uint256) <<LidoFixedPriceMultiLpARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -    _postClaimHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> + +LidoFixedPriceMultiLpARM +../src/contracts/LidoFixedPriceMultiLpARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[47] <<MultiLP>> +   _gap: uint256[48] <<PerformanceFee>> +   _gap: uint256[49] <<LiquidityProviderControllerARM>> +   _gap: uint256[48] <<FixedPriceARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> +   DEAD_ACCOUNT: address <<MultiLP>> +   liquidityAsset: address <<MultiLP>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<MultiLP>> +   withdrawsQueued: uint128 <<MultiLP>> +   withdrawsClaimed: uint128 <<MultiLP>> +   withdrawsClaimable: uint128 <<MultiLP>> +   nextWithdrawalIndex: uint128 <<MultiLP>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> +   FEE_SCALE: uint256 <<PerformanceFee>> +   feeCollector: address <<PerformanceFee>> +   fee: uint16 <<PerformanceFee>> +   feesAccrued: uint112 <<PerformanceFee>> +   lastTotalAssets: uint128 <<PerformanceFee>> +   liquidityProviderController: address <<LiquidityProviderControllerARM>> +   traderate0: uint256 <<FixedPriceARM>> +   traderate1: uint256 <<FixedPriceARM>> +   MAX_PRICE_DEVIATION: uint256 <<FixedPriceARM>> +   PRICE_SCALE: uint256 <<FixedPriceARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<LidoFixedPriceMultiLpARM>> +    _transferAssetFrom(asset: address, from: address, to: address, amount: uint256) <<AbstractARM>> +    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> +    _preDepositHook() <<PerformanceFee>> +    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> +    _preWithdrawHook() <<PerformanceFee>> +    _postRequestRedeemHook() <<LidoFixedPriceMultiLpARM>> +    _updateWithdrawalQueueLiquidity() <<MultiLP>> +    _liquidityAvailable(): uint256 <<MultiLP>> +    _externalWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> +    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> +    _calcFee() <<PerformanceFee>> +    _rawTotalAssets(): uint256 <<PerformanceFee>> +    _setFee(_fee: uint256) <<PerformanceFee>> +    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> +    _initLPControllerARM(_liquidityProviderController: address) <<LiquidityProviderControllerARM>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> External:    <<payable>> null() <<LidoLiquidityManager>>    owner(): address <<Ownable>> @@ -118,7 +122,7 @@    totalAssets(): uint256 <<LidoFixedPriceMultiLpARM>>    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>>    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>> -    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoFixedPriceMultiLpARM>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoFixedPriceMultiLpARM>> diff --git a/docs/OEthARMHierarchy.svg b/docs/OEthARMHierarchy.svg index 2f99897..89dbae3 100644 --- a/docs/OEthARMHierarchy.svg +++ b/docs/OEthARMHierarchy.svg @@ -17,95 +17,95 @@ AbstractARM ../src/contracts/AbstractARM.sol - + -24 +22 OwnableOperable ../src/contracts/OwnableOperable.sol - + -0->24 +0->22 - + -21 +19 OethARM ../src/contracts/OethARM.sol - + -22 +20 OethLiquidityManager ../src/contracts/OethLiquidityManager.sol - + -21->22 +19->20 - + -25 +23 <<Abstract>> OwnerLP ../src/contracts/OwnerLP.sol - + -21->25 +19->23 - + -26 +24 <<Abstract>> PeggedARM ../src/contracts/PeggedARM.sol - + -21->26 +19->24 - + -22->24 +20->22 - + -23 +21 Ownable ../src/contracts/Ownable.sol - + -24->23 +22->21 - + -25->23 +23->21 - + -26->0 +24->0 diff --git a/docs/OEthARMSquashed.svg b/docs/OEthARMSquashed.svg index 516cd18..62cc57f 100644 --- a/docs/OEthARMSquashed.svg +++ b/docs/OEthARMSquashed.svg @@ -4,40 +4,42 @@ - - + + UmlClassDiagram - - + + -21 - -OethARM -../src/contracts/OethARM.sol - -Private: -   _gap: uint256[50] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   bothDirections: bool <<PeggedARM>> -   oeth: address <<OethLiquidityManager>> -   oethVault: address <<OethLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<PeggedARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<PeggedARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> +19 + +OethARM +../src/contracts/OethARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +Public: +   operator: address <<OwnableOperable>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   bothDirections: bool <<PeggedARM>> +   oeth: address <<OethLiquidityManager>> +   oethVault: address <<OethLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<PeggedARM>> +    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<PeggedARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<AbstractARM>> +    _transferAssetFrom(asset: address, from: address, to: address, amount: uint256) <<AbstractARM>>    _swap(inToken: IERC20, outToken: IERC20, amount: uint256, to: address): uint256 <<PeggedARM>>    _approvals() <<OethLiquidityManager>> External: diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index f56e200..8132261 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -154,4 +154,14 @@ 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); + } + + /// @dev Hook to transfer assets into the ARM contract + function _transferAssetFrom(address asset, address from, address to, uint256 amount) internal virtual { + IERC20(asset).transferFrom(from, to, amount); + } } diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol index 1e9e60f..c6587aa 100644 --- a/src/contracts/FixedPriceARM.sol +++ b/src/contracts/FixedPriceARM.sol @@ -54,11 +54,10 @@ abstract contract FixedPriceARM is AbstractARM { amountOut = amountIn * price / PRICE_SCALE; // Transfer the input tokens from the caller to this ARM contract - inToken.transferFrom(msg.sender, address(this), amountIn); + _transferAssetFrom(address(inToken), 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) @@ -79,19 +78,10 @@ abstract contract FixedPriceARM is AbstractARM { amountIn = ((amountOut * PRICE_SCALE) / price) + 1; // +1 to always round in our favor // Transfer the input tokens from the caller to this ARM contract - inToken.transferFrom(msg.sender, address(this), amountIn); + _transferAssetFrom(address(inToken), 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); } /** diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoFixedPriceMultiLpARM.sol index be7fd25..5b2a1a8 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoFixedPriceMultiLpARM.sol @@ -14,6 +14,9 @@ import {PerformanceFee} from "./PerformanceFee.sol"; /** * @title Lido (stETH) Application Redemption Manager (ARM) * @dev This implementation supports multiple Liquidity Providers (LPs) with single buy and sell prices. + * It also integrates to a LiquidityProviderController contract that caps the amount of assets a liquidity provider + * can deposit and caps the ARM's total assets. + * A performance fee is also collected on increases in the ARM's total assets. * @author Origin Protocol Inc */ contract LidoFixedPriceMultiLpARM is @@ -24,17 +27,18 @@ contract LidoFixedPriceMultiLpARM is FixedPriceARM, LidoLiquidityManager { - /// @param _stEth The address of the stETH token + /// @param _steth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract - constructor(address _stEth, address _weth, address _lidoWithdrawalQueue) - AbstractARM(_stEth, _weth) + constructor(address _steth, address _weth, address _lidoWithdrawalQueue) + AbstractARM(_steth, _weth) MultiLP(_weth) FixedPriceARM() - LidoLiquidityManager(_stEth, _weth, _lidoWithdrawalQueue) + LidoLiquidityManager(_steth, _weth, _lidoWithdrawalQueue) {} /// @notice Initialize the contract. + /// The deployer that calls initialize has to approve the this ARM's proxy contract to transfer 1e12 WETH. /// @param _name The name of the liquidity provider (LP) token. /// @param _symbol The symbol of the liquidity provider (LP) token. /// @param _operator The address of the account that can request and claim Lido withdrawals. @@ -58,25 +62,28 @@ contract LidoFixedPriceMultiLpARM is } /** - * @notice Calculate transfer amount for outToken. - * 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. + * @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 not 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); } + /** + * @dev Calculates the amount of stETH in the Lido Withdrawal Queue. + */ function _externalWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { return LidoLiquidityManager._externalWithdrawQueue(); } + /** + * @dev Is called after assets are transferred to the ARM in the `deposit` method. + */ function _postDepositHook(uint256 assets) internal override(MultiLP, LiquidityProviderControllerARM, PerformanceFee) @@ -88,15 +95,18 @@ contract LidoFixedPriceMultiLpARM is LiquidityProviderControllerARM._postDepositHook(assets); } - function _postWithdrawHook(uint256 assets) internal override(MultiLP, PerformanceFee) { + /** + * @dev Is called after the performance fee is accrued in the `requestRedeem` method. + */ + function _postRequestRedeemHook() internal override(MultiLP, PerformanceFee) { // Store the new total assets after the withdrawal and performance fee accrued - PerformanceFee._postWithdrawHook(assets); - } - - function _postClaimHook(uint256 assets) internal override { - // do nothing + PerformanceFee._postRequestRedeemHook(); } + /** + * @notice The total amount of assets in the ARM and Lido withdrawal queue, + * less the WETH reserved for the ARM's withdrawal queue and accrued fees. + */ function totalAssets() public view override(MultiLP, PerformanceFee) returns (uint256) { return PerformanceFee.totalAssets(); } diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 76235b0..311438b 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -72,12 +72,8 @@ abstract contract LidoLiquidityManager is OwnableOperable { // Wrap all the received ETH to WETH. (bool success,) = address(weth).call{value: etherAfter}(new bytes(0)); require(success, "ARM: ETH transfer failed"); - - _postClaimHook(etherAfter); } - function _postClaimHook(uint256 assets) internal virtual; - function _externalWithdrawQueue() internal view virtual returns (uint256 assets) { return outstandingEther; } diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol index 69a9fe2..651927e 100644 --- a/src/contracts/MultiLP.sol +++ b/src/contracts/MultiLP.sol @@ -21,24 +21,14 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { /// @notice The address of the asset that is used to add and remove liquidity. eg WETH address internal immutable liquidityAsset; - struct WithdrawalQueueMetadata { - // cumulative total of all withdrawal requests included the ones that have already been claimed - uint128 queued; - // cumulative total of all the requests that can be claimed including the ones that have already been claimed - uint128 claimable; - // total of all the requests that have been claimed - uint128 claimed; - // index of the next withdrawal request starting at 0 - uint128 nextWithdrawalIndex; - } - - /// @notice Global metadata for the withdrawal queue including: - /// queued - cumulative total of all withdrawal requests included the ones that have already been claimed - /// claimable - cumulative total of all the requests that can be claimed including the ones already claimed - /// claimed - total of all the requests that have been claimed - /// nextWithdrawalIndex - index of the next withdrawal request starting at 0 - // slither-disable-next-line uninitialized-state - WithdrawalQueueMetadata public withdrawalQueueMetadata; + /// @notice cumulative total of all withdrawal requests included 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 cumulative total of all the withdrawal requests that can be claimed including the ones already claimed + uint128 public withdrawsClaimable; + /// @notice index of the next withdrawal request starting at 0 + uint128 public nextWithdrawalIndex; struct WithdrawalRequest { address withdrawer; @@ -101,9 +91,6 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // mint shares _mint(msg.sender, shares); - // Add WETH to the withdrawal queue if the queue has a shortfall - _addWithdrawalQueueLiquidity(); - _postDepositHook(assets); } @@ -127,55 +114,54 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // Calculate the amount of assets to transfer to the redeemer assets = convertToAssets(shares); - requestId = withdrawalQueueMetadata.nextWithdrawalIndex; - uint256 queued = withdrawalQueueMetadata.queued + assets; - uint256 claimTimestamp = block.timestamp + CLAIM_DELAY; + requestId = nextWithdrawalIndex; + uint128 queued = SafeCast.toUint128(withdrawsQueued + assets); + uint40 claimTimestamp = uint40(block.timestamp + CLAIM_DELAY); // Store the next withdrawal request - withdrawalQueueMetadata.nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); + nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); // Store the updated queued amount which reserves WETH in the withdrawal queue - withdrawalQueueMetadata.queued = SafeCast.toUint128(queued); + withdrawsQueued = queued; // Store requests withdrawalRequests[requestId] = WithdrawalRequest({ withdrawer: msg.sender, claimed: false, - claimTimestamp: uint40(claimTimestamp), + claimTimestamp: claimTimestamp, assets: SafeCast.toUint128(assets), - queued: SafeCast.toUint128(queued) + queued: queued }); // burn redeemer's shares _burn(msg.sender, shares); - _postWithdrawHook(assets); + _postRequestRedeemHook(); emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); } function _preWithdrawHook() internal virtual; - function _postWithdrawHook(uint256 assets) internal virtual; + function _postRequestRedeemHook() internal virtual; /// @notice Claim liquidity assets from a previous withdrawal request after the claim delay has passed /// @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) { - // Update the ARM's withdrawal queue details - _addWithdrawalQueueLiquidity(); + // Update the ARM's withdrawal queue's claimable amount + _updateWithdrawalQueueLiquidity(); // Load the structs from storage into memory WithdrawalRequest memory request = withdrawalRequests[requestId]; - WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; require(request.claimTimestamp <= block.timestamp, "Claim delay not met"); // If there isn't enough reserved liquidity in the queue to claim - require(request.queued <= queue.claimable, "Queue pending liquidity"); + require(request.queued <= withdrawsClaimable, "Queue pending liquidity"); require(request.withdrawer == msg.sender, "Not requester"); require(request.claimed == false, "Already claimed"); // Store the request as claimed withdrawalRequests[requestId].claimed = true; // Store the updated claimed amount - withdrawalQueueMetadata.claimed = queue.claimed + request.assets; + withdrawsClaimed += request.assets; assets = request.assets; @@ -185,37 +171,65 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { IERC20(liquidityAsset).transfer(msg.sender, assets); } - /// @dev Adds liquidity to the withdrawal queue if there is a funding shortfall. - function _addWithdrawalQueueLiquidity() internal returns (uint256 addedClaimable) { - WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; + /// @dev Updates the claimable amount in the ARM's withdrawal queue. + /// That's the amount that is used to check if a request can be claimed or not. + function _updateWithdrawalQueueLiquidity() internal { + // Load the claimable amount from storage into memory + uint256 withdrawsClaimableMem = withdrawsClaimable; // Check if the claimable amount is less than the queued amount - uint256 queueShortfall = queue.queued - queue.claimable; + uint256 queueShortfall = withdrawsQueued - withdrawsClaimableMem; - // 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; + return; } uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this)); // Of the claimable withdrawal requests, how much is unclaimed? // That is, the amount of the liquidity assets that is currently allocated for the withdrawal queue - uint256 allocatedLiquidity = queue.claimable - queue.claimed; + uint256 allocatedLiquidity = withdrawsClaimableMem - withdrawsClaimed; // If there is no unallocated liquidity assets then there is nothing to add to the queue if (liquidityBalance <= allocatedLiquidity) { - return 0; + return; } uint256 unallocatedLiquidity = liquidityBalance - allocatedLiquidity; // the new claimable amount is the smaller of the queue shortfall or unallocated weth - addedClaimable = queueShortfall < unallocatedLiquidity ? queueShortfall : unallocatedLiquidity; - uint256 newClaimable = queue.claimable + addedClaimable; + uint256 addedClaimable = queueShortfall < unallocatedLiquidity ? queueShortfall : unallocatedLiquidity; // Store the new claimable amount back to storage - withdrawalQueueMetadata.claimable = SafeCast.toUint128(newClaimable); + withdrawsClaimable = SafeCast.toUint128(withdrawsClaimableMem + addedClaimable); + } + + /// @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) { + // The amount of WETH that is still to be claimed in the withdrawal queue + uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; + + // 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 { + if (asset == liquidityAsset) { + require(amount <= _liquidityAvailable(), "ARM: Insufficient liquidity"); + } + + IERC20(asset).transfer(to, amount); } /// @notice The total amount of assets in the ARM and external withdrawal queue, @@ -224,16 +238,18 @@ abstract contract MultiLP is AbstractARM, ERC20Upgradeable { // Get the assets in the ARM and external withdrawal queue assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); - WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; + // Load the queue metadata from storage into memory + uint256 queuedMem = withdrawsQueued; + uint256 claimedMem = withdrawsClaimed; // If the ARM becomes insolvent enough that the total value in the ARM and external withdrawal queue // is less than the outstanding withdrawals. - if (assets + queue.claimed < queue.queued) { + if (assets + claimedMem < queuedMem) { return 0; } // Need to remove the liquidity assets that have been reserved for the withdrawal queue - return assets + queue.claimed - queue.queued; + return assets + claimedMem - queuedMem; } /// @notice Calculates the amount of shares for a given amount of liquidity assets diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol index 351ddc1..32abc8c 100644 --- a/src/contracts/PerformanceFee.sol +++ b/src/contracts/PerformanceFee.sol @@ -61,8 +61,8 @@ abstract contract PerformanceFee is MultiLP { lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); } - /// @dev Save the new total assets after the withdrawal and performance fee accrued - function _postWithdrawHook(uint256) internal virtual override { + /// @dev Save the new total assets after the requestRedeem and performance fee accrued + function _postRequestRedeemHook() internal virtual override { lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); } @@ -87,6 +87,8 @@ abstract contract PerformanceFee is MultiLP { emit FeeCalculated(newFeesAccrued, assetIncrease); } + /// @notice The total amount of assets in the ARM and external withdrawal queue, + /// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees. function totalAssets() public view virtual override returns (uint256) { uint256 totalAssetsBeforeFees = _rawTotalAssets(); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 13d0bbf..3878c93 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -371,7 +371,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used // withdrawal request is now claimable - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 0, 1); + assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index c2fd61e..c920476 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -116,7 +116,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is initialBalance = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); deal(address(steth), address(this), initialBalance * 2); - vm.expectRevert(); // Lido error + vm.expectRevert("ARM: Insufficient liquidity"); lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( steth, // inToken weth, // outToken diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index 29c6bb4..448aa2a 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -33,19 +33,17 @@ abstract contract Helpers is Base_Test_ { } } - /// @notice Asserts the equality bewteen value of `withdrawalQueueMetadata()` and the expected values. + /// @notice Asserts the equality between value of `withdrawalQueueMetadata()` and the expected values. function assertEqQueueMetadata( uint256 expectedQueued, uint256 expectedClaimable, uint256 expectedClaimed, uint256 expectedNextIndex ) public view { - (uint256 queued, uint256 claimable, uint256 claimed, uint256 nextWithdrawalIndex) = - lidoFixedPriceMulltiLpARM.withdrawalQueueMetadata(); - assertEq(queued, expectedQueued, "metadata queued"); - assertEq(claimable, expectedClaimable, "metadata claimable"); - assertEq(claimed, expectedClaimed, "metadata claimed"); - assertEq(nextWithdrawalIndex, expectedNextIndex, "metadata nextWithdrawalIndex"); + assertEq(lidoFixedPriceMulltiLpARM.withdrawsQueued(), expectedQueued, "metadata queued"); + assertEq(lidoFixedPriceMulltiLpARM.withdrawsClaimable(), expectedClaimable, "metadata claimable"); + assertEq(lidoFixedPriceMulltiLpARM.withdrawsClaimed(), expectedClaimed, "metadata claimed"); + assertEq(lidoFixedPriceMulltiLpARM.nextWithdrawalIndex(), expectedNextIndex, "metadata nextWithdrawalIndex"); } /// @notice Asserts the equality bewteen value of `withdrawalRequests()` and the expected values. From 5387d79da6b1abdeb4398ebc1b6fadeb365cf77b Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 25 Sep 2024 16:47:31 -0400 Subject: [PATCH 082/196] Don't allow prices to cross 1 --- test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 34d19c6..7accd17 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -111,6 +111,16 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te lidoFixedPriceMulltiLpARM.setPrices(0, 0); } + function test_SellPriceCannotCrossOne() public asLidoFixedPriceMulltiLpARMOperator { + vm.expectRevert("ARM: sell price too low"); + lidoFixedPriceMulltiLpARM.setPrices(0.9997 * 1e36, 0.99999 * 1e36); + } + + function test_BuyPriceCannotCrossOne() public asLidoFixedPriceMulltiLpARMOperator { + vm.expectRevert("ARM: buy price too high"); + lidoFixedPriceMulltiLpARM.setPrices(1.0 * 1e36, 1.0001 * 1e36); + } + ////////////////////////////////////////////////////// /// --- FIXED PRICE ARM - PASSING TESTS ////////////////////////////////////////////////////// From 59ebf117d09815c3e58650ffe65c8cf546a5ff6b Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Thu, 26 Sep 2024 10:17:38 +1000 Subject: [PATCH 083/196] Bring in the latest tests (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add test for requestStETHWithdrawamForETH. * chore: use last version of forge for `mockFunction()`. * feat: create a fake Lido Withdraw to mock it when testing. * [WIP] test: add test for `claimStETHWithdrawalForWETH()`. * fix: tests for claimWithdrawals. * fix: typo on variable name. --------- Co-authored-by: Clément --- foundry.toml | 2 +- src/contracts/Interfaces.sol | 1 + test/Base.sol | 2 +- .../ClaimRedeem.t.sol | 116 ++++----- .../ClaimStETHWithdrawalForWETH.t.sol | 100 ++++++++ .../CollectFees.t.sol | 14 +- .../Constructor.t.sol | 22 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 220 +++++++++--------- .../RequestRedeem.t.sol | 126 +++++----- .../RequestStETHWithdrawalForETH.t.sol | 124 ++++++++++ .../LidoFixedPriceMultiLpARM/Setters.t.sol | 80 +++---- .../SwapExactTokensForTokens.t.sol | 100 ++++---- .../SwapTokensForExactTokens.t.sol | 96 ++++---- .../TotalAssets.t.sol | 63 +++-- test/fork/shared/Shared.sol | 6 +- test/fork/utils/Helpers.sol | 10 +- test/fork/utils/MockCall.sol | 43 ++++ test/fork/utils/Modifiers.sol | 96 +++++++- 18 files changed, 781 insertions(+), 440 deletions(-) create mode 100644 test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol create mode 100644 test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol diff --git a/foundry.toml b/foundry.toml index 7be56ab..e755671 100644 --- a/foundry.toml +++ b/foundry.toml @@ -23,9 +23,9 @@ remappings = [ runs = 1_000 [dependencies] -forge-std = "1.9.2" "@openzeppelin-contracts" = "5.0.2" "@openzeppelin-contracts-upgradeable" = "5.0.2" +forge-std = { version = "1.9.2", git = "https://github.com/foundry-rs/forge-std.git", rev = "5a802d7c10abb4bbfb3e7214c75052ef9e6a06f8" } [soldeer] recursive_deps = false diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index c91657b..701be44 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -246,4 +246,5 @@ interface IStETHWithdrawal { view returns (WithdrawalRequestStatus[] memory statuses); function getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestsIds); + function getLastRequestId() external view returns (uint256); } diff --git a/test/Base.sol b/test/Base.sol index 0abcdd6..7be4705 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -35,7 +35,7 @@ abstract contract Base_Test_ is Test { Proxy public lidoProxy; Proxy public lidoOwnerProxy; OethARM public oethARM; - LidoFixedPriceMultiLpARM public lidoFixedPriceMulltiLpARM; + LidoFixedPriceMultiLpARM public lidoFixedPriceMultiLpARM; LiquidityProviderController public liquidityProviderController; IERC20 public oeth; diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index fbd76b7..6e1bf00 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -18,7 +18,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared function setUp() public override { super.setUp(); - delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); deal(address(weth), address(this), 1_000 ether); } @@ -35,7 +35,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared { skip(delay - 1); vm.expectRevert("Claim delay not met"); - lidoFixedPriceMulltiLpARM.claimRedeem(0); + lidoFixedPriceMultiLpARM.claimRedeem(0); } function test_RevertWhen_ClaimRequest_Because_QueuePendingLiquidity_NoLiquidity() @@ -46,14 +46,14 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) { // Remove all weth liquidity from ARM - deal(address(weth), address(lidoFixedPriceMulltiLpARM), 0); + deal(address(weth), address(lidoFixedPriceMultiLpARM), 0); // Time jump claim delay skip(delay); // Expect revert vm.expectRevert("Queue pending liquidity"); - lidoFixedPriceMulltiLpARM.claimRedeem(0); + lidoFixedPriceMultiLpARM.claimRedeem(0); } function test_RevertWhen_ClaimRequest_Because_QueuePendingLiquidity_NoEnoughLiquidity() @@ -64,15 +64,15 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) { // Remove half of weth liquidity from ARM - uint256 halfAmount = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) / 2; - deal(address(weth), address(lidoFixedPriceMulltiLpARM), halfAmount); + uint256 halfAmount = weth.balanceOf(address(lidoFixedPriceMultiLpARM)) / 2; + deal(address(weth), address(lidoFixedPriceMultiLpARM), halfAmount); // Time jump claim delay skip(delay); // Expect revert vm.expectRevert("Queue pending liquidity"); - lidoFixedPriceMulltiLpARM.claimRedeem(0); + lidoFixedPriceMultiLpARM.claimRedeem(0); } function test_RevertWhen_ClaimRequest_Because_NotRequester() @@ -88,7 +88,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared // Expect revert vm.startPrank(vm.randomAddress()); vm.expectRevert("Not requester"); - lidoFixedPriceMulltiLpARM.claimRedeem(0); + lidoFixedPriceMultiLpARM.claimRedeem(0); } function test_RevertWhen_ClaimRequest_Because_AlreadyClaimed() @@ -102,7 +102,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared { // Expect revert vm.expectRevert("Already claimed"); - lidoFixedPriceMulltiLpARM.claimRedeem(0); + lidoFixedPriceMultiLpARM.claimRedeem(0); } ////////////////////////////////////////////////////// @@ -118,34 +118,34 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared skipTime(delay) { // Assertions before - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); // Expected events - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit MultiLP.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), DEFAULT_AMOUNT); // Main call - (uint256 assets) = lidoFixedPriceMulltiLpARM.claimRedeem(0); + (uint256 assets) = lidoFixedPriceMultiLpARM.claimRedeem(0); // Assertions after - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); @@ -164,32 +164,32 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared // Same situation as above // Swap MIN_TOTAL_SUPPLY from WETH in STETH - deal(address(weth), address(lidoFixedPriceMulltiLpARM), DEFAULT_AMOUNT); - deal(address(steth), address(lidoFixedPriceMulltiLpARM), MIN_TOTAL_SUPPLY); + deal(address(weth), address(lidoFixedPriceMultiLpARM), DEFAULT_AMOUNT); + deal(address(steth), address(lidoFixedPriceMultiLpARM), MIN_TOTAL_SUPPLY); // Handle lido rounding issue to ensure that balance is exactly MIN_TOTAL_SUPPLY - if (steth.balanceOf(address(lidoFixedPriceMulltiLpARM)) == MIN_TOTAL_SUPPLY - 1) { - deal(address(steth), address(lidoFixedPriceMulltiLpARM), 0); - deal(address(steth), address(lidoFixedPriceMulltiLpARM), MIN_TOTAL_SUPPLY + 1); + if (steth.balanceOf(address(lidoFixedPriceMultiLpARM)) == MIN_TOTAL_SUPPLY - 1) { + deal(address(steth), address(lidoFixedPriceMultiLpARM), 0); + deal(address(steth), address(lidoFixedPriceMultiLpARM), MIN_TOTAL_SUPPLY + 1); } // Expected events - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit MultiLP.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), DEFAULT_AMOUNT); // Main call - (uint256 assets) = lidoFixedPriceMulltiLpARM.claimRedeem(0); + (uint256 assets) = lidoFixedPriceMultiLpARM.claimRedeem(0); // Assertions after - assertApproxEqAbs(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY, 1); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertApproxEqAbs(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY, 1); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); @@ -207,36 +207,36 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT / 2) { // Assertions before - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2, 2); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); // Expected events - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit MultiLP.RedeemClaimed(address(this), 1, DEFAULT_AMOUNT / 2); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), DEFAULT_AMOUNT / 2); + emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), DEFAULT_AMOUNT / 2); // Main call skip(delay); - (uint256 assets) = lidoFixedPriceMulltiLpARM.claimRedeem(1); + (uint256 assets) = lidoFixedPriceMultiLpARM.claimRedeem(1); // Assertions after - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); assertEqUserRequest(0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol new file mode 100644 index 0000000..6b8e9d3 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {IStETHWithdrawal} from "contracts/Interfaces.sol"; +import {Mainnet} from "contracts/utils/Addresses.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared_Test_ { + uint256[] amounts0; + uint256[] amounts1; + uint256[] amounts2; + + IStETHWithdrawal public stETHWithdrawal = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL); + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + + function setUp() public override { + super.setUp(); + + deal(address(steth), address(lidoFixedPriceMultiLpARM), 10_000 ether); + + amounts0 = new uint256[](0); + + amounts1 = new uint256[](1); + amounts1[0] = DEFAULT_AMOUNT; + + amounts2 = new uint256[](2); + amounts2[0] = DEFAULT_AMOUNT; + amounts2[1] = DEFAULT_AMOUNT; + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_ClaimStETHWithdrawalForWETH_EmptyList() + public + asLidoFixedPriceMulltiLpARMOperator + requestStETHWithdrawalForETHOnLidoFixedPriceMultiLpARM(new uint256[](0)) + { + assertEq(address(lidoFixedPriceMultiLpARM).balance, 0); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + + // Main call + lidoFixedPriceMultiLpARM.claimStETHWithdrawalForWETH(new uint256[](0)); + + assertEq(address(lidoFixedPriceMultiLpARM).balance, 0); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + } + + function test_ClaimStETHWithdrawalForWETH_SingleRequest() + public + asLidoFixedPriceMulltiLpARMOperator + approveStETHOnLidoFixedPriceMultiLpARM + requestStETHWithdrawalForETHOnLidoFixedPriceMultiLpARM(amounts1) + mockFunctionClaimWithdrawOnLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT) + { + // Assertions before + uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), DEFAULT_AMOUNT); + + stETHWithdrawal.getLastRequestId(); + uint256[] memory requests = new uint256[](1); + requests[0] = stETHWithdrawal.getLastRequestId(); + // Main call + lidoFixedPriceMultiLpARM.claimStETHWithdrawalForWETH(requests); + + // Assertions after + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), balanceBefore + DEFAULT_AMOUNT); + } + + function test_ClaimStETHWithdrawalForWETH_MultiRequest() + public + asLidoFixedPriceMulltiLpARMOperator + approveStETHOnLidoFixedPriceMultiLpARM + requestStETHWithdrawalForETHOnLidoFixedPriceMultiLpARM(amounts2) + mockCallLidoFindCheckpointHints + mockFunctionClaimWithdrawOnLidoFixedPriceMultiLpARM(amounts2[0] + amounts2[1]) + { + // Assertions before + uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), amounts2[0] + amounts2[1]); + + stETHWithdrawal.getLastRequestId(); + uint256[] memory requests = new uint256[](2); + requests[0] = stETHWithdrawal.getLastRequestId() - 1; + requests[1] = stETHWithdrawal.getLastRequestId(); + // Main call + lidoFixedPriceMultiLpARM.claimStETHWithdrawalForWETH(requests); + + // Assertions after + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), balanceBefore + amounts2[0] + amounts2[1]); + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol b/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol index e84b8d6..c10f272 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol @@ -25,7 +25,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_CollectFees_Test_ is Fork_Shared simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(steth), true) { vm.expectRevert("ARM: insufficient liquidity"); - lidoFixedPriceMulltiLpARM.collectFees(); + lidoFixedPriceMultiLpARM.collectFees(); } ////////////////////////////////////////////////////// @@ -35,21 +35,21 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_CollectFees_Test_ is Fork_Shared public simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) { - address feeCollector = lidoFixedPriceMulltiLpARM.feeCollector(); + address feeCollector = lidoFixedPriceMultiLpARM.feeCollector(); uint256 fee = DEFAULT_AMOUNT * 20 / 100; // Expected Events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), feeCollector, fee); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), feeCollector, fee); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit PerformanceFee.FeeCollected(feeCollector, fee); // Main call - uint256 claimedFee = lidoFixedPriceMulltiLpARM.collectFees(); + uint256 claimedFee = lidoFixedPriceMultiLpARM.collectFees(); // Assertions after assertEq(claimedFee, fee); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); } function test_CollectFees_Twice() @@ -59,7 +59,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_CollectFees_Test_ is Fork_Shared simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) { // Main call - uint256 claimedFee = lidoFixedPriceMulltiLpARM.collectFees(); + uint256 claimedFee = lidoFixedPriceMultiLpARM.collectFees(); // Assertions after assertEq(claimedFee, DEFAULT_AMOUNT * 20 / 100); // This test should pass! diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol index 8dfe37e..dc7f048 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -16,18 +16,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Constructor_Test is Fork_Shared_ /// --- PASSING TESTS ////////////////////////////////////////////////////// function test_Initial_State() public view { - assertEq(lidoFixedPriceMulltiLpARM.name(), "Lido ARM"); - assertEq(lidoFixedPriceMulltiLpARM.symbol(), "ARM-ST"); - assertEq(lidoFixedPriceMulltiLpARM.owner(), address(this)); - assertEq(lidoFixedPriceMulltiLpARM.operator(), operator); - assertEq(lidoFixedPriceMulltiLpARM.feeCollector(), feeCollector); - assertEq(lidoFixedPriceMulltiLpARM.fee(), 2000); - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), 1e12); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); + assertEq(lidoFixedPriceMultiLpARM.name(), "Lido ARM"); + assertEq(lidoFixedPriceMultiLpARM.symbol(), "ARM-ST"); + assertEq(lidoFixedPriceMultiLpARM.owner(), address(this)); + assertEq(lidoFixedPriceMultiLpARM.operator(), operator); + assertEq(lidoFixedPriceMultiLpARM.feeCollector(), feeCollector); + assertEq(lidoFixedPriceMultiLpARM.fee(), 2000); + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), 1e12); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // the 20% performance fee is removed on initialization - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), 1e12); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), 1e12); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 1e12); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), 1e12); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), 1e12); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), 1e12); assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 3878c93..c88c02a 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -38,7 +38,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes // Alice deal(address(weth), alice, 1_000 ether); vm.prank(alice); - weth.approve(address(lidoFixedPriceMulltiLpARM), type(uint256).max); + weth.approve(address(lidoFixedPriceMultiLpARM), type(uint256).max); } ////////////////////////////////////////////////////// @@ -49,7 +49,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), 0) { vm.expectRevert("LPC: LP cap exceeded"); - lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT); + lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT); } function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapNotNull() @@ -57,7 +57,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { vm.expectRevert("LPC: LP cap exceeded"); - lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT + 1); + lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT + 1); } function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapReached() @@ -65,11 +65,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // Initial deposit - lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT / 2); + lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT / 2); // Cap is now 0.5 ether vm.expectRevert("LPC: LP cap exceeded"); - lidoFixedPriceMulltiLpARM.deposit((DEFAULT_AMOUNT / 2) + 1); + lidoFixedPriceMultiLpARM.deposit((DEFAULT_AMOUNT / 2) + 1); } function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapNull() @@ -78,7 +78,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT + 1) { vm.expectRevert("LPC: Total assets cap exceeded"); - lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT); + lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT); } function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapNotNull() @@ -87,7 +87,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { vm.expectRevert("LPC: Total assets cap exceeded"); - lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT - MIN_TOTAL_SUPPLY + 1); + lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT - MIN_TOTAL_SUPPLY + 1); } function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapReached() @@ -95,9 +95,9 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setTotalAssetsCap(DEFAULT_AMOUNT) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { - lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT / 2); + lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT / 2); vm.expectRevert("LPC: Total assets cap exceeded"); - lidoFixedPriceMulltiLpARM.deposit((DEFAULT_AMOUNT / 2) - MIN_TOTAL_SUPPLY + 1); // This should revert! + lidoFixedPriceMultiLpARM.deposit((DEFAULT_AMOUNT / 2) - MIN_TOTAL_SUPPLY + 1); // This should revert! } ////////////////////////////////////////////////////// @@ -113,37 +113,37 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes { uint256 amount = DEFAULT_AMOUNT; // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); // Ensure no shares before + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amount); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amount); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); // Main call - uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + uint256 shares = lidoFixedPriceMultiLpARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), shares); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether @@ -159,37 +159,37 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes { uint256 amount = DEFAULT_AMOUNT; // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), amount); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), amount); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amount); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amount); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); // Main call - uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + uint256 shares = lidoFixedPriceMultiLpARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), shares * 2); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), shares * 2); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -206,38 +206,38 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes { uint256 amount = DEFAULT_AMOUNT; // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(alice), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(alice), amount); assertEqQueueMetadata(0, 0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(alice, address(lidoFixedPriceMulltiLpARM), amount); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(alice, address(lidoFixedPriceMultiLpARM), amount); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(0), alice, amount); // shares == amount here vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(alice, 0); vm.prank(alice); // Main call - uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + uint256 shares = lidoFixedPriceMultiLpARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), shares); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(alice), shares); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(alice), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -251,59 +251,59 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // simulate asset gain - uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); uint256 assetGain = DEFAULT_AMOUNT; - deal(address(weth), address(lidoFixedPriceMulltiLpARM), balanceBefore + assetGain); + deal(address(weth), address(lidoFixedPriceMultiLpARM), balanceBefore + assetGain); // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + assetGain); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether before"); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + assetGain); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0, "Outstanding ether before"); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy // 80% of the asset gain goes to the total assets - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), balanceBefore + assetGain * 80 / 100, "Total assets before"); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), balanceBefore + assetGain * 80 / 100, "Total assets before"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT, "lp cap before"); assertEqQueueMetadata(0, 0, 0, 0); // 20% of the asset gain goes to the performance fees uint256 feesAccrued = assetGain * 20 / 100; - uint256 rawTotalAsset = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - feesAccrued; // No steth and no externalWithdrawQueue + uint256 rawTotalAsset = weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - feesAccrued; // No steth and no externalWithdrawQueue uint256 depositedAssets = DEFAULT_AMOUNT; uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / rawTotalAsset; // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), depositedAssets); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), depositedAssets); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(0), address(this), expectedShares); vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); // deposit assets - uint256 shares = lidoFixedPriceMulltiLpARM.deposit(depositedAssets); + uint256 shares = lidoFixedPriceMultiLpARM.deposit(depositedAssets); assertEq(shares, expectedShares, "minted shares"); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0, "stETH balance after"); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0, "stETH balance after"); assertEq( - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), + weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "WETH balance after" ); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether after"); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0, "Outstanding ether after"); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees assertEq( - lidoFixedPriceMulltiLpARM.lastTotalAssets(), + lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + (assetGain * 80 / 100) + depositedAssets, "last total assets after" ); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), expectedShares, "user shares after"); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), expectedShares, "user shares after"); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); } @@ -318,29 +318,29 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) { // set stETH/WETH buy price to 1 - lidoFixedPriceMulltiLpARM.setPrices(1e36, 1e36 + 1); + lidoFixedPriceMultiLpARM.setPrices(1e36, 1e36 + 1); // User Swap stETH for 3/4 of WETH in the ARM deal(address(steth), address(this), DEFAULT_AMOUNT); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( steth, weth, 3 * DEFAULT_AMOUNT / 4, DEFAULT_AMOUNT, address(this) ); // First user requests a full withdrawal - uint256 firstUserShares = lidoFixedPriceMulltiLpARM.balanceOf(address(this)); - lidoFixedPriceMulltiLpARM.requestRedeem(firstUserShares); + uint256 firstUserShares = lidoFixedPriceMultiLpARM.balanceOf(address(this)); + lidoFixedPriceMultiLpARM.requestRedeem(firstUserShares); // Assertions Before uint256 stethBalanceBefore = 3 * DEFAULT_AMOUNT / 4; - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), stethBalanceBefore, "stETH ARM balance before"); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), stethBalanceBefore, "stETH ARM balance before"); uint256 wethBalanceBefore = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - 3 * DEFAULT_AMOUNT / 4; - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), wethBalanceBefore, "WETH ARM balance before"); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether before"); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0, "Fees accrued before"); - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), 0, "alice shares before"); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), wethBalanceBefore, "WETH ARM balance before"); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0, "Outstanding ether before"); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0, "Fees accrued before"); + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(alice), 0, "alice shares before"); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before"); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); @@ -348,27 +348,27 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(alice, address(lidoFixedPriceMulltiLpARM), amount); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit IERC20.Transfer(alice, address(lidoFixedPriceMultiLpARM), amount); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(0), alice, amount); // shares == amount here vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); vm.prank(alice); // Main call - uint256 shares = lidoFixedPriceMulltiLpARM.deposit(amount); + uint256 shares = lidoFixedPriceMultiLpARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), stethBalanceBefore, "stETH ARM balance after"); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), stethBalanceBefore, "stETH ARM balance after"); assertEq( - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), wethBalanceBefore + amount, "WETH ARM balance after" + weth.balanceOf(address(lidoFixedPriceMultiLpARM)), wethBalanceBefore + amount, "WETH ARM balance after" ); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0, "Outstanding ether after"); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount, "last total assets after"); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(alice), shares, "alice shares after"); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0, "Outstanding ether after"); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount, "last total assets after"); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(alice), shares, "alice shares after"); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used // withdrawal request is now claimable assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 6e0c127..4e8ff32 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -30,37 +30,37 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) { // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(0, 0, 0, 0); - uint256 delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + uint256 delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit MultiLP.RedeemRequested(address(this), 0, DEFAULT_AMOUNT, DEFAULT_AMOUNT, block.timestamp + delay); // Main Call - (uint256 requestId, uint256 assets) = lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT); + (uint256 requestId, uint256 assets) = lidoFixedPriceMultiLpARM.requestRedeem(DEFAULT_AMOUNT); // Assertions After assertEq(requestId, 0); // First request assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); // One request in the queue assertEqUserRequest(0, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT, DEFAULT_AMOUNT); // Requested the full amount assertEq(assets, DEFAULT_AMOUNT, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); } @@ -73,26 +73,26 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT / 4) { // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 0, 1); - uint256 delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + uint256 delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT / 2); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit MultiLP.RedeemRequested( address(this), 1, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT * 3 / 4, block.timestamp + delay ); // Main Call - (uint256 requestId, uint256 assets) = lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT / 2); + (uint256 requestId, uint256 assets) = lidoFixedPriceMultiLpARM.requestRedeem(DEFAULT_AMOUNT / 2); // Assertions After assertEq(requestId, 1); // Second request @@ -101,13 +101,13 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar 1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT * 3 / 4 ); assertEq(assets, DEFAULT_AMOUNT / 2, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMulltiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only } @@ -125,32 +125,32 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar uint256 assetsGain = DEFAULT_AMOUNT; deal( address(weth), - address(lidoFixedPriceMulltiLpARM), - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) + assetsGain + address(lidoFixedPriceMultiLpARM), + weth.balanceOf(address(lidoFixedPriceMultiLpARM)) + assetsGain ); // Calculate expected values uint256 feeAccrued = assetsGain * 20 / 100; // 20% fee - uint256 totalAsset = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - feeAccrued; + uint256 totalAsset = weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - feeAccrued; uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit PerformanceFee.FeeCalculated(feeAccrued, assetsGain); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); // Main call - lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT); + lidoFixedPriceMultiLpARM.requestRedeem(DEFAULT_AMOUNT); - uint256 delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + uint256 delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); // +perfs - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoFixedPriceMulltiLpARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); // +perfs + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), feeAccrued); + assertApproxEqAbs(lidoFixedPriceMultiLpARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssets, 0, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); @@ -170,30 +170,30 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar uint256 assetsLoss = DEFAULT_AMOUNT / 10; // 0.1 ether of loss deal( address(weth), - address(lidoFixedPriceMulltiLpARM), - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - assetsLoss + address(lidoFixedPriceMultiLpARM), + weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - assetsLoss ); // Calculate expected values uint256 feeAccrued = 0; // No profits - uint256 totalAsset = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 totalAsset = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); // Main call - lidoFixedPriceMulltiLpARM.requestRedeem(DEFAULT_AMOUNT); + lidoFixedPriceMultiLpARM.requestRedeem(DEFAULT_AMOUNT); - uint256 delay = lidoFixedPriceMulltiLpARM.CLAIM_DELAY(); + uint256 delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMulltiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); - assertEq(lidoFixedPriceMulltiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoFixedPriceMulltiLpARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error - assertEq(lidoFixedPriceMulltiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMulltiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); + assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), feeAccrued); + assertApproxEqAbs(lidoFixedPriceMultiLpARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error + assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); + assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssets, 0, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol new file mode 100644 index 0000000..1b3c259 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + deal(address(steth), address(lidoFixedPriceMultiLpARM), 10_000 ether); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_RequestStETHWithdrawalForETH_NotOperator() public asRandomAddress { + vm.expectRevert("ARM: Only operator or owner can call this function."); + lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(new uint256[](0)); + } + + function test_RevertWhen_RequestStETHWithdrawalForETH_Because_AllowanceExceeded() + public + asLidoFixedPriceMulltiLpARMOperator + { + uint256[] memory amounts = new uint256[](1); + amounts[0] = DEFAULT_AMOUNT; + + vm.expectRevert("ALLOWANCE_EXCEEDED"); + lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + } + + function test_RevertWhen_RequestStETHWithdrawalForETH_Because_BalanceExceeded() + public + asLidoFixedPriceMulltiLpARMOperator + approveStETHOnLidoFixedPriceMultiLpARM + { + // Remove all stETH from the contract + deal(address(steth), address(lidoFixedPriceMultiLpARM), 0); + + uint256[] memory amounts = new uint256[](1); + amounts[0] = DEFAULT_AMOUNT; + + vm.expectRevert("BALANCE_EXCEEDED"); + lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_RequestStETHWithdrawalForETH_EmptyList() public asLidoFixedPriceMulltiLpARMOperator { + uint256[] memory requestIds = lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(new uint256[](0)); + assertEq(requestIds.length, 0); + } + + function test_RequestStETHWithdrawalForETH_SingleAmount_1ether() + public + asLidoFixedPriceMulltiLpARMOperator + approveStETHOnLidoFixedPriceMultiLpARM + { + uint256[] memory amounts = new uint256[](1); + amounts[0] = DEFAULT_AMOUNT; + + // Expected events + vm.expectEmit({emitter: address(steth)}); + emit IERC20.Transfer( + address(lidoFixedPriceMultiLpARM), address(lidoFixedPriceMultiLpARM.withdrawalQueue()), amounts[0] + ); + + // Main call + uint256[] memory requestIds = lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + + assertEq(requestIds.length, 1); + assertGt(requestIds[0], 0); + } + + function test_RequestStETHWithdrawalForETH_SingleAmount_1000ethers() + public + asLidoFixedPriceMulltiLpARMOperator + approveStETHOnLidoFixedPriceMultiLpARM + { + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1_000 ether; + + // Expected events + vm.expectEmit({emitter: address(steth)}); + emit IERC20.Transfer( + address(lidoFixedPriceMultiLpARM), address(lidoFixedPriceMultiLpARM.withdrawalQueue()), amounts[0] + ); + + // Main call + uint256[] memory requestIds = lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + + assertEq(requestIds.length, 1); + assertGt(requestIds[0], 0); + } + + function test_RequestStETHWithdrawalForETH_MultipleAmount() + public + asLidoFixedPriceMulltiLpARMOperator + approveStETHOnLidoFixedPriceMultiLpARM + { + uint256 length = _bound(vm.randomUint(), 2, 10); + uint256[] memory amounts = new uint256[](length); + for (uint256 i = 0; i < amounts.length; i++) { + amounts[i] = _bound(vm.randomUint(), 0, 1_000 ether); + } + + // Main call + uint256[] memory requestIds = lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + + uint256 initialRequestId = requestIds[0]; + assertGt(initialRequestId, 0); + for (uint256 i = 1; i < amounts.length; i++) { + assertEq(requestIds[i], initialRequestId + i); + } + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 7accd17..1881d65 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -23,18 +23,18 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te ////////////////////////////////////////////////////// function test_RevertWhen_PerformanceFee_SetFee_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMulltiLpARM.setFee(0); + lidoFixedPriceMultiLpARM.setFee(0); } function test_RevertWhen_PerformanceFee_SetFee_Because_FeeIsTooHigh() public asLidoFixedPriceMultiLpARMOwner { - uint256 max = lidoFixedPriceMulltiLpARM.FEE_SCALE(); + uint256 max = lidoFixedPriceMultiLpARM.FEE_SCALE(); vm.expectRevert("ARM: fee too high"); - lidoFixedPriceMulltiLpARM.setFee(max + 1); + lidoFixedPriceMultiLpARM.setFee(max + 1); } function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMulltiLpARM.setFeeCollector(address(0)); + lidoFixedPriceMultiLpARM.setFeeCollector(address(0)); } function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_FeeCollectorIsZero() @@ -42,36 +42,36 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te asLidoFixedPriceMultiLpARMOwner { vm.expectRevert("ARM: invalid fee collector"); - lidoFixedPriceMulltiLpARM.setFeeCollector(address(0)); + lidoFixedPriceMultiLpARM.setFeeCollector(address(0)); } ////////////////////////////////////////////////////// /// --- PERFORMANCE FEE - PASSING TEST ////////////////////////////////////////////////////// function test_PerformanceFee_SetFee_() public asLidoFixedPriceMultiLpARMOwner { - uint256 feeBefore = lidoFixedPriceMulltiLpARM.fee(); + uint256 feeBefore = lidoFixedPriceMultiLpARM.fee(); - uint256 newFee = _bound(vm.randomUint(), 0, lidoFixedPriceMulltiLpARM.FEE_SCALE()); + uint256 newFee = _bound(vm.randomUint(), 0, lidoFixedPriceMultiLpARM.FEE_SCALE()); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit PerformanceFee.FeeUpdated(newFee); - lidoFixedPriceMulltiLpARM.setFee(newFee); + lidoFixedPriceMultiLpARM.setFee(newFee); - assertEq(lidoFixedPriceMulltiLpARM.fee(), newFee); - assertNotEq(feeBefore, lidoFixedPriceMulltiLpARM.fee()); + assertEq(lidoFixedPriceMultiLpARM.fee(), newFee); + assertNotEq(feeBefore, lidoFixedPriceMultiLpARM.fee()); } function test_PerformanceFee_SetFeeCollector() public asLidoFixedPriceMultiLpARMOwner { - address feeCollectorBefore = lidoFixedPriceMulltiLpARM.feeCollector(); + address feeCollectorBefore = lidoFixedPriceMultiLpARM.feeCollector(); address newFeeCollector = vm.randomAddress(); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit PerformanceFee.FeeCollectorUpdated(newFeeCollector); - lidoFixedPriceMulltiLpARM.setFeeCollector(newFeeCollector); + lidoFixedPriceMultiLpARM.setFeeCollector(newFeeCollector); - assertEq(lidoFixedPriceMulltiLpARM.feeCollector(), newFeeCollector); - assertNotEq(feeCollectorBefore, lidoFixedPriceMulltiLpARM.feeCollector()); + assertEq(lidoFixedPriceMultiLpARM.feeCollector(), newFeeCollector); + assertNotEq(feeCollectorBefore, lidoFixedPriceMultiLpARM.feeCollector()); } ////////////////////////////////////////////////////// @@ -79,36 +79,36 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te ////////////////////////////////////////////////////// function test_RevertWhen_SetPrices_Because_PriceCross() public { vm.expectRevert("ARM: Price cross"); - lidoFixedPriceMulltiLpARM.setPrices(90 * 1e33, 89 * 1e33); + lidoFixedPriceMultiLpARM.setPrices(90 * 1e33, 89 * 1e33); vm.expectRevert("ARM: Price cross"); - lidoFixedPriceMulltiLpARM.setPrices(72, 70); + lidoFixedPriceMultiLpARM.setPrices(72, 70); vm.expectRevert("ARM: Price cross"); - lidoFixedPriceMulltiLpARM.setPrices(1005 * 1e33, 1000 * 1e33); + lidoFixedPriceMultiLpARM.setPrices(1005 * 1e33, 1000 * 1e33); // Both set to 1.0 vm.expectRevert("ARM: Price cross"); - lidoFixedPriceMulltiLpARM.setPrices(1e36, 1e36); + lidoFixedPriceMultiLpARM.setPrices(1e36, 1e36); } function test_RevertWhen_FixedPriceARM_SetPrices_Because_PriceRange() public asLidoFixedPriceMulltiLpARMOperator { // buy price 11 basis points higher than 1.0 vm.expectRevert("ARM: buy price too high"); - lidoFixedPriceMulltiLpARM.setPrices(10011e32, 10020e32); + lidoFixedPriceMultiLpARM.setPrices(10011e32, 10020e32); // sell price 11 basis points lower than 1.0 vm.expectRevert("ARM: sell price too low"); - lidoFixedPriceMulltiLpARM.setPrices(9980e32, 9989e32); + lidoFixedPriceMultiLpARM.setPrices(9980e32, 9989e32); // Forgot to scale up to 36 decimals vm.expectRevert("ARM: sell price too low"); - lidoFixedPriceMulltiLpARM.setPrices(1e18, 1e18); + lidoFixedPriceMultiLpARM.setPrices(1e18, 1e18); } function test_RevertWhen_FixedPriceARM_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); - lidoFixedPriceMulltiLpARM.setPrices(0, 0); + lidoFixedPriceMultiLpARM.setPrices(0, 0); } function test_SellPriceCannotCrossOne() public asLidoFixedPriceMulltiLpARMOperator { @@ -126,27 +126,27 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te ////////////////////////////////////////////////////// function test_FixedPriceARM_SetPrices_Operator() public asLidoFixedPriceMulltiLpARMOperator { // buy price 10 basis points higher than 1.0 - lidoFixedPriceMulltiLpARM.setPrices(1001e33, 1002e33); + lidoFixedPriceMultiLpARM.setPrices(1001e33, 1002e33); // sell price 10 basis points lower than 1.0 - lidoFixedPriceMulltiLpARM.setPrices(9980e32, 9991e32); + lidoFixedPriceMultiLpARM.setPrices(9980e32, 9991e32); // 2% of one basis point spread - lidoFixedPriceMulltiLpARM.setPrices(999999e30, 1000001e30); + lidoFixedPriceMultiLpARM.setPrices(999999e30, 1000001e30); - lidoFixedPriceMulltiLpARM.setPrices(992 * 1e33, 1001 * 1e33); - lidoFixedPriceMulltiLpARM.setPrices(1001 * 1e33, 1004 * 1e33); - lidoFixedPriceMulltiLpARM.setPrices(992 * 1e33, 2000 * 1e33); + lidoFixedPriceMultiLpARM.setPrices(992 * 1e33, 1001 * 1e33); + lidoFixedPriceMultiLpARM.setPrices(1001 * 1e33, 1004 * 1e33); + lidoFixedPriceMultiLpARM.setPrices(992 * 1e33, 2000 * 1e33); // Check the traderates - assertEq(lidoFixedPriceMulltiLpARM.traderate0(), 500 * 1e33); - assertEq(lidoFixedPriceMulltiLpARM.traderate1(), 992 * 1e33); + assertEq(lidoFixedPriceMultiLpARM.traderate0(), 500 * 1e33); + assertEq(lidoFixedPriceMultiLpARM.traderate1(), 992 * 1e33); } function test_FixedPriceARM_SetPrices_Owner() public { // buy price 11 basis points higher than 1.0 - lidoFixedPriceMulltiLpARM.setPrices(10011e32, 10020e32); + lidoFixedPriceMultiLpARM.setPrices(10011e32, 10020e32); // sell price 11 basis points lower than 1.0 - lidoFixedPriceMulltiLpARM.setPrices(9980e32, 9989e32); + lidoFixedPriceMultiLpARM.setPrices(9980e32, 9989e32); } ////////////////////////////////////////////////////// @@ -154,12 +154,12 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te ////////////////////////////////////////////////////// function test_RevertWhen_Ownable_SetOwner_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMulltiLpARM.setOwner(address(0)); + lidoFixedPriceMultiLpARM.setOwner(address(0)); } function test_RevertWhen_Ownable_SetOperator_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMulltiLpARM.setOperator(address(0)); + lidoFixedPriceMultiLpARM.setOperator(address(0)); } ////////////////////////////////////////////////////// @@ -170,7 +170,7 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMulltiLpARM.setLiquidityProviderController(address(0)); + lidoFixedPriceMultiLpARM.setLiquidityProviderController(address(0)); } ////////////////////////////////////////////////////// @@ -179,10 +179,10 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te function test_LiquidityProviderController_SetLiquidityProvider() public asLidoFixedPriceMultiLpARMOwner { address newLiquidityProviderController = vm.randomAddress(); - vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); emit LiquidityProviderControllerARM.LiquidityProviderControllerUpdated(newLiquidityProviderController); - lidoFixedPriceMulltiLpARM.setLiquidityProviderController(newLiquidityProviderController); + lidoFixedPriceMultiLpARM.setLiquidityProviderController(newLiquidityProviderController); - assertEq(lidoFixedPriceMulltiLpARM.liquidityProviderController(), newLiquidityProviderController); + assertEq(lidoFixedPriceMultiLpARM.liquidityProviderController(), newLiquidityProviderController); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index c920476..9f2f14b 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -26,17 +26,17 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is deal(address(weth), address(this), 1_000 ether); deal(address(steth), address(this), 1_000 ether); - deal(address(weth), address(lidoFixedPriceMulltiLpARM), 1_000 ether); - deal(address(steth), address(lidoFixedPriceMulltiLpARM), 1_000 ether); + deal(address(weth), address(lidoFixedPriceMultiLpARM), 1_000 ether); + deal(address(steth), address(lidoFixedPriceMultiLpARM), 1_000 ether); } ////////////////////////////////////////////////////// /// --- REVERTING TESTS ////////////////////////////////////////////////////// function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut1() public { - lidoFixedPriceMulltiLpARM.token0(); + lidoFixedPriceMultiLpARM.token0(); vm.expectRevert("ARM: Invalid out token"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( steth, // inToken badToken, // outToken 1, // amountIn @@ -47,7 +47,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut0() public { vm.expectRevert("ARM: Invalid out token"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( weth, // inToken badToken, // outToken 1, // amountIn @@ -58,7 +58,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenIn() public { vm.expectRevert("ARM: Invalid in token"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( badToken, // inToken steth, // outToken 1, // amountIn @@ -69,7 +69,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_BothInvalidTokens() public { vm.expectRevert("ARM: Invalid in token"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( badToken, // inToken badToken, // outToken 1, // amountIn @@ -82,7 +82,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is uint256 initialBalance = weth.balanceOf(address(this)); vm.expectRevert(); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( weth, // inToken steth, // outToken initialBalance + 1, // amountIn @@ -92,7 +92,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is initialBalance = steth.balanceOf(address(this)); vm.expectRevert("BALANCE_EXCEEDED"); // Lido error - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( steth, // inToken weth, // outToken initialBalance + 3, // amountIn @@ -102,11 +102,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is } function test_RevertWhen_SwapExactTokensForTokens_Because_NotEnoughTokenOut() public { - uint256 initialBalance = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 initialBalance = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); deal(address(weth), address(this), initialBalance * 2); vm.expectRevert("BALANCE_EXCEEDED"); // Lido error - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( weth, // inToken steth, // outToken initialBalance * 2, // amountIn @@ -114,10 +114,10 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is address(this) // to ); - initialBalance = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + initialBalance = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); deal(address(steth), address(this), initialBalance * 2); vm.expectRevert("ARM: Insufficient liquidity"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( steth, // inToken weth, // outToken initialBalance * 2, // amountIn @@ -127,11 +127,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is } function test_RevertWhen_SwapExactTokensForTokens_Because_InsufficientOutputAmount() public { - deal(address(steth), address(lidoFixedPriceMulltiLpARM), 100 wei); + deal(address(steth), address(lidoFixedPriceMultiLpARM), 100 wei); // Test for this function signature: swapExactTokensForTokens(IERC20,IERC20,uint56,uint256,address) vm.expectRevert("ARM: Insufficient output amount"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( weth, // inToken steth, // outToken 1, // amountIn @@ -144,7 +144,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is path[0] = address(weth); path[1] = address(steth); vm.expectRevert("ARM: Insufficient output amount"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( 1, // amountIn 1_000 ether, // amountOutMin path, // path @@ -155,7 +155,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidePathLength() public { vm.expectRevert("ARM: Invalid path length"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( 1, // amountIn 1, // amountOutMin new address[](3), // path @@ -166,7 +166,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_DeadlineExpired() public { vm.expectRevert("ARM: Deadline expired"); - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( 1, // amountIn 1, // amountOutMin new address[](2), // path @@ -187,18 +187,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoFixedPriceMulltiLpARM.traderate1(); + uint256 traderates1 = lidoFixedPriceMultiLpARM.traderate1(); uint256 minAmount = amountIn * traderates1 / 1e36; // Expected events: Already checked in fuzz tests uint256[] memory outputs = new uint256[](2); // Main call - outputs = lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + outputs = lidoFixedPriceMultiLpARM.swapExactTokensForTokens( amountIn, // amountIn minAmount, // amountOutMin path, // path @@ -209,8 +209,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Assertions assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); @@ -230,18 +230,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoFixedPriceMulltiLpARM.traderate0(); + uint256 traderates0 = lidoFixedPriceMultiLpARM.traderate0(); uint256 minAmount = amountIn * traderates0 / 1e36; // Expected events: Already checked in fuzz tests uint256[] memory outputs = new uint256[](2); // Main call - outputs = lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + outputs = lidoFixedPriceMultiLpARM.swapExactTokensForTokens( amountIn, // amountIn minAmount, // amountOutMin path, // path @@ -252,8 +252,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Assertions assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis); @@ -277,11 +277,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // Use random price between 0.98 and 1 for traderate1, // Traderate0 value doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); - lidoFixedPriceMulltiLpARM.setPrices(price, MAX_PRICE1); + lidoFixedPriceMultiLpARM.setPrices(price, MAX_PRICE1); // Set random amount of stETH in the ARM stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoFixedPriceMulltiLpARM), stethReserve); + deal(address(steth), address(lidoFixedPriceMultiLpARM), stethReserve); // Calculate maximum amount of WETH to swap // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. @@ -291,20 +291,20 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoFixedPriceMulltiLpARM.traderate1(); + uint256 traderates1 = lidoFixedPriceMultiLpARM.traderate1(); uint256 minAmount = amountIn * traderates1 / 1e36; // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amountIn); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amountIn); vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), minAmount + STETH_ERROR_ROUNDING); + emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), minAmount + STETH_ERROR_ROUNDING); // Main call - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( weth, // inToken steth, // outToken amountIn, // amountIn @@ -315,8 +315,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Assertions assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); @@ -333,11 +333,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // Use random price between MIN_PRICE1 and MAX_PRICE1 for traderate1, // Traderate0 value doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); - lidoFixedPriceMulltiLpARM.setPrices(MIN_PRICE0, price); + lidoFixedPriceMultiLpARM.setPrices(MIN_PRICE0, price); // Set random amount of WETH in the ARM wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); - deal(address(weth), address(lidoFixedPriceMulltiLpARM), wethReserve); + deal(address(weth), address(lidoFixedPriceMultiLpARM), wethReserve); // Calculate maximum amount of stETH to swap // As the price is below 1, we can take 100% of the balance of WETH of the ARM. @@ -347,21 +347,21 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoFixedPriceMulltiLpARM.traderate0(); + uint256 traderates0 = lidoFixedPriceMultiLpARM.traderate0(); uint256 minAmount = amountIn * traderates0 / 1e36; // Expected events vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amountIn); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amountIn); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), minAmount); + emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), minAmount); // Main call - lidoFixedPriceMulltiLpARM.swapExactTokensForTokens( + lidoFixedPriceMultiLpARM.swapExactTokensForTokens( steth, // inToken weth, // outToken amountIn, // amountIn @@ -372,8 +372,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Assertions assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis); diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol index edfa5ff..ee59dc0 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -26,17 +26,17 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(weth), address(this), 1_000 ether); deal(address(steth), address(this), 1_000 ether); - deal(address(weth), address(lidoFixedPriceMulltiLpARM), 1_000 ether); - deal(address(steth), address(lidoFixedPriceMulltiLpARM), 1_000 ether); + deal(address(weth), address(lidoFixedPriceMultiLpARM), 1_000 ether); + deal(address(steth), address(lidoFixedPriceMultiLpARM), 1_000 ether); } ////////////////////////////////////////////////////// /// --- REVERTING TESTS ////////////////////////////////////////////////////// function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut1() public { - lidoFixedPriceMulltiLpARM.token0(); + lidoFixedPriceMultiLpARM.token0(); vm.expectRevert("ARM: Invalid out token"); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( steth, // inToken badToken, // outToken 1, // amountOut @@ -47,7 +47,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut0() public { vm.expectRevert("ARM: Invalid out token"); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( weth, // inToken badToken, // outToken 1, // amountOut @@ -58,7 +58,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenIn() public { vm.expectRevert("ARM: Invalid in token"); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( badToken, // inToken steth, // outToken 1, // amountOut @@ -69,7 +69,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_BothInvalidTokens() public { vm.expectRevert("ARM: Invalid in token"); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( badToken, // inToken badToken, // outToken 1, // amountOut @@ -82,7 +82,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(weth), address(this), 0); vm.expectRevert(); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( weth, // inToken steth, // outToken 1, // amountOut @@ -92,7 +92,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(steth), address(this), 0); vm.expectRevert(); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( steth, // inToken weth, // outToken STETH_ERROR_ROUNDING + 1, // amountOut * @@ -106,7 +106,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(weth), address(this), 0); vm.expectRevert(); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( weth, // inToken steth, // outToken 1 ether, // amountOut @@ -116,7 +116,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(steth), address(this), 0); vm.expectRevert("BALANCE_EXCEEDED"); // Lido error - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( steth, // inToken weth, // outToken 1 ether, // amountOut @@ -126,11 +126,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is } function test_RevertWhen_SwapTokensForExactTokens_Because_InsufficientOutputAmount() public { - deal(address(steth), address(lidoFixedPriceMulltiLpARM), 100 wei); + deal(address(steth), address(lidoFixedPriceMultiLpARM), 100 wei); // Test for this function signature: swapTokensForExactTokens(IERC20,IERC20,uint56,uint256,address) vm.expectRevert("ARM: Excess input amount"); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( weth, // inToken steth, // outToken 1, // amountOut @@ -143,7 +143,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is path[0] = address(weth); path[1] = address(steth); vm.expectRevert("ARM: Excess input amount"); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( 1, // amountOut 0, // amountInMax path, // path @@ -154,7 +154,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidePathLength() public { vm.expectRevert("ARM: Invalid path length"); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( 1, // amountOut 1, // amountInMax new address[](3), // path @@ -165,7 +165,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_DeadlineExpired() public { vm.expectRevert("ARM: Deadline expired"); - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( 1, // amountOut 1, // amountInMax new address[](2), // path @@ -186,18 +186,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoFixedPriceMulltiLpARM.traderate1(); + uint256 traderates1 = lidoFixedPriceMultiLpARM.traderate1(); uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; // Expected events: Already checked in fuzz tests uint256[] memory outputs = new uint256[](2); // Main call - outputs = lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + outputs = lidoFixedPriceMultiLpARM.swapTokensForExactTokens( amountOut, // amountOut amountIn, // amountInMax path, // path @@ -208,8 +208,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Assertions assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); @@ -229,18 +229,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoFixedPriceMulltiLpARM.traderate0(); + uint256 traderates0 = lidoFixedPriceMultiLpARM.traderate0(); uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; // Expected events: Already checked in fuzz tests uint256[] memory outputs = new uint256[](2); // Main call - outputs = lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + outputs = lidoFixedPriceMultiLpARM.swapTokensForExactTokens( amountOut, // amountOut amountIn, // amountInMax path, // path @@ -251,8 +251,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Assertions assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis); @@ -276,11 +276,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // Use random price between 0.98 and 1 for traderate1, // Traderate0 value doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); - lidoFixedPriceMulltiLpARM.setPrices(price, MAX_PRICE1); + lidoFixedPriceMultiLpARM.setPrices(price, MAX_PRICE1); // Set random amount of stETH in the ARM stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoFixedPriceMulltiLpARM), stethReserve); + deal(address(steth), address(lidoFixedPriceMultiLpARM), stethReserve); // Calculate maximum amount of WETH to swap // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. @@ -290,20 +290,20 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoFixedPriceMulltiLpARM.traderate1(); + uint256 traderates1 = lidoFixedPriceMultiLpARM.traderate1(); uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amountIn); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amountIn); vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), amountOut + STETH_ERROR_ROUNDING); + emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), amountOut + STETH_ERROR_ROUNDING); // Main call - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( weth, // inToken steth, // outToken amountOut, // amountOut @@ -314,8 +314,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Assertions assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); @@ -334,11 +334,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // Use random price between MIN_PRICE1 and MAX_PRICE1 for traderate1, // Traderate0 value doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); - lidoFixedPriceMulltiLpARM.setPrices(MIN_PRICE0, price); + lidoFixedPriceMultiLpARM.setPrices(MIN_PRICE0, price); // Set random amount of WETH in the ARM wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); - deal(address(weth), address(lidoFixedPriceMulltiLpARM), wethReserve); + deal(address(weth), address(lidoFixedPriceMultiLpARM), wethReserve); // Calculate maximum amount of stETH to swap // As the price is below 1, we can take 100% of the balance of WETH of the ARM. @@ -348,21 +348,21 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoFixedPriceMulltiLpARM.traderate0(); + uint256 traderates0 = lidoFixedPriceMultiLpARM.traderate0(); uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; // Expected events vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMulltiLpARM), amountIn); + emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amountIn); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), address(this), amountOut); + emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), amountOut); // Main call - lidoFixedPriceMulltiLpARM.swapTokensForExactTokens( + lidoFixedPriceMultiLpARM.swapTokensForExactTokens( steth, // inToken weth, // outToken amountOut, // amountOut @@ -373,8 +373,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMulltiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMulltiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); // Assertions assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis); diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index fe3286e..16d9633 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -26,10 +26,10 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared liquidityProviderController.setTotalAssetsCap(type(uint256).max); // Approve STETH for Lido - lidoFixedPriceMulltiLpARM.approveStETH(); + lidoFixedPriceMultiLpARM.approveStETH(); deal(address(weth), address(this), 1_000 ether); - weth.approve(address(lidoFixedPriceMulltiLpARM), type(uint256).max); + weth.approve(address(lidoFixedPriceMultiLpARM), type(uint256).max); } ////////////////////////////////////////////////////// @@ -43,21 +43,21 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT * 2, address(weth), false) { vm.expectRevert(stdError.arithmeticError); - lidoFixedPriceMulltiLpARM.totalAssets(); + lidoFixedPriceMultiLpARM.totalAssets(); } ////////////////////////////////////////////////////// /// --- PASSING TEST ////////////////////////////////////////////////////// function test_TotalAssets_AfterInitialization() public view { - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); } function test_TotalAssets_AfterDeposit_NoAssetGainOrLoss() public depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) { - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); } function test_TotalAssets_AfterDeposit_WithAssetGain_InWETH() @@ -68,31 +68,31 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared uint256 assetGain = DEFAULT_AMOUNT / 2; deal( address(weth), - address(lidoFixedPriceMulltiLpARM), - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) + assetGain + address(lidoFixedPriceMultiLpARM), + weth.balanceOf(address(lidoFixedPriceMultiLpARM)) + assetGain ); // Calculate Fees uint256 fee = assetGain * 20 / 100; // 20% fee - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee); } function test_TotalAssets_AfterDeposit_WithAssetGain_InSTETH() public depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) { - assertEq(steth.balanceOf(address(lidoFixedPriceMulltiLpARM)), 0); + assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); // Simulate asset gain uint256 assetGain = DEFAULT_AMOUNT / 2 + 1; // We are sure that steth balance is empty, so we can deal directly final amount. - deal(address(steth), address(lidoFixedPriceMulltiLpARM), assetGain); + deal(address(steth), address(lidoFixedPriceMultiLpARM), assetGain); // Calculate Fees uint256 fee = assetGain * 20 / 100; // 20% fee assertApproxEqAbs( - lidoFixedPriceMulltiLpARM.totalAssets(), + lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee, STETH_ERROR_ROUNDING ); @@ -106,11 +106,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared uint256 assetLoss = DEFAULT_AMOUNT / 2; deal( address(weth), - address(lidoFixedPriceMulltiLpARM), - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - assetLoss + address(lidoFixedPriceMultiLpARM), + weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - assetLoss ); - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss); } function test_TotalAssets_AfterDeposit_WithAssetLoss_InSTETH() @@ -121,15 +121,15 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared uint256 swapAmount = DEFAULT_AMOUNT / 2; deal( address(weth), - address(lidoFixedPriceMulltiLpARM), - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - swapAmount + address(lidoFixedPriceMultiLpARM), + weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - swapAmount ); // Then simulate a loss on stETH, do all in the same deal uint256 assetLoss = swapAmount / 2; - deal(address(steth), address(lidoFixedPriceMulltiLpARM), swapAmount / 2); + deal(address(steth), address(lidoFixedPriceMultiLpARM), swapAmount / 2); assertApproxEqAbs( - lidoFixedPriceMulltiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss, STETH_ERROR_ROUNDING + lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss, STETH_ERROR_ROUNDING ); } @@ -138,20 +138,20 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared uint256 swapAmount = MIN_TOTAL_SUPPLY / 2; deal( address(weth), - address(lidoFixedPriceMulltiLpARM), - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) - swapAmount + address(lidoFixedPriceMultiLpARM), + weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - swapAmount ); - deal(address(steth), address(lidoFixedPriceMulltiLpARM), swapAmount); // Empty stETH balance, so we can deal directly + deal(address(steth), address(lidoFixedPriceMultiLpARM), swapAmount); // Empty stETH balance, so we can deal directly - uint256 totalAssetsBefore = lidoFixedPriceMulltiLpARM.totalAssets(); + uint256 totalAssetsBefore = lidoFixedPriceMultiLpARM.totalAssets(); // Request a redeem on Lido uint256[] memory amounts = new uint256[](1); amounts[0] = swapAmount; - lidoFixedPriceMulltiLpARM.requestStETHWithdrawalForETH(amounts); + lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); // Check total assets after withdrawal is the same as before - assertApproxEqAbs(lidoFixedPriceMulltiLpARM.totalAssets(), totalAssetsBefore, STETH_ERROR_ROUNDING); + assertApproxEqAbs(lidoFixedPriceMultiLpARM.totalAssets(), totalAssetsBefore, STETH_ERROR_ROUNDING); } function test_TotalAssets_With_FeeAccrued_NotNull() public { @@ -159,19 +159,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared // Simulate asset gain deal( address(weth), - address(lidoFixedPriceMulltiLpARM), - weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) + assetGain + address(lidoFixedPriceMultiLpARM), + weth.balanceOf(address(lidoFixedPriceMultiLpARM)) + assetGain ); // User deposit, this will trigger a fee calculation - lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT); + lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT); // Assert fee accrued is not null - assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), assetGain * 20 / 100); + assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), assetGain * 20 / 100); assertEq( - lidoFixedPriceMulltiLpARM.totalAssets(), - MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - assetGain * 20 / 100 + lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - assetGain * 20 / 100 ); } @@ -181,8 +180,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) { // Simulate a loss of assets - deal(address(weth), address(lidoFixedPriceMulltiLpARM), DEFAULT_AMOUNT - 1); + deal(address(weth), address(lidoFixedPriceMultiLpARM), DEFAULT_AMOUNT - 1); - assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), 0); + assertEq(lidoFixedPriceMultiLpARM.totalAssets(), 0); } } diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 96ad079..99824b3 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -162,10 +162,10 @@ abstract contract Fork_Shared_Test_ is Modifiers { lidoProxy.initialize(address(lidoImpl), address(this), data); // Set the Proxy as the LidoARM. - lidoFixedPriceMulltiLpARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); + lidoFixedPriceMultiLpARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); // set prices - lidoFixedPriceMulltiLpARM.setPrices(992 * 1e33, 1001 * 1e33); + lidoFixedPriceMultiLpARM.setPrices(992 * 1e33, 1001 * 1e33); } function _label() internal { @@ -176,7 +176,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { vm.label(address(vault), "OETH VAULT"); vm.label(address(oethARM), "OETH ARM"); vm.label(address(proxy), "OETH ARM PROXY"); - vm.label(address(lidoFixedPriceMulltiLpARM), "LIDO ARM"); + vm.label(address(lidoFixedPriceMultiLpARM), "LIDO ARM"); vm.label(address(lidoProxy), "LIDO ARM PROXY"); vm.label(address(liquidityProviderController), "LIQUIDITY PROVIDER CONTROLLER"); vm.label(operator, "OPERATOR"); diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index 448aa2a..b4bfc70 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -40,10 +40,10 @@ abstract contract Helpers is Base_Test_ { uint256 expectedClaimed, uint256 expectedNextIndex ) public view { - assertEq(lidoFixedPriceMulltiLpARM.withdrawsQueued(), expectedQueued, "metadata queued"); - assertEq(lidoFixedPriceMulltiLpARM.withdrawsClaimable(), expectedClaimable, "metadata claimable"); - assertEq(lidoFixedPriceMulltiLpARM.withdrawsClaimed(), expectedClaimed, "metadata claimed"); - assertEq(lidoFixedPriceMulltiLpARM.nextWithdrawalIndex(), expectedNextIndex, "metadata nextWithdrawalIndex"); + assertEq(lidoFixedPriceMultiLpARM.withdrawsQueued(), expectedQueued, "metadata queued"); + assertEq(lidoFixedPriceMultiLpARM.withdrawsClaimable(), expectedClaimable, "metadata claimable"); + assertEq(lidoFixedPriceMultiLpARM.withdrawsClaimed(), expectedClaimed, "metadata claimed"); + assertEq(lidoFixedPriceMultiLpARM.nextWithdrawalIndex(), expectedNextIndex, "metadata nextWithdrawalIndex"); } /// @notice Asserts the equality bewteen value of `withdrawalRequests()` and the expected values. @@ -56,7 +56,7 @@ abstract contract Helpers is Base_Test_ { uint256 queued ) public view { (address _withdrawer, bool _claimed, uint40 _claimTimestamp, uint128 _assets, uint128 _queued) = - lidoFixedPriceMulltiLpARM.withdrawalRequests(requestId); + lidoFixedPriceMultiLpARM.withdrawalRequests(requestId); assertEq(_withdrawer, withdrawer, "Wrong withdrawer"); assertEq(_claimed, claimed, "Wrong claimed"); assertEq(_claimTimestamp, claimTimestamp, "Wrong claimTimestamp"); diff --git a/test/fork/utils/MockCall.sol b/test/fork/utils/MockCall.sol index 56bd606..e1d4f23 100644 --- a/test/fork/utils/MockCall.sol +++ b/test/fork/utils/MockCall.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.23; // Foundry import {Vm} from "forge-std/Vm.sol"; +import {Mainnet} from "contracts/utils/Addresses.sol"; + /// @notice This contract should be used to mock calls to other contracts. library MockCall { Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); @@ -11,4 +13,45 @@ library MockCall { function mockCallDripperCollect(address dripper) external { vm.mockCall({callee: dripper, data: abi.encodeWithSignature("collect()"), returnData: abi.encode(true)}); } + + function mockCallLidoFindCheckpointHints() external { + vm.mockCall({ + callee: Mainnet.LIDO_WITHDRAWAL, + data: abi.encodeWithSignature("findCheckpointHints(uint256[],uint256,uint256)"), + returnData: abi.encode(new uint256[](1)) + }); + } + + function mockCallLidoClaimWithdrawals(address target) external { + vm.mockFunction({ + callee: Mainnet.LIDO_WITHDRAWAL, + target: target, + data: abi.encodeWithSignature("claimWithdrawals(uint256[],uint256[])") + }); + } +} + +contract MockLidoWithdraw { + ETHSender public immutable ethSender; + address public immutable lidoFixedPriceMultiLpARM; + + constructor(address _lidoFixedPriceMulltiLpARM) { + ethSender = new ETHSender(); + lidoFixedPriceMultiLpARM = _lidoFixedPriceMulltiLpARM; + } + + /// @notice Mock the call to the Lido contract's `claimWithdrawals` function. + /// @dev as it is not possible to transfer ETH from the mocked contract (seems to be an issue with forge) + /// we use the ETHSender contract intermediary to send the ETH to the target contract. + function claimWithdrawals(uint256[] memory, uint256[] memory) external { + ethSender.sendETH(lidoFixedPriceMultiLpARM); + } +} + +contract ETHSender { + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function sendETH(address target) external { + vm.deal(target, address(this).balance); + } } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index c5d7464..f9c592c 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -7,6 +7,8 @@ import {VmSafe} from "forge-std/Vm.sol"; // Test imports import {Helpers} from "test/fork/utils/Helpers.sol"; import {MockCall} from "test/fork/utils/MockCall.sol"; +import {MockLidoWithdraw} from "test/fork/utils/MockCall.sol"; +import {ETHSender} from "test/fork/utils/MockCall.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; @@ -35,14 +37,14 @@ abstract contract Modifiers is Helpers { /// @notice Impersonate the operator of LidoOwnerLpARM contract. modifier asLidoFixedPriceMulltiLpARMOperator() { - vm.startPrank(lidoFixedPriceMulltiLpARM.operator()); + vm.startPrank(lidoFixedPriceMultiLpARM.operator()); _; vm.stopPrank(); } /// @notice Impersonate the owner of LidoFixedPriceMultiLpARM contract. modifier asLidoFixedPriceMultiLpARMOwner() { - vm.startPrank(lidoFixedPriceMulltiLpARM.owner()); + vm.startPrank(lidoFixedPriceMultiLpARM.owner()); _; vm.stopPrank(); } @@ -87,8 +89,8 @@ abstract contract Modifiers is Helpers { // Deal amount as "extra" to user deal(address(weth), user, amount + balance); vm.startPrank(user); - weth.approve(address(lidoFixedPriceMulltiLpARM), type(uint256).max); - lidoFixedPriceMulltiLpARM.deposit(amount); + weth.approve(address(lidoFixedPriceMultiLpARM), type(uint256).max); + lidoFixedPriceMultiLpARM.deposit(amount); vm.stopPrank(); if (mode == VmSafe.CallerMode.Prank) { @@ -106,7 +108,7 @@ abstract contract Modifiers is Helpers { vm.stopPrank(); vm.startPrank(user); - lidoFixedPriceMulltiLpARM.requestRedeem(amount); + lidoFixedPriceMultiLpARM.requestRedeem(amount); vm.stopPrank(); if (mode == VmSafe.CallerMode.Prank) { @@ -124,7 +126,7 @@ abstract contract Modifiers is Helpers { vm.stopPrank(); vm.startPrank(user); - lidoFixedPriceMulltiLpARM.claimRedeem(requestId); + lidoFixedPriceMultiLpARM.claimRedeem(requestId); vm.stopPrank(); if (mode == VmSafe.CallerMode.Prank) { @@ -144,14 +146,14 @@ abstract contract Modifiers is Helpers { if (gain) { deal( token, - address(lidoFixedPriceMulltiLpARM), - IERC20(token).balanceOf(address(lidoFixedPriceMulltiLpARM)) + uint256(assetGain) + address(lidoFixedPriceMultiLpARM), + IERC20(token).balanceOf(address(lidoFixedPriceMultiLpARM)) + uint256(assetGain) ); } else { deal( token, - address(lidoFixedPriceMulltiLpARM), - IERC20(token).balanceOf(address(lidoFixedPriceMulltiLpARM)) - uint256(assetGain) + address(lidoFixedPriceMultiLpARM), + IERC20(token).balanceOf(address(lidoFixedPriceMultiLpARM)) - uint256(assetGain) ); } @@ -165,7 +167,79 @@ abstract contract Modifiers is Helpers { /// @notice Collect fees on LidoFixedPriceMultiLpARM contract. modifier collectFeesOnLidoFixedPriceMultiLpARM() { - lidoFixedPriceMulltiLpARM.collectFees(); + lidoFixedPriceMultiLpARM.collectFees(); + _; + } + + /// @notice Approve stETH on LidoFixedPriceMultiLpARM contract. + modifier approveStETHOnLidoFixedPriceMultiLpARM() { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + vm.prank(lidoFixedPriceMultiLpARM.owner()); + lidoFixedPriceMultiLpARM.approveStETH(); + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } + _; + } + + /// @notice Request stETH withdrawal for ETH on LidoFixedPriceMultiLpARM contract. + modifier requestStETHWithdrawalForETHOnLidoFixedPriceMultiLpARM(uint256[] memory amounts) { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + vm.prank(lidoFixedPriceMultiLpARM.owner()); + lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } + _; + } + + /// @notice mock call for `findCheckpointHints`on lido withdraw contracts. + modifier mockCallLidoFindCheckpointHints() { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + MockCall.mockCallLidoFindCheckpointHints(); + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } + _; + } + + /// @notice mock call for `claimWithdrawals` on lido withdraw contracts. + /// @dev this will send eth directly to the lidoFixedPriceMultiLpARM contract. + modifier mockFunctionClaimWithdrawOnLidoFixedPriceMultiLpARM(uint256 amount) { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + // Deploy fake lido withdraw contract + MockLidoWithdraw mocklidoWithdraw = new MockLidoWithdraw(address(lidoFixedPriceMultiLpARM)); + // Give ETH to the ETH Sender contract + vm.deal(address(mocklidoWithdraw.ethSender()), amount); + // Mock all the call to the fake lido withdraw contract + MockCall.mockCallLidoClaimWithdrawals(address(mocklidoWithdraw)); + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } _; } From 6a8652bdf5aedaf206446c1d28938a067767fadd Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Thu, 26 Sep 2024 17:18:47 +1000 Subject: [PATCH 084/196] Flatten the LidoARM inheritance (#25) * Flatten LidoARM's inheritance * Renamed LidoARM contract * Changed to LidoARM name in tests * Generated new contract diagrams * Minor changes to the LidoARM * Renamed lidoARM variable in tests * Updated deposit test comments * Updated setPrice tests * Small change to setPrices test --- docs/LidoARMHierarchy.svg | 80 +++ docs/LidoARMPublicSquashed.svg | 87 +++ docs/LidoARMSquashed.svg | 117 ++++ docs/generate.sh | 8 +- .../mainnet/003_UpgradeLidoARMScript.sol | 8 +- src/contracts/AbstractARM.sol | 547 +++++++++++++++++- src/contracts/FixedPriceARM.sol | 113 ---- ...doFixedPriceMultiLpARM.sol => LidoARM.sol} | 63 +- src/contracts/LidoLiquidityManager.sol | 7 + .../LiquidityProviderControllerARM.sol | 40 -- src/contracts/MultiLP.sol | 269 --------- src/contracts/OethARM.sol | 6 +- src/contracts/PerformanceFee.sol | 161 ------ src/contracts/README.md | 6 +- test/Base.sol | 4 +- .../ClaimRedeem.t.sol | 165 +++--- .../ClaimStETHWithdrawalForWETH.t.sol | 48 +- .../CollectFees.t.sol | 33 +- .../Constructor.t.sol | 24 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 254 ++++---- .../RequestRedeem.t.sol | 151 +++-- .../RequestStETHWithdrawalForETH.t.sol | 45 +- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 113 ++-- .../SwapExactTokensForTokens.t.sol | 102 ++-- .../SwapTokensForExactTokens.t.sol | 98 ++-- .../TotalAssets.t.sol | 123 ++-- test/fork/shared/Shared.sol | 15 +- test/fork/utils/Helpers.sol | 10 +- test/fork/utils/MockCall.sol | 6 +- test/fork/utils/Modifiers.sol | 72 +-- test/smoke/LidoARMSmokeTest.t.sol | 6 +- 31 files changed, 1433 insertions(+), 1348 deletions(-) create mode 100644 docs/LidoARMHierarchy.svg create mode 100644 docs/LidoARMPublicSquashed.svg create mode 100644 docs/LidoARMSquashed.svg delete mode 100644 src/contracts/FixedPriceARM.sol rename src/contracts/{LidoFixedPriceMultiLpARM.sol => LidoARM.sol} (56%) delete mode 100644 src/contracts/LiquidityProviderControllerARM.sol delete mode 100644 src/contracts/MultiLP.sol delete mode 100644 src/contracts/PerformanceFee.sol diff --git a/docs/LidoARMHierarchy.svg b/docs/LidoARMHierarchy.svg new file mode 100644 index 0000000..2fcc331 --- /dev/null +++ b/docs/LidoARMHierarchy.svg @@ -0,0 +1,80 @@ + + + + + + +UmlClassDiagram + + + +0 + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol + + + +19 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + + +0->19 + + + + + +13 + +LidoARM +../src/contracts/LidoARM.sol + + + +13->0 + + + + + +14 + +<<Abstract>> +LidoLiquidityManager +../src/contracts/LidoLiquidityManager.sol + + + +13->14 + + + + + +14->19 + + + + + +18 + +Ownable +../src/contracts/Ownable.sol + + + +19->18 + + + + + diff --git a/docs/LidoARMPublicSquashed.svg b/docs/LidoARMPublicSquashed.svg new file mode 100644 index 0000000..9b0c980 --- /dev/null +++ b/docs/LidoARMPublicSquashed.svg @@ -0,0 +1,87 @@ + + + + + + +UmlClassDiagram + + + +13 + +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>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   traderate0: uint256 <<AbstractARM>> +   traderate1: uint256 <<AbstractARM>> +   withdrawsQueued: uint128 <<AbstractARM>> +   withdrawsClaimed: uint128 <<AbstractARM>> +   withdrawsClaimable: uint128 <<AbstractARM>> +   nextWithdrawalIndex: uint128 <<AbstractARM>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> +   feeCollector: address <<AbstractARM>> +   fee: uint16 <<AbstractARM>> +   feesAccrued: uint112 <<AbstractARM>> +   lastTotalAssets: uint128 <<AbstractARM>> +   liquidityProviderController: address <<AbstractARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: 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>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<AbstractARM>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>> +    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> +    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    totalAssets(): uint256 <<AbstractARM>> +    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> +    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> + + + diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg new file mode 100644 index 0000000..880ffda --- /dev/null +++ b/docs/LidoARMSquashed.svg @@ -0,0 +1,117 @@ + + + + + + +UmlClassDiagram + + + +13 + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> +   DEAD_ACCOUNT: address <<AbstractARM>> +   liquidityAsset: address <<AbstractARM>> +Public: +   operator: address <<OwnableOperable>> +   MAX_PRICE_DEVIATION: uint256 <<AbstractARM>> +   PRICE_SCALE: uint256 <<AbstractARM>> +   CLAIM_DELAY: uint256 <<AbstractARM>> +   FEE_SCALE: uint256 <<AbstractARM>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   traderate0: uint256 <<AbstractARM>> +   traderate1: uint256 <<AbstractARM>> +   withdrawsQueued: uint128 <<AbstractARM>> +   withdrawsClaimed: uint128 <<AbstractARM>> +   withdrawsClaimable: uint128 <<AbstractARM>> +   nextWithdrawalIndex: uint128 <<AbstractARM>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> +   feeCollector: address <<AbstractARM>> +   fee: uint16 <<AbstractARM>> +   feesAccrued: uint112 <<AbstractARM>> +   lastTotalAssets: uint128 <<AbstractARM>> +   liquidityProviderController: address <<AbstractARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<AbstractARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> +    _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>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<AbstractARM>> +    _updateWithdrawalQueueLiquidity() <<AbstractARM>> +    _liquidityAvailable(): uint256 <<AbstractARM>> +    _rawTotalAssets(): uint256 <<AbstractARM>> +    _externalWithdrawQueue(): uint256 <<LidoARM>> +    _calcFee() <<AbstractARM>> +    _setFee(_fee: uint256) <<AbstractARM>> +    _setFeeCollector(_feeCollector: address) <<AbstractARM>> +    _initLidoLiquidityManager() <<LidoLiquidityManager>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoARM>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> TraderateChanged(traderate0: uint256, traderate1: 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>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<AbstractARM>> +    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>> +    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>> +    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> +    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    totalAssets(): uint256 <<AbstractARM>> +    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> +    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> + + + diff --git a/docs/generate.sh b/docs/generate.sh index aa0c8b5..4589e56 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -13,10 +13,10 @@ sol2uml storage ../src/contracts -c OethARM -o OethARMStorage.svg \ -st address,address \ --hideExpand gap,_gap -sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMHierarchy.svg -sol2uml ../src/contracts -s -d 0 -b LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMSquashed.svg -sol2uml ../src/contracts -hp -s -d 0 -b LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMPublicSquashed.svg -sol2uml storage ../src/contracts,../lib -c LidoFixedPriceMultiLpARM -o LidoFixedPriceMultiLpARMStorage.svg \ +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LidoARM -o LidoARMHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b LidoARM -o LidoARMSquashed.svg +sol2uml ../src/contracts -hp -s -d 0 -b LidoARM -o LidoARMPublicSquashed.svg +sol2uml storage ../src/contracts,../lib -c LidoARM -o LidoARMStorage.svg \ -sn eip1967.proxy.implementation,eip1967.proxy.admin \ -st address,address \ --hideExpand gap,_gap diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 62b3d51..9f8ae83 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -6,7 +6,7 @@ import "forge-std/console.sol"; import {Vm} from "forge-std/Vm.sol"; import {IERC20, IWETH, LegacyAMM} from "contracts/Interfaces.sol"; -import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; import {Proxy} from "contracts/Proxy.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; @@ -23,7 +23,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { Proxy lidoARMProxy; Proxy lpcProxy; - LidoFixedPriceMultiLpARM lidoARMImpl; + LidoARM lidoARMImpl; function _execute() internal override { console.log("Deploy:", DEPLOY_NAME); @@ -53,7 +53,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { liquidityProviderController.setLiquidityProviderCaps(liquidityProviders, 10 ether); // 6. Deploy Lido implementation - lidoARMImpl = new LidoFixedPriceMultiLpARM(Mainnet.STETH, Mainnet.WETH, Mainnet.LIDO_WITHDRAWAL); + lidoARMImpl = new LidoARM(Mainnet.STETH, Mainnet.WETH, Mainnet.LIDO_WITHDRAWAL); _recordDeploy("LIDO_ARM_IMPL", address(lidoARMImpl)); // 7. Transfer ownership of LiquidityProviderController to the mainnet 5/8 multisig @@ -113,7 +113,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { lidoARMProxy.upgradeToAndCall(address(lidoARMImpl), data); // Set the buy price with a 8 basis point discount. The sell price is 1.0 - LidoFixedPriceMultiLpARM(payable(Mainnet.LIDO_ARM)).setPrices(9994e32, 1e36); + LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(9994e32, 1e36); // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 8132261..74d8c85 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -1,10 +1,38 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + import {OwnableOperable} from "./OwnableOperable.sol"; -import {IERC20} from "./Interfaces.sol"; +import {IERC20, ILiquidityProviderController} from "./Interfaces.sol"; + +abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { + //////////////////////////////////////////////////// + /// Constants + //////////////////////////////////////////////////// + + /// @notice Maximum amount the Operator can set the price from 1 scaled to 36 decimals. + /// 1e33 is a 0.1% deviation, or 10 basis points. + uint256 public constant MAX_PRICE_DEVIATION = 1e33; + /// @notice Scale of the prices. + uint256 public constant PRICE_SCALE = 1e36; + /// @notice The delay before a withdrawal request can be claimed in seconds + uint256 public constant CLAIM_DELAY = 10 minutes; + /// @dev The amount of shares that are minted to a dead address on initalization + 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; + /// @notice The scale of the performance fee + /// 10,000 = 100% performance fee + uint256 public constant FEE_SCALE = 10000; -abstract contract AbstractARM is OwnableOperable { + //////////////////////////////////////////////////// + /// Immutable Variables + //////////////////////////////////////////////////// + + /// @notice The address of the asset that is used to add and remove liquidity. eg WETH + address internal immutable liquidityAsset; /// @notice The swap input token that is transferred to this contract. /// From a User perspective, this is the token being sold. /// token0 is also compatible with the Uniswap V2 Router interface. @@ -14,9 +42,85 @@ abstract contract AbstractARM is OwnableOperable { /// token1 is also compatible with the Uniswap V2 Router interface. IERC20 public immutable token1; - uint256[50] private _gap; + //////////////////////////////////////////////////// + /// Storage Variables + //////////////////////////////////////////////////// + + /** + * @notice For one `token0` from a Trader, how many `token1` does the pool send. + * For example, if `token0` is WETH and `token1` is stETH then + * `traderate0` is the WETH/stETH price. + * From a Trader's perspective, this is the stETH/WETH buy price. + * Rate is to 36 decimals (1e36). + */ + uint256 public traderate0; + /** + * @notice For one `token1` from a Trader, how many `token0` does the pool send. + * For example, if `token0` is WETH and `token1` is stETH then + * `traderate1` is the stETH/WETH price. + * From a Trader's perspective, this is the stETH/WETH sell price. + * Rate is to 36 decimals (1e36). + */ + uint256 public traderate1; + + /// @notice cumulative total of all withdrawal requests included 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 cumulative total of all the withdrawal requests that can be claimed including the ones already claimed + uint128 public withdrawsClaimable; + /// @notice index of the next withdrawal request starting at 0 + uint128 public nextWithdrawalIndex; + + struct WithdrawalRequest { + address withdrawer; + bool claimed; + // When the withdrawal can be claimed + uint40 claimTimestamp; + // Amount of assets to withdraw + uint128 assets; + // cumulative total of all withdrawal requests including this one. + // this request can be claimed when this queued amount is less than or equal to the queue's claimable amount. + uint128 queued; + } + + /// @notice Mapping of withdrawal request indices to the user withdrawal request data + mapping(uint256 requestId => WithdrawalRequest) public withdrawalRequests; + + /// @notice The account that can collect the performance fee + address public feeCollector; + /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent) + /// 10,000 = 100% performance fee + /// 2,000 = 20% performance fee + /// 500 = 5% performance fee + uint16 public fee; + /// @notice The performance fees accrued but not collected. + /// This is removed from the total assets. + uint112 public feesAccrued; + /// @notice The total assets at the last time performance fees were calculated. + /// This can only go up so is a high watermark. + uint128 public lastTotalAssets; + + address public liquidityProviderController; + + uint256[42] private _gap; + + //////////////////////////////////////////////////// + /// Events + //////////////////////////////////////////////////// + + event TraderateChanged(uint256 traderate0, uint256 traderate1); + event RedeemRequested( + address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp + ); + event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); + event FeeCalculated(uint256 newFeesAccrued, uint256 assetIncrease); + event FeeCollected(address indexed feeCollector, uint256 fee); + event FeeUpdated(uint256 fee); + event FeeCollectorUpdated(address indexed newFeeCollector); + event LiquidityProviderControllerUpdated(address indexed liquidityProviderController); - constructor(address _inputToken, address _outputToken1) { + constructor(address _inputToken, address _outputToken1, address _liquidityAsset) { require(IERC20(_inputToken).decimals() == 18); require(IERC20(_outputToken1).decimals() == 18); @@ -24,8 +128,54 @@ abstract contract AbstractARM is OwnableOperable { token1 = IERC20(_outputToken1); _setOwner(address(0)); // Revoke owner for implementation contract at deployment + + require(_liquidityAsset == address(token0) || _liquidityAsset == address(token1), "invalid liquidity asset"); + liquidityAsset = _liquidityAsset; } + /// @notice Initialize the contract. + /// The deployer that calls initialize has to approve the this ARM's proxy contract to transfer 1e12 WETH. + /// @param _operator The address of the account that can request and claim Lido withdrawals. + /// @param _name The name of the liquidity provider (LP) token. + /// @param _symbol The symbol of the liquidity provider (LP) token. + /// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent). + /// 10,000 = 100% performance fee + /// 500 = 5% performance fee + /// @param _feeCollector The account that can collect the performance fee + /// @param _liquidityProviderController The address of the Liquidity Provider Controller + function _initARM( + address _operator, + string calldata _name, + string calldata _symbol, + uint256 _fee, + address _feeCollector, + address _liquidityProviderController + ) internal { + _initOwnableOperable(_operator); + + __ERC20_init(_name, _symbol); + + // Transfer a small bit of liquidity from the intializer 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 + // This avoids donation attacks when there are no assets in the ARM contract + _mint(DEAD_ACCOUNT, MIN_TOTAL_SUPPLY); + + // Initialize the last total assets to the current total assets + // This ensures no performance fee is accrued when the performance fee is calculated when the fee is set + lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); + _setFee(_fee); + _setFeeCollector(_feeCollector); + + liquidityProviderController = _liquidityProviderController; + emit LiquidityProviderControllerUpdated(_liquidityProviderController); + } + + //////////////////////////////////////////////////// + /// Swap Functions + //////////////////////////////////////////////////// + /** * @notice Swaps an exact amount of input tokens for as many output tokens as possible. * msg.sender should have already given the ARM contract an allowance of @@ -141,27 +291,396 @@ abstract contract AbstractARM is OwnableOperable { amounts[1] = amountOut; } + function _inDeadline(uint256 deadline) internal view { + require(deadline >= block.timestamp, "ARM: Deadline expired"); + } + + /// @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 { + if (asset == liquidityAsset) { + require(amount <= _liquidityAvailable(), "ARM: Insufficient liquidity"); + } + + IERC20(asset).transfer(to, amount); + } + + /// @dev Hook to transfer assets into the ARM contract + function _transferAssetFrom(address asset, address from, address to, uint256 amount) internal virtual { + IERC20(asset).transferFrom(from, to, amount); + } + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to) internal virtual - returns (uint256 amountOut); + returns (uint256 amountOut) + { + uint256 price; + if (inToken == token0) { + require(outToken == token1, "ARM: Invalid out token"); + price = traderate0; + } else if (inToken == token1) { + require(outToken == token0, "ARM: Invalid out token"); + price = traderate1; + } else { + revert("ARM: Invalid in token"); + } + amountOut = amountIn * price / PRICE_SCALE; + + // Transfer the input tokens from the caller to this ARM contract + _transferAssetFrom(address(inToken), msg.sender, address(this), amountIn); + + // Transfer the output tokens to the recipient + _transferAsset(address(outToken), to, amountOut); + } function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to) internal virtual - returns (uint256 amountIn); + returns (uint256 amountIn) + { + uint256 price; + if (inToken == token0) { + require(outToken == token1, "ARM: Invalid out token"); + price = traderate0; + } else if (inToken == token1) { + require(outToken == token0, "ARM: Invalid out token"); + price = traderate1; + } else { + revert("ARM: Invalid in token"); + } + amountIn = ((amountOut * PRICE_SCALE) / price) + 1; // +1 to always round in our favor - function _inDeadline(uint256 deadline) internal view { - require(deadline >= block.timestamp, "ARM: Deadline expired"); + // Transfer the input tokens from the caller to this ARM contract + _transferAssetFrom(address(inToken), msg.sender, address(this), amountIn); + + // Transfer the output tokens to the recipient + _transferAsset(address(outToken), to, amountOut); } - /// @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); + /** + * @notice Set exchange rates from an operator account from the ARM's perspective. + * If token 0 is WETH and token 1 is stETH, then both prices will be set using the stETH/WETH price. + * @param buyT1 The price the ARM buys Token 1 from the Trader, denominated in Token 0, scaled to 36 decimals. + * From the Trader's perspective, this is the sell price. + * @param sellT1 The price the ARM sells Token 1 to the Trader, denominated in Token 0, scaled to 36 decimals. + * From the Trader's perspective, this is the buy price. + */ + function setPrices(uint256 buyT1, uint256 sellT1) external onlyOperatorOrOwner { + // Limit funds and loss when called by the Operator + if (msg.sender == operator) { + require(sellT1 >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: sell price too low"); + require(buyT1 <= PRICE_SCALE + MAX_PRICE_DEVIATION, "ARM: buy price too high"); + } + uint256 _traderate0 = 1e72 / sellT1; // base (t0) -> token (t1) + uint256 _traderate1 = buyT1; // token (t1) -> base (t0) + _setTraderates(_traderate0, _traderate1); } - /// @dev Hook to transfer assets into the ARM contract - function _transferAssetFrom(address asset, address from, address to, uint256 amount) internal virtual { - IERC20(asset).transferFrom(from, to, amount); + function _setTraderates(uint256 _traderate0, uint256 _traderate1) internal { + require((1e72 / (_traderate0)) > _traderate1, "ARM: Price cross"); + traderate0 = _traderate0; + traderate1 = _traderate1; + + emit TraderateChanged(_traderate0, _traderate1); + } + + //////////////////////////////////////////////////// + /// Liquidity Provider Functions + //////////////////////////////////////////////////// + + /// @notice Preview the amount of shares that would be minted for a given amount of assets + /// @param assets The amount of liquidity assets to deposit + /// @return shares The amount of shares that would be minted + function previewDeposit(uint256 assets) external view returns (uint256 shares) { + shares = convertToShares(assets); + } + + /// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares. + /// The caller needs to have approved the contract to transfer the assets. + /// @param assets The amount of liquidity assets to deposit + /// @return shares The amount of shares that were minted + function deposit(uint256 assets) external returns (uint256 shares) { + // Accrue any performance fees based on the increase in total assets before + // the liquidity asset from the deposit is transferred into the ARM + _accruePerformanceFee(); + + // Calculate the amount of shares to mint after the performance fees have been accrued + // which reduces the total assets and before new assets are deposited. + shares = convertToShares(assets); + + // Transfer the liquidity asset from the sender to this contract + IERC20(liquidityAsset).transferFrom(msg.sender, address(this), assets); + + // mint shares + _mint(msg.sender, shares); + + // Save the new total assets after the performance fee accrued and new assets deposited + lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); + + // Check the liquidity provider caps after the new assets have been deposited + if (liquidityProviderController != address(0)) { + ILiquidityProviderController(liquidityProviderController).postDepositHook(msg.sender, assets); + } + } + + /// @notice Preview the amount of assets that would be received for burning a given amount of shares + /// @param shares The amount of shares to burn + /// @return assets The amount of liquidity assets that would be received + function previewRedeem(uint256 shares) external view returns (uint256 assets) { + assets = convertToAssets(shares); + } + + /// @notice Request to redeem liquidity provider shares for liquidity assets + /// @param shares The amount of shares the redeemer wants to burn for liquidity assets + /// @return requestId The index of the withdrawal request + /// @return assets The amount of liquidity assets that will be claimable by the redeemer + function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { + // Accrue any performance fees based on the increase in total assets before + // the liquidity asset from the redeem is reserved for the ARM withdrawal queue + _accruePerformanceFee(); + + // Calculate the amount of assets to transfer to the redeemer + assets = convertToAssets(shares); + + requestId = nextWithdrawalIndex; + uint128 queued = SafeCast.toUint128(withdrawsQueued + assets); + uint40 claimTimestamp = uint40(block.timestamp + CLAIM_DELAY); + + // Store the next withdrawal request + nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); + // Store the updated queued amount which reserves WETH in the withdrawal queue + withdrawsQueued = queued; + // Store requests + withdrawalRequests[requestId] = WithdrawalRequest({ + withdrawer: msg.sender, + claimed: false, + claimTimestamp: claimTimestamp, + assets: SafeCast.toUint128(assets), + queued: queued + }); + + // burn redeemer's shares + _burn(msg.sender, shares); + + // Save the new total assets after performance fee accrued and withdrawal queue updated + lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); + + emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); + } + + /// @notice Claim liquidity assets from a previous withdrawal request after the claim delay has passed + /// @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) { + // Update the ARM's withdrawal queue's claimable amount + _updateWithdrawalQueueLiquidity(); + + // Load the structs from storage into memory + WithdrawalRequest memory request = withdrawalRequests[requestId]; + + require(request.claimTimestamp <= block.timestamp, "Claim delay not met"); + // If there isn't enough reserved liquidity in the queue to claim + require(request.queued <= withdrawsClaimable, "Queue pending liquidity"); + require(request.withdrawer == msg.sender, "Not requester"); + require(request.claimed == false, "Already claimed"); + + // Store the request as claimed + withdrawalRequests[requestId].claimed = true; + // Store the updated claimed amount + withdrawsClaimed += request.assets; + + assets = request.assets; + + emit RedeemClaimed(msg.sender, requestId, assets); + + // transfer the liquidity asset to the withdrawer + IERC20(liquidityAsset).transfer(msg.sender, assets); + } + + /// @dev Updates the claimable amount in the ARM's withdrawal queue. + /// That's the amount that is used to check if a request can be claimed or not. + function _updateWithdrawalQueueLiquidity() internal { + // Load the claimable amount from storage into memory + uint256 withdrawsClaimableMem = withdrawsClaimable; + + // Check if the claimable amount is less than the queued amount + uint256 queueShortfall = withdrawsQueued - withdrawsClaimableMem; + + // No need to do anything is the withdrawal queue is fully funded + if (queueShortfall == 0) { + return; + } + + uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this)); + + // Of the claimable withdrawal requests, how much is unclaimed? + // That is, the amount of the liquidity assets that is currently allocated for the withdrawal queue + uint256 allocatedLiquidity = withdrawsClaimableMem - withdrawsClaimed; + + // If there is no unallocated liquidity assets then there is nothing to add to the queue + if (liquidityBalance <= allocatedLiquidity) { + return; + } + + uint256 unallocatedLiquidity = liquidityBalance - allocatedLiquidity; + + // the new claimable amount is the smaller of the queue shortfall or unallocated weth + uint256 addedClaimable = queueShortfall < unallocatedLiquidity ? queueShortfall : unallocatedLiquidity; + + // Store the new claimable amount back to storage + withdrawsClaimable = SafeCast.toUint128(withdrawsClaimableMem + addedClaimable); + } + + /// @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) { + // The amount of WETH that is still to be claimed in the withdrawal queue + uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; + + // 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; + } + + /// @notice The total amount of assets in the ARM and external withdrawal queue, + /// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees. + function totalAssets() public view virtual returns (uint256) { + uint256 totalAssetsBeforeFees = _rawTotalAssets(); + + // If the total assets have decreased, then we don't charge a performance fee + if (totalAssetsBeforeFees <= lastTotalAssets) return totalAssetsBeforeFees; + + // Calculate the increase in assets since the last time fees were calculated + uint256 assetIncrease = totalAssetsBeforeFees - lastTotalAssets; + + // Calculate the performance fee and remove from the total assets before new fees are removed + return totalAssetsBeforeFees - ((assetIncrease * fee) / FEE_SCALE); + } + + /// @dev Calculate the total assets in the ARM, external withdrawal queue, + /// less liquidity assets reserved for the ARM's withdrawal queue and past accrued fees. + /// The accrued fees are from the last time fees were calculated. + function _rawTotalAssets() internal view returns (uint256) { + // Get the assets in the ARM and external withdrawal queue + uint256 assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); + + // Load the queue metadata from storage into memory + uint256 queuedMem = withdrawsQueued; + uint256 claimedMem = withdrawsClaimed; + + // If the ARM becomes insolvent enough that the total value in the ARM and external withdrawal queue + // is less than the outstanding withdrawals. + if (assets + claimedMem < queuedMem) { + return 0; + } + + // Need to remove the liquidity assets that have been reserved for the withdrawal queue + // and any accrued fees + return assets + claimedMem - queuedMem - feesAccrued; + } + + /// @dev Hook for calculating the amount of assets in an external withdrawal queue like Lido or OETH + /// This is not the ARM's withdrawal queue + function _externalWithdrawQueue() internal view virtual returns (uint256 assets); + + /// @notice Calculates the amount of shares for a given amount of liquidity assets + function convertToShares(uint256 assets) public view returns (uint256 shares) { + uint256 totalAssetsMem = totalAssets(); + shares = (totalAssetsMem == 0) ? assets : (assets * totalSupply()) / totalAssetsMem; + } + + /// @notice Calculates the amount of liquidity assets for a given amount of shares + function convertToAssets(uint256 shares) public view returns (uint256 assets) { + assets = (shares * totalAssets()) / totalSupply(); + } + + /// @notice Set the Liquidity Provider Controller contract address. + /// Set to a zero address to disable the controller. + function setLiquidityProviderController(address _liquidityProviderController) external onlyOwner { + liquidityProviderController = _liquidityProviderController; + + emit LiquidityProviderControllerUpdated(_liquidityProviderController); + } + + //////////////////////////////////////////////////// + /// Performance Fee Functions + //////////////////////////////////////////////////// + + /// @dev Accrues the performance fee based on the increase in total assets + /// Needs to be called before any action that changes the liquidity provider shares. eg deposit and redeem + function _accruePerformanceFee() internal { + uint256 newTotalAssets = _rawTotalAssets(); + + // Do not accrued a performance fee if the total assets has decreased + if (newTotalAssets <= lastTotalAssets) return; + + uint256 assetIncrease = newTotalAssets - lastTotalAssets; + uint256 newFeesAccrued = (assetIncrease * fee) / FEE_SCALE; + + // Save the new accrued fees back to storage + feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); + // Save the new total assets back to storage less the new accrued fees. + // This is be updated again in the post deposit and post withdraw hooks to include + // the assets deposited or withdrawn + lastTotalAssets = SafeCast.toUint128(newTotalAssets - newFeesAccrued); + + emit FeeCalculated(newFeesAccrued, assetIncrease); + } + + /// @notice Owner sets the performance fee on increased assets + /// @param _fee The performance fee measured in basis points (1/100th of a percent) + /// 10,000 = 100% performance fee + /// 500 = 5% performance fee + function setFee(uint256 _fee) external onlyOwner { + _setFee(_fee); + } + + /// @notice Owner sets the account/contract that receives the performance fee + function setFeeCollector(address _feeCollector) external onlyOwner { + _setFeeCollector(_feeCollector); + } + + function _setFee(uint256 _fee) internal { + require(_fee <= FEE_SCALE, "ARM: fee too high"); + + // Accrued any performance fees up to this point using the old fee + _accruePerformanceFee(); + + fee = SafeCast.toUint16(_fee); + + emit FeeUpdated(_fee); + } + + function _setFeeCollector(address _feeCollector) internal { + require(_feeCollector != address(0), "ARM: invalid fee collector"); + + feeCollector = _feeCollector; + + emit FeeCollectorUpdated(_feeCollector); + } + + /// @notice Transfer accrued performance fees to the fee collector + /// This requires enough liquidity assets in the ARM to cover the accrued fees. + function collectFees() external returns (uint256 fees) { + // Accrue any performance fees up to this point + _accruePerformanceFee(); + + // Read the updated accrued fees from storage + fees = feesAccrued; + require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); + + // Reset the accrued fees in storage + feesAccrued = 0; + + IERC20(liquidityAsset).transfer(feeCollector, fees); + + emit FeeCollected(feeCollector, fees); } } diff --git a/src/contracts/FixedPriceARM.sol b/src/contracts/FixedPriceARM.sol deleted file mode 100644 index c6587aa..0000000 --- a/src/contracts/FixedPriceARM.sol +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {AbstractARM} from "./AbstractARM.sol"; -import {IERC20} from "./Interfaces.sol"; - -/** - * @title Abstract support to an ARM with a single buy and sell price. - * @author Origin Protocol Inc - */ -abstract contract FixedPriceARM is AbstractARM { - /** - * @notice For one `token0` from a Trader, how many `token1` does the pool send. - * For example, if `token0` is WETH and `token1` is stETH then - * `traderate0` is the WETH/stETH price. - * From a Trader's perspective, this is the stETH/WETH buy price. - * Rate is to 36 decimals (1e36). - */ - uint256 public traderate0; - /** - * @notice For one `token1` from a Trader, how many `token0` does the pool send. - * For example, if `token0` is WETH and `token1` is stETH then - * `traderate1` is the stETH/WETH price. - * From a Trader's perspective, this is the stETH/WETH sell price. - * Rate is to 36 decimals (1e36). - */ - uint256 public traderate1; - - /// @notice Maximum amount the Operator can set the price from 1 scaled to 36 decimals. - /// 1e33 is a 0.1% deviation, or 10 basis points. - uint256 public constant MAX_PRICE_DEVIATION = 1e33; - /// @notice Scale of the prices. - uint256 public constant PRICE_SCALE = 1e36; - - uint256[48] private _gap; - - event TraderateChanged(uint256 traderate0, uint256 traderate1); - - function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to) - internal - override - returns (uint256 amountOut) - { - uint256 price; - if (inToken == token0) { - require(outToken == token1, "ARM: Invalid out token"); - price = traderate0; - } else if (inToken == token1) { - require(outToken == token0, "ARM: Invalid out token"); - price = traderate1; - } else { - revert("ARM: Invalid in token"); - } - amountOut = amountIn * price / PRICE_SCALE; - - // Transfer the input tokens from the caller to this ARM contract - _transferAssetFrom(address(inToken), msg.sender, address(this), amountIn); - - // Transfer the output tokens to the recipient - _transferAsset(address(outToken), to, amountOut); - } - - function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to) - internal - override - returns (uint256 amountIn) - { - uint256 price; - if (inToken == token0) { - require(outToken == token1, "ARM: Invalid out token"); - price = traderate0; - } else if (inToken == token1) { - require(outToken == token0, "ARM: Invalid out token"); - price = traderate1; - } else { - revert("ARM: Invalid in token"); - } - amountIn = ((amountOut * PRICE_SCALE) / price) + 1; // +1 to always round in our favor - - // Transfer the input tokens from the caller to this ARM contract - _transferAssetFrom(address(inToken), msg.sender, address(this), amountIn); - - // Transfer the output tokens to the recipient - _transferAsset(address(outToken), to, amountOut); - } - - /** - * @notice Set exchange rates from an operator account from the ARM's perspective. - * If token 0 is WETH and token 1 is stETH, then both prices will be set using the stETH/WETH price. - * @param buyT1 The price the ARM buys Token 1 from the Trader, denominated in Token 0, scaled to 36 decimals. - * From the Trader's perspective, this is the sell price. - * @param sellT1 The price the ARM sells Token 1 to the Trader, denominated in Token 0, scaled to 36 decimals. - * From the Trader's perspective, this is the buy price. - */ - function setPrices(uint256 buyT1, uint256 sellT1) external onlyOperatorOrOwner { - // Limit funds and loss when called by the Operator - if (msg.sender == operator) { - require(sellT1 >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: sell price too low"); - require(buyT1 <= PRICE_SCALE + MAX_PRICE_DEVIATION, "ARM: buy price too high"); - } - uint256 _traderate0 = 1e72 / sellT1; // base (t0) -> token (t1) - uint256 _traderate1 = buyT1; // token (t1) -> base (t0) - _setTraderates(_traderate0, _traderate1); - } - - function _setTraderates(uint256 _traderate0, uint256 _traderate1) internal { - require((1e72 / (_traderate0)) > _traderate1, "ARM: Price cross"); - traderate0 = _traderate0; - traderate1 = _traderate1; - - emit TraderateChanged(_traderate0, _traderate1); - } -} diff --git a/src/contracts/LidoFixedPriceMultiLpARM.sol b/src/contracts/LidoARM.sol similarity index 56% rename from src/contracts/LidoFixedPriceMultiLpARM.sol rename to src/contracts/LidoARM.sol index 5b2a1a8..b2aeed5 100644 --- a/src/contracts/LidoFixedPriceMultiLpARM.sol +++ b/src/contracts/LidoARM.sol @@ -5,11 +5,7 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AbstractARM} from "./AbstractARM.sol"; -import {LiquidityProviderControllerARM} from "./LiquidityProviderControllerARM.sol"; -import {FixedPriceARM} from "./FixedPriceARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; -import {MultiLP} from "./MultiLP.sol"; -import {PerformanceFee} from "./PerformanceFee.sol"; /** * @title Lido (stETH) Application Redemption Manager (ARM) @@ -19,32 +15,23 @@ import {PerformanceFee} from "./PerformanceFee.sol"; * A performance fee is also collected on increases in the ARM's total assets. * @author Origin Protocol Inc */ -contract LidoFixedPriceMultiLpARM is - Initializable, - MultiLP, - PerformanceFee, - LiquidityProviderControllerARM, - FixedPriceARM, - LidoLiquidityManager -{ +contract LidoARM is Initializable, AbstractARM, LidoLiquidityManager { /// @param _steth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract constructor(address _steth, address _weth, address _lidoWithdrawalQueue) - AbstractARM(_steth, _weth) - MultiLP(_weth) - FixedPriceARM() + AbstractARM(_steth, _weth, _weth) LidoLiquidityManager(_steth, _weth, _lidoWithdrawalQueue) {} - /// @notice Initialize the contract. + /// @notice Initialize the storage variables stored in the proxy contract. /// The deployer that calls initialize has to approve the this ARM's proxy contract to transfer 1e12 WETH. /// @param _name The name of the liquidity provider (LP) token. /// @param _symbol The symbol of the liquidity provider (LP) token. /// @param _operator The address of the account that can request and claim Lido withdrawals. /// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent). /// 10,000 = 100% performance fee - /// 500 = 5% performance fee + /// 1,500 = 15% performance fee /// @param _feeCollector The account that can collect the performance fee /// @param _liquidityProviderController The address of the Liquidity Provider Controller function initialize( @@ -55,10 +42,8 @@ contract LidoFixedPriceMultiLpARM is address _feeCollector, address _liquidityProviderController ) external initializer { - _initOwnableOperable(_operator); - _initMultiLP(_name, _symbol); - _initPerformanceFee(_fee, _feeCollector); - _initLPControllerARM(_liquidityProviderController); + _initARM(_operator, _name, _symbol, _fee, _feeCollector, _liquidityProviderController); + _initLidoLiquidityManager(); } /** @@ -67,47 +52,17 @@ contract LidoFixedPriceMultiLpARM is * * The MultiLP implementation ensures any WETH reserved for the withdrawal queue is not used in swaps from stETH to WETH. */ - function _transferAsset(address asset, address to, uint256 amount) internal override(AbstractARM, MultiLP) { + function _transferAsset(address asset, address to, uint256 amount) internal override { // Add 2 wei if transferring stETH uint256 transferAmount = asset == address(token0) ? amount + 2 : amount; - MultiLP._transferAsset(asset, to, transferAmount); + super._transferAsset(asset, to, transferAmount); } /** * @dev Calculates the amount of stETH in the Lido Withdrawal Queue. */ - function _externalWithdrawQueue() internal view override(MultiLP, LidoLiquidityManager) returns (uint256) { + function _externalWithdrawQueue() internal view override(AbstractARM, LidoLiquidityManager) returns (uint256) { return LidoLiquidityManager._externalWithdrawQueue(); } - - /** - * @dev Is called after assets are transferred to the ARM in the `deposit` method. - */ - function _postDepositHook(uint256 assets) - internal - override(MultiLP, LiquidityProviderControllerARM, PerformanceFee) - { - // Store the new total assets after the deposit and performance fee accrued - PerformanceFee._postDepositHook(assets); - - // Check the LP can deposit the assets - LiquidityProviderControllerARM._postDepositHook(assets); - } - - /** - * @dev Is called after the performance fee is accrued in the `requestRedeem` method. - */ - function _postRequestRedeemHook() internal override(MultiLP, PerformanceFee) { - // Store the new total assets after the withdrawal and performance fee accrued - PerformanceFee._postRequestRedeemHook(); - } - - /** - * @notice The total amount of assets in the ARM and Lido withdrawal queue, - * less the WETH reserved for the ARM's withdrawal queue and accrued fees. - */ - function totalAssets() public view override(MultiLP, PerformanceFee) returns (uint256) { - return PerformanceFee.totalAssets(); - } } diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 311438b..5b27093 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -23,6 +23,13 @@ abstract contract LidoLiquidityManager is OwnableOperable { withdrawalQueue = IStETHWithdrawal(_lidoWithdrawalQueue); } + /** + * @dev Approve the stETH withdrawal contract. Used for redemption requests. + */ + function _initLidoLiquidityManager() internal { + steth.approve(address(withdrawalQueue), type(uint256).max); + } + /** * @notice Approve the stETH withdrawal contract. Used for redemption requests. */ diff --git a/src/contracts/LiquidityProviderControllerARM.sol b/src/contracts/LiquidityProviderControllerARM.sol deleted file mode 100644 index 07aefa9..0000000 --- a/src/contracts/LiquidityProviderControllerARM.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {MultiLP} from "./MultiLP.sol"; -import {ILiquidityProviderController} from "./Interfaces.sol"; - -/** - * @title ARM integration to the Liquidity Provider Controller that whitelists liquidity providers - * and enforces a total assets cap. - * @author Origin Protocol Inc - */ -abstract contract LiquidityProviderControllerARM is MultiLP { - address public liquidityProviderController; - - uint256[49] private _gap; - - event LiquidityProviderControllerUpdated(address indexed liquidityProviderController); - - /// @dev called in the ARM's initialize function to set the Liquidity Provider Controller - function _initLPControllerARM(address _liquidityProviderController) internal { - liquidityProviderController = _liquidityProviderController; - - emit LiquidityProviderControllerUpdated(_liquidityProviderController); - } - - /// @dev calls the liquidity provider controller if one is configured to check the liquidity provider and total assets caps - function _postDepositHook(uint256 assets) internal virtual override { - if (liquidityProviderController != address(0)) { - ILiquidityProviderController(liquidityProviderController).postDepositHook(msg.sender, assets); - } - } - - /// @notice Set the Liquidity Provider Controller contract address. - /// Set to a zero address to disable the controller. - function setLiquidityProviderController(address _liquidityProviderController) external onlyOwner { - liquidityProviderController = _liquidityProviderController; - - emit LiquidityProviderControllerUpdated(_liquidityProviderController); - } -} diff --git a/src/contracts/MultiLP.sol b/src/contracts/MultiLP.sol deleted file mode 100644 index 651927e..0000000 --- a/src/contracts/MultiLP.sol +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {IERC20, ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {AbstractARM} from "./AbstractARM.sol"; - -/** - * @title Abstract support to an ARM for multiple Liquidity Providers (LP) - * @author Origin Protocol Inc - */ -abstract contract MultiLP is AbstractARM, ERC20Upgradeable { - /// @notice The delay before a withdrawal request can be claimed in seconds - uint256 public constant CLAIM_DELAY = 10 minutes; - /// @dev The amount of shares that are minted to a dead address on initalization - 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; - - /// @notice The address of the asset that is used to add and remove liquidity. eg WETH - address internal immutable liquidityAsset; - - /// @notice cumulative total of all withdrawal requests included 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 cumulative total of all the withdrawal requests that can be claimed including the ones already claimed - uint128 public withdrawsClaimable; - /// @notice index of the next withdrawal request starting at 0 - uint128 public nextWithdrawalIndex; - - struct WithdrawalRequest { - address withdrawer; - bool claimed; - // When the withdrawal can be claimed - uint40 claimTimestamp; - // Amount of assets to withdraw - uint128 assets; - // cumulative total of all withdrawal requests including this one. - // this request can be claimed when this queued amount is less than or equal to the queue's claimable amount. - uint128 queued; - } - - /// @notice Mapping of withdrawal request indices to the user withdrawal request data - mapping(uint256 requestId => WithdrawalRequest) public withdrawalRequests; - - uint256[47] private _gap; - - event RedeemRequested( - address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp - ); - event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); - - constructor(address _liquidityAsset) { - require(_liquidityAsset == address(token0) || _liquidityAsset == address(token1), "invalid liquidity asset"); - liquidityAsset = _liquidityAsset; - } - - /// @dev called by the concrete contract's `initialize` function - function _initMultiLP(string calldata _name, string calldata _symbol) internal { - __ERC20_init(_name, _symbol); - - // Transfer a small bit of liquidity from the intializer 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 - // This avoids donation attacks when there are no assets in the ARM contract - _mint(DEAD_ACCOUNT, MIN_TOTAL_SUPPLY); - } - - /// @notice Preview the amount of shares that would be minted for a given amount of assets - /// @param assets The amount of liquidity assets to deposit - /// @return shares The amount of shares that would be minted - function previewDeposit(uint256 assets) external view returns (uint256 shares) { - shares = convertToShares(assets); - } - - /// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares. - /// The caller needs to have approved the contract to transfer the assets. - /// @param assets The amount of liquidity assets to deposit - /// @return shares The amount of shares that were minted - function deposit(uint256 assets) external returns (uint256 shares) { - _preDepositHook(); - - shares = convertToShares(assets); - - // Transfer the liquidity asset from the sender to this contract - IERC20(liquidityAsset).transferFrom(msg.sender, address(this), assets); - - // mint shares - _mint(msg.sender, shares); - - _postDepositHook(assets); - } - - function _preDepositHook() internal virtual; - function _postDepositHook(uint256 assets) internal virtual; - - /// @notice Preview the amount of assets that would be received for burning a given amount of shares - /// @param shares The amount of shares to burn - /// @return assets The amount of liquidity assets that would be received - function previewRedeem(uint256 shares) external view returns (uint256 assets) { - assets = convertToAssets(shares); - } - - /// @notice Request to redeem liquidity provider shares for liquidity assets - /// @param shares The amount of shares the redeemer wants to burn for liquidity assets - /// @return requestId The index of the withdrawal request - /// @return assets The amount of liquidity assets that will be claimable by the redeemer - function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { - _preWithdrawHook(); - - // Calculate the amount of assets to transfer to the redeemer - assets = convertToAssets(shares); - - requestId = nextWithdrawalIndex; - uint128 queued = SafeCast.toUint128(withdrawsQueued + assets); - uint40 claimTimestamp = uint40(block.timestamp + CLAIM_DELAY); - - // Store the next withdrawal request - nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); - // Store the updated queued amount which reserves WETH in the withdrawal queue - withdrawsQueued = queued; - // Store requests - withdrawalRequests[requestId] = WithdrawalRequest({ - withdrawer: msg.sender, - claimed: false, - claimTimestamp: claimTimestamp, - assets: SafeCast.toUint128(assets), - queued: queued - }); - - // burn redeemer's shares - _burn(msg.sender, shares); - - _postRequestRedeemHook(); - - emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); - } - - function _preWithdrawHook() internal virtual; - function _postRequestRedeemHook() internal virtual; - - /// @notice Claim liquidity assets from a previous withdrawal request after the claim delay has passed - /// @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) { - // Update the ARM's withdrawal queue's claimable amount - _updateWithdrawalQueueLiquidity(); - - // Load the structs from storage into memory - WithdrawalRequest memory request = withdrawalRequests[requestId]; - - require(request.claimTimestamp <= block.timestamp, "Claim delay not met"); - // If there isn't enough reserved liquidity in the queue to claim - require(request.queued <= withdrawsClaimable, "Queue pending liquidity"); - require(request.withdrawer == msg.sender, "Not requester"); - require(request.claimed == false, "Already claimed"); - - // Store the request as claimed - withdrawalRequests[requestId].claimed = true; - // Store the updated claimed amount - withdrawsClaimed += request.assets; - - assets = request.assets; - - emit RedeemClaimed(msg.sender, requestId, assets); - - // transfer the liquidity asset to the withdrawer - IERC20(liquidityAsset).transfer(msg.sender, assets); - } - - /// @dev Updates the claimable amount in the ARM's withdrawal queue. - /// That's the amount that is used to check if a request can be claimed or not. - function _updateWithdrawalQueueLiquidity() internal { - // Load the claimable amount from storage into memory - uint256 withdrawsClaimableMem = withdrawsClaimable; - - // Check if the claimable amount is less than the queued amount - uint256 queueShortfall = withdrawsQueued - withdrawsClaimableMem; - - // No need to do anything is the withdrawal queue is fully funded - if (queueShortfall == 0) { - return; - } - - uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this)); - - // Of the claimable withdrawal requests, how much is unclaimed? - // That is, the amount of the liquidity assets that is currently allocated for the withdrawal queue - uint256 allocatedLiquidity = withdrawsClaimableMem - withdrawsClaimed; - - // If there is no unallocated liquidity assets then there is nothing to add to the queue - if (liquidityBalance <= allocatedLiquidity) { - return; - } - - uint256 unallocatedLiquidity = liquidityBalance - allocatedLiquidity; - - // the new claimable amount is the smaller of the queue shortfall or unallocated weth - uint256 addedClaimable = queueShortfall < unallocatedLiquidity ? queueShortfall : unallocatedLiquidity; - - // Store the new claimable amount back to storage - withdrawsClaimable = SafeCast.toUint128(withdrawsClaimableMem + addedClaimable); - } - - /// @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) { - // The amount of WETH that is still to be claimed in the withdrawal queue - uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; - - // 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 { - if (asset == liquidityAsset) { - require(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) { - // Get the assets in the ARM and external withdrawal queue - assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); - - // Load the queue metadata from storage into memory - uint256 queuedMem = withdrawsQueued; - uint256 claimedMem = withdrawsClaimed; - - // If the ARM becomes insolvent enough that the total value in the ARM and external withdrawal queue - // is less than the outstanding withdrawals. - if (assets + claimedMem < queuedMem) { - return 0; - } - - // Need to remove the liquidity assets that have been reserved for the withdrawal queue - return assets + claimedMem - queuedMem; - } - - /// @notice Calculates the amount of shares for a given amount of liquidity assets - function convertToShares(uint256 assets) public view returns (uint256 shares) { - uint256 totalAssetsMem = totalAssets(); - shares = (totalAssetsMem == 0) ? assets : (assets * totalSupply()) / totalAssetsMem; - } - - /// @notice Calculates the amount of liquidity assets for a given amount of shares - function convertToAssets(uint256 shares) public view returns (uint256 assets) { - assets = (shares * totalAssets()) / totalSupply(); - } - - /// @dev Hook for calculating the amount of assets in an external withdrawal queue like Lido or OETH - /// This is not the ARM's withdrawal queue - function _externalWithdrawQueue() internal view virtual returns (uint256 assets); -} diff --git a/src/contracts/OethARM.sol b/src/contracts/OethARM.sol index 64174ff..2d546ac 100644 --- a/src/contracts/OethARM.sol +++ b/src/contracts/OethARM.sol @@ -17,7 +17,7 @@ contract OethARM is Initializable, OwnerLP, PeggedARM, OethLiquidityManager { /// @param _weth The address of the WETH token that is being swapped out of this contract. /// @param _oethVault The address of the OETH Vault proxy. constructor(address _oeth, address _weth, address _oethVault) - AbstractARM(_oeth, _weth) + AbstractARM(_oeth, _weth, _weth) PeggedARM(false) OethLiquidityManager(_oeth, _oethVault) {} @@ -28,4 +28,8 @@ contract OethARM is Initializable, OwnerLP, PeggedARM, OethLiquidityManager { _setOperator(_operator); _approvals(); } + + function _externalWithdrawQueue() internal view override returns (uint256 assets) { + // TODO track OETH sent to the OETH Vault's withdrawal queue + } } diff --git a/src/contracts/PerformanceFee.sol b/src/contracts/PerformanceFee.sol deleted file mode 100644 index 32abc8c..0000000 --- a/src/contracts/PerformanceFee.sol +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {MultiLP} from "./MultiLP.sol"; - -/** - * @title Added a performance fee to an ARM with Liquidity Providers (LP) - * @author Origin Protocol Inc - */ -abstract contract PerformanceFee is MultiLP { - /// @notice The scale of the performance fee - /// 10,000 = 100% performance fee - uint256 public constant FEE_SCALE = 10000; - - /// @notice The account that can collect the performance fee - address public feeCollector; - /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent) - /// 10,000 = 100% performance fee - /// 2,000 = 20% performance fee - /// 500 = 5% performance fee - uint16 public fee; - /// @notice The performance fees accrued but not collected. - /// This is removed from the total assets. - uint112 public feesAccrued; - /// @notice The total assets at the last time performance fees were calculated. - /// This can only go up so is a high watermark. - uint128 public lastTotalAssets; - - uint256[48] private _gap; - - event FeeCalculated(uint256 newFeesAccrued, uint256 assetIncrease); - event FeeCollected(address indexed feeCollector, uint256 fee); - event FeeUpdated(uint256 fee); - event FeeCollectorUpdated(address indexed newFeeCollector); - - function _initPerformanceFee(uint256 _fee, address _feeCollector) internal { - // Initialize the last total assets to the current total assets - // This ensures no performance fee is accrued when the performance fee is calculated when the fee is set - lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); - _setFee(_fee); - _setFeeCollector(_feeCollector); - } - - /// @dev Calculate the performance fee based on the increase in total assets before - /// the liquidity asset from the deposit is transferred into the ARM - function _preDepositHook() internal virtual override { - _calcFee(); - } - - /// @dev Calculate the performance fee based on the increase in total assets before - /// the liquidity asset from the redeem is reserved for the ARM withdrawal queue - function _preWithdrawHook() internal virtual override { - _calcFee(); - } - - /// @dev Save the new total assets after the deposit and performance fee accrued - function _postDepositHook(uint256) internal virtual override { - lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); - } - - /// @dev Save the new total assets after the requestRedeem and performance fee accrued - function _postRequestRedeemHook() internal virtual override { - lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); - } - - /// @dev Calculate the performance fee based on the increase in total assets - /// Needs to be called before any action that changes the liquidity provider shares. eg deposit and redeem - function _calcFee() internal { - uint256 newTotalAssets = _rawTotalAssets(); - - // Do not accrued a performance fee if the total assets has decreased - if (newTotalAssets <= lastTotalAssets) return; - - uint256 assetIncrease = newTotalAssets - lastTotalAssets; - uint256 newFeesAccrued = (assetIncrease * fee) / FEE_SCALE; - - // Save the new accrued fees back to storage - feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); - // Save the new total assets back to storage less the new accrued fees. - // This is be updated again in the post deposit and post withdraw hooks to include - // the assets deposited or withdrawn - lastTotalAssets = SafeCast.toUint128(newTotalAssets - newFeesAccrued); - - emit FeeCalculated(newFeesAccrued, assetIncrease); - } - - /// @notice The total amount of assets in the ARM and external withdrawal queue, - /// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees. - function totalAssets() public view virtual override returns (uint256) { - uint256 totalAssetsBeforeFees = _rawTotalAssets(); - - // If the total assets have decreased, then we don't charge a performance fee - if (totalAssetsBeforeFees <= lastTotalAssets) return totalAssetsBeforeFees; - - // Calculate the increase in assets since the last time fees were calculated - uint256 assetIncrease = totalAssetsBeforeFees - lastTotalAssets; - - // Calculate the performance fee and remove from the total assets before new fees are removed - return totalAssetsBeforeFees - ((assetIncrease * fee) / FEE_SCALE); - } - - /// @dev Calculate the total assets in the ARM, external withdrawal queue, - /// less liquidity assets reserved for the ARM's withdrawal queue and past accrued fees. - /// The accrued fees are from the last time fees were calculated. - function _rawTotalAssets() internal view returns (uint256) { - return super.totalAssets() - feesAccrued; - } - - /// @notice Owner sets the performance fee on increased assets - /// @param _fee The performance fee measured in basis points (1/100th of a percent) - /// 10,000 = 100% performance fee - /// 500 = 5% performance fee - function setFee(uint256 _fee) external onlyOwner { - _setFee(_fee); - } - - /// @notice Owner sets the account/contract that receives the performance fee - function setFeeCollector(address _feeCollector) external onlyOwner { - _setFeeCollector(_feeCollector); - } - - function _setFee(uint256 _fee) internal { - require(_fee <= FEE_SCALE, "ARM: fee too high"); - - // Calculate fees up to this point using the old fee - _calcFee(); - - fee = SafeCast.toUint16(_fee); - - emit FeeUpdated(_fee); - } - - function _setFeeCollector(address _feeCollector) internal { - require(_feeCollector != address(0), "ARM: invalid fee collector"); - - feeCollector = _feeCollector; - - emit FeeCollectorUpdated(_feeCollector); - } - - /// @notice Transfer accrued performance fees to the fee collector - /// This requires enough liquidity assets in the ARM to cover the accrued fees. - function collectFees() external returns (uint256 fees) { - // Accrued all fees up to this point - _calcFee(); - - // Read the updated accrued fees from storage - fees = feesAccrued; - require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); - - // Reset the accrued fees in storage - feesAccrued = 0; - - IERC20(liquidityAsset).transfer(feeCollector, fees); - - emit FeeCollected(feeCollector, fees); - } -} diff --git a/src/contracts/README.md b/src/contracts/README.md index 494648e..d421790 100644 --- a/src/contracts/README.md +++ b/src/contracts/README.md @@ -30,12 +30,12 @@ ### Hierarchy -![Lido ARM Hierarchy](../../docs/LidoFixedPriceMultiLpARMHierarchy.svg) +![Lido ARM Hierarchy](../../docs/LidoARMHierarchy.svg) ## OETH ARM Squashed -![Lido ARM Squashed](../../docs/LidoFixedPriceMultiLpARMSquashed.svg) +![Lido ARM Squashed](../../docs/LidoARMSquashed.svg) +![Lido ARM Storage](../../docs/LidoARMStorage.svg) --> diff --git a/test/Base.sol b/test/Base.sol index 7be4705..f4b4b57 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -7,7 +7,7 @@ import {Test} from "forge-std/Test.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; -import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; // Interfaces @@ -35,7 +35,7 @@ abstract contract Base_Test_ is Test { Proxy public lidoProxy; Proxy public lidoOwnerProxy; OethARM public oethARM; - LidoFixedPriceMultiLpARM public lidoFixedPriceMultiLpARM; + LidoARM public lidoARM; LiquidityProviderController public liquidityProviderController; IERC20 public oeth; diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index 6e1bf00..fc738f4 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -6,10 +6,9 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -import {MultiLP} from "contracts/MultiLP.sol"; -import {PerformanceFee} from "contracts/PerformanceFee.sol"; +import {AbstractARM} from "contracts/AbstractARM.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { uint256 private delay; ////////////////////////////////////////////////////// /// --- SETUP @@ -18,7 +17,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared function setUp() public override { super.setUp(); - delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); + delay = lidoARM.CLAIM_DELAY(); deal(address(weth), address(this), 1_000 ether); } @@ -30,57 +29,57 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) { skip(delay - 1); vm.expectRevert("Claim delay not met"); - lidoFixedPriceMultiLpARM.claimRedeem(0); + lidoARM.claimRedeem(0); } function test_RevertWhen_ClaimRequest_Because_QueuePendingLiquidity_NoLiquidity() public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) { // Remove all weth liquidity from ARM - deal(address(weth), address(lidoFixedPriceMultiLpARM), 0); + deal(address(weth), address(lidoARM), 0); // Time jump claim delay skip(delay); // Expect revert vm.expectRevert("Queue pending liquidity"); - lidoFixedPriceMultiLpARM.claimRedeem(0); + lidoARM.claimRedeem(0); } function test_RevertWhen_ClaimRequest_Because_QueuePendingLiquidity_NoEnoughLiquidity() public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) { // Remove half of weth liquidity from ARM - uint256 halfAmount = weth.balanceOf(address(lidoFixedPriceMultiLpARM)) / 2; - deal(address(weth), address(lidoFixedPriceMultiLpARM), halfAmount); + uint256 halfAmount = weth.balanceOf(address(lidoARM)) / 2; + deal(address(weth), address(lidoARM), halfAmount); // Time jump claim delay skip(delay); // Expect revert vm.expectRevert("Queue pending liquidity"); - lidoFixedPriceMultiLpARM.claimRedeem(0); + lidoARM.claimRedeem(0); } function test_RevertWhen_ClaimRequest_Because_NotRequester() public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) { // Time jump claim delay skip(delay); @@ -88,21 +87,21 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared // Expect revert vm.startPrank(vm.randomAddress()); vm.expectRevert("Not requester"); - lidoFixedPriceMultiLpARM.claimRedeem(0); + lidoARM.claimRedeem(0); } function test_RevertWhen_ClaimRequest_Because_AlreadyClaimed() public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) skipTime(delay) - claimRequestOnLidoFixedPriceMultiLpARM(address(this), 0) + claimRequestOnLidoARM(address(this), 0) { // Expect revert vm.expectRevert("Already claimed"); - lidoFixedPriceMultiLpARM.claimRedeem(0); + lidoARM.claimRedeem(0); } ////////////////////////////////////////////////////// @@ -113,39 +112,39 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) skipTime(delay) { // Assertions before - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); // Expected events - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit MultiLP.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(lidoARM), address(this), DEFAULT_AMOUNT); // Main call - (uint256 assets) = lidoFixedPriceMultiLpARM.claimRedeem(0); + (uint256 assets) = lidoARM.claimRedeem(0); // Assertions after - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); @@ -156,40 +155,40 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) skipTime(delay) { // Assertions before // Same situation as above // Swap MIN_TOTAL_SUPPLY from WETH in STETH - deal(address(weth), address(lidoFixedPriceMultiLpARM), DEFAULT_AMOUNT); - deal(address(steth), address(lidoFixedPriceMultiLpARM), MIN_TOTAL_SUPPLY); + deal(address(weth), address(lidoARM), DEFAULT_AMOUNT); + deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY); // Handle lido rounding issue to ensure that balance is exactly MIN_TOTAL_SUPPLY - if (steth.balanceOf(address(lidoFixedPriceMultiLpARM)) == MIN_TOTAL_SUPPLY - 1) { - deal(address(steth), address(lidoFixedPriceMultiLpARM), 0); - deal(address(steth), address(lidoFixedPriceMultiLpARM), MIN_TOTAL_SUPPLY + 1); + if (steth.balanceOf(address(lidoARM)) == MIN_TOTAL_SUPPLY - 1) { + deal(address(steth), address(lidoARM), 0); + deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY + 1); } // Expected events - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit MultiLP.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(lidoARM), address(this), DEFAULT_AMOUNT); // Main call - (uint256 assets) = lidoFixedPriceMultiLpARM.claimRedeem(0); + (uint256 assets) = lidoARM.claimRedeem(0); // Assertions after - assertApproxEqAbs(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY, 1); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertApproxEqAbs(steth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY, 1); + assertEq(weth.balanceOf(address(lidoARM)), 0); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); @@ -200,43 +199,43 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_ClaimRedeem_Test_ is Fork_Shared public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT / 2) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT / 2) skipTime(delay) - claimRequestOnLidoFixedPriceMultiLpARM(address(this), 0) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT / 2) + claimRequestOnLidoARM(address(this), 0) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT / 2) { // Assertions before - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2, 2); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); // Expected events - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit MultiLP.RedeemClaimed(address(this), 1, DEFAULT_AMOUNT / 2); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.RedeemClaimed(address(this), 1, DEFAULT_AMOUNT / 2); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), DEFAULT_AMOUNT / 2); + emit IERC20.Transfer(address(lidoARM), address(this), DEFAULT_AMOUNT / 2); // Main call skip(delay); - (uint256 assets) = lidoFixedPriceMultiLpARM.claimRedeem(1); + (uint256 assets) = lidoARM.claimRedeem(1); // Assertions after - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); assertEqUserRequest(0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol index 6b8e9d3..ad75713 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol @@ -9,7 +9,7 @@ import {IERC20} from "contracts/Interfaces.sol"; import {IStETHWithdrawal} from "contracts/Interfaces.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared_Test_ { uint256[] amounts0; uint256[] amounts1; uint256[] amounts2; @@ -22,7 +22,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Tes function setUp() public override { super.setUp(); - deal(address(steth), address(lidoFixedPriceMultiLpARM), 10_000 ether); + deal(address(steth), address(lidoARM), 10_000 ether); amounts0 = new uint256[](0); @@ -40,61 +40,61 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Tes function test_ClaimStETHWithdrawalForWETH_EmptyList() public asLidoFixedPriceMulltiLpARMOperator - requestStETHWithdrawalForETHOnLidoFixedPriceMultiLpARM(new uint256[](0)) + requestStETHWithdrawalForETHOnLidoARM(new uint256[](0)) { - assertEq(address(lidoFixedPriceMultiLpARM).balance, 0); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(address(lidoARM).balance, 0); + assertEq(lidoARM.outstandingEther(), 0); // Main call - lidoFixedPriceMultiLpARM.claimStETHWithdrawalForWETH(new uint256[](0)); + lidoARM.claimStETHWithdrawalForWETH(new uint256[](0)); - assertEq(address(lidoFixedPriceMultiLpARM).balance, 0); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); + assertEq(address(lidoARM).balance, 0); + assertEq(lidoARM.outstandingEther(), 0); } function test_ClaimStETHWithdrawalForWETH_SingleRequest() public asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoFixedPriceMultiLpARM - requestStETHWithdrawalForETHOnLidoFixedPriceMultiLpARM(amounts1) - mockFunctionClaimWithdrawOnLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT) + approveStETHOnLidoARM + requestStETHWithdrawalForETHOnLidoARM(amounts1) + mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT) { // Assertions before - uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), DEFAULT_AMOUNT); + uint256 balanceBefore = weth.balanceOf(address(lidoARM)); + assertEq(lidoARM.outstandingEther(), DEFAULT_AMOUNT); stETHWithdrawal.getLastRequestId(); uint256[] memory requests = new uint256[](1); requests[0] = stETHWithdrawal.getLastRequestId(); // Main call - lidoFixedPriceMultiLpARM.claimStETHWithdrawalForWETH(requests); + lidoARM.claimStETHWithdrawalForWETH(requests); // Assertions after - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), balanceBefore + DEFAULT_AMOUNT); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(weth.balanceOf(address(lidoARM)), balanceBefore + DEFAULT_AMOUNT); } function test_ClaimStETHWithdrawalForWETH_MultiRequest() public asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoFixedPriceMultiLpARM - requestStETHWithdrawalForETHOnLidoFixedPriceMultiLpARM(amounts2) + approveStETHOnLidoARM + requestStETHWithdrawalForETHOnLidoARM(amounts2) mockCallLidoFindCheckpointHints - mockFunctionClaimWithdrawOnLidoFixedPriceMultiLpARM(amounts2[0] + amounts2[1]) + mockFunctionClaimWithdrawOnLidoARM(amounts2[0] + amounts2[1]) { // Assertions before - uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), amounts2[0] + amounts2[1]); + uint256 balanceBefore = weth.balanceOf(address(lidoARM)); + assertEq(lidoARM.outstandingEther(), amounts2[0] + amounts2[1]); stETHWithdrawal.getLastRequestId(); uint256[] memory requests = new uint256[](2); requests[0] = stETHWithdrawal.getLastRequestId() - 1; requests[1] = stETHWithdrawal.getLastRequestId(); // Main call - lidoFixedPriceMultiLpARM.claimStETHWithdrawalForWETH(requests); + lidoARM.claimStETHWithdrawalForWETH(requests); // Assertions after - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), balanceBefore + amounts2[0] + amounts2[1]); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(weth.balanceOf(address(lidoARM)), balanceBefore + amounts2[0] + amounts2[1]); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol b/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol index c10f272..dd23f07 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol @@ -6,9 +6,9 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -import {PerformanceFee} from "contracts/PerformanceFee.sol"; +import {AbstractARM} from "contracts/AbstractARM.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_CollectFees_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_CollectFees_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -22,44 +22,41 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_CollectFees_Test_ is Fork_Shared /// @notice This test is expected to revert as almost all the liquidity is in stETH function test_RevertWhen_CollectFees_Because_InsufficientLiquidity() public - simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(steth), true) + simulateAssetGainInLidoARM(DEFAULT_AMOUNT, address(steth), true) { vm.expectRevert("ARM: insufficient liquidity"); - lidoFixedPriceMultiLpARM.collectFees(); + lidoARM.collectFees(); } ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_CollectFees_Once() - public - simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) - { - address feeCollector = lidoFixedPriceMultiLpARM.feeCollector(); + function test_CollectFees_Once() public simulateAssetGainInLidoARM(DEFAULT_AMOUNT, address(weth), true) { + address feeCollector = lidoARM.feeCollector(); uint256 fee = DEFAULT_AMOUNT * 20 / 100; // Expected Events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), feeCollector, fee); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit PerformanceFee.FeeCollected(feeCollector, fee); + emit IERC20.Transfer(address(lidoARM), feeCollector, fee); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.FeeCollected(feeCollector, fee); // Main call - uint256 claimedFee = lidoFixedPriceMultiLpARM.collectFees(); + uint256 claimedFee = lidoARM.collectFees(); // Assertions after assertEq(claimedFee, fee); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); + assertEq(lidoARM.feesAccrued(), 0); } function test_CollectFees_Twice() public - simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) - collectFeesOnLidoFixedPriceMultiLpARM - simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + simulateAssetGainInLidoARM(DEFAULT_AMOUNT, address(weth), true) + collectFeesOnLidoARM + simulateAssetGainInLidoARM(DEFAULT_AMOUNT, address(weth), true) { // Main call - uint256 claimedFee = lidoFixedPriceMultiLpARM.collectFees(); + uint256 claimedFee = lidoARM.collectFees(); // Assertions after assertEq(claimedFee, DEFAULT_AMOUNT * 20 / 100); // This test should pass! diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol index dc7f048..debd3a3 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.23; // Test imports import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_Constructor_Test is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_Constructor_Test is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -16,18 +16,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Constructor_Test is Fork_Shared_ /// --- PASSING TESTS ////////////////////////////////////////////////////// function test_Initial_State() public view { - assertEq(lidoFixedPriceMultiLpARM.name(), "Lido ARM"); - assertEq(lidoFixedPriceMultiLpARM.symbol(), "ARM-ST"); - assertEq(lidoFixedPriceMultiLpARM.owner(), address(this)); - assertEq(lidoFixedPriceMultiLpARM.operator(), operator); - assertEq(lidoFixedPriceMultiLpARM.feeCollector(), feeCollector); - assertEq(lidoFixedPriceMultiLpARM.fee(), 2000); - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), 1e12); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); + assertEq(lidoARM.name(), "Lido ARM"); + assertEq(lidoARM.symbol(), "ARM-ST"); + assertEq(lidoARM.owner(), address(this)); + assertEq(lidoARM.operator(), operator); + assertEq(lidoARM.feeCollector(), feeCollector); + assertEq(lidoARM.fee(), 2000); + assertEq(lidoARM.lastTotalAssets(), 1e12); + assertEq(lidoARM.feesAccrued(), 0); // the 20% performance fee is removed on initialization - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), 1e12); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), 1e12); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), 1e12); + assertEq(lidoARM.totalAssets(), 1e12); + assertEq(lidoARM.totalSupply(), 1e12); + assertEq(weth.balanceOf(address(lidoARM)), 1e12); assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index c88c02a..9ac0e9b 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -6,27 +6,9 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -import {MultiLP} from "contracts/MultiLP.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Test_ { - /** - * As Deposit is complex function due to the entanglement of virtual and override functions in inheritance. - * This is a small recap of the functions that are called in the deposit function. - * 1. ML: _preDepositHook() -> PF: _calcFee() -> PF: _rawTotalAssets() -> ML: _totalAssets() - * 2. ML: convertToShares() -> ML: _totalAssets() -> ARM: totalAssets() -> PF : totalAssets() -> - * -> PF: _rawTotalAssets() -> ML: _totalAssets() - * 3. ML: _postDepositHook() -> ARM: _postDepositHook() => - * | -> LCPARM: postDepositHook() -> LPC: postDepositHook() -> ARM: totalAssets() -> - * -> PF : totalAssets() -> PF: _rawTotalAssets() -> ML: _totalAssets() - * | -> PF: _postDepositHook() -> PF: _rawTotalAssets() -> ML: _totalAssets() - * - * ML = MultiLP - * PF = PerformanceFee - * ARM = LidoFixedPriceMultiLpARM - * LPC = LiquidityProviderController - * LCPARM = LiquidityProviderControllerARM - */ +contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -38,7 +20,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes // Alice deal(address(weth), alice, 1_000 ether); vm.prank(alice); - weth.approve(address(lidoFixedPriceMultiLpARM), type(uint256).max); + weth.approve(address(lidoARM), type(uint256).max); } ////////////////////////////////////////////////////// @@ -49,7 +31,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), 0) { vm.expectRevert("LPC: LP cap exceeded"); - lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT); + lidoARM.deposit(DEFAULT_AMOUNT); } function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapNotNull() @@ -57,7 +39,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { vm.expectRevert("LPC: LP cap exceeded"); - lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT + 1); + lidoARM.deposit(DEFAULT_AMOUNT + 1); } function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapReached() @@ -65,11 +47,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // Initial deposit - lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT / 2); + lidoARM.deposit(DEFAULT_AMOUNT / 2); // Cap is now 0.5 ether vm.expectRevert("LPC: LP cap exceeded"); - lidoFixedPriceMultiLpARM.deposit((DEFAULT_AMOUNT / 2) + 1); + lidoARM.deposit((DEFAULT_AMOUNT / 2) + 1); } function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapNull() @@ -78,7 +60,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT + 1) { vm.expectRevert("LPC: Total assets cap exceeded"); - lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT); + lidoARM.deposit(DEFAULT_AMOUNT); } function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapNotNull() @@ -87,7 +69,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { vm.expectRevert("LPC: Total assets cap exceeded"); - lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT - MIN_TOTAL_SUPPLY + 1); + lidoARM.deposit(DEFAULT_AMOUNT - MIN_TOTAL_SUPPLY + 1); } function test_RevertWhen_Deposit_Because_TotalAssetsCapExceeded_WithCapReached() @@ -95,9 +77,9 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setTotalAssetsCap(DEFAULT_AMOUNT) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { - lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT / 2); + lidoARM.deposit(DEFAULT_AMOUNT / 2); vm.expectRevert("LPC: Total assets cap exceeded"); - lidoFixedPriceMultiLpARM.deposit((DEFAULT_AMOUNT / 2) - MIN_TOTAL_SUPPLY + 1); // This should revert! + lidoARM.deposit((DEFAULT_AMOUNT / 2) - MIN_TOTAL_SUPPLY + 1); // This should revert! } ////////////////////////////////////////////////////// @@ -113,37 +95,37 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes { uint256 amount = DEFAULT_AMOUNT; // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amount); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + emit IERC20.Transfer(address(this), address(lidoARM), amount); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); // Main call - uint256 shares = lidoFixedPriceMultiLpARM.deposit(amount); + uint256 shares = lidoARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), shares); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.balanceOf(address(this)), shares); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether @@ -155,41 +137,41 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes public setTotalAssetsCap(DEFAULT_AMOUNT * 2 + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT * 2) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { uint256 amount = DEFAULT_AMOUNT; // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), amount); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.balanceOf(address(this)), amount); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amount); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + emit IERC20.Transfer(address(this), address(lidoARM), amount); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); // Main call - uint256 shares = lidoFixedPriceMultiLpARM.deposit(amount); + uint256 shares = lidoARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), shares * 2); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.balanceOf(address(this)), shares * 2); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -202,42 +184,42 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setTotalAssetsCap(DEFAULT_AMOUNT * 2 + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) setLiquidityProviderCap(alice, DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { uint256 amount = DEFAULT_AMOUNT; // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(alice), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.balanceOf(alice), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(alice), amount); assertEqQueueMetadata(0, 0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(alice, address(lidoFixedPriceMultiLpARM), amount); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + emit IERC20.Transfer(alice, address(lidoARM), amount); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), alice, amount); // shares == amount here vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(alice, 0); vm.prank(alice); // Main call - uint256 shares = lidoFixedPriceMultiLpARM.deposit(amount); + uint256 shares = lidoARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(alice), shares); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.balanceOf(alice), shares); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(alice), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -251,59 +233,55 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // simulate asset gain - uint256 balanceBefore = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceBefore = weth.balanceOf(address(lidoARM)); uint256 assetGain = DEFAULT_AMOUNT; - deal(address(weth), address(lidoFixedPriceMultiLpARM), balanceBefore + assetGain); + deal(address(weth), address(lidoARM), balanceBefore + assetGain); // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + assetGain); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0, "Outstanding ether before"); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain); + assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); + assertEq(lidoARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); + assertEq(lidoARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy // 80% of the asset gain goes to the total assets - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), balanceBefore + assetGain * 80 / 100, "Total assets before"); + assertEq(lidoARM.totalAssets(), balanceBefore + assetGain * 80 / 100, "Total assets before"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT, "lp cap before"); assertEqQueueMetadata(0, 0, 0, 0); // 20% of the asset gain goes to the performance fees uint256 feesAccrued = assetGain * 20 / 100; - uint256 rawTotalAsset = weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - feesAccrued; // No steth and no externalWithdrawQueue + uint256 rawTotalAsset = weth.balanceOf(address(lidoARM)) - feesAccrued; // No steth and no externalWithdrawQueue uint256 depositedAssets = DEFAULT_AMOUNT; uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / rawTotalAsset; // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), depositedAssets); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + emit IERC20.Transfer(address(this), address(lidoARM), depositedAssets); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), expectedShares); vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); // deposit assets - uint256 shares = lidoFixedPriceMultiLpARM.deposit(depositedAssets); + uint256 shares = lidoARM.deposit(depositedAssets); assertEq(shares, expectedShares, "minted shares"); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0, "stETH balance after"); + assertEq(steth.balanceOf(address(lidoARM)), 0, "stETH balance after"); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "WETH balance after"); + assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); + assertEq(lidoARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees assertEq( - weth.balanceOf(address(lidoFixedPriceMultiLpARM)), - MIN_TOTAL_SUPPLY + assetGain + depositedAssets, - "WETH balance after" - ); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0, "Outstanding ether after"); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees - assertEq( - lidoFixedPriceMultiLpARM.lastTotalAssets(), + lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + (assetGain * 80 / 100) + depositedAssets, "last total assets after" ); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), expectedShares, "user shares after"); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); + assertEq(lidoARM.balanceOf(address(this)), expectedShares, "user shares after"); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); } @@ -315,32 +293,30 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes setTotalAssetsCap(DEFAULT_AMOUNT * 3 + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) setLiquidityProviderCap(alice, DEFAULT_AMOUNT * 5) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { // set stETH/WETH buy price to 1 - lidoFixedPriceMultiLpARM.setPrices(1e36, 1e36 + 1); + lidoARM.setPrices(1e36, 1e36 + 1); // User Swap stETH for 3/4 of WETH in the ARM deal(address(steth), address(this), DEFAULT_AMOUNT); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( - steth, weth, 3 * DEFAULT_AMOUNT / 4, DEFAULT_AMOUNT, address(this) - ); + lidoARM.swapTokensForExactTokens(steth, weth, 3 * DEFAULT_AMOUNT / 4, DEFAULT_AMOUNT, address(this)); // First user requests a full withdrawal - uint256 firstUserShares = lidoFixedPriceMultiLpARM.balanceOf(address(this)); - lidoFixedPriceMultiLpARM.requestRedeem(firstUserShares); + uint256 firstUserShares = lidoARM.balanceOf(address(this)); + lidoARM.requestRedeem(firstUserShares); // Assertions Before uint256 stethBalanceBefore = 3 * DEFAULT_AMOUNT / 4; - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), stethBalanceBefore, "stETH ARM balance before"); + assertEq(steth.balanceOf(address(lidoARM)), stethBalanceBefore, "stETH ARM balance before"); uint256 wethBalanceBefore = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - 3 * DEFAULT_AMOUNT / 4; - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), wethBalanceBefore, "WETH ARM balance before"); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0, "Outstanding ether before"); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0, "Fees accrued before"); - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(alice), 0, "alice shares before"); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); + assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); + assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); + assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before"); + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total 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"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before"); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); @@ -348,27 +324,25 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_Deposit_Test_ is Fork_Shared_Tes // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(alice, address(lidoFixedPriceMultiLpARM), amount); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + emit IERC20.Transfer(alice, address(lidoARM), amount); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), alice, amount); // shares == amount here vm.expectEmit({emitter: address(liquidityProviderController)}); emit LiquidityProviderController.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); vm.prank(alice); // Main call - uint256 shares = lidoFixedPriceMultiLpARM.deposit(amount); + uint256 shares = lidoARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), stethBalanceBefore, "stETH ARM balance after"); - assertEq( - weth.balanceOf(address(lidoFixedPriceMultiLpARM)), wethBalanceBefore + amount, "WETH ARM balance after" - ); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0, "Outstanding ether after"); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount, "last total assets after"); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(alice), shares, "alice shares after"); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); + assertEq(steth.balanceOf(address(lidoARM)), stethBalanceBefore, "stETH ARM balance after"); + assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after"); + assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); + assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount, "last total 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"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used // withdrawal request is now claimable assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 4e8ff32..dc5b949 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -6,10 +6,9 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -import {MultiLP} from "contracts/MultiLP.sol"; -import {PerformanceFee} from "contracts/PerformanceFee.sol"; +import {AbstractARM} from "contracts/AbstractARM.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -27,40 +26,40 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(0, 0, 0, 0); - uint256 delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); + uint256 delay = lidoARM.CLAIM_DELAY(); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit MultiLP.RedeemRequested(address(this), 0, DEFAULT_AMOUNT, DEFAULT_AMOUNT, block.timestamp + delay); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.RedeemRequested(address(this), 0, DEFAULT_AMOUNT, DEFAULT_AMOUNT, block.timestamp + delay); // Main Call - (uint256 requestId, uint256 assets) = lidoFixedPriceMultiLpARM.requestRedeem(DEFAULT_AMOUNT); + (uint256 requestId, uint256 assets) = lidoARM.requestRedeem(DEFAULT_AMOUNT); // Assertions After assertEq(requestId, 0); // First request assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); // One request in the queue assertEqUserRequest(0, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT, DEFAULT_AMOUNT); // Requested the full amount assertEq(assets, DEFAULT_AMOUNT, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); } @@ -69,30 +68,30 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT / 4) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT / 4) { // Assertions Before - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 0, 1); - uint256 delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); + uint256 delay = lidoARM.CLAIM_DELAY(); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT / 2); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit MultiLP.RedeemRequested( + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.RedeemRequested( address(this), 1, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT * 3 / 4, block.timestamp + delay ); // Main Call - (uint256 requestId, uint256 assets) = lidoFixedPriceMultiLpARM.requestRedeem(DEFAULT_AMOUNT / 2); + (uint256 requestId, uint256 assets) = lidoARM.requestRedeem(DEFAULT_AMOUNT / 2); // Assertions After assertEq(requestId, 1); // Second request @@ -101,13 +100,13 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar 1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT * 3 / 4 ); assertEq(assets, DEFAULT_AMOUNT / 2, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoFixedPriceMultiLpARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only } @@ -116,41 +115,37 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { // Assertions Before // Not needed as the same as in `test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue` // Simulate assets gain in ARM uint256 assetsGain = DEFAULT_AMOUNT; - deal( - address(weth), - address(lidoFixedPriceMultiLpARM), - weth.balanceOf(address(lidoFixedPriceMultiLpARM)) + assetsGain - ); + deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) + assetsGain); // Calculate expected values uint256 feeAccrued = assetsGain * 20 / 100; // 20% fee - uint256 totalAsset = weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - feeAccrued; + uint256 totalAsset = weth.balanceOf(address(lidoARM)) - feeAccrued; uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit PerformanceFee.FeeCalculated(feeAccrued, assetsGain); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.FeeCalculated(feeAccrued, assetsGain); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); // Main call - lidoFixedPriceMultiLpARM.requestRedeem(DEFAULT_AMOUNT); + lidoARM.requestRedeem(DEFAULT_AMOUNT); - uint256 delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); + uint256 delay = lidoARM.CLAIM_DELAY(); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); // +perfs - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoFixedPriceMultiLpARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); // +perfs + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), feeAccrued); + assertApproxEqAbs(lidoARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error + assertEq(lidoARM.balanceOf(address(this)), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssets, 0, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); @@ -161,39 +156,35 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestRedeem_Test_ is Fork_Shar public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { // Assertions Before // Not needed as the same as in `test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue` // Simulate assets loss in ARM uint256 assetsLoss = DEFAULT_AMOUNT / 10; // 0.1 ether of loss - deal( - address(weth), - address(lidoFixedPriceMultiLpARM), - weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - assetsLoss - ); + deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) - assetsLoss); // Calculate expected values uint256 feeAccrued = 0; // No profits - uint256 totalAsset = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 totalAsset = weth.balanceOf(address(lidoARM)); uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); + vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); // Main call - lidoFixedPriceMultiLpARM.requestRedeem(DEFAULT_AMOUNT); + lidoARM.requestRedeem(DEFAULT_AMOUNT); - uint256 delay = lidoFixedPriceMultiLpARM.CLAIM_DELAY(); + uint256 delay = lidoARM.CLAIM_DELAY(); // Assertions After - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); - assertEq(weth.balanceOf(address(lidoFixedPriceMultiLpARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); - assertEq(lidoFixedPriceMultiLpARM.outstandingEther(), 0); - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoFixedPriceMultiLpARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error - assertEq(lidoFixedPriceMultiLpARM.balanceOf(address(this)), 0); - assertEq(lidoFixedPriceMultiLpARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), feeAccrued); + assertApproxEqAbs(lidoARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error + assertEq(lidoARM.balanceOf(address(this)), 0); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssets, 0, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol index 1b3c259..8a4d896 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol @@ -7,14 +7,14 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// function setUp() public override { super.setUp(); - deal(address(steth), address(lidoFixedPriceMultiLpARM), 10_000 ether); + deal(address(steth), address(lidoARM), 10_000 ether); } ////////////////////////////////////////////////////// @@ -22,59 +22,46 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Tes ////////////////////////////////////////////////////// function test_RevertWhen_RequestStETHWithdrawalForETH_NotOperator() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); - lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(new uint256[](0)); - } - - function test_RevertWhen_RequestStETHWithdrawalForETH_Because_AllowanceExceeded() - public - asLidoFixedPriceMulltiLpARMOperator - { - uint256[] memory amounts = new uint256[](1); - amounts[0] = DEFAULT_AMOUNT; - - vm.expectRevert("ALLOWANCE_EXCEEDED"); - lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + lidoARM.requestStETHWithdrawalForETH(new uint256[](0)); } function test_RevertWhen_RequestStETHWithdrawalForETH_Because_BalanceExceeded() public asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoFixedPriceMultiLpARM + approveStETHOnLidoARM { // Remove all stETH from the contract - deal(address(steth), address(lidoFixedPriceMultiLpARM), 0); + deal(address(steth), address(lidoARM), 0); uint256[] memory amounts = new uint256[](1); amounts[0] = DEFAULT_AMOUNT; vm.expectRevert("BALANCE_EXCEEDED"); - lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + lidoARM.requestStETHWithdrawalForETH(amounts); } ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// function test_RequestStETHWithdrawalForETH_EmptyList() public asLidoFixedPriceMulltiLpARMOperator { - uint256[] memory requestIds = lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(new uint256[](0)); + uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(new uint256[](0)); assertEq(requestIds.length, 0); } function test_RequestStETHWithdrawalForETH_SingleAmount_1ether() public asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoFixedPriceMultiLpARM + approveStETHOnLidoARM { uint256[] memory amounts = new uint256[](1); amounts[0] = DEFAULT_AMOUNT; // Expected events vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer( - address(lidoFixedPriceMultiLpARM), address(lidoFixedPriceMultiLpARM.withdrawalQueue()), amounts[0] - ); + emit IERC20.Transfer(address(lidoARM), address(lidoARM.withdrawalQueue()), amounts[0]); // Main call - uint256[] memory requestIds = lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(amounts); assertEq(requestIds.length, 1); assertGt(requestIds[0], 0); @@ -83,19 +70,17 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Tes function test_RequestStETHWithdrawalForETH_SingleAmount_1000ethers() public asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoFixedPriceMultiLpARM + approveStETHOnLidoARM { uint256[] memory amounts = new uint256[](1); amounts[0] = 1_000 ether; // Expected events vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer( - address(lidoFixedPriceMultiLpARM), address(lidoFixedPriceMultiLpARM.withdrawalQueue()), amounts[0] - ); + emit IERC20.Transfer(address(lidoARM), address(lidoARM.withdrawalQueue()), amounts[0]); // Main call - uint256[] memory requestIds = lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(amounts); assertEq(requestIds.length, 1); assertGt(requestIds[0], 0); @@ -104,7 +89,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Tes function test_RequestStETHWithdrawalForETH_MultipleAmount() public asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoFixedPriceMultiLpARM + approveStETHOnLidoARM { uint256 length = _bound(vm.randomUint(), 2, 10); uint256[] memory amounts = new uint256[](length); @@ -113,7 +98,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_RequestStETHWithdrawalForETH_Tes } // Main call - uint256[] memory requestIds = lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(amounts); uint256 initialRequestId = requestIds[0]; assertGt(initialRequestId, 0); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 1881d65..4cf049c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -6,11 +6,9 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -import {MultiLP} from "contracts/MultiLP.sol"; -import {PerformanceFee} from "contracts/PerformanceFee.sol"; -import {LiquidityProviderControllerARM} from "contracts/LiquidityProviderControllerARM.sol"; +import {AbstractARM} from "contracts/AbstractARM.sol"; -contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -23,55 +21,52 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te ////////////////////////////////////////////////////// function test_RevertWhen_PerformanceFee_SetFee_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMultiLpARM.setFee(0); + lidoARM.setFee(0); } - function test_RevertWhen_PerformanceFee_SetFee_Because_FeeIsTooHigh() public asLidoFixedPriceMultiLpARMOwner { - uint256 max = lidoFixedPriceMultiLpARM.FEE_SCALE(); + function test_RevertWhen_PerformanceFee_SetFee_Because_FeeIsTooHigh() public asLidoARMOwner { + uint256 max = lidoARM.FEE_SCALE(); vm.expectRevert("ARM: fee too high"); - lidoFixedPriceMultiLpARM.setFee(max + 1); + lidoARM.setFee(max + 1); } function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMultiLpARM.setFeeCollector(address(0)); + lidoARM.setFeeCollector(address(0)); } - function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_FeeCollectorIsZero() - public - asLidoFixedPriceMultiLpARMOwner - { + function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_FeeCollectorIsZero() public asLidoARMOwner { vm.expectRevert("ARM: invalid fee collector"); - lidoFixedPriceMultiLpARM.setFeeCollector(address(0)); + lidoARM.setFeeCollector(address(0)); } ////////////////////////////////////////////////////// /// --- PERFORMANCE FEE - PASSING TEST ////////////////////////////////////////////////////// - function test_PerformanceFee_SetFee_() public asLidoFixedPriceMultiLpARMOwner { - uint256 feeBefore = lidoFixedPriceMultiLpARM.fee(); + function test_PerformanceFee_SetFee_() public asLidoARMOwner { + uint256 feeBefore = lidoARM.fee(); - uint256 newFee = _bound(vm.randomUint(), 0, lidoFixedPriceMultiLpARM.FEE_SCALE()); + uint256 newFee = _bound(vm.randomUint(), 0, lidoARM.FEE_SCALE()); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit PerformanceFee.FeeUpdated(newFee); - lidoFixedPriceMultiLpARM.setFee(newFee); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.FeeUpdated(newFee); + lidoARM.setFee(newFee); - assertEq(lidoFixedPriceMultiLpARM.fee(), newFee); - assertNotEq(feeBefore, lidoFixedPriceMultiLpARM.fee()); + assertEq(lidoARM.fee(), newFee); + assertNotEq(feeBefore, lidoARM.fee()); } - function test_PerformanceFee_SetFeeCollector() public asLidoFixedPriceMultiLpARMOwner { - address feeCollectorBefore = lidoFixedPriceMultiLpARM.feeCollector(); + function test_PerformanceFee_SetFeeCollector() public asLidoARMOwner { + address feeCollectorBefore = lidoARM.feeCollector(); address newFeeCollector = vm.randomAddress(); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit PerformanceFee.FeeCollectorUpdated(newFeeCollector); - lidoFixedPriceMultiLpARM.setFeeCollector(newFeeCollector); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.FeeCollectorUpdated(newFeeCollector); + lidoARM.setFeeCollector(newFeeCollector); - assertEq(lidoFixedPriceMultiLpARM.feeCollector(), newFeeCollector); - assertNotEq(feeCollectorBefore, lidoFixedPriceMultiLpARM.feeCollector()); + assertEq(lidoARM.feeCollector(), newFeeCollector); + assertNotEq(feeCollectorBefore, lidoARM.feeCollector()); } ////////////////////////////////////////////////////// @@ -79,46 +74,46 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te ////////////////////////////////////////////////////// function test_RevertWhen_SetPrices_Because_PriceCross() public { vm.expectRevert("ARM: Price cross"); - lidoFixedPriceMultiLpARM.setPrices(90 * 1e33, 89 * 1e33); + lidoARM.setPrices(90 * 1e33, 89 * 1e33); vm.expectRevert("ARM: Price cross"); - lidoFixedPriceMultiLpARM.setPrices(72, 70); + lidoARM.setPrices(72, 70); vm.expectRevert("ARM: Price cross"); - lidoFixedPriceMultiLpARM.setPrices(1005 * 1e33, 1000 * 1e33); + lidoARM.setPrices(1005 * 1e33, 1000 * 1e33); // Both set to 1.0 vm.expectRevert("ARM: Price cross"); - lidoFixedPriceMultiLpARM.setPrices(1e36, 1e36); + lidoARM.setPrices(1e36, 1e36); } function test_RevertWhen_FixedPriceARM_SetPrices_Because_PriceRange() public asLidoFixedPriceMulltiLpARMOperator { // buy price 11 basis points higher than 1.0 vm.expectRevert("ARM: buy price too high"); - lidoFixedPriceMultiLpARM.setPrices(10011e32, 10020e32); + lidoARM.setPrices(1.0011 * 1e36, 1.002 * 1e36); // sell price 11 basis points lower than 1.0 vm.expectRevert("ARM: sell price too low"); - lidoFixedPriceMultiLpARM.setPrices(9980e32, 9989e32); + lidoARM.setPrices(0.998 * 1e36, 0.9989 * 1e36); // Forgot to scale up to 36 decimals vm.expectRevert("ARM: sell price too low"); - lidoFixedPriceMultiLpARM.setPrices(1e18, 1e18); + lidoARM.setPrices(1e18, 1e18); } function test_RevertWhen_FixedPriceARM_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); - lidoFixedPriceMultiLpARM.setPrices(0, 0); + lidoARM.setPrices(0, 0); } - function test_SellPriceCannotCrossOne() public asLidoFixedPriceMulltiLpARMOperator { + function test_SellPriceCannotCrossOneByMoreThanTenBps() public asLidoFixedPriceMulltiLpARMOperator { vm.expectRevert("ARM: sell price too low"); - lidoFixedPriceMulltiLpARM.setPrices(0.9997 * 1e36, 0.99999 * 1e36); + lidoARM.setPrices(0.998 * 1e36, 0.9989 * 1e36); } - function test_BuyPriceCannotCrossOne() public asLidoFixedPriceMulltiLpARMOperator { + function test_BuyPriceCannotCrossOneByMoreThanTenBps() public asLidoFixedPriceMulltiLpARMOperator { vm.expectRevert("ARM: buy price too high"); - lidoFixedPriceMulltiLpARM.setPrices(1.0 * 1e36, 1.0001 * 1e36); + lidoARM.setPrices(1.0011 * 1e36, 1.002 * 1e36); } ////////////////////////////////////////////////////// @@ -126,27 +121,27 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te ////////////////////////////////////////////////////// function test_FixedPriceARM_SetPrices_Operator() public asLidoFixedPriceMulltiLpARMOperator { // buy price 10 basis points higher than 1.0 - lidoFixedPriceMultiLpARM.setPrices(1001e33, 1002e33); + lidoARM.setPrices(1001e33, 1002e33); // sell price 10 basis points lower than 1.0 - lidoFixedPriceMultiLpARM.setPrices(9980e32, 9991e32); + lidoARM.setPrices(9980e32, 9991e32); // 2% of one basis point spread - lidoFixedPriceMultiLpARM.setPrices(999999e30, 1000001e30); + lidoARM.setPrices(999999e30, 1000001e30); - lidoFixedPriceMultiLpARM.setPrices(992 * 1e33, 1001 * 1e33); - lidoFixedPriceMultiLpARM.setPrices(1001 * 1e33, 1004 * 1e33); - lidoFixedPriceMultiLpARM.setPrices(992 * 1e33, 2000 * 1e33); + lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + lidoARM.setPrices(1001 * 1e33, 1004 * 1e33); + lidoARM.setPrices(992 * 1e33, 2000 * 1e33); // Check the traderates - assertEq(lidoFixedPriceMultiLpARM.traderate0(), 500 * 1e33); - assertEq(lidoFixedPriceMultiLpARM.traderate1(), 992 * 1e33); + assertEq(lidoARM.traderate0(), 500 * 1e33); + assertEq(lidoARM.traderate1(), 992 * 1e33); } function test_FixedPriceARM_SetPrices_Owner() public { // buy price 11 basis points higher than 1.0 - lidoFixedPriceMultiLpARM.setPrices(10011e32, 10020e32); + lidoARM.setPrices(10011e32, 10020e32); // sell price 11 basis points lower than 1.0 - lidoFixedPriceMultiLpARM.setPrices(9980e32, 9989e32); + lidoARM.setPrices(9980e32, 9989e32); } ////////////////////////////////////////////////////// @@ -154,12 +149,12 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te ////////////////////////////////////////////////////// function test_RevertWhen_Ownable_SetOwner_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMultiLpARM.setOwner(address(0)); + lidoARM.setOwner(address(0)); } function test_RevertWhen_Ownable_SetOperator_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMultiLpARM.setOperator(address(0)); + lidoARM.setOperator(address(0)); } ////////////////////////////////////////////////////// @@ -170,19 +165,19 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoFixedPriceMultiLpARM.setLiquidityProviderController(address(0)); + lidoARM.setLiquidityProviderController(address(0)); } ////////////////////////////////////////////////////// /// --- LIQUIIDITY PROVIDER CONTROLLER - PASSING TESTS ////////////////////////////////////////////////////// - function test_LiquidityProviderController_SetLiquidityProvider() public asLidoFixedPriceMultiLpARMOwner { + function test_LiquidityProviderController_SetLiquidityProvider() public asLidoARMOwner { address newLiquidityProviderController = vm.randomAddress(); - vm.expectEmit({emitter: address(lidoFixedPriceMultiLpARM)}); - emit LiquidityProviderControllerARM.LiquidityProviderControllerUpdated(newLiquidityProviderController); - lidoFixedPriceMultiLpARM.setLiquidityProviderController(newLiquidityProviderController); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.LiquidityProviderControllerUpdated(newLiquidityProviderController); + lidoARM.setLiquidityProviderController(newLiquidityProviderController); - assertEq(lidoFixedPriceMultiLpARM.liquidityProviderController(), newLiquidityProviderController); + assertEq(lidoARM.liquidityProviderController(), newLiquidityProviderController); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index 9f2f14b..219b322 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -6,7 +6,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; import {IERC20} from "contracts/Interfaces.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- CONSTANTS ////////////////////////////////////////////////////// @@ -26,17 +26,17 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is deal(address(weth), address(this), 1_000 ether); deal(address(steth), address(this), 1_000 ether); - deal(address(weth), address(lidoFixedPriceMultiLpARM), 1_000 ether); - deal(address(steth), address(lidoFixedPriceMultiLpARM), 1_000 ether); + deal(address(weth), address(lidoARM), 1_000 ether); + deal(address(steth), address(lidoARM), 1_000 ether); } ////////////////////////////////////////////////////// /// --- REVERTING TESTS ////////////////////////////////////////////////////// function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut1() public { - lidoFixedPriceMultiLpARM.token0(); + lidoARM.token0(); vm.expectRevert("ARM: Invalid out token"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( steth, // inToken badToken, // outToken 1, // amountIn @@ -47,7 +47,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenOut0() public { vm.expectRevert("ARM: Invalid out token"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( weth, // inToken badToken, // outToken 1, // amountIn @@ -58,7 +58,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidTokenIn() public { vm.expectRevert("ARM: Invalid in token"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( badToken, // inToken steth, // outToken 1, // amountIn @@ -69,7 +69,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_BothInvalidTokens() public { vm.expectRevert("ARM: Invalid in token"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( badToken, // inToken badToken, // outToken 1, // amountIn @@ -82,7 +82,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is uint256 initialBalance = weth.balanceOf(address(this)); vm.expectRevert(); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( weth, // inToken steth, // outToken initialBalance + 1, // amountIn @@ -92,7 +92,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is initialBalance = steth.balanceOf(address(this)); vm.expectRevert("BALANCE_EXCEEDED"); // Lido error - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( steth, // inToken weth, // outToken initialBalance + 3, // amountIn @@ -102,11 +102,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is } function test_RevertWhen_SwapExactTokensForTokens_Because_NotEnoughTokenOut() public { - uint256 initialBalance = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 initialBalance = steth.balanceOf(address(lidoARM)); deal(address(weth), address(this), initialBalance * 2); vm.expectRevert("BALANCE_EXCEEDED"); // Lido error - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( weth, // inToken steth, // outToken initialBalance * 2, // amountIn @@ -114,10 +114,10 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is address(this) // to ); - initialBalance = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); + initialBalance = weth.balanceOf(address(lidoARM)); deal(address(steth), address(this), initialBalance * 2); vm.expectRevert("ARM: Insufficient liquidity"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( steth, // inToken weth, // outToken initialBalance * 2, // amountIn @@ -127,11 +127,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is } function test_RevertWhen_SwapExactTokensForTokens_Because_InsufficientOutputAmount() public { - deal(address(steth), address(lidoFixedPriceMultiLpARM), 100 wei); + deal(address(steth), address(lidoARM), 100 wei); // Test for this function signature: swapExactTokensForTokens(IERC20,IERC20,uint56,uint256,address) vm.expectRevert("ARM: Insufficient output amount"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( weth, // inToken steth, // outToken 1, // amountIn @@ -144,7 +144,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is path[0] = address(weth); path[1] = address(steth); vm.expectRevert("ARM: Insufficient output amount"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( 1, // amountIn 1_000 ether, // amountOutMin path, // path @@ -155,7 +155,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_InvalidePathLength() public { vm.expectRevert("ARM: Invalid path length"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( 1, // amountIn 1, // amountOutMin new address[](3), // path @@ -166,7 +166,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is function test_RevertWhen_SwapExactTokensForTokens_Because_DeadlineExpired() public { vm.expectRevert("ARM: Deadline expired"); - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( 1, // amountIn 1, // amountOutMin new address[](2), // path @@ -187,18 +187,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoFixedPriceMultiLpARM.traderate1(); + uint256 traderates1 = lidoARM.traderate1(); uint256 minAmount = amountIn * traderates1 / 1e36; // Expected events: Already checked in fuzz tests uint256[] memory outputs = new uint256[](2); // Main call - outputs = lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + outputs = lidoARM.swapExactTokensForTokens( amountIn, // amountIn minAmount, // amountOutMin path, // path @@ -209,8 +209,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); @@ -230,18 +230,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoFixedPriceMultiLpARM.traderate0(); + uint256 traderates0 = lidoARM.traderate0(); uint256 minAmount = amountIn * traderates0 / 1e36; // Expected events: Already checked in fuzz tests uint256[] memory outputs = new uint256[](2); // Main call - outputs = lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + outputs = lidoARM.swapExactTokensForTokens( amountIn, // amountIn minAmount, // amountOutMin path, // path @@ -252,8 +252,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis); @@ -277,11 +277,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // Use random price between 0.98 and 1 for traderate1, // Traderate0 value doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); - lidoFixedPriceMultiLpARM.setPrices(price, MAX_PRICE1); + lidoARM.setPrices(price, MAX_PRICE1); // Set random amount of stETH in the ARM stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoFixedPriceMultiLpARM), stethReserve); + deal(address(steth), address(lidoARM), stethReserve); // Calculate maximum amount of WETH to swap // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. @@ -291,20 +291,20 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoFixedPriceMultiLpARM.traderate1(); + uint256 traderates1 = lidoARM.traderate1(); uint256 minAmount = amountIn * traderates1 / 1e36; // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amountIn); + emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), minAmount + STETH_ERROR_ROUNDING); + emit IERC20.Transfer(address(lidoARM), address(this), minAmount + STETH_ERROR_ROUNDING); // Main call - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( weth, // inToken steth, // outToken amountIn, // amountIn @@ -315,8 +315,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); @@ -333,11 +333,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // Use random price between MIN_PRICE1 and MAX_PRICE1 for traderate1, // Traderate0 value doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); - lidoFixedPriceMultiLpARM.setPrices(MIN_PRICE0, price); + lidoARM.setPrices(MIN_PRICE0, price); // Set random amount of WETH in the ARM wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); - deal(address(weth), address(lidoFixedPriceMultiLpARM), wethReserve); + deal(address(weth), address(lidoARM), wethReserve); // Calculate maximum amount of stETH to swap // As the price is below 1, we can take 100% of the balance of WETH of the ARM. @@ -347,21 +347,21 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoFixedPriceMultiLpARM.traderate0(); + uint256 traderates0 = lidoARM.traderate0(); uint256 minAmount = amountIn * traderates0 / 1e36; // Expected events vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amountIn); + emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), minAmount); + emit IERC20.Transfer(address(lidoARM), address(this), minAmount); // Main call - lidoFixedPriceMultiLpARM.swapExactTokensForTokens( + lidoARM.swapExactTokensForTokens( steth, // inToken weth, // outToken amountIn, // amountIn @@ -372,8 +372,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapExactTokensForTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis); diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol index ee59dc0..28eb17f 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -6,7 +6,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; import {IERC20} from "contracts/Interfaces.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- CONSTANTS ////////////////////////////////////////////////////// @@ -26,17 +26,17 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(weth), address(this), 1_000 ether); deal(address(steth), address(this), 1_000 ether); - deal(address(weth), address(lidoFixedPriceMultiLpARM), 1_000 ether); - deal(address(steth), address(lidoFixedPriceMultiLpARM), 1_000 ether); + deal(address(weth), address(lidoARM), 1_000 ether); + deal(address(steth), address(lidoARM), 1_000 ether); } ////////////////////////////////////////////////////// /// --- REVERTING TESTS ////////////////////////////////////////////////////// function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut1() public { - lidoFixedPriceMultiLpARM.token0(); + lidoARM.token0(); vm.expectRevert("ARM: Invalid out token"); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( steth, // inToken badToken, // outToken 1, // amountOut @@ -47,7 +47,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenOut0() public { vm.expectRevert("ARM: Invalid out token"); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( weth, // inToken badToken, // outToken 1, // amountOut @@ -58,7 +58,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidTokenIn() public { vm.expectRevert("ARM: Invalid in token"); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( badToken, // inToken steth, // outToken 1, // amountOut @@ -69,7 +69,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_BothInvalidTokens() public { vm.expectRevert("ARM: Invalid in token"); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( badToken, // inToken badToken, // outToken 1, // amountOut @@ -82,7 +82,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(weth), address(this), 0); vm.expectRevert(); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( weth, // inToken steth, // outToken 1, // amountOut @@ -92,7 +92,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(steth), address(this), 0); vm.expectRevert(); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( steth, // inToken weth, // outToken STETH_ERROR_ROUNDING + 1, // amountOut * @@ -106,7 +106,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(weth), address(this), 0); vm.expectRevert(); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( weth, // inToken steth, // outToken 1 ether, // amountOut @@ -116,7 +116,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is deal(address(steth), address(this), 0); vm.expectRevert("BALANCE_EXCEEDED"); // Lido error - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( steth, // inToken weth, // outToken 1 ether, // amountOut @@ -126,11 +126,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is } function test_RevertWhen_SwapTokensForExactTokens_Because_InsufficientOutputAmount() public { - deal(address(steth), address(lidoFixedPriceMultiLpARM), 100 wei); + deal(address(steth), address(lidoARM), 100 wei); // Test for this function signature: swapTokensForExactTokens(IERC20,IERC20,uint56,uint256,address) vm.expectRevert("ARM: Excess input amount"); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( weth, // inToken steth, // outToken 1, // amountOut @@ -143,7 +143,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is path[0] = address(weth); path[1] = address(steth); vm.expectRevert("ARM: Excess input amount"); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( 1, // amountOut 0, // amountInMax path, // path @@ -154,7 +154,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_InvalidePathLength() public { vm.expectRevert("ARM: Invalid path length"); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( 1, // amountOut 1, // amountInMax new address[](3), // path @@ -165,7 +165,7 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is function test_RevertWhen_SwapTokensForExactTokens_Because_DeadlineExpired() public { vm.expectRevert("ARM: Deadline expired"); - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( 1, // amountOut 1, // amountInMax new address[](2), // path @@ -186,18 +186,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoFixedPriceMultiLpARM.traderate1(); + uint256 traderates1 = lidoARM.traderate1(); uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; // Expected events: Already checked in fuzz tests uint256[] memory outputs = new uint256[](2); // Main call - outputs = lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + outputs = lidoARM.swapTokensForExactTokens( amountOut, // amountOut amountIn, // amountInMax path, // path @@ -208,8 +208,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); @@ -229,18 +229,18 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoFixedPriceMultiLpARM.traderate0(); + uint256 traderates0 = lidoARM.traderate0(); uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; // Expected events: Already checked in fuzz tests uint256[] memory outputs = new uint256[](2); // Main call - outputs = lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + outputs = lidoARM.swapTokensForExactTokens( amountOut, // amountOut amountIn, // amountInMax path, // path @@ -251,8 +251,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis); @@ -276,11 +276,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // Use random price between 0.98 and 1 for traderate1, // Traderate0 value doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); - lidoFixedPriceMultiLpARM.setPrices(price, MAX_PRICE1); + lidoARM.setPrices(price, MAX_PRICE1); // Set random amount of stETH in the ARM stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoFixedPriceMultiLpARM), stethReserve); + deal(address(steth), address(lidoARM), stethReserve); // Calculate maximum amount of WETH to swap // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. @@ -290,20 +290,20 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoFixedPriceMultiLpARM.traderate1(); + uint256 traderates1 = lidoARM.traderate1(); uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amountIn); + emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), amountOut + STETH_ERROR_ROUNDING); + emit IERC20.Transfer(address(lidoARM), address(this), amountOut + STETH_ERROR_ROUNDING); // Main call - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( weth, // inToken steth, // outToken amountOut, // amountOut @@ -314,8 +314,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); @@ -334,11 +334,11 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // Use random price between MIN_PRICE1 and MAX_PRICE1 for traderate1, // Traderate0 value doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); - lidoFixedPriceMultiLpARM.setPrices(MIN_PRICE0, price); + lidoARM.setPrices(MIN_PRICE0, price); // Set random amount of WETH in the ARM wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); - deal(address(weth), address(lidoFixedPriceMultiLpARM), wethReserve); + deal(address(weth), address(lidoARM), wethReserve); // Calculate maximum amount of stETH to swap // As the price is below 1, we can take 100% of the balance of WETH of the ARM. @@ -348,21 +348,21 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State before uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoFixedPriceMultiLpARM.traderate0(); + uint256 traderates0 = lidoARM.traderate0(); uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; // Expected events vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(this), address(lidoFixedPriceMultiLpARM), amountIn); + emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoFixedPriceMultiLpARM), address(this), amountOut); + emit IERC20.Transfer(address(lidoARM), address(this), amountOut); // Main call - lidoFixedPriceMultiLpARM.swapTokensForExactTokens( + lidoARM.swapTokensForExactTokens( steth, // inToken weth, // outToken amountOut, // amountOut @@ -373,8 +373,8 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_SwapTokensForExactTokens_Test is // State after uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoFixedPriceMultiLpARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoFixedPriceMultiLpARM)); + uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); + uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis); diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index 16d9633..4599b4b 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -9,10 +9,9 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -import {MultiLP} from "contracts/MultiLP.sol"; -import {PerformanceFee} from "contracts/PerformanceFee.sol"; +import {AbstractARM} from "contracts/AbstractARM.sol"; -contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -26,162 +25,130 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared liquidityProviderController.setTotalAssetsCap(type(uint256).max); // Approve STETH for Lido - lidoFixedPriceMultiLpARM.approveStETH(); + lidoARM.approveStETH(); deal(address(weth), address(this), 1_000 ether); - weth.approve(address(lidoFixedPriceMultiLpARM), type(uint256).max); - } - - ////////////////////////////////////////////////////// - /// --- REVERTING TEST - ////////////////////////////////////////////////////// - function test_RevertWhen_TotalAssets_Because_MathError() - public - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT * 2, address(weth), false) - { - vm.expectRevert(stdError.arithmeticError); - lidoFixedPriceMultiLpARM.totalAssets(); + weth.approve(address(lidoARM), type(uint256).max); } ////////////////////////////////////////////////////// /// --- PASSING TEST ////////////////////////////////////////////////////// function test_TotalAssets_AfterInitialization() public view { - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY); } - function test_TotalAssets_AfterDeposit_NoAssetGainOrLoss() - public - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - { - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + function test_TotalAssets_AfterDeposit_NoAssetGainOrLoss() public depositInLidoARM(address(this), DEFAULT_AMOUNT) { + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); } function test_TotalAssets_AfterDeposit_WithAssetGain_InWETH() public - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { // Simulate asset gain uint256 assetGain = DEFAULT_AMOUNT / 2; - deal( - address(weth), - address(lidoFixedPriceMultiLpARM), - weth.balanceOf(address(lidoFixedPriceMultiLpARM)) + assetGain - ); + deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) + assetGain); // Calculate Fees uint256 fee = assetGain * 20 / 100; // 20% fee - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee); } function test_TotalAssets_AfterDeposit_WithAssetGain_InSTETH() public - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { - assertEq(steth.balanceOf(address(lidoFixedPriceMultiLpARM)), 0); + assertEq(steth.balanceOf(address(lidoARM)), 0); // Simulate asset gain uint256 assetGain = DEFAULT_AMOUNT / 2 + 1; // We are sure that steth balance is empty, so we can deal directly final amount. - deal(address(steth), address(lidoFixedPriceMultiLpARM), assetGain); + deal(address(steth), address(lidoARM), assetGain); // Calculate Fees uint256 fee = assetGain * 20 / 100; // 20% fee assertApproxEqAbs( - lidoFixedPriceMultiLpARM.totalAssets(), - MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee, - STETH_ERROR_ROUNDING + lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee, STETH_ERROR_ROUNDING ); } function test_TotalAssets_AfterDeposit_WithAssetLoss_InWETH() public - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { // Simulate asset loss uint256 assetLoss = DEFAULT_AMOUNT / 2; - deal( - address(weth), - address(lidoFixedPriceMultiLpARM), - weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - assetLoss - ); + deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) - assetLoss); - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss); } function test_TotalAssets_AfterDeposit_WithAssetLoss_InSTETH() public - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { // Simulate Swap at 1:1 between WETH and stETH uint256 swapAmount = DEFAULT_AMOUNT / 2; - deal( - address(weth), - address(lidoFixedPriceMultiLpARM), - weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - swapAmount - ); + deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) - swapAmount); // Then simulate a loss on stETH, do all in the same deal uint256 assetLoss = swapAmount / 2; - deal(address(steth), address(lidoFixedPriceMultiLpARM), swapAmount / 2); + deal(address(steth), address(lidoARM), swapAmount / 2); - assertApproxEqAbs( - lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss, STETH_ERROR_ROUNDING - ); + assertApproxEqAbs(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetLoss, STETH_ERROR_ROUNDING); } function test_TotalAssets_After_WithdrawingFromLido() public { // Simulate a Swap at 1:1 between WETH and stETH using initial liquidity uint256 swapAmount = MIN_TOTAL_SUPPLY / 2; - deal( - address(weth), - address(lidoFixedPriceMultiLpARM), - weth.balanceOf(address(lidoFixedPriceMultiLpARM)) - swapAmount - ); - deal(address(steth), address(lidoFixedPriceMultiLpARM), swapAmount); // Empty stETH balance, so we can deal directly + deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) - swapAmount); + deal(address(steth), address(lidoARM), swapAmount); // Empty stETH balance, so we can deal directly - uint256 totalAssetsBefore = lidoFixedPriceMultiLpARM.totalAssets(); + uint256 totalAssetsBefore = lidoARM.totalAssets(); // Request a redeem on Lido uint256[] memory amounts = new uint256[](1); amounts[0] = swapAmount; - lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + lidoARM.requestStETHWithdrawalForETH(amounts); // Check total assets after withdrawal is the same as before - assertApproxEqAbs(lidoFixedPriceMultiLpARM.totalAssets(), totalAssetsBefore, STETH_ERROR_ROUNDING); + assertApproxEqAbs(lidoARM.totalAssets(), totalAssetsBefore, STETH_ERROR_ROUNDING); } function test_TotalAssets_With_FeeAccrued_NotNull() public { uint256 assetGain = DEFAULT_AMOUNT; // Simulate asset gain - deal( - address(weth), - address(lidoFixedPriceMultiLpARM), - weth.balanceOf(address(lidoFixedPriceMultiLpARM)) + assetGain - ); + deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) + assetGain); // User deposit, this will trigger a fee calculation - lidoFixedPriceMultiLpARM.deposit(DEFAULT_AMOUNT); + lidoARM.deposit(DEFAULT_AMOUNT); // Assert fee accrued is not null - assertEq(lidoFixedPriceMultiLpARM.feesAccrued(), assetGain * 20 / 100); + assertEq(lidoARM.feesAccrued(), assetGain * 20 / 100); - assertEq( - lidoFixedPriceMultiLpARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - assetGain * 20 / 100 - ); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - assetGain * 20 / 100); } function test_TotalAssets_When_ARMIsInsolvent() public - depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) - requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) { // Simulate a loss of assets - deal(address(weth), address(lidoFixedPriceMultiLpARM), DEFAULT_AMOUNT - 1); + deal(address(weth), address(lidoARM), DEFAULT_AMOUNT - 1); - assertEq(lidoFixedPriceMultiLpARM.totalAssets(), 0); + assertEq(lidoARM.totalAssets(), 0); + } + + function test_RevertWhen_TotalAssets_Because_MathError() + public + depositInLidoARM(address(this), DEFAULT_AMOUNT) + simulateAssetGainInLidoARM(DEFAULT_AMOUNT, address(weth), true) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT) + simulateAssetGainInLidoARM(DEFAULT_AMOUNT * 2, address(weth), false) + { + // vm.expectRevert(stdError.arithmeticError); + assertEq(lidoARM.totalAssets(), 0); } } diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 99824b3..dda8910 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -10,7 +10,7 @@ import {Modifiers} from "test/fork/utils/Modifiers.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; -import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; // Interfaces @@ -139,17 +139,16 @@ abstract contract Fork_Shared_Test_ is Modifiers { liquidityProviderController.setLiquidityProviderCaps(liquidityProviders, 20 ether); liquidityProviderController.setTotalAssetsCap(100 ether); - // --- Deploy LidoFixedPriceMultiLpARM implementation --- + // --- Deploy LidoARM implementation --- // Deploy LidoARM implementation. - LidoFixedPriceMultiLpARM lidoImpl = - new LidoFixedPriceMultiLpARM(address(steth), address(weth), Mainnet.LIDO_WITHDRAWAL); + LidoARM lidoImpl = new LidoARM(address(steth), address(weth), Mainnet.LIDO_WITHDRAWAL); // Deployer will need WETH to initialize the ARM. deal(address(weth), address(this), 1e12); weth.approve(address(lidoProxy), type(uint256).max); steth.approve(address(lidoProxy), type(uint256).max); - // Initialize Proxy with LidoFixedPriceMultiLpARM implementation. + // Initialize Proxy with LidoARM implementation. data = abi.encodeWithSignature( "initialize(string,string,address,uint256,address,address)", "Lido ARM", @@ -162,10 +161,10 @@ abstract contract Fork_Shared_Test_ is Modifiers { lidoProxy.initialize(address(lidoImpl), address(this), data); // Set the Proxy as the LidoARM. - lidoFixedPriceMultiLpARM = LidoFixedPriceMultiLpARM(payable(address(lidoProxy))); + lidoARM = LidoARM(payable(address(lidoProxy))); // set prices - lidoFixedPriceMultiLpARM.setPrices(992 * 1e33, 1001 * 1e33); + lidoARM.setPrices(992 * 1e33, 1001 * 1e33); } function _label() internal { @@ -176,7 +175,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { vm.label(address(vault), "OETH VAULT"); vm.label(address(oethARM), "OETH ARM"); vm.label(address(proxy), "OETH ARM PROXY"); - vm.label(address(lidoFixedPriceMultiLpARM), "LIDO ARM"); + vm.label(address(lidoARM), "LIDO ARM"); vm.label(address(lidoProxy), "LIDO ARM PROXY"); vm.label(address(liquidityProviderController), "LIQUIDITY PROVIDER CONTROLLER"); vm.label(operator, "OPERATOR"); diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index b4bfc70..9223da7 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -40,10 +40,10 @@ abstract contract Helpers is Base_Test_ { uint256 expectedClaimed, uint256 expectedNextIndex ) public view { - assertEq(lidoFixedPriceMultiLpARM.withdrawsQueued(), expectedQueued, "metadata queued"); - assertEq(lidoFixedPriceMultiLpARM.withdrawsClaimable(), expectedClaimable, "metadata claimable"); - assertEq(lidoFixedPriceMultiLpARM.withdrawsClaimed(), expectedClaimed, "metadata claimed"); - assertEq(lidoFixedPriceMultiLpARM.nextWithdrawalIndex(), expectedNextIndex, "metadata nextWithdrawalIndex"); + assertEq(lidoARM.withdrawsQueued(), expectedQueued, "metadata queued"); + assertEq(lidoARM.withdrawsClaimable(), expectedClaimable, "metadata claimable"); + assertEq(lidoARM.withdrawsClaimed(), expectedClaimed, "metadata claimed"); + assertEq(lidoARM.nextWithdrawalIndex(), expectedNextIndex, "metadata nextWithdrawalIndex"); } /// @notice Asserts the equality bewteen value of `withdrawalRequests()` and the expected values. @@ -56,7 +56,7 @@ abstract contract Helpers is Base_Test_ { uint256 queued ) public view { (address _withdrawer, bool _claimed, uint40 _claimTimestamp, uint128 _assets, uint128 _queued) = - lidoFixedPriceMultiLpARM.withdrawalRequests(requestId); + lidoARM.withdrawalRequests(requestId); assertEq(_withdrawer, withdrawer, "Wrong withdrawer"); assertEq(_claimed, claimed, "Wrong claimed"); assertEq(_claimTimestamp, claimTimestamp, "Wrong claimTimestamp"); diff --git a/test/fork/utils/MockCall.sol b/test/fork/utils/MockCall.sol index e1d4f23..1a6de11 100644 --- a/test/fork/utils/MockCall.sol +++ b/test/fork/utils/MockCall.sol @@ -33,18 +33,18 @@ library MockCall { contract MockLidoWithdraw { ETHSender public immutable ethSender; - address public immutable lidoFixedPriceMultiLpARM; + address public immutable lidoARM; constructor(address _lidoFixedPriceMulltiLpARM) { ethSender = new ETHSender(); - lidoFixedPriceMultiLpARM = _lidoFixedPriceMulltiLpARM; + lidoARM = _lidoFixedPriceMulltiLpARM; } /// @notice Mock the call to the Lido contract's `claimWithdrawals` function. /// @dev as it is not possible to transfer ETH from the mocked contract (seems to be an issue with forge) /// we use the ETHSender contract intermediary to send the ETH to the target contract. function claimWithdrawals(uint256[] memory, uint256[] memory) external { - ethSender.sendETH(lidoFixedPriceMultiLpARM); + ethSender.sendETH(lidoARM); } } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index f9c592c..23b69cd 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -37,14 +37,14 @@ abstract contract Modifiers is Helpers { /// @notice Impersonate the operator of LidoOwnerLpARM contract. modifier asLidoFixedPriceMulltiLpARMOperator() { - vm.startPrank(lidoFixedPriceMultiLpARM.operator()); + vm.startPrank(lidoARM.operator()); _; vm.stopPrank(); } - /// @notice Impersonate the owner of LidoFixedPriceMultiLpARM contract. - modifier asLidoFixedPriceMultiLpARMOwner() { - vm.startPrank(lidoFixedPriceMultiLpARM.owner()); + /// @notice Impersonate the owner of LidoARM contract. + modifier asLidoARMOwner() { + vm.startPrank(lidoARM.owner()); _; vm.stopPrank(); } @@ -77,8 +77,8 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Deposit WETH into the LidoFixedPriceMultiLpARM contract. - modifier depositInLidoFixedPriceMultiLpARM(address user, uint256 amount) { + /// @notice Deposit WETH into the LidoARM contract. + modifier depositInLidoARM(address user, uint256 amount) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); @@ -89,8 +89,8 @@ abstract contract Modifiers is Helpers { // Deal amount as "extra" to user deal(address(weth), user, amount + balance); vm.startPrank(user); - weth.approve(address(lidoFixedPriceMultiLpARM), type(uint256).max); - lidoFixedPriceMultiLpARM.deposit(amount); + weth.approve(address(lidoARM), type(uint256).max); + lidoARM.deposit(amount); vm.stopPrank(); if (mode == VmSafe.CallerMode.Prank) { @@ -101,14 +101,14 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Request redeem from LidoFixedPriceMultiLpARM contract. - modifier requestRedeemFromLidoFixedPriceMultiLpARM(address user, uint256 amount) { + /// @notice Request redeem from LidoARM contract. + modifier requestRedeemFromLidoARM(address user, uint256 amount) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); vm.startPrank(user); - lidoFixedPriceMultiLpARM.requestRedeem(amount); + lidoARM.requestRedeem(amount); vm.stopPrank(); if (mode == VmSafe.CallerMode.Prank) { @@ -119,14 +119,14 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Claim redeem from LidoFixedPriceMultiLpARM contract. - modifier claimRequestOnLidoFixedPriceMultiLpARM(address user, uint256 requestId) { + /// @notice Claim redeem from LidoARM contract. + modifier claimRequestOnLidoARM(address user, uint256 requestId) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); vm.startPrank(user); - lidoFixedPriceMultiLpARM.claimRedeem(requestId); + lidoARM.claimRedeem(requestId); vm.stopPrank(); if (mode == VmSafe.CallerMode.Prank) { @@ -137,24 +137,16 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Simulate asset gain or loss in LidoFixedPriceMultiLpARM contract. - modifier simulateAssetGainInLidoFixedPriceMultiLpARM(uint256 assetGain, address token, bool gain) { + /// @notice Simulate asset gain or loss in LidoARM contract. + modifier simulateAssetGainInLidoARM(uint256 assetGain, address token, bool gain) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); if (gain) { - deal( - token, - address(lidoFixedPriceMultiLpARM), - IERC20(token).balanceOf(address(lidoFixedPriceMultiLpARM)) + uint256(assetGain) - ); + deal(token, address(lidoARM), IERC20(token).balanceOf(address(lidoARM)) + uint256(assetGain)); } else { - deal( - token, - address(lidoFixedPriceMultiLpARM), - IERC20(token).balanceOf(address(lidoFixedPriceMultiLpARM)) - uint256(assetGain) - ); + deal(token, address(lidoARM), IERC20(token).balanceOf(address(lidoARM)) - uint256(assetGain)); } if (mode == VmSafe.CallerMode.Prank) { @@ -165,20 +157,20 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Collect fees on LidoFixedPriceMultiLpARM contract. - modifier collectFeesOnLidoFixedPriceMultiLpARM() { - lidoFixedPriceMultiLpARM.collectFees(); + /// @notice Collect fees on LidoARM contract. + modifier collectFeesOnLidoARM() { + lidoARM.collectFees(); _; } - /// @notice Approve stETH on LidoFixedPriceMultiLpARM contract. - modifier approveStETHOnLidoFixedPriceMultiLpARM() { + /// @notice Approve stETH on LidoARM contract. + modifier approveStETHOnLidoARM() { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); - vm.prank(lidoFixedPriceMultiLpARM.owner()); - lidoFixedPriceMultiLpARM.approveStETH(); + vm.prank(lidoARM.owner()); + lidoARM.approveStETH(); if (mode == VmSafe.CallerMode.Prank) { vm.prank(_address, _origin); @@ -188,14 +180,14 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Request stETH withdrawal for ETH on LidoFixedPriceMultiLpARM contract. - modifier requestStETHWithdrawalForETHOnLidoFixedPriceMultiLpARM(uint256[] memory amounts) { + /// @notice Request stETH withdrawal for ETH on LidoARM contract. + modifier requestStETHWithdrawalForETHOnLidoARM(uint256[] memory amounts) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); - vm.prank(lidoFixedPriceMultiLpARM.owner()); - lidoFixedPriceMultiLpARM.requestStETHWithdrawalForETH(amounts); + vm.prank(lidoARM.owner()); + lidoARM.requestStETHWithdrawalForETH(amounts); if (mode == VmSafe.CallerMode.Prank) { vm.prank(_address, _origin); @@ -222,14 +214,14 @@ abstract contract Modifiers is Helpers { } /// @notice mock call for `claimWithdrawals` on lido withdraw contracts. - /// @dev this will send eth directly to the lidoFixedPriceMultiLpARM contract. - modifier mockFunctionClaimWithdrawOnLidoFixedPriceMultiLpARM(uint256 amount) { + /// @dev this will send eth directly to the lidoARM contract. + modifier mockFunctionClaimWithdrawOnLidoARM(uint256 amount) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); // Deploy fake lido withdraw contract - MockLidoWithdraw mocklidoWithdraw = new MockLidoWithdraw(address(lidoFixedPriceMultiLpARM)); + MockLidoWithdraw mocklidoWithdraw = new MockLidoWithdraw(address(lidoARM)); // Give ETH to the ETH Sender contract vm.deal(address(mocklidoWithdraw.ethSender()), amount); // Mock all the call to the fake lido withdraw contract diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 081f8e1..1852417 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -6,7 +6,7 @@ import {Test, console2} from "forge-std/Test.sol"; import {AbstractSmokeTest} from "./AbstractSmokeTest.sol"; import {IERC20} from "contracts/Interfaces.sol"; -import {LidoFixedPriceMultiLpARM} from "contracts/LidoFixedPriceMultiLpARM.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; import {Proxy} from "contracts/Proxy.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; import {console} from "forge-std/console.sol"; @@ -17,7 +17,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { IERC20 weth; IERC20 steth; Proxy proxy; - LidoFixedPriceMultiLpARM lidoARM; + LidoARM lidoARM; address operator; function setUp() public { @@ -30,7 +30,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { vm.label(address(operator), "OPERATOR"); proxy = Proxy(deployManager.getDeployment("LIDO_ARM")); - lidoARM = LidoFixedPriceMultiLpARM(payable(deployManager.getDeployment("LIDO_ARM"))); + lidoARM = LidoARM(payable(deployManager.getDeployment("LIDO_ARM"))); // Only fuzz from this address. Big speedup on fork. targetSender(address(this)); From 070f9ff79b152bd6a4699368726f579fc8a1318e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Thu, 26 Sep 2024 11:46:32 +0200 Subject: [PATCH 085/196] test: add extra tests for deposit while lido withdraw. --- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 166 ++++++++++++++++++ test/fork/utils/Modifiers.sol | 22 ++- 2 files changed, 182 insertions(+), 6 deletions(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 9ac0e9b..b9a74fc 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -7,11 +7,17 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; +import {IStETHWithdrawal} from "contracts/Interfaces.sol"; +import {Mainnet} from "contracts/utils/Addresses.sol"; contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { + uint256[] amounts1 = new uint256[](1); + IStETHWithdrawal public stETHWithdrawal = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL); + ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// + function setUp() public override { super.setUp(); @@ -21,6 +27,9 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { deal(address(weth), alice, 1_000 ether); vm.prank(alice); weth.approve(address(lidoARM), type(uint256).max); + + // Amounts arrays + amounts1[0] = DEFAULT_AMOUNT; } ////////////////////////////////////////////////////// @@ -348,4 +357,161 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } + + /// @notice Test the following scenario: + /// 1. ARM gain assets + /// 2. Operator request a withdraw from Lido on steth + /// 3. User deposit liquidity + /// 4. Operator claim the withdrawal on Lido + /// 5. User burn shares + /// Checking that amount deposited can be retrieved + function test_Deposit_WithOutStandingWithdrawRequest_BeforeDeposit_ClaimedLidoWithdraw_WithAssetGain() + public + deal_(address(steth), address(lidoARM), DEFAULT_AMOUNT) + approveStETHOnLidoARM + requestStETHWithdrawalForETHOnLidoARM(amounts1) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + { + // Assertions Before + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.outstandingEther(), DEFAULT_AMOUNT); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); + assertEqQueueMetadata(0, 0, 0, 0); + + // Expected values + uint256 expectShares = DEFAULT_AMOUNT * MIN_TOTAL_SUPPLY / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100); + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(this), address(lidoARM), DEFAULT_AMOUNT); + vm.expectEmit({emitter: address(lidoARM)}); + emit IERC20.Transfer(address(0), address(this), expectShares); + + uint256 requestId = stETHWithdrawal.getLastRequestId(); + uint256[] memory requests = new uint256[](1); + requests[0] = requestId; + + // Main calls + // 1. User mint shares + uint256 shares = lidoARM.deposit(DEFAULT_AMOUNT); + // 2. Lido finalization process is simulated + lidoARM.totalAssets(); + _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); + // 3. Operator claim withdrawal on lido + lidoARM.totalAssets(); + lidoARM.claimStETHWithdrawalForWETH(requests); + // 4. User burn shares + (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); + + uint256 excessLeftover = DEFAULT_AMOUNT - receivedAssets; + // Assertions After + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100 + excessLeftover); + assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEqQueueMetadata(receivedAssets, 0, 0, 1); + assertApproxEqRel(receivedAssets, DEFAULT_AMOUNT, 1e6, "received assets"); // 1e6 -> 0.0000000001%, + // This difference comes from the small value of shares, which reduces the precision of the calculation + } + + /// @notice Test the following scenario: + /// 1. User deposit liquidity + /// 2. ARM swap between WETH and stETH (no assets gains) + /// 2. Operator request a withdraw from Lido on steth + /// 4. Operator claim the withdrawal on Lido + /// 5. User burn shares + /// Checking that amount deposited can be retrieved + function test_Deposit_WithOutStandingWithdrawRequest_AfterDeposit_ClaimedLidoWithdraw_WithoutAssetGain() + public + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + { + // Assertions Before + // Not needed, as one of the previous test already covers this scenario + + // Main calls: + // 1. User mint shares + uint256 shares = lidoARM.deposit(DEFAULT_AMOUNT); + // Simulate a swap from WETH to stETH + deal(address(weth), address(lidoARM), MIN_TOTAL_SUPPLY); + deal(address(steth), address(lidoARM), DEFAULT_AMOUNT); + // 2. Operator request a claim on withdraw + lidoARM.requestStETHWithdrawalForETH(amounts1); + // 3. We simulate the finalization of the process + _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); + uint256 requestId = stETHWithdrawal.getLastRequestId(); + uint256[] memory requests = new uint256[](1); + requests[0] = requestId; + // 4. Operator claim the withdrawal on lido + lidoARM.claimStETHWithdrawalForWETH(requests); + // 5. User burn shares + (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); + + // Assertions After + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees + assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEqQueueMetadata(receivedAssets, 0, 0, 1); + assertEq(receivedAssets, DEFAULT_AMOUNT, "received assets"); + } + + /// @notice Test the following scenario: + /// 1. User deposit liquidity + /// 2. ARM asset gain (on steth) + /// 2. Operator request a withdraw from Lido on steth + /// 4. Operator claim the withdrawal on Lido + /// 5. User burn shares + /// Checking that amount deposited + benefice can be retrieved + function test_Deposit_WithOutStandingWithdrawRequest_AfterDeposit_ClaimedLidoWithdraw_WithAssetGain() + public + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + { + // Assertions Before + // Not needed, as one of the previous test already covers this scenario + + // Main calls: + // 1. User mint shares + uint256 shares = lidoARM.deposit(DEFAULT_AMOUNT); + // Simulate a swap from WETH to stETH + deal(address(steth), address(lidoARM), DEFAULT_AMOUNT); + // 2. Operator request a claim on withdraw + lidoARM.requestStETHWithdrawalForETH(amounts1); + // 3. We simulate the finalization of the process + _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); + uint256 requestId = stETHWithdrawal.getLastRequestId(); + uint256[] memory requests = new uint256[](1); + requests[0] = requestId; + // 4. Operator claim the withdrawal on lido + lidoARM.claimStETHWithdrawalForWETH(requests); + // 5. User burn shares + (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); + + uint256 userBenef = (DEFAULT_AMOUNT * 80 / 100) * DEFAULT_AMOUNT / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + uint256 deadAddressBenef = (DEFAULT_AMOUNT * 80 / 100) * MIN_TOTAL_SUPPLY / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + // Assertions After + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); + assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100); // No perfs so no fees + assertApproxEqAbs(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + deadAddressBenef, STETH_ERROR_ROUNDING); + assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEqQueueMetadata(receivedAssets, 0, 0, 1); + assertEq(receivedAssets, DEFAULT_AMOUNT + userBenef, "received assets"); + } } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index 23b69cd..290f435 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -77,6 +77,12 @@ abstract contract Modifiers is Helpers { _; } + /// @notice Modifier for deal function. + modifier deal_(address token, address to, uint256 amount) { + deal(token, to, amount); + _; + } + /// @notice Deposit WETH into the LidoARM contract. modifier depositInLidoARM(address user, uint256 amount) { // Todo: extend this logic to other modifier if needed @@ -220,12 +226,7 @@ abstract contract Modifiers is Helpers { (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); - // Deploy fake lido withdraw contract - MockLidoWithdraw mocklidoWithdraw = new MockLidoWithdraw(address(lidoARM)); - // Give ETH to the ETH Sender contract - vm.deal(address(mocklidoWithdraw.ethSender()), amount); - // Mock all the call to the fake lido withdraw contract - MockCall.mockCallLidoClaimWithdrawals(address(mocklidoWithdraw)); + _mockFunctionClaimWithdrawOnLidoARM(amount); if (mode == VmSafe.CallerMode.Prank) { vm.prank(_address, _origin); @@ -235,6 +236,15 @@ abstract contract Modifiers is Helpers { _; } + function _mockFunctionClaimWithdrawOnLidoARM(uint256 amount) internal { + // Deploy fake lido withdraw contract + MockLidoWithdraw mocklidoWithdraw = new MockLidoWithdraw(address(lidoARM)); + // Give ETH to the ETH Sender contract + vm.deal(address(mocklidoWithdraw.ethSender()), amount); + // Mock all the call to the fake lido withdraw contract + MockCall.mockCallLidoClaimWithdrawals(address(mocklidoWithdraw)); + } + /// @notice Skip time by a given delay. modifier skipTime(uint256 delay) { skip(delay); From 133f5e0b759f73c403fb7d29fe90ae2cc2157778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Thu, 26 Sep 2024 12:04:36 +0200 Subject: [PATCH 086/196] test: add extra test for swap with withdrawQueue full. --- .../SwapExactTokensForTokens.t.sol | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index 219b322..03a169c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -175,6 +175,31 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test ); } + /// @notice Test the following scenario: + /// 1. Set steth balance of the ARM to 0. + /// 2. Set weth balance of the ARM to MIN_TOTAL_SUPPLY. + /// 3. Deposit DEFAULT_AMOUNT in the ARM. + /// 4. Request redeem of DEFAULT_AMOUNT * 90%. + /// 5. Try to swap DEFAULT_AMOUNT of stETH to WETH. + function test_RevertWhen_SwapExactTokensForTokens_Because_InsufficientLiquidity_DueToRedeemRequest() + public + setTotalAssetsCap(DEFAULT_AMOUNT * 10 + MIN_TOTAL_SUPPLY) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + deal_(address(steth), address(lidoARM), 0) + deal_(address(weth), address(lidoARM), MIN_TOTAL_SUPPLY) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoARM(address(this), DEFAULT_AMOUNT * 90 / 100) + { + vm.expectRevert("ARM: Insufficient liquidity"); + lidoARM.swapExactTokensForTokens( + steth, // inToken + weth, // outToken + DEFAULT_AMOUNT, // amountIn + 0, // amountOutMin + address(this) // to + ); + } + ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// From 61f83d2e6f9d9699443e58384d81b92e24d333cd Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Sep 2024 17:07:26 +1000 Subject: [PATCH 087/196] Added switch to LiquidityProviderController so the account level caps can be disabled --- src/contracts/LiquidityProviderController.sol | 25 +++++++++++++++---- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 12 ++++----- .../TotalAssets.t.sol | 2 +- test/fork/shared/Shared.sol | 2 +- test/fork/utils/Modifiers.sol | 2 +- test/smoke/LidoARMSmokeTest.t.sol | 7 ++++++ 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/contracts/LiquidityProviderController.sol b/src/contracts/LiquidityProviderController.sol index 633d153..a3d06f9 100644 --- a/src/contracts/LiquidityProviderController.sol +++ b/src/contracts/LiquidityProviderController.sol @@ -14,8 +14,10 @@ contract LiquidityProviderController is Initializable, OwnableOperable { /// @notice The address of the linked Application Redemption Manager (ARM). address public immutable arm; + /// @notice true if a cap is placed on each liquidity provider's account. + bool public accountCapEnabled; /// @notice The ARM's maximum allowed total assets. - uint256 public totalAssetsCap; + uint248 public totalAssetsCap; /// @notice The maximum allowed assets for each liquidity provider. /// This is effectively a whitelist of liquidity providers as a zero amount prevents any deposits. mapping(address liquidityProvider => uint256 cap) public liquidityProviderCaps; @@ -24,6 +26,7 @@ contract LiquidityProviderController is Initializable, OwnableOperable { event LiquidityProviderCap(address indexed liquidityProvider, uint256 cap); event TotalAssetsCap(uint256 cap); + event AccountCapEnabled(bool enabled); constructor(address _arm) { arm = _arm; @@ -31,17 +34,20 @@ contract LiquidityProviderController is Initializable, OwnableOperable { function initialize(address _operator) external initializer { _initOwnableOperable(_operator); + accountCapEnabled = true; } function postDepositHook(address liquidityProvider, uint256 assets) external { require(msg.sender == arm, "LPC: Caller is not ARM"); - uint256 oldCap = liquidityProviderCaps[liquidityProvider]; - require(oldCap >= assets, "LPC: LP cap exceeded"); - // total assets has already been updated with the new assets require(totalAssetsCap >= ILiquidityProviderARM(arm).totalAssets(), "LPC: Total assets cap exceeded"); + if (!accountCapEnabled) return; + + uint256 oldCap = liquidityProviderCaps[liquidityProvider]; + require(oldCap >= assets, "LPC: LP cap exceeded"); + uint256 newCap = oldCap - assets; // Save the new LP cap to storage @@ -64,9 +70,18 @@ contract LiquidityProviderController is Initializable, OwnableOperable { /// @notice Set the ARM's maximum total assets. /// Setting to zero will prevent any further deposits. /// The liquidity provider can still withdraw assets. - function setTotalAssetsCap(uint256 _totalAssetsCap) external onlyOperatorOrOwner { + function setTotalAssetsCap(uint248 _totalAssetsCap) external onlyOperatorOrOwner { totalAssetsCap = _totalAssetsCap; emit TotalAssetsCap(_totalAssetsCap); } + + /// @notice Enable or disable the account cap. + function setAccountCapEnabled(bool _accountCapEnabled) external onlyOperatorOrOwner { + require(accountCapEnabled != _accountCapEnabled, "LPC: Account cap already set"); + + accountCapEnabled = _accountCapEnabled; + + emit AccountCapEnabled(_accountCapEnabled); + } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index b9a74fc..0e451a5 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -110,9 +110,9 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); // Minted to dead on deploy + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount, "lp cap before"); assertEqQueueMetadata(0, 0, 0, 0); // Expected events @@ -133,9 +133,9 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.balanceOf(address(this)), shares); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether } diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index 4599b4b..04c7a93 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -22,7 +22,7 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { address[] memory providers = new address[](1); providers[0] = address(this); liquidityProviderController.setLiquidityProviderCaps(providers, type(uint256).max); - liquidityProviderController.setTotalAssetsCap(type(uint256).max); + liquidityProviderController.setTotalAssetsCap(type(uint248).max); // Approve STETH for Lido lidoARM.approveStETH(); diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index dda8910..ef02344 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -127,7 +127,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { LiquidityProviderController lpcImpl = new LiquidityProviderController(address(lidoProxy)); // Initialize Proxy with LiquidityProviderController implementation. - lpcProxy.initialize(address(lpcImpl), address(this), ""); + lpcProxy.initialize(address(lpcImpl), address(this), data); // Set the Proxy as the LiquidityProviderController. liquidityProviderController = LiquidityProviderController(payable(address(lpcProxy))); diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index 290f435..d17027c 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -73,7 +73,7 @@ abstract contract Modifiers is Helpers { /// @notice Set the total assets cap on the LiquidityProviderController contract. modifier setTotalAssetsCap(uint256 cap) { - liquidityProviderController.setTotalAssetsCap(cap); + liquidityProviderController.setTotalAssetsCap(uint248(cap)); _; } diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 1852417..e6fb684 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -7,6 +7,7 @@ import {AbstractSmokeTest} from "./AbstractSmokeTest.sol"; import {IERC20} from "contracts/Interfaces.sol"; import {LidoARM} from "contracts/LidoARM.sol"; +import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; import {Proxy} from "contracts/Proxy.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; import {console} from "forge-std/console.sol"; @@ -18,6 +19,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { IERC20 steth; Proxy proxy; LidoARM lidoARM; + LiquidityProviderController liquidityProviderController; address operator; function setUp() public { @@ -31,6 +33,8 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { proxy = Proxy(deployManager.getDeployment("LIDO_ARM")); lidoARM = LidoARM(payable(deployManager.getDeployment("LIDO_ARM"))); + liquidityProviderController = + LiquidityProviderController(deployManager.getDeployment("LIDO_ARM_LPC")); // Only fuzz from this address. Big speedup on fork. targetSender(address(this)); @@ -49,6 +53,9 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(lidoARM.lastTotalAssets(), 1e12 + 1, "Last total assets"); assertEq(lidoARM.totalSupply(), 1e12, "Total supply"); assertEq(weth.balanceOf(address(lidoARM)), 1e12, "WETH balance"); + + assertEq(liquidityProviderController.accountCapEnabled(), true, "account cap enabled"); + assertEq(liquidityProviderController.operator(), Mainnet.ARM_RELAYER, "Operator"); } function test_swapExactTokensForTokens() external { From 39933ec70fe3a4d6cdcc2572bfa132fed0f694f8 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Sep 2024 17:10:53 +1000 Subject: [PATCH 088/196] Changed setAccountCapEnabled to only be called by Owner --- src/contracts/LiquidityProviderController.sol | 2 +- test/smoke/LidoARMSmokeTest.t.sol | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/contracts/LiquidityProviderController.sol b/src/contracts/LiquidityProviderController.sol index a3d06f9..d9755e9 100644 --- a/src/contracts/LiquidityProviderController.sol +++ b/src/contracts/LiquidityProviderController.sol @@ -77,7 +77,7 @@ contract LiquidityProviderController is Initializable, OwnableOperable { } /// @notice Enable or disable the account cap. - function setAccountCapEnabled(bool _accountCapEnabled) external onlyOperatorOrOwner { + function setAccountCapEnabled(bool _accountCapEnabled) external onlyOwner { require(accountCapEnabled != _accountCapEnabled, "LPC: Account cap already set"); accountCapEnabled = _accountCapEnabled; diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index e6fb684..c1fb88e 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -33,8 +33,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { proxy = Proxy(deployManager.getDeployment("LIDO_ARM")); lidoARM = LidoARM(payable(deployManager.getDeployment("LIDO_ARM"))); - liquidityProviderController = - LiquidityProviderController(deployManager.getDeployment("LIDO_ARM_LPC")); + liquidityProviderController = LiquidityProviderController(deployManager.getDeployment("LIDO_ARM_LPC")); // Only fuzz from this address. Big speedup on fork. targetSender(address(this)); From 4db8f3649d0be1f0e1a9788d98b56e778baaa14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 27 Sep 2024 09:37:07 +0200 Subject: [PATCH 089/196] fix: use approx eq for steth balance. --- test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 0e451a5..28ef690 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -317,7 +317,9 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions Before uint256 stethBalanceBefore = 3 * DEFAULT_AMOUNT / 4; - assertEq(steth.balanceOf(address(lidoARM)), stethBalanceBefore, "stETH ARM balance before"); + assertApproxEqAbs( + steth.balanceOf(address(lidoARM)), stethBalanceBefore, STETH_ERROR_ROUNDING, "stETH ARM balance before" + ); uint256 wethBalanceBefore = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - 3 * DEFAULT_AMOUNT / 4; assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); From 4703a90f5742d1ea4b6ed7b0c3e13f702b37a758 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Sep 2024 17:54:03 +1000 Subject: [PATCH 090/196] Added accountCapEnabled tests --- .../ClaimStETHWithdrawalForWETH.t.sol | 6 +- .../RequestStETHWithdrawalForETH.t.sol | 22 ++----- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 64 +++++++++++++++++-- test/fork/utils/Modifiers.sol | 6 +- 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol index ad75713..5c7cec1 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol @@ -39,7 +39,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared ////////////////////////////////////////////////////// function test_ClaimStETHWithdrawalForWETH_EmptyList() public - asLidoFixedPriceMulltiLpARMOperator + asOperator requestStETHWithdrawalForETHOnLidoARM(new uint256[](0)) { assertEq(address(lidoARM).balance, 0); @@ -54,7 +54,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared function test_ClaimStETHWithdrawalForWETH_SingleRequest() public - asLidoFixedPriceMulltiLpARMOperator + asOperator approveStETHOnLidoARM requestStETHWithdrawalForETHOnLidoARM(amounts1) mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT) @@ -76,7 +76,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared function test_ClaimStETHWithdrawalForWETH_MultiRequest() public - asLidoFixedPriceMulltiLpARMOperator + asOperator approveStETHOnLidoARM requestStETHWithdrawalForETHOnLidoARM(amounts2) mockCallLidoFindCheckpointHints diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol index 8a4d896..3c71723 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol @@ -27,7 +27,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared function test_RevertWhen_RequestStETHWithdrawalForETH_Because_BalanceExceeded() public - asLidoFixedPriceMulltiLpARMOperator + asOperator approveStETHOnLidoARM { // Remove all stETH from the contract @@ -43,16 +43,12 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_RequestStETHWithdrawalForETH_EmptyList() public asLidoFixedPriceMulltiLpARMOperator { + function test_RequestStETHWithdrawalForETH_EmptyList() public asOperator { uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(new uint256[](0)); assertEq(requestIds.length, 0); } - function test_RequestStETHWithdrawalForETH_SingleAmount_1ether() - public - asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoARM - { + function test_RequestStETHWithdrawalForETH_SingleAmount_1ether() public asOperator approveStETHOnLidoARM { uint256[] memory amounts = new uint256[](1); amounts[0] = DEFAULT_AMOUNT; @@ -67,11 +63,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared assertGt(requestIds[0], 0); } - function test_RequestStETHWithdrawalForETH_SingleAmount_1000ethers() - public - asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoARM - { + function test_RequestStETHWithdrawalForETH_SingleAmount_1000ethers() public asOperator approveStETHOnLidoARM { uint256[] memory amounts = new uint256[](1); amounts[0] = 1_000 ether; @@ -86,11 +78,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared assertGt(requestIds[0], 0); } - function test_RequestStETHWithdrawalForETH_MultipleAmount() - public - asLidoFixedPriceMulltiLpARMOperator - approveStETHOnLidoARM - { + function test_RequestStETHWithdrawalForETH_MultipleAmount() public asOperator approveStETHOnLidoARM { uint256 length = _bound(vm.randomUint(), 2, 10); uint256[] memory amounts = new uint256[](length); for (uint256 i = 0; i < amounts.length; i++) { diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 4cf049c..9fbc440 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -7,6 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; import {AbstractARM} from "contracts/AbstractARM.sol"; +import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// @@ -24,6 +25,11 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setFee(0); } + function test_RevertWhen_PerformanceFee_SetFee_Because_Operator() public asOperator { + vm.expectRevert("ARM: Only owner can call this function."); + lidoARM.setFee(0); + } + function test_RevertWhen_PerformanceFee_SetFee_Because_FeeIsTooHigh() public asLidoARMOwner { uint256 max = lidoARM.FEE_SCALE(); vm.expectRevert("ARM: fee too high"); @@ -87,7 +93,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setPrices(1e36, 1e36); } - function test_RevertWhen_FixedPriceARM_SetPrices_Because_PriceRange() public asLidoFixedPriceMulltiLpARMOperator { + function test_RevertWhen_FixedPriceARM_SetPrices_Because_PriceRange() public asOperator { // buy price 11 basis points higher than 1.0 vm.expectRevert("ARM: buy price too high"); lidoARM.setPrices(1.0011 * 1e36, 1.002 * 1e36); @@ -106,12 +112,12 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setPrices(0, 0); } - function test_SellPriceCannotCrossOneByMoreThanTenBps() public asLidoFixedPriceMulltiLpARMOperator { + function test_SellPriceCannotCrossOneByMoreThanTenBps() public asOperator { vm.expectRevert("ARM: sell price too low"); lidoARM.setPrices(0.998 * 1e36, 0.9989 * 1e36); } - function test_BuyPriceCannotCrossOneByMoreThanTenBps() public asLidoFixedPriceMulltiLpARMOperator { + function test_BuyPriceCannotCrossOneByMoreThanTenBps() public asOperator { vm.expectRevert("ARM: buy price too high"); lidoARM.setPrices(1.0011 * 1e36, 1.002 * 1e36); } @@ -119,7 +125,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- FIXED PRICE ARM - PASSING TESTS ////////////////////////////////////////////////////// - function test_FixedPriceARM_SetPrices_Operator() public asLidoFixedPriceMulltiLpARMOperator { + function test_FixedPriceARM_SetPrices_Operator() public asOperator { // buy price 10 basis points higher than 1.0 lidoARM.setPrices(1001e33, 1002e33); // sell price 10 basis points lower than 1.0 @@ -157,6 +163,16 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setOperator(address(0)); } + function test_RevertWhen_Ownable_SetOwner_Because_Operator() public asOperator { + vm.expectRevert("ARM: Only owner can call this function."); + lidoARM.setOwner(address(0)); + } + + function test_RevertWhen_Ownable_SetOperator_Because_Operator() public asOperator { + vm.expectRevert("ARM: Only owner can call this function."); + lidoARM.setOperator(address(0)); + } + ////////////////////////////////////////////////////// /// --- LIQUIIDITY PROVIDER CONTROLLER - REVERTING TESTS ////////////////////////////////////////////////////// @@ -168,6 +184,11 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setLiquidityProviderController(address(0)); } + function test_RevertWhen_LiquidityProviderController_SetLiquidityProvider_Because_Operator() public asOperator { + vm.expectRevert("ARM: Only owner can call this function."); + lidoARM.setLiquidityProviderController(address(0)); + } + ////////////////////////////////////////////////////// /// --- LIQUIIDITY PROVIDER CONTROLLER - PASSING TESTS ////////////////////////////////////////////////////// @@ -180,4 +201,39 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.liquidityProviderController(), newLiquidityProviderController); } + + ////////////////////////////////////////////////////// + /// --- AccountCapEnabled - REVERTING TEST + ////////////////////////////////////////////////////// + function test_RevertWhen_LiquidityProviderController_SetAccountCapEnabled_Because_NotOwner() + public + asRandomAddress + { + vm.expectRevert("ARM: Only owner can call this function."); + liquidityProviderController.setAccountCapEnabled(false); + } + + function test_RevertWhen_LiquidityProviderController_SetAccountCapEnabled_Because_Operator() public asOperator { + vm.expectRevert("ARM: Only owner can call this function."); + liquidityProviderController.setAccountCapEnabled(false); + } + + function test_RevertWhen_LiquidityProviderController_SetAccountCapEnabled_Because_AlreadySet() + public + asLidoARMOwner + { + vm.expectRevert("LPC: Account cap already set"); + liquidityProviderController.setAccountCapEnabled(true); + } + + ////////////////////////////////////////////////////// + /// --- AccountCapEnabled - PASSING TESTS + ////////////////////////////////////////////////////// + function test_LiquidityProviderController_SetAccountCapEnabled() public asLidoARMOwner { + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.AccountCapEnabled(false); + liquidityProviderController.setAccountCapEnabled(false); + + assertEq(liquidityProviderController.accountCapEnabled(), false); + } } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index d17027c..ce64605 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -21,7 +21,7 @@ abstract contract Modifiers is Helpers { vm.stopPrank(); } - /// @notice Impersonate the owner of the contract. + /// @notice Impersonate the owner of the OethARM contract. modifier asOwner() { vm.startPrank(oethARM.owner()); _; @@ -35,8 +35,8 @@ abstract contract Modifiers is Helpers { vm.stopPrank(); } - /// @notice Impersonate the operator of LidoOwnerLpARM contract. - modifier asLidoFixedPriceMulltiLpARMOperator() { + /// @notice Impersonate the Operator of LidoARM contract. + modifier asOperator() { vm.startPrank(lidoARM.operator()); _; vm.stopPrank(); From 8568f6d15bcac3bd5813a4ec8931e0803ca6067b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 27 Sep 2024 09:54:29 +0200 Subject: [PATCH 091/196] fix: use approx eq for steth balance (again). --- test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 28ef690..8bbebb0 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -313,7 +313,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // First user requests a full withdrawal uint256 firstUserShares = lidoARM.balanceOf(address(this)); - lidoARM.requestRedeem(firstUserShares); + (, uint256 assetsRedeem) = lidoARM.requestRedeem(firstUserShares); // Assertions Before uint256 stethBalanceBefore = 3 * DEFAULT_AMOUNT / 4; @@ -329,7 +329,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before"); - assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); + assertEqQueueMetadata(assetsRedeem, 0, 0, 1); + assertApproxEqAbs(assetsRedeem, DEFAULT_AMOUNT, STETH_ERROR_ROUNDING, "assets redeem before"); uint256 amount = DEFAULT_AMOUNT * 2; @@ -346,7 +347,9 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { uint256 shares = lidoARM.deposit(amount); // Assertions After - assertEq(steth.balanceOf(address(lidoARM)), stethBalanceBefore, "stETH ARM balance after"); + assertApproxEqAbs( + steth.balanceOf(address(lidoARM)), stethBalanceBefore, STETH_ERROR_ROUNDING, "stETH ARM balance after" + ); assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees @@ -356,8 +359,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used // withdrawal request is now claimable - assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); - assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether + assertEqQueueMetadata(assetsRedeem, 0, 0, 1); + assertApproxEqAbs(shares, amount, STETH_ERROR_ROUNDING, "shares after"); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } /// @notice Test the following scenario: From 7b33788ceba06750d3ce6db98289b27bfc400a98 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Sep 2024 17:58:57 +1000 Subject: [PATCH 092/196] More setter tests --- test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 9fbc440..1861a50 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -41,6 +41,11 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setFeeCollector(address(0)); } + function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_Operator() public asOperator { + vm.expectRevert("ARM: Only owner can call this function."); + lidoARM.setFeeCollector(address(0)); + } + function test_RevertWhen_PerformanceFee_SetFeeCollector_Because_FeeCollectorIsZero() public asLidoARMOwner { vm.expectRevert("ARM: invalid fee collector"); lidoARM.setFeeCollector(address(0)); @@ -158,14 +163,14 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setOwner(address(0)); } - function test_RevertWhen_Ownable_SetOperator_Because_NotOwner() public asRandomAddress { + function test_RevertWhen_Ownable_SetOwner_Because_Operator() public asOperator { vm.expectRevert("ARM: Only owner can call this function."); - lidoARM.setOperator(address(0)); + lidoARM.setOwner(address(0)); } - function test_RevertWhen_Ownable_SetOwner_Because_Operator() public asOperator { + function test_RevertWhen_Ownable_SetOperator_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoARM.setOwner(address(0)); + lidoARM.setOperator(address(0)); } function test_RevertWhen_Ownable_SetOperator_Because_Operator() public asOperator { From fa71e972fc088cca396549730e6538e9a6b403f2 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Sep 2024 18:08:10 +1000 Subject: [PATCH 093/196] Added setTotalAssetsCap tests --- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 1861a50..421a62e 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -241,4 +241,31 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { assertEq(liquidityProviderController.accountCapEnabled(), false); } + + ////////////////////////////////////////////////////// + /// --- TotalAssetsCap - REVERTING TEST + ////////////////////////////////////////////////////// + function test_RevertWhen_LiquidityProviderController_SetTotalAssetsCap_Because_NotOwner() public asRandomAddress { + vm.expectRevert("ARM: Only operator or owner can call this function."); + liquidityProviderController.setTotalAssetsCap(100 ether); + } + + ////////////////////////////////////////////////////// + /// --- TotalAssetsCap - PASSING TESTS + ////////////////////////////////////////////////////// + function test_LiquidityProviderController_SetTotalAssetsCap_Owner() public asLidoARMOwner { + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.TotalAssetsCap(100 ether); + liquidityProviderController.setTotalAssetsCap(100 ether); + + assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); + } + + function test_LiquidityProviderController_SetTotalAssetsCap_Operator() public asOperator { + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.TotalAssetsCap(0); + liquidityProviderController.setTotalAssetsCap(0); + + assertEq(liquidityProviderController.totalAssetsCap(), 0); + } } From 2f1f7eeab6664f59e4c02142ece1d3f8dbc94ce8 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Sep 2024 18:27:50 +1000 Subject: [PATCH 094/196] Added setLiquidityProviderCaps tests --- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 421a62e..569e353 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -10,11 +10,17 @@ import {AbstractARM} from "contracts/AbstractARM.sol"; import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { + address[] testProviders; + ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// function setUp() public override { super.setUp(); + + testProviders = new address[](2); + testProviders[0] = vm.randomAddress(); + testProviders[1] = vm.randomAddress(); } ////////////////////////////////////////////////////// @@ -268,4 +274,54 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { assertEq(liquidityProviderController.totalAssetsCap(), 0); } + + ////////////////////////////////////////////////////// + /// --- LiquidityProviderCaps - REVERTING TEST + ////////////////////////////////////////////////////// + function test_RevertWhen_LiquidityProviderController_SetLiquidityProviderCaps_Because_NotOwner() + public + asRandomAddress + { + vm.expectRevert("ARM: Only operator or owner can call this function."); + liquidityProviderController.setLiquidityProviderCaps(testProviders, 50 ether); + } + + ////////////////////////////////////////////////////// + /// --- LiquidityProviderCaps - PASSING TESTS + ////////////////////////////////////////////////////// + function test_LiquidityProviderController_SetLiquidityProviderCaps_Owner() public asLidoARMOwner { + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.LiquidityProviderCap(testProviders[0], 50 ether); + emit LiquidityProviderController.LiquidityProviderCap(testProviders[1], 50 ether); + liquidityProviderController.setLiquidityProviderCaps(testProviders, 50 ether); + + assertEq(liquidityProviderController.liquidityProviderCaps(testProviders[0]), 50 ether); + assertEq(liquidityProviderController.liquidityProviderCaps(testProviders[1]), 50 ether); + } + + function test_LiquidityProviderController_SetLiquidityProviderCaps_Operator() public asOperator { + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.LiquidityProviderCap(testProviders[0], 50 ether); + emit LiquidityProviderController.LiquidityProviderCap(testProviders[1], 50 ether); + liquidityProviderController.setLiquidityProviderCaps(testProviders, 50 ether); + + assertEq(liquidityProviderController.liquidityProviderCaps(testProviders[0]), 50 ether); + assertEq(liquidityProviderController.liquidityProviderCaps(testProviders[1]), 50 ether); + } + + function test_LiquidityProviderController_SetLiquidityProviderCaps_ToZero() + public + asOperator + setLiquidityProviderCap(testProviders[0], 10 ether) + { + address[] memory providers = new address[](1); + providers[0] = testProviders[0]; + + vm.expectEmit({emitter: address(liquidityProviderController)}); + emit LiquidityProviderController.LiquidityProviderCap(providers[0], 0); + + liquidityProviderController.setLiquidityProviderCaps(providers, 0); + + assertEq(liquidityProviderController.liquidityProviderCaps(providers[0]), 0); + } } From a243cbdc820293101d21d0f4de7eb5a5c5bdc171 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Sep 2024 18:41:34 +1000 Subject: [PATCH 095/196] Updated assertions of initial deploy --- src/contracts/AbstractARM.sol | 2 +- test/smoke/LidoARMSmokeTest.t.sol | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 74d8c85..7d66390 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -32,7 +32,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { //////////////////////////////////////////////////// /// @notice The address of the asset that is used to add and remove liquidity. eg WETH - address internal immutable liquidityAsset; + address public immutable liquidityAsset; /// @notice The swap input token that is transferred to this contract. /// From a User perspective, this is the token being sold. /// token0 is also compatible with the Uniswap V2 Router interface. diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index c1fb88e..e57ca38 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -48,13 +48,20 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq((100 * uint256(lidoARM.fee())) / lidoARM.FEE_SCALE(), 15, "Performance fee as a percentage"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued"); // Some dust stETH is left in AMM v1 when stETH is transferred to the Treasury. + assertEq(steth.balanceOf(address(lidoARM)), 1, "stETH balance"); assertEq(lidoARM.totalAssets(), 1e12 + 1, "Total assets"); assertEq(lidoARM.lastTotalAssets(), 1e12 + 1, "Last total assets"); assertEq(lidoARM.totalSupply(), 1e12, "Total supply"); assertEq(weth.balanceOf(address(lidoARM)), 1e12, "WETH balance"); + // LidoLiquidityManager + assertEq(address(lidoARM.withdrawalQueue()), Mainnet.LIDO_WITHDRAWAL, "Lido withdrawal queue"); + assertEq(address(lidoARM.steth()), Mainnet.STETH, "stETH"); + assertEq(address(lidoARM.weth()), Mainnet.WETH, "WETH"); + assertEq(lidoARM.liquidityAsset(), Mainnet.WETH, "liquidityAsset"); assertEq(liquidityProviderController.accountCapEnabled(), true, "account cap enabled"); assertEq(liquidityProviderController.operator(), Mainnet.ARM_RELAYER, "Operator"); + assertEq(liquidityProviderController.arm(), address(lidoARM), "arm"); } function test_swapExactTokensForTokens() external { From 624578de422562f2fd2adc98a4ee9fdf96169058 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Sep 2024 18:44:02 +1000 Subject: [PATCH 096/196] Generated latest contract diagrams --- docs/LidoARMSquashed.svg | 26 ++++----- docs/LiquidityProviderControllerHierarchy.svg | 46 +++++++++++++++ docs/LiquidityProviderControllerSquashed.svg | 58 +++++++++++++++++++ docs/generate.sh | 3 + src/contracts/README.md | 20 +++++-- 5 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 docs/LiquidityProviderControllerHierarchy.svg create mode 100644 docs/LiquidityProviderControllerSquashed.svg diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 880ffda..387be2f 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -18,19 +18,19 @@ Private:   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> +   _gap: uint256[42] <<AbstractARM>>   _gap: uint256[49] <<LidoLiquidityManager>> Internal:   OWNER_SLOT: bytes32 <<Ownable>>   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>>   DEAD_ACCOUNT: address <<AbstractARM>> -   liquidityAsset: address <<AbstractARM>> -Public: -   operator: address <<OwnableOperable>> -   MAX_PRICE_DEVIATION: uint256 <<AbstractARM>> -   PRICE_SCALE: uint256 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<AbstractARM>> -   FEE_SCALE: uint256 <<AbstractARM>> +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>> @@ -67,7 +67,7 @@    _liquidityAvailable(): uint256 <<AbstractARM>>    _rawTotalAssets(): uint256 <<AbstractARM>>    _externalWithdrawQueue(): uint256 <<LidoARM>> -    _calcFee() <<AbstractARM>> +    _accruePerformanceFee() <<AbstractARM>>    _setFee(_fee: uint256) <<AbstractARM>>    _setFeeCollector(_feeCollector: address) <<AbstractARM>>    _initLidoLiquidityManager() <<LidoLiquidityManager>> @@ -86,10 +86,10 @@    previewRedeem(shares: uint256): (assets: uint256) <<AbstractARM>>    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<AbstractARM>>    claimRedeem(requestId: uint256): (assets: uint256) <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    collectFees(): (fees: uint256) <<AbstractARM>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>>    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> diff --git a/docs/LiquidityProviderControllerHierarchy.svg b/docs/LiquidityProviderControllerHierarchy.svg new file mode 100644 index 0000000..ab3c507 --- /dev/null +++ b/docs/LiquidityProviderControllerHierarchy.svg @@ -0,0 +1,46 @@ + + + + + + +UmlClassDiagram + + + +15 + +LiquidityProviderController +../src/contracts/LiquidityProviderController.sol + + + +19 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + + +15->19 + + + + + +18 + +Ownable +../src/contracts/Ownable.sol + + + +19->18 + + + + + diff --git a/docs/LiquidityProviderControllerSquashed.svg b/docs/LiquidityProviderControllerSquashed.svg new file mode 100644 index 0000000..56543b3 --- /dev/null +++ b/docs/LiquidityProviderControllerSquashed.svg @@ -0,0 +1,58 @@ + + + + + + +UmlClassDiagram + + + +15 + +LiquidityProviderController +../src/contracts/LiquidityProviderController.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[48] <<LiquidityProviderController>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +Public: +   operator: address <<OwnableOperable>> +   arm: address <<LiquidityProviderController>> +   accountCapEnabled: bool <<LiquidityProviderController>> +   totalAssetsCap: uint248 <<LiquidityProviderController>> +   liquidityProviderCaps: mapping(address=>uint256) <<LiquidityProviderController>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +External: +    owner(): address <<Ownable>> +    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +    setOperator(newOperator: address) <<onlyOwner>> <<OwnableOperable>> +    initialize(_operator: address) <<initializer>> <<LiquidityProviderController>> +    postDepositHook(liquidityProvider: address, assets: uint256) <<LiquidityProviderController>> +    setLiquidityProviderCaps(_liquidityProviders: address[], cap: uint256) <<onlyOperatorOrOwner>> <<LiquidityProviderController>> +    setTotalAssetsCap(_totalAssetsCap: uint248) <<onlyOperatorOrOwner>> <<LiquidityProviderController>> +    setAccountCapEnabled(_accountCapEnabled: bool) <<onlyOwner>> <<LiquidityProviderController>> +Public: +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> +    <<event>> LiquidityProviderCap(liquidityProvider: address, cap: uint256) <<LiquidityProviderController>> +    <<event>> TotalAssetsCap(cap: uint256) <<LiquidityProviderController>> +    <<event>> AccountCapEnabled(enabled: bool) <<LiquidityProviderController>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_arm: address) <<LiquidityProviderController>> + + + diff --git a/docs/generate.sh b/docs/generate.sh index 4589e56..9ca11e9 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -20,3 +20,6 @@ sol2uml storage ../src/contracts,../lib -c LidoARM -o LidoARMStorage.svg \ -sn eip1967.proxy.implementation,eip1967.proxy.admin \ -st address,address \ --hideExpand gap,_gap + +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LiquidityProviderController -o LiquidityProviderControllerHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b LiquidityProviderController -o LiquidityProviderControllerSquashed.svg diff --git a/src/contracts/README.md b/src/contracts/README.md index d421790..d25dce4 100644 --- a/src/contracts/README.md +++ b/src/contracts/README.md @@ -4,11 +4,11 @@ ![Proxy Hierarchy](../../docs/ProxyHierarchy.svg) -## Proxy Squashed +## Squashed ![Proxy Squashed](../../docs/ProxySquashed.svg) -## Proxy Storage +## Storage ![Proxy Storage](../../docs/ProxyStorage.svg) @@ -18,11 +18,11 @@ ![OETH ARM Hierarchy](../../docs/OEthARMHierarchy.svg) -## OETH ARM Squashed +## Squashed ![OETH ARM Squashed](../../docs/OEthARMSquashed.svg) -## OETH ARM Storage +## Storage ![OETH ARM Storage](../../docs/OEthARMStorage.svg) @@ -32,10 +32,20 @@ ![Lido ARM Hierarchy](../../docs/LidoARMHierarchy.svg) -## OETH ARM Squashed +## Squashed ![Lido ARM Squashed](../../docs/LidoARMSquashed.svg) + +## Liquidity Provider Controller + +### Hierarchy + +![Liquidity Provider Controller Hierarchy](../../docs/LiquidityProviderControllerHierarchy.svg) + +## Squashed + +![Liquidity Provider Controller Squashed](../../docs/LiquidityProviderControllerSquashed.svg) From 1653927094a73337a64e01a11fa6cd2bac337508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 27 Sep 2024 16:09:34 +0200 Subject: [PATCH 097/196] feat: return true when running test against tenderly testnet. --- script/deploy/AbstractDeployScript.sol | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/script/deploy/AbstractDeployScript.sol b/script/deploy/AbstractDeployScript.sol index 4fa9a6b..8a1b9be 100644 --- a/script/deploy/AbstractDeployScript.sol +++ b/script/deploy/AbstractDeployScript.sol @@ -40,8 +40,19 @@ abstract contract AbstractDeployScript is Script { deployedContracts[name] = addr; } - function isForked() public view returns (bool) { - return vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.TestGroup); + function isForked() public returns (bool) { + return isRcpUrlTestnet() || vm.isContext(VmSafe.ForgeContext.ScriptDryRun) + || vm.isContext(VmSafe.ForgeContext.TestGroup); + } + + /// @notice Detect if the RPC URL is a tendrly testnet, by trying to call a specific tenderly method on rpc. + /// @dev if the call success, it means we are on a tenderly testnet, otherwise we arn't. + function isRcpUrlTestnet() public returns (bool) { + try vm.rpc("tenderly_setBalance", "[[\"0x000000000000000000000000000000000000000b\"], \"0x0\"]") { + return true; + } catch { + return false; + } } function setUp() external virtual {} From 28482b2474aeb904fe998b546ac414a35d9bf6f3 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 30 Sep 2024 17:17:40 +1000 Subject: [PATCH 098/196] Added parseDeployedAddress --- src/js/utils/addressParser.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/js/utils/addressParser.js b/src/js/utils/addressParser.js index 810c6c3..ef80dd3 100644 --- a/src/js/utils/addressParser.js +++ b/src/js/utils/addressParser.js @@ -3,6 +3,33 @@ const { readFileSync } = require("fs"); const log = require("./logger")("utils:addressParser"); +const parseDeployedAddress = async (name) => { + const network = await ethers.provider.getNetwork(); + const chainId = network.chainId; + + const fileName = `./build/deployments-${chainId}.json`; + log(`Parsing deployed contract ${name} from ${fileName}.`); + try { + const data = await readFileSync(fileName, "utf-8"); + + // Parse the JSON data + const deploymentData = JSON.parse(data); + + if (!deploymentData?.contracts[name]) { + throw new Error(`Failed to find deployed address for ${name}.`); + } + + return deploymentData.contracts[name]; + } catch (err) { + throw new Error( + `Failed to parse deployed contract "${name}" from "${fileName}".`, + { + cause: err, + } + ); + } +}; + // Parse an address from the Solidity Addresses file const parseAddress = async (name) => { // parse from Addresses.sol file @@ -57,4 +84,5 @@ const parseAddress = async (name) => { module.exports = { parseAddress, + parseDeployedAddress, }; From 7abcc1df65c20abebd9d00a111885dfbae2e680f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 30 Sep 2024 17:18:16 +1000 Subject: [PATCH 099/196] Updated postDeploy Hardhat task --- .../mainnet/003_UpgradeLidoARMScript.sol | 2 ++ src/js/tasks/tasks.js | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 9f8ae83..72bebee 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -118,6 +118,8 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); + console.log("Finished running initializing Lido ARM as ARM_MULTISIG"); + vm.stopPrank(); } } diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 895bef6..2304e34 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -1,7 +1,10 @@ const { subtask, task, types } = require("hardhat/config"); const { formatUnits, parseUnits } = require("ethers"); -const { parseAddress } = require("../utils/addressParser"); +const { + parseAddress, + parseDeployedAddress, +} = require("../utils/addressParser"); const { setAutotaskVars } = require("./autotask"); const { setActionVars } = require("./defender"); const { @@ -457,16 +460,18 @@ subtask( const wethAddress = await parseAddress("WETH"); const stethAddress = await parseAddress("STETH"); - const lidoArmAddress = await parseAddress("LIDO_ARM"); + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoArmImpl = parseDeployedAddress("LIDO_ARM_IMPL"); const relayerAddress = await parseAddress("ARM_RELAYER"); + const liquidityProviderController = await parseDeployedAddress( + "LIDO_ARM_LPC" + ); + const feeCollector = await parseAddress("ARM_BUYBACK"); const weth = await ethers.getContractAt("IWETH", wethAddress); const steth = await ethers.getContractAt("IWETH", stethAddress); const legacyAMM = await ethers.getContractAt("LegacyAMM", lidoArmAddress); - const lidoARM = await ethers.getContractAt( - "LidoFixedPriceMultiLpARM", - lidoArmAddress - ); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); const lidoProxy = await ethers.getContractAt("Proxy", lidoArmAddress); const wethBalance = await weth.balanceOf(lidoArmAddress); @@ -495,20 +500,18 @@ subtask( "ARM-ST", relayerAddress, 1500, // 15% performance fee - // TODO set to Buyback contract - relayerAddress, - "0x187FfF686a5f42ACaaF56469FcCF8e6Feca18248", + liquidityProviderController, + feeCollector, ] ); - const lidoArmImpl = "0x3d724176c8f1F965eF4020CB5DA5ad1a891BEEf1"; console.log(`Amount to upgradeToAndCall the Lido ARM`); await lidoProxy.connect(signer).upgradeToAndCall(lidoArmImpl, initData); console.log(`Amount to setPrices on the Lido ARM`); await lidoARM .connect(signer) - .setPrices(parseUnits("9994", 32), parseUnits("1", 36)); + .setPrices(parseUnits("9994", 32), parseUnits("9999", 32)); console.log(`Amount to setOwner on the Lido ARM`); await lidoProxy.connect(signer).setOwner(await parseAddress("GOV_MULTISIG")); From 3940168f3897d8bebc134e505834a52f5226135f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 30 Sep 2024 19:57:23 +1000 Subject: [PATCH 100/196] WIP Broadcast fork section of deploy script --- Makefile | 4 +-- script/deploy/AbstractDeployScript.sol | 31 ++++++++++++++----- .../mainnet/003_UpgradeLidoARMScript.sol | 16 ++++++++-- src/contracts/utils/Addresses.sol | 8 +---- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index b8f7787..ec06ab5 100644 --- a/Makefile +++ b/Makefile @@ -56,10 +56,10 @@ deploy: @forge script script/deploy/DeployManager.sol --rpc-url $(PROVIDER_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvv deploy-testnet: - @forge script script/deploy/DeployManager.sol --rpc-url $(TESTNET_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow -vvvv + @forge script script/deploy/DeployManager.sol --rpc-url $(TESTNET_URL) --broadcast --slow -vv deploy-holesky: - @forge script script/deploy/DeployManager.sol --rpc-url $(HOLESKY_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvv + @forge script script/deploy/DeployManager.sol --rpc-url $(HOLESKY_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvv # Override default `test` and `coverage` targets .PHONY: test coverage diff --git a/script/deploy/AbstractDeployScript.sol b/script/deploy/AbstractDeployScript.sol index 8a1b9be..a5c9e38 100644 --- a/script/deploy/AbstractDeployScript.sol +++ b/script/deploy/AbstractDeployScript.sol @@ -15,6 +15,8 @@ abstract contract AbstractDeployScript is Script { address deployer; uint256 public deployBlockNum = type(uint256).max; + bool public tenderlyTestnet; + // DeployerRecord stuff to be extracted as well struct DeployRecord { string name; @@ -41,21 +43,25 @@ abstract contract AbstractDeployScript is Script { } function isForked() public returns (bool) { - return isRcpUrlTestnet() || vm.isContext(VmSafe.ForgeContext.ScriptDryRun) + return isTenderlyRpc() || vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.TestGroup); } /// @notice Detect if the RPC URL is a tendrly testnet, by trying to call a specific tenderly method on rpc. /// @dev if the call success, it means we are on a tenderly testnet, otherwise we arn't. - function isRcpUrlTestnet() public returns (bool) { - try vm.rpc("tenderly_setBalance", "[[\"0x000000000000000000000000000000000000000b\"], \"0x0\"]") { + function isTenderlyRpc() public returns (bool) { + // Try to give ethers to "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf" which is the address for pk = 0x0....01 + try vm.rpc("tenderly_setBalance", "[[\"0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf\"], \"0xDE0B6B3A7640000\"]") { + tenderlyTestnet = true; return true; } catch { return false; } } - function setUp() external virtual {} + function setUp() external virtual { + isTenderlyRpc(); + } function run() external { // Will not execute script if after this block number @@ -65,9 +71,14 @@ abstract contract AbstractDeployScript is Script { } if (this.isForked()) { - deployer = Mainnet.INITIAL_DEPLOYER; - console.log("Running script on mainnet fork impersonating: %s", deployer); - vm.startPrank(deployer); + deployer = vm.rememberKey(uint256(1)); + if (tenderlyTestnet) { + console.log("Deploying on Tenderly testnet with deployer: %s", deployer); + vm.startBroadcast(deployer); + } else { + console.log("Running script on mainnet fork impersonating: %s", deployer); + vm.startPrank(deployer); + } } else { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); deployer = vm.rememberKey(deployerPrivateKey); @@ -78,7 +89,11 @@ abstract contract AbstractDeployScript is Script { _execute(); if (this.isForked()) { - vm.stopPrank(); + if (tenderlyTestnet) { + vm.stopBroadcast(); + } else { + vm.stopPrank(); + } _buildGovernanceProposal(); _fork(); } else { diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 72bebee..620a654 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -59,6 +59,8 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // 7. Transfer ownership of LiquidityProviderController to the mainnet 5/8 multisig lpcProxy.setOwner(Mainnet.GOV_MULTISIG); + console.log("Finished deploying", DEPLOY_NAME); + // Post deploy // 1. The Lido ARM multisig needs to set the owner to the mainnet 5/8 multisig // 1. The mainnet 5/8 multisig needs to upgrade and call initialize on the Lido ARM @@ -68,7 +70,13 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { function _buildGovernanceProposal() internal override {} function _fork() internal override { - vm.startPrank(Mainnet.ARM_MULTISIG); + if (tenderlyTestnet) { + console.log("Broadcasting fork script to Tenderly as: %s", Mainnet.ARM_MULTISIG); + vm.startBroadcast(Mainnet.ARM_MULTISIG); + } else { + console.log("Executing fork script against a fork as: %s", Mainnet.ARM_MULTISIG); + vm.startPrank(Mainnet.ARM_MULTISIG); + } if (lidoARMProxy == Proxy(0x0000000000000000000000000000000000000000)) { revert("Lido ARM proxy not found"); @@ -120,6 +128,10 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("Finished running initializing Lido ARM as ARM_MULTISIG"); - vm.stopPrank(); + if (tenderlyTestnet) { + vm.stopBroadcast(); + } else { + vm.stopPrank(); + } } } diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index 4d45187..c716131 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -13,10 +13,9 @@ library Mainnet { address public constant GOVERNOR_FIVE = 0x3cdD07c16614059e66344a7b579DAB4f9516C0b6; address public constant GOVERNOR_SIX = 0x1D3Fbd4d129Ddd2372EA85c5Fa00b2682081c9EC; address public constant STRATEGIST = 0xF14BBdf064E3F67f51cd9BD646aE3716aD938FDC; - address public constant TREASURY = 0x0000000000000000000000000000000000000001; + address public constant TREASURY = 0x6E3fddab68Bf1EBaf9daCF9F7907c7Bc0951D1dc; // Multisig and EOAs - address public constant INITIAL_DEPLOYER = address(0x1001); address public constant GOV_MULTISIG = 0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899; address public constant ARM_MULTISIG = 0xC8F2cF4742C86295653f893214725813B16f7410; address public constant OETH_RELAYER = 0x4b91827516f79d6F6a1F292eD99671663b09169a; @@ -40,7 +39,6 @@ library Mainnet { library Holesky { // Multisig and EOAs - address public constant INITIAL_DEPLOYER = 0x1b94CA50D3Ad9f8368851F8526132272d1a5028C; address public constant RELAYER = 0x3C6B0c7835a2E2E0A45889F64DcE4ee14c1D5CB4; // Tokens @@ -84,7 +82,6 @@ contract AddressResolver { resolver[MAINNET]["LIDO_ARM"] = Mainnet.LIDO_ARM; // Test accounts - resolver[MAINNET]["INITIAL_DEPLOYER"] = address(0x1001); resolver[MAINNET]["WHALE_OETH"] = 0x8E02247D3eE0E6153495c971FFd45Aa131f4D7cB; ///// Holesky ////// @@ -99,9 +96,6 @@ contract AddressResolver { // Contracts resolver[HOLESKY]["OETH_VAULT"] = Holesky.OETH_VAULT; resolver[HOLESKY]["OETH_ARM"] = Mainnet.OETH_ARM; - - // Test accounts - resolver[HOLESKY]["INITIAL_DEPLOYER"] = Holesky.INITIAL_DEPLOYER; } function resolve(string memory name) public view returns (address resolved) { From c4c0c8612d5a84af300dd72f5990f81e9ee86536 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 30 Sep 2024 19:59:25 +1000 Subject: [PATCH 101/196] Added liquidity provider Hardhat tasks --- src/js/tasks/liquidityProvider.js | 66 +++++++++++++++++++++++++++++++ src/js/tasks/tasks.js | 49 +++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/js/tasks/liquidityProvider.js diff --git a/src/js/tasks/liquidityProvider.js b/src/js/tasks/liquidityProvider.js new file mode 100644 index 0000000..8bae8b4 --- /dev/null +++ b/src/js/tasks/liquidityProvider.js @@ -0,0 +1,66 @@ +const { parseUnits } = require("ethers"); + +const { getSigner } = require("../utils/signers"); +const { parseDeployedAddress } = require("../utils/addressParser"); +const { logTxDetails } = require("../utils/txLogger"); + +const log = require("../utils/logger")("task:lpCap"); + +async function lpDeposit({ amount }) { + const signer = await getSigner(); + + const amountBn = parseUnits(amount.toString()); + + const lidArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidArmAddress); + + log(`About to deposit ${amount} WETH to the Lido ARM`); + const tx = await lidoARM.connect(signer).deposit(amountBn); + await logTxDetails(tx, "deposit"); +} + +async function setLiquidityProviderCaps({ accounts, cap }) { + const signer = await getSigner(); + + const capBn = parseUnits(cap.toString()); + + const liquidityProviders = accounts.split(","); + + const lpcAddress = await parseDeployedAddress("LIDO_ARM_LPC"); + const liquidityProviderController = await ethers.getContractAt( + "LiquidityProviderController", + lpcAddress + ); + + log( + `About to set deposit cap of ${cap} WETH for liquidity providers ${liquidityProviders}` + ); + const tx = await liquidityProviderController + .connect(signer) + .setLiquidityProviderCaps(liquidityProviders, capBn); + await logTxDetails(tx, "setLiquidityProviderCaps"); +} + +async function setTotalAssetsCap({ cap }) { + const signer = await getSigner(); + + const capBn = parseUnits(cap.toString()); + + const lpcAddress = await parseDeployedAddress("LIDO_ARM_LPC"); + const liquidityProviderController = await ethers.getContractAt( + "LiquidityProviderController", + lpcAddress + ); + + log(`About to set total asset cap of ${cap} WETH`); + const tx = await liquidityProviderController + .connect(signer) + .setTotalAssetsCap(capBn); + await logTxDetails(tx, "setTotalAssetsCap"); +} + +module.exports = { + lpDeposit, + setLiquidityProviderCaps, + setTotalAssetsCap, +}; diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 2304e34..46138a4 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -15,6 +15,11 @@ const { logLiquidity, withdrawRequestStatus, } = require("./liquidity"); +const { + lpDeposit, + setLiquidityProviderCaps, + setTotalAssetsCap, +} = require("./liquidityProvider"); const { swap } = require("./swap"); const { tokenAllowance, @@ -422,6 +427,50 @@ task("redeemAll").setAction(async (_, __, runSuper) => { return runSuper(); }); +// ARM Liquidity Provider Functions + +subtask("lpDeposit", "Set total assets cap") + .addParam( + "amount", + "Amount of WETH not scaled to 18 decimals", + undefined, + types.float + ) + .setAction(lpDeposit); +task("lpDeposit").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("setLiquidityProviderCaps", "Set deposit cap for liquidity providers") + .addParam( + "cap", + "Amount of WETH not scaled to 18 decimals", + undefined, + types.float + ) + .addParam( + "accounts", + "Comma separated list of addresses", + undefined, + types.string + ) + .setAction(setLiquidityProviderCaps); +task("setLiquidityProviderCaps").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("setTotalAssetsCap", "Set total assets cap") + .addParam( + "cap", + "Amount of WETH not scaled to 18 decimals", + undefined, + types.float + ) + .setAction(setTotalAssetsCap); +task("setTotalAssetsCap").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + // Proxies subtask("upgradeProxy", "Upgrade a proxy contract to a new implementation") From 35915a6ac5abec4f7ff174750a6bd3edccb1b0ad Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 1 Oct 2024 20:04:44 +1000 Subject: [PATCH 102/196] Fixed setting of token0 and token1 in LidoARM (#27) * Restore deploy script so fork section uses prank * Fix hardhat postDeploy task * Added HH task to snap Lido ARM * Add Hardhat tasks snapLido, swapLido, submitLido Renamed the depositLido task * Added Deposit event LidoARM was setting token0 and token1 the wrong way around Used PRICE_SCALE in setPrices Improved smoke tests * Generated latest LidoARM contract diagram --- docs/LidoARMSquashed.svg | 185 +++++++++--------- .../mainnet/003_UpgradeLidoARMScript.sol | 15 +- src/contracts/AbstractARM.sol | 21 +- src/contracts/Interfaces.sol | 7 + src/contracts/LidoARM.sol | 4 +- src/js/tasks/lido.js | 126 ++++++++++++ src/js/tasks/liquidity.js | 11 +- src/js/tasks/liquidityProvider.js | 4 +- src/js/tasks/tasks.js | 89 ++++++++- src/js/utils/block.js | 8 +- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 12 +- .../SwapExactTokensForTokens.t.sol | 86 ++++---- .../SwapTokensForExactTokens.t.sol | 101 ++++++---- test/smoke/LidoARMSmokeTest.t.sol | 102 +++++++--- 14 files changed, 523 insertions(+), 248 deletions(-) create mode 100644 src/js/tasks/lido.js diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 387be2f..7d4280a 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,100 +4,101 @@ - - + + UmlClassDiagram - - + + -13 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[42] <<AbstractARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> -   DEAD_ACCOUNT: address <<AbstractARM>> -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: uint128 <<AbstractARM>> -   withdrawsClaimed: uint128 <<AbstractARM>> -   withdrawsClaimable: uint128 <<AbstractARM>> -   nextWithdrawalIndex: uint128 <<AbstractARM>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> -   feeCollector: address <<AbstractARM>> -   fee: uint16 <<AbstractARM>> -   feesAccrued: uint112 <<AbstractARM>> -   lastTotalAssets: uint128 <<AbstractARM>> -   liquidityProviderController: address <<AbstractARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<AbstractARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> -    _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>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<AbstractARM>> -    _updateWithdrawalQueueLiquidity() <<AbstractARM>> -    _liquidityAvailable(): uint256 <<AbstractARM>> -    _rawTotalAssets(): uint256 <<AbstractARM>> -    _externalWithdrawQueue(): uint256 <<LidoARM>> -    _accruePerformanceFee() <<AbstractARM>> -    _setFee(_fee: uint256) <<AbstractARM>> -    _setFeeCollector(_feeCollector: address) <<AbstractARM>> -    _initLidoLiquidityManager() <<LidoLiquidityManager>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    collectFees(): (fees: uint256) <<AbstractARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<AbstractARM>> +14 + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[42] <<AbstractARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> +   DEAD_ACCOUNT: address <<AbstractARM>> +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: uint128 <<AbstractARM>> +   withdrawsClaimed: uint128 <<AbstractARM>> +   withdrawsClaimable: uint128 <<AbstractARM>> +   nextWithdrawalIndex: uint128 <<AbstractARM>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> +   feeCollector: address <<AbstractARM>> +   fee: uint16 <<AbstractARM>> +   feesAccrued: uint112 <<AbstractARM>> +   lastTotalAssets: uint128 <<AbstractARM>> +   liquidityProviderController: address <<AbstractARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<AbstractARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> +    _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>> +    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<AbstractARM>> +    _updateWithdrawalQueueLiquidity() <<AbstractARM>> +    _liquidityAvailable(): uint256 <<AbstractARM>> +    _rawTotalAssets(): uint256 <<AbstractARM>> +    _externalWithdrawQueue(): uint256 <<LidoARM>> +    _accruePerformanceFee() <<AbstractARM>> +    _setFee(_fee: uint256) <<AbstractARM>> +    _setFeeCollector(_feeCollector: address) <<AbstractARM>> +    _initLidoLiquidityManager() <<LidoLiquidityManager>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>> +    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<AbstractARM>> diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 620a654..d114145 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -70,13 +70,8 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { function _buildGovernanceProposal() internal override {} function _fork() internal override { - if (tenderlyTestnet) { - console.log("Broadcasting fork script to Tenderly as: %s", Mainnet.ARM_MULTISIG); - vm.startBroadcast(Mainnet.ARM_MULTISIG); - } else { - console.log("Executing fork script against a fork as: %s", Mainnet.ARM_MULTISIG); - vm.startPrank(Mainnet.ARM_MULTISIG); - } + console.log("Executing fork script against a fork as: %s", Mainnet.ARM_MULTISIG); + vm.startPrank(Mainnet.ARM_MULTISIG); if (lidoARMProxy == Proxy(0x0000000000000000000000000000000000000000)) { revert("Lido ARM proxy not found"); @@ -128,10 +123,6 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("Finished running initializing Lido ARM as ARM_MULTISIG"); - if (tenderlyTestnet) { - vm.stopBroadcast(); - } else { - vm.stopPrank(); - } + vm.stopPrank(); } } diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 7d66390..6a08caa 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -110,6 +110,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { //////////////////////////////////////////////////// event TraderateChanged(uint256 traderate0, uint256 traderate1); + event Deposit(address indexed owner, uint256 assets, uint256 shares); event RedeemRequested( address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp ); @@ -120,12 +121,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { event FeeCollectorUpdated(address indexed newFeeCollector); event LiquidityProviderControllerUpdated(address indexed liquidityProviderController); - constructor(address _inputToken, address _outputToken1, address _liquidityAsset) { - require(IERC20(_inputToken).decimals() == 18); - require(IERC20(_outputToken1).decimals() == 18); + constructor(address _token0, address _token1, address _liquidityAsset) { + require(IERC20(_token0).decimals() == 18); + require(IERC20(_token1).decimals() == 18); - token0 = IERC20(_inputToken); - token1 = IERC20(_outputToken1); + token0 = IERC20(_token0); + token1 = IERC20(_token1); _setOwner(address(0)); // Revoke owner for implementation contract at deployment @@ -361,9 +362,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /** * @notice Set exchange rates from an operator account from the ARM's perspective. * If token 0 is WETH and token 1 is stETH, then both prices will be set using the stETH/WETH price. - * @param buyT1 The price the ARM buys Token 1 from the Trader, denominated in Token 0, scaled to 36 decimals. + * @param buyT1 The price the ARM buys Token 1 (stETH) from the Trader, denominated in Token 0 (WETH), scaled to 36 decimals. * From the Trader's perspective, this is the sell price. - * @param sellT1 The price the ARM sells Token 1 to the Trader, denominated in Token 0, scaled to 36 decimals. + * @param sellT1 The price the ARM sells Token 1 (stETH) to the Trader, denominated in Token 0 (WETH), scaled to 36 decimals. * From the Trader's perspective, this is the buy price. */ function setPrices(uint256 buyT1, uint256 sellT1) external onlyOperatorOrOwner { @@ -372,13 +373,13 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { require(sellT1 >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: sell price too low"); require(buyT1 <= PRICE_SCALE + MAX_PRICE_DEVIATION, "ARM: buy price too high"); } - uint256 _traderate0 = 1e72 / sellT1; // base (t0) -> token (t1) + uint256 _traderate0 = PRICE_SCALE * PRICE_SCALE / sellT1; // base (t0) -> token (t1) uint256 _traderate1 = buyT1; // token (t1) -> base (t0) _setTraderates(_traderate0, _traderate1); } function _setTraderates(uint256 _traderate0, uint256 _traderate1) internal { - require((1e72 / (_traderate0)) > _traderate1, "ARM: Price cross"); + require((PRICE_SCALE * PRICE_SCALE / (_traderate0)) > _traderate1, "ARM: Price cross"); traderate0 = _traderate0; traderate1 = _traderate1; @@ -422,6 +423,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { if (liquidityProviderController != address(0)) { ILiquidityProviderController(liquidityProviderController).postDepositHook(msg.sender, assets); } + + emit Deposit(msg.sender, assets, shares); } /// @notice Preview the amount of assets that would be received for burning a given amount of shares diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 701be44..705f72d 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -200,6 +200,13 @@ interface IWETH is IERC20 { function withdraw(uint256 wad) external; } +interface ISTETH is IERC20 { + event Submitted(address indexed sender, uint256 amount, address referral); + + // function() external payable; + function submit(address _referral) external payable returns (uint256); +} + interface IStETHWithdrawal { event WithdrawalRequested( uint256 indexed requestId, diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index b2aeed5..cd9e368 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -20,7 +20,7 @@ contract LidoARM is Initializable, AbstractARM, LidoLiquidityManager { /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract constructor(address _steth, address _weth, address _lidoWithdrawalQueue) - AbstractARM(_steth, _weth, _weth) + AbstractARM(_weth, _steth, _weth) LidoLiquidityManager(_steth, _weth, _lidoWithdrawalQueue) {} @@ -54,7 +54,7 @@ contract LidoARM is Initializable, AbstractARM, LidoLiquidityManager { */ function _transferAsset(address asset, address to, uint256 amount) internal override { // Add 2 wei if transferring stETH - uint256 transferAmount = asset == address(token0) ? amount + 2 : amount; + uint256 transferAmount = asset == address(token1) ? amount + 2 : amount; super._transferAsset(asset, to, transferAmount); } diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js new file mode 100644 index 0000000..fad6e07 --- /dev/null +++ b/src/js/tasks/lido.js @@ -0,0 +1,126 @@ +const { formatUnits, parseUnits, MaxInt256 } = require("ethers"); + +const { getBlock } = require("../utils/block"); +const { getSigner } = require("../utils/signers"); +const { logTxDetails } = require("../utils/txLogger"); +const { parseAddress } = require("../utils/addressParser"); +const { resolveAddress, resolveAsset } = require("../utils/assets"); + +const log = require("../utils/logger")("task:lido"); + +const submitLido = async ({ amount }) => { + const signer = await getSigner(); + + const stethAddress = await parseAddress("STETH"); + // const steth = await ethers.getContractAt("ISTETH", stethAddress); + + const etherAmount = parseUnits(amount.toString()); + + log(`About to send ${amount} ETH to Lido's stETH`); + const tx = await signer.sendTransaction({ + to: stethAddress, + value: etherAmount, + }); + // const tx = await steth.connect(signer)({ value: etherAmount }); + await logTxDetails(tx, "submit"); +}; + +const snapLido = async ({ block }) => { + const blockTag = await getBlock(block); + console.log(`\nLiquidity`); + + const armAddress = await parseAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", armAddress); + + const weth = await resolveAsset("WETH"); + const liquidityWeth = await weth.balanceOf(armAddress, { blockTag }); + + const steth = await resolveAsset("STETH"); + const liquiditySteth = await steth.balanceOf(armAddress, { blockTag }); + const liquidityLidoWithdraws = await lidoARM.outstandingEther({ blockTag }); + + const total = liquidityWeth + liquiditySteth + liquidityLidoWithdraws; + const wethPercent = total == 0 ? 0 : (liquidityWeth * 10000n) / total; + const stethWithdrawsPercent = + total == 0 ? 0 : (liquidityLidoWithdraws * 10000n) / total; + const oethPercent = total == 0 ? 0 : (liquiditySteth * 10000n) / total; + + console.log( + `${formatUnits(liquidityWeth, 18)} WETH ${formatUnits(wethPercent, 2)}%` + ); + console.log( + `${formatUnits(liquiditySteth, 18)} stETH ${formatUnits(oethPercent, 2)}%` + ); + console.log( + `${formatUnits( + liquidityLidoWithdraws, + 18 + )} Lido withdrawal requests ${formatUnits(stethWithdrawsPercent, 2)}%` + ); + console.log(`${formatUnits(total, 18)} total WETH and stETH`); +}; + +const swapLido = async ({ from, to, amount }) => { + if (from && to) { + throw new Error( + `Cannot specify both from and to asset. It has to be one or the other` + ); + } + const signer = await getSigner(); + const signerAddress = await signer.getAddress(); + + const armAddress = await parseAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", armAddress); + + if (from) { + const fromAddress = await resolveAddress(from.toUpperCase()); + + const to = from === "stETH" ? "WETH" : "stETH"; + const toAddress = await resolveAddress(to.toUpperCase()); + + const fromAmount = parseUnits(amount.toString(), 18); + + log(`About to swap ${amount} ${from} to ${to} for ${signerAddress}`); + + const tx = await lidoARM + .connect(signer) + ["swapExactTokensForTokens(address,address,uint256,uint256,address)"]( + fromAddress, + toAddress, + fromAmount, + 0, + signerAddress + ); + + await logTxDetails(tx, "swap exact from"); + } else if (to) { + const from = to === "stETH" ? "WETH" : "stETH"; + const fromAddress = await resolveAddress(from.toUpperCase()); + + const toAddress = await resolveAddress(to.toUpperCase()); + + const toAmount = parseUnits(amount.toString(), 18); + + log(`About to swap ${from} to ${amount} ${to} for ${signerAddress}`); + + const tx = await lidoARM + .connect(signer) + ["swapTokensForExactTokens(address,address,uint256,uint256,address)"]( + fromAddress, + toAddress, + toAmount, + MaxInt256, + signerAddress + ); + + await logTxDetails(tx, "swap exact to"); + } else { + throw new Error(`Must specify either from or to asset`); + } +}; + +module.exports = { + submitLido, + swapLido, + snapLido, +}; diff --git a/src/js/tasks/liquidity.js b/src/js/tasks/liquidity.js index fea79b2..a663f1b 100644 --- a/src/js/tasks/liquidity.js +++ b/src/js/tasks/liquidity.js @@ -1,5 +1,6 @@ const { formatUnits, parseUnits } = require("ethers"); +const { getBlock } = require("../utils/block"); const { parseAddress } = require("../utils/addressParser"); const { resolveAsset } = require("../utils/assets"); const { @@ -100,19 +101,19 @@ const withdrawRequestStatus = async ({ id, oethARM, vault }) => { } }; -const logLiquidity = async () => { +const logLiquidity = async ({ block }) => { + const blockTag = await getBlock(block); console.log(`\nLiquidity`); const oethArmAddress = await parseAddress("OETH_ARM"); - const oethARM = await ethers.getContractAt("OethARM", oethArmAddress); const weth = await resolveAsset("WETH"); - const liquidityWeth = await weth.balanceOf(await oethARM.getAddress()); + const liquidityWeth = await weth.balanceOf(oethArmAddress, { blockTag }); const oeth = await resolveAsset("OETH"); - const liquidityOeth = await oeth.balanceOf(await oethARM.getAddress()); + const liquidityOeth = await oeth.balanceOf(oethArmAddress, { blockTag }); const liquidityOethWithdraws = await outstandingWithdrawalAmount({ - withdrawer: await oethARM.getAddress(), + withdrawer: oethArmAddress, }); const total = liquidityWeth + liquidityOeth + liquidityOethWithdraws; diff --git a/src/js/tasks/liquidityProvider.js b/src/js/tasks/liquidityProvider.js index 8bae8b4..ed36477 100644 --- a/src/js/tasks/liquidityProvider.js +++ b/src/js/tasks/liquidityProvider.js @@ -6,7 +6,7 @@ const { logTxDetails } = require("../utils/txLogger"); const log = require("../utils/logger")("task:lpCap"); -async function lpDeposit({ amount }) { +async function depositLido({ amount }) { const signer = await getSigner(); const amountBn = parseUnits(amount.toString()); @@ -60,7 +60,7 @@ async function setTotalAssetsCap({ cap }) { } module.exports = { - lpDeposit, + depositLido, setLiquidityProviderCaps, setTotalAssetsCap, }; diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 46138a4..901c44a 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -7,6 +7,7 @@ const { } = require("../utils/addressParser"); const { setAutotaskVars } = require("./autotask"); const { setActionVars } = require("./defender"); +const { submitLido, snapLido, swapLido } = require("./lido"); const { autoRequestWithdraw, autoClaimWithdraw, @@ -16,7 +17,7 @@ const { withdrawRequestStatus, } = require("./liquidity"); const { - lpDeposit, + depositLido, setLiquidityProviderCaps, setTotalAssetsCap, } = require("./liquidityProvider"); @@ -42,17 +43,34 @@ const { } = require("./vault"); const { upgradeProxy } = require("./proxy"); -subtask("snap", "Take a snapshot of the ARM").setAction(logLiquidity); +subtask("snap", "Take a snapshot of the OETH ARM") + .addOptionalParam( + "block", + "Block number. (default: latest)", + undefined, + types.int + ) + .setAction(logLiquidity); task("snap").setAction(async (_, __, runSuper) => { return runSuper(); }); subtask( "swap", - "Swap from one asset to another. Can only specify the from or to asset" + "Swap from one asset to another. Can only specify the from or to asset as that will be the exact amount." ) - .addOptionalParam("from", "Symbol of the from asset", "OETH", types.string) - .addOptionalParam("to", "Symbol of the to asset", undefined, types.string) + .addOptionalParam( + "from", + "Symbol of the from asset when swapping from an exact amount", + "OETH", + types.string + ) + .addOptionalParam( + "to", + "Symbol of the to asset when swapping to an exact amount", + undefined, + types.string + ) .addParam( "amount", "Swap quantity in either the from or to asset", @@ -64,6 +82,33 @@ task("swap").setAction(async (_, __, runSuper) => { return runSuper(); }); +subtask( + "swapLido", + "Swap from one asset to another. Can only specify the from or to asset as that will be the exact amount." +) + .addOptionalParam( + "from", + "Symbol of the from asset when swapping from an exact amount", + undefined, + types.string + ) + .addOptionalParam( + "to", + "Symbol of the to asset when swapping to an exact amount", + undefined, + types.string + ) + .addParam( + "amount", + "Swap quantity in either the from or to asset", + undefined, + types.float + ) + .setAction(swapLido); +task("swapLido").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + // Liquidity management subtask("autoRequestWithdraw", "Request withdrawal of WETH from the OETH Vault") @@ -313,6 +358,15 @@ task("withdrawWETH").setAction(async (_, __, runSuper) => { return runSuper(); }); +// Lido tasks + +subtask("submitLido", "Convert ETH to Lido's stETH") + .addParam("amount", "Amount of ETH to convert", undefined, types.float) + .setAction(submitLido); +task("submitLido").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + // Vault tasks. task( @@ -429,15 +483,18 @@ task("redeemAll").setAction(async (_, __, runSuper) => { // ARM Liquidity Provider Functions -subtask("lpDeposit", "Set total assets cap") +subtask( + "depositLido", + "Deposit WETH into the Lido ARM as receive ARM LP tokens" +) .addParam( "amount", "Amount of WETH not scaled to 18 decimals", undefined, types.float ) - .setAction(lpDeposit); -task("lpDeposit").setAction(async (_, __, runSuper) => { + .setAction(depositLido); +task("depositLido").setAction(async (_, __, runSuper) => { return runSuper(); }); @@ -471,6 +528,20 @@ task("setTotalAssetsCap").setAction(async (_, __, runSuper) => { return runSuper(); }); +// Lido + +subtask("snapLido", "Take a snapshot of the Lido ARM") + .addOptionalParam( + "block", + "Block number. (default: latest)", + undefined, + types.int + ) + .setAction(snapLido); +task("snapLido").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + // Proxies subtask("upgradeProxy", "Upgrade a proxy contract to a new implementation") @@ -549,8 +620,8 @@ subtask( "ARM-ST", relayerAddress, 1500, // 15% performance fee - liquidityProviderController, feeCollector, + liquidityProviderController, ] ); diff --git a/src/js/utils/block.js b/src/js/utils/block.js index 433ab07..a334c67 100644 --- a/src/js/utils/block.js +++ b/src/js/utils/block.js @@ -1,11 +1,11 @@ const log = require("../utils/logger")("task:block"); -// Get the block details like number and timestamp +// Get the block number const getBlock = async (block) => { - const blockDetails = await hre.ethers.provider.getBlock(block); - log(`block: ${blockDetails.number}`); + const blockTag = !block ? await hre.ethers.provider.getBlockNumber() : block; + log(`block: ${blockTag}`); - return blockDetails; + return blockTag; }; const logBlock = async (blockTag) => { diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 569e353..dbbb7ad 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -87,7 +87,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { } ////////////////////////////////////////////////////// - /// --- FIXED PRICE ARM - REVERTING TESTS + /// --- Set Prices - REVERTING TESTS ////////////////////////////////////////////////////// function test_RevertWhen_SetPrices_Because_PriceCross() public { vm.expectRevert("ARM: Price cross"); @@ -104,7 +104,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setPrices(1e36, 1e36); } - function test_RevertWhen_FixedPriceARM_SetPrices_Because_PriceRange() public asOperator { + function test_RevertWhen_SetPrices_Because_PriceRange() public asOperator { // buy price 11 basis points higher than 1.0 vm.expectRevert("ARM: buy price too high"); lidoARM.setPrices(1.0011 * 1e36, 1.002 * 1e36); @@ -118,7 +118,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setPrices(1e18, 1e18); } - function test_RevertWhen_FixedPriceARM_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { + function test_RevertWhen_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); lidoARM.setPrices(0, 0); } @@ -134,9 +134,9 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { } ////////////////////////////////////////////////////// - /// --- FIXED PRICE ARM - PASSING TESTS + /// --- Set Prices - PASSING TESTS ////////////////////////////////////////////////////// - function test_FixedPriceARM_SetPrices_Operator() public asOperator { + function test_SetPrices_Operator() public asOperator { // buy price 10 basis points higher than 1.0 lidoARM.setPrices(1001e33, 1002e33); // sell price 10 basis points lower than 1.0 @@ -153,7 +153,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.traderate1(), 992 * 1e33); } - function test_FixedPriceARM_SetPrices_Owner() public { + function test_SetPrices_Owner() public { // buy price 11 basis points higher than 1.0 lidoARM.setPrices(10011e32, 10020e32); diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index 03a169c..543d6f0 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -215,9 +215,9 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); - // Get minimum amount of STETH to receive - uint256 traderates1 = lidoARM.traderate1(); - uint256 minAmount = amountIn * traderates1 / 1e36; + // Get minimum amount of stETH to receive + uint256 traderates0 = lidoARM.traderate0(); + uint256 minAmount = amountIn * traderates0 / 1e36; // Expected events: Already checked in fuzz tests @@ -238,12 +238,16 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions - assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); - assertApproxEqAbs(balanceSTETHBeforeThis + minAmount, balanceSTETHAfterThis, STETH_ERROR_ROUNDING); - assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM); - assertApproxEqAbs(balanceSTETHBeforeARM, balanceSTETHAfterARM + minAmount, STETH_ERROR_ROUNDING); - assertEq(outputs[0], amountIn); - assertEq(outputs[1], minAmount); + assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn, "user WETH balance"); + assertApproxEqAbs( + balanceSTETHBeforeThis + minAmount, balanceSTETHAfterThis, STETH_ERROR_ROUNDING, "user stETH balance" + ); + assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM, "ARM WETH balance"); + assertApproxEqAbs( + balanceSTETHBeforeARM, balanceSTETHAfterARM + minAmount, STETH_ERROR_ROUNDING, "ARM stETH balance" + ); + assertEq(outputs[0], amountIn, "amount in"); + assertEq(outputs[1], minAmount, "amount out"); } function test_SwapExactTokensForTokens_WithDeadLine_Steth_To_Weth() public { @@ -259,8 +263,8 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoARM.traderate0(); - uint256 minAmount = amountIn * traderates0 / 1e36; + uint256 traderates1 = lidoARM.traderate1(); + uint256 minAmount = amountIn * traderates1 / 1e36; // Expected events: Already checked in fuzz tests @@ -299,14 +303,14 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test function test_SwapExactTokensForTokens_Weth_To_Steth(uint256 amountIn, uint256 stethReserve, uint256 price) public { - // Use random price between 0.98 and 1 for traderate1, - // Traderate0 value doesn't matter as it is not used in this test. - price = _bound(price, MIN_PRICE0, MAX_PRICE0); - lidoARM.setPrices(price, MAX_PRICE1); + // Use random stETH/WETH sell price between 0.98 and 1, + // the buy price doesn't matter as it is not used in this test. + price = _bound(price, MIN_PRICE1, MAX_PRICE1); + lidoARM.setPrices(MIN_PRICE0, price); // Set random amount of stETH in the ARM stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoARM), stethReserve); + deal(address(steth), address(lidoARM), stethReserve + (2 * STETH_ERROR_ROUNDING)); // Calculate maximum amount of WETH to swap // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. @@ -319,21 +323,24 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); - // Get minimum amount of STETH to receive - uint256 traderates1 = lidoARM.traderate1(); - uint256 minAmount = amountIn * traderates1 / 1e36; + // Get minimum amount of stETH to receive + // stETH = WETH / price + uint256 amountOutMin = + amountIn > STETH_ERROR_ROUNDING ? amountIn * 1e36 / price - STETH_ERROR_ROUNDING : amountIn * 1e36 / price; // Expected events vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(this), address(lidoARM), amountIn); - vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoARM), address(this), minAmount + STETH_ERROR_ROUNDING); + // TODO hard to get the exact amount of stETH transferred as it depends on the rounding + // vm.expectEmit({emitter: address(steth)}); + // emit IERC20.Transfer(address(lidoARM), address(this), amountOutMin); + // Main call lidoARM.swapExactTokensForTokens( weth, // inToken steth, // outToken amountIn, // amountIn - minAmount, // amountOutMin + amountOutMin, // amountOutMin address(this) // to ); @@ -344,10 +351,14 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions - assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); - assertApproxEqAbs(balanceSTETHBeforeThis + minAmount, balanceSTETHAfterThis, STETH_ERROR_ROUNDING); - assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM); - assertApproxEqAbs(balanceSTETHBeforeARM, balanceSTETHAfterARM + minAmount, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn, "user WETH balance"); + assertApproxEqAbs( + balanceSTETHBeforeThis + amountOutMin, balanceSTETHAfterThis, STETH_ERROR_ROUNDING * 2, "user stETH balance" + ); + assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM, "ARM WETH balance"); + assertApproxEqAbs( + balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOutMin, STETH_ERROR_ROUNDING * 2, "ARM stETH balance" + ); } /// @notice Fuzz test for swapExactTokensForTokens(IERC20,IERC20,uint256,uint256,address), with stETH to WETH. @@ -355,10 +366,10 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test /// @param wethReserve Amount of WETH in the ARM. Fuzzed between 0 and MAX_WETH_RESERVE. /// @param price Price of the stETH in WETH. Fuzzed between 1 and 1.02. function test_SwapExactTokensForTokens_Steth_To_Weth(uint256 amountIn, uint256 wethReserve, uint256 price) public { - // Use random price between MIN_PRICE1 and MAX_PRICE1 for traderate1, - // Traderate0 value doesn't matter as it is not used in this test. - price = _bound(price, MIN_PRICE1, MAX_PRICE1); - lidoARM.setPrices(MIN_PRICE0, price); + // Use random stETH/WETH buy price between MIN_PRICE0 and MAX_PRICE0, + // the sell price doesn't matter as it is not used in this test. + price = _bound(price, MIN_PRICE0, MAX_PRICE0); + lidoARM.setPrices(price, MAX_PRICE1); // Set random amount of WETH in the ARM wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); @@ -376,8 +387,7 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoARM.traderate0(); - uint256 minAmount = amountIn * traderates0 / 1e36; + uint256 minAmount = amountIn * price / 1e36; // Expected events vm.expectEmit({emitter: address(steth)}); @@ -401,9 +411,13 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions - assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis); - assertApproxEqAbs(balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING); - assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + minAmount); - assertApproxEqAbs(balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis, "user WETH balance"); + assertApproxEqAbs( + balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING, "user stETH balance" + ); + assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + minAmount, "ARM WETH balance"); + assertApproxEqAbs( + balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING, "ARM stETH balance" + ); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol index 28eb17f..6541bb5 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -189,9 +189,9 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); - // Get minimum amount of STETH to receive - uint256 traderates1 = lidoARM.traderate1(); - uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; + // Get maximum amount of WETH to send to the ARM + uint256 traderates0 = lidoARM.traderate0(); + uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; // Expected events: Already checked in fuzz tests @@ -212,12 +212,16 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions - assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); - assertApproxEqAbs(balanceSTETHBeforeThis + amountOut, balanceSTETHAfterThis, STETH_ERROR_ROUNDING); - assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM); - assertApproxEqAbs(balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOut, STETH_ERROR_ROUNDING); - assertEq(outputs[0], amountIn); - assertEq(outputs[1], amountOut); + assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn, "WETH user balance"); + assertApproxEqAbs( + balanceSTETHBeforeThis + amountOut, balanceSTETHAfterThis, STETH_ERROR_ROUNDING, "STETH user balance" + ); + assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM, "WETH ARM balance"); + assertApproxEqAbs( + balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOut, STETH_ERROR_ROUNDING, "STETH ARM balance" + ); + assertEq(outputs[0], amountIn, "Amount in"); + assertEq(outputs[1], amountOut, "Amount out"); } function test_SwapTokensForExactTokens_WithDeadLine_Steth_To_Weth() public { @@ -232,9 +236,9 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); - // Get minimum amount of WETH to receive - uint256 traderates0 = lidoARM.traderate0(); - uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; + // Get maximum amount of stETH to send to the ARM + uint256 traderates1 = lidoARM.traderate1(); + uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; // Expected events: Already checked in fuzz tests @@ -255,12 +259,16 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions - assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis); - assertApproxEqAbs(balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING); - assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + amountOut); - assertApproxEqAbs(balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING); - assertEq(outputs[0], amountIn); - assertEq(outputs[1], amountOut); + assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis, "WETH user balance"); + assertApproxEqAbs( + balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING, "STETH user balance" + ); + assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + amountOut, "WETH ARM balance"); + assertApproxEqAbs( + balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING, "STETH ARM balance" + ); + assertEq(outputs[0], amountIn, "Amount in"); + assertEq(outputs[1], amountOut, "Amount out"); } ////////////////////////////////////////////////////// @@ -273,10 +281,10 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test function test_SwapTokensForExactTokens_Weth_To_Steth(uint256 amountOut, uint256 stethReserve, uint256 price) public { - // Use random price between 0.98 and 1 for traderate1, - // Traderate0 value doesn't matter as it is not used in this test. - price = _bound(price, MIN_PRICE0, MAX_PRICE0); - lidoARM.setPrices(price, MAX_PRICE1); + // Use random sell price between 0.98 and 1 for the stETH/WETH price, + // The buy price doesn't matter as it is not used in this test. + price = _bound(price, MIN_PRICE1, MAX_PRICE1); + lidoARM.setPrices(MIN_PRICE0, price); // Set random amount of stETH in the ARM stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); @@ -294,14 +302,14 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of STETH to receive - uint256 traderates1 = lidoARM.traderate1(); - uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; + // weth = steth * stETH/WETH price + uint256 amountIn = (amountOut * price / 1e36) + 1; // Expected events vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoARM), address(this), amountOut + STETH_ERROR_ROUNDING); + emit IERC20.Transfer(address(lidoARM), address(this), amountOut + 2); // Main call lidoARM.swapTokensForExactTokens( weth, // inToken @@ -318,10 +326,14 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions - assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn); - assertApproxEqAbs(balanceSTETHBeforeThis + amountOut, balanceSTETHAfterThis, STETH_ERROR_ROUNDING); - assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM); - assertApproxEqAbs(balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOut, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn, "WETH user balance"); + assertApproxEqAbs( + balanceSTETHBeforeThis + amountOut, balanceSTETHAfterThis, STETH_ERROR_ROUNDING, "STETH user balance" + ); + assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM, "WETH ARM balance"); + assertApproxEqAbs( + balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOut, STETH_ERROR_ROUNDING, "STETH ARM balance" + ); } /// @notice Fuzz test for swapTokensForExactTokens(IERC20,IERC20,uint256,uint256,address), with stETH to WETH. @@ -331,10 +343,10 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test function test_SwapTokensForExactTokens_Steth_To_Weth(uint256 amountOut, uint256 wethReserve, uint256 price) public { - // Use random price between MIN_PRICE1 and MAX_PRICE1 for traderate1, - // Traderate0 value doesn't matter as it is not used in this test. - price = _bound(price, MIN_PRICE1, MAX_PRICE1); - lidoARM.setPrices(MIN_PRICE0, price); + // Use random stETH/WETH buy price between 0.98 and 1, + // sell price doesn't matter as it is not used in this test. + price = _bound(price, MIN_PRICE0, MAX_PRICE0); + lidoARM.setPrices(price, MAX_PRICE1); // Set random amount of WETH in the ARM wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); @@ -352,12 +364,13 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); // Get minimum amount of WETH to receive - uint256 traderates0 = lidoARM.traderate0(); - uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; + // stETH = WETH / stETH/WETH price + uint256 amountIn = (amountOut * 1e36 / price); // Expected events - vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(this), address(lidoARM), amountIn); + // TODO hard to check the exact amount of stETH due to rounding + // vm.expectEmit({emitter: address(steth)}); + // emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(lidoARM), address(this), amountOut); @@ -366,7 +379,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test steth, // inToken weth, // outToken amountOut, // amountOut - amountIn, // amountInMax + amountIn + 2 * STETH_ERROR_ROUNDING, // amountInMax address(this) // to ); @@ -377,9 +390,13 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); // Assertions - assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis); - assertApproxEqAbs(balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING); - assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + amountOut); - assertApproxEqAbs(balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING); + assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis, "WETH user balance"); + assertApproxEqAbs( + balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING, "STETH user balance" + ); + assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + amountOut, "WETH ARM balance"); + assertApproxEqAbs( + balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING, "STETH ARM balance" + ); } } diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index e57ca38..33d69c9 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -46,13 +46,6 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(lidoARM.operator(), Mainnet.ARM_RELAYER, "Operator"); assertEq(lidoARM.feeCollector(), Mainnet.ARM_BUYBACK, "Fee collector"); assertEq((100 * uint256(lidoARM.fee())) / lidoARM.FEE_SCALE(), 15, "Performance fee as a percentage"); - assertEq(lidoARM.feesAccrued(), 0, "Fees accrued"); - // Some dust stETH is left in AMM v1 when stETH is transferred to the Treasury. - assertEq(steth.balanceOf(address(lidoARM)), 1, "stETH balance"); - assertEq(lidoARM.totalAssets(), 1e12 + 1, "Total assets"); - assertEq(lidoARM.lastTotalAssets(), 1e12 + 1, "Last total assets"); - assertEq(lidoARM.totalSupply(), 1e12, "Total supply"); - assertEq(weth.balanceOf(address(lidoARM)), 1e12, "WETH balance"); // LidoLiquidityManager assertEq(address(lidoARM.withdrawalQueue()), Mainnet.LIDO_WITHDRAWAL, "Lido withdrawal queue"); assertEq(address(lidoARM.steth()), Mainnet.STETH, "stETH"); @@ -64,23 +57,59 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(liquidityProviderController.arm(), address(lidoARM), "arm"); } - function test_swapExactTokensForTokens() external { - _swapExactTokensForTokens(steth, weth, 10 ether, 10 ether); + function test_swap_exact_steth_for_weth() external { + // trader sells stETH and buys WETH, the ARM buys stETH as a + // 4 bps discount + _swapExactTokensForTokens(steth, weth, 9996e32, 100 ether); + // 10 bps discount + _swapExactTokensForTokens(steth, weth, 9990e32, 1e15); + // 20 bps discount + _swapExactTokensForTokens(steth, weth, 9980e32, 1 ether); + } + + function test_swap_exact_weth_for_steth() external { + // trader buys stETH and sells WETH, the ARM sells stETH at a + // 1 bps discount + _swapExactTokensForTokens(weth, steth, 9999e32, 10 ether); + // 5 bps discount + _swapExactTokensForTokens(weth, steth, 9995e32, 100 ether); + // 10 bps discount + _swapExactTokensForTokens(weth, steth, 9990e32, 100 ether); } function test_swapTokensForExactTokens() external { - _swapTokensForExactTokens(steth, weth, 10 ether, 10 ether); + // trader sells stETH and buys WETH, the ARM buys stETH at a + // 4 bps discount + _swapTokensForExactTokens(steth, weth, 9996e32, 10 ether); + // 10 bps discount + _swapTokensForExactTokens(steth, weth, 9990e32, 100 ether); + // 50 bps discount + _swapTokensForExactTokens(steth, weth, 9950e32, 10 ether); } - function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - _dealWETH(address(lidoARM), 100 ether); - _dealStETH(address(lidoARM), 100 ether); + function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 price, uint256 amountIn) internal { + uint256 expectedOut; if (inToken == weth) { - _dealWETH(address(this), amountIn + 1000); + // Trader is buying stETH and selling WETH + // the ARM is selling stETH and buying WETH + _dealWETH(address(this), 1000 ether); + _dealStETH(address(lidoARM), 1000 ether); + + expectedOut = amountIn * 1e36 / price; + + vm.prank(Mainnet.ARM_RELAYER); + lidoARM.setPrices(price - 2e32, price); } else { - _dealStETH(address(this), amountIn + 1000); + // Trader is selling stETH and buying WETH + // the ARM is buying stETH and selling WETH + _dealStETH(address(this), 1000 ether); + _dealWETH(address(lidoARM), 1000 ether); + + expectedOut = amountIn * price / 1e36; + + vm.prank(Mainnet.ARM_RELAYER); + uint256 sellPrice = price < 9988e32 ? 9990e32 : price + 2e32; + lidoARM.setPrices(price, sellPrice); } // Approve the ARM to transfer the input token of the swap. inToken.approve(address(lidoARM), amountIn); @@ -91,30 +120,45 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { lidoARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); assertApproxEqAbs(inToken.balanceOf(address(this)), startIn - amountIn, 2, "In actual"); - assertEq(outToken.balanceOf(address(this)), startOut + expectedOut, "Out actual"); + assertApproxEqAbs(outToken.balanceOf(address(this)), startOut + expectedOut, 2, "Out actual"); } - function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - _dealWETH(address(lidoARM), 100 ether); - _dealStETH(address(lidoARM), 100 ether); + function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 price, uint256 amountOut) internal { + uint256 expectedIn; if (inToken == weth) { - _dealWETH(address(this), amountIn + 1000); + // Trader is buying stETH and selling WETH + // the ARM is selling stETH and buying WETH + _dealWETH(address(this), 1000 ether); + _dealStETH(address(lidoARM), 1000 ether); + + expectedIn = amountOut * price / 1e36; + + vm.prank(Mainnet.ARM_RELAYER); + lidoARM.setPrices(price - 2e32, price); } else { - _dealStETH(address(this), amountIn + 1000); + // Trader is selling stETH and buying WETH + // the ARM is buying stETH and selling WETH + _dealStETH(address(this), 1000 ether); + _dealWETH(address(lidoARM), 1000 ether); + + expectedIn = amountOut * 1e36 / price; + + vm.prank(Mainnet.ARM_RELAYER); + uint256 sellPrice = price < 9988e32 ? 9990e32 : price + 2e32; + lidoARM.setPrices(price, sellPrice); } // Approve the ARM to transfer the input token of the swap. - inToken.approve(address(lidoARM), amountIn + 10000); + inToken.approve(address(lidoARM), expectedIn + 10000); console.log("Approved Lido ARM to spend %d", inToken.allowance(address(this), address(lidoARM))); console.log("In token balance: %d", inToken.balanceOf(address(this))); uint256 startIn = inToken.balanceOf(address(this)); + uint256 startOut = outToken.balanceOf(address(this)); - lidoARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); + lidoARM.swapTokensForExactTokens(inToken, outToken, amountOut, 3 * amountOut, address(this)); - assertApproxEqAbs(inToken.balanceOf(address(this)), startIn - amountIn, 2, "In actual"); - assertEq(outToken.balanceOf(address(this)), expectedOut, "Out actual"); + assertApproxEqAbs(inToken.balanceOf(address(this)), startIn - expectedIn, 2, "In actual"); + assertApproxEqAbs(outToken.balanceOf(address(this)), startOut + amountOut, 2, "Out actual"); } function test_proxy_unauthorizedAccess() external { From 8ceb827bc729975720a97c8602c092f0c483165e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:09:51 +0200 Subject: [PATCH 103/196] Handler Tenderly testnet (#26) * fix: handler better tenderly testnet * fix: use -unlocked keywork to broadcast & impersonate * feat: add initial deployer test address. * fix: give enough gas to deployer for Tenderly test. --------- Co-authored-by: Nicholas Addison --- Makefile | 2 +- script/deploy/AbstractDeployScript.sol | 20 ++++++++++++------- .../mainnet/003_UpgradeLidoARMScript.sol | 14 ++++++++++--- src/contracts/utils/Addresses.sol | 6 ++++++ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index ec06ab5..cdcaa82 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ deploy: @forge script script/deploy/DeployManager.sol --rpc-url $(PROVIDER_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvv deploy-testnet: - @forge script script/deploy/DeployManager.sol --rpc-url $(TESTNET_URL) --broadcast --slow -vv + @forge script script/deploy/DeployManager.sol --rpc-url $(TESTNET_URL) --broadcast --slow --unlocked -vvvv deploy-holesky: @forge script script/deploy/DeployManager.sol --rpc-url $(HOLESKY_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvv diff --git a/script/deploy/AbstractDeployScript.sol b/script/deploy/AbstractDeployScript.sol index a5c9e38..effa442 100644 --- a/script/deploy/AbstractDeployScript.sol +++ b/script/deploy/AbstractDeployScript.sol @@ -42,16 +42,16 @@ abstract contract AbstractDeployScript is Script { deployedContracts[name] = addr; } - function isForked() public returns (bool) { - return isTenderlyRpc() || vm.isContext(VmSafe.ForgeContext.ScriptDryRun) + function isForked() public view returns (bool) { + return tenderlyTestnet || vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.TestGroup); } /// @notice Detect if the RPC URL is a tendrly testnet, by trying to call a specific tenderly method on rpc. /// @dev if the call success, it means we are on a tenderly testnet, otherwise we arn't. function isTenderlyRpc() public returns (bool) { - // Try to give ethers to "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf" which is the address for pk = 0x0....01 - try vm.rpc("tenderly_setBalance", "[[\"0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf\"], \"0xDE0B6B3A7640000\"]") { + // Try to give ethers to "ARM_MULTISIG" + try vm.rpc("tenderly_setBalance", "[[\"0xC8F2cF4742C86295653f893214725813B16f7410\"], \"0xDE0B6B3A7640000\"]") { tenderlyTestnet = true; return true; } catch { @@ -71,8 +71,12 @@ abstract contract AbstractDeployScript is Script { } if (this.isForked()) { - deployer = vm.rememberKey(uint256(1)); + deployer = Mainnet.INITIAL_DEPLOYER; if (tenderlyTestnet) { + // Give enough ethers to deployer + vm.rpc( + "tenderly_setBalance", "[[\"0x0000000000000000000000000000000000001001\"], \"0xDE0B6B3A7640000\"]" + ); console.log("Deploying on Tenderly testnet with deployer: %s", deployer); vm.startBroadcast(deployer); } else { @@ -90,12 +94,14 @@ abstract contract AbstractDeployScript is Script { if (this.isForked()) { if (tenderlyTestnet) { + _buildGovernanceProposal(); vm.stopBroadcast(); + _fork(); } else { vm.stopPrank(); + _buildGovernanceProposal(); + _fork(); } - _buildGovernanceProposal(); - _fork(); } else { vm.stopBroadcast(); } diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index d114145..dc66778 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -70,8 +70,12 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { function _buildGovernanceProposal() internal override {} function _fork() internal override { - console.log("Executing fork script against a fork as: %s", Mainnet.ARM_MULTISIG); - vm.startPrank(Mainnet.ARM_MULTISIG); + if (tenderlyTestnet) { + vm.startBroadcast(Mainnet.ARM_MULTISIG); + } else { + console.log("Broadcasting fork script to Tenderly as: %s", Mainnet.ARM_MULTISIG); + vm.startBroadcast(Mainnet.ARM_MULTISIG); + } if (lidoARMProxy == Proxy(0x0000000000000000000000000000000000000000)) { revert("Lido ARM proxy not found"); @@ -123,6 +127,10 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("Finished running initializing Lido ARM as ARM_MULTISIG"); - vm.stopPrank(); + if (tenderlyTestnet) { + vm.stopBroadcast(); + } else { + vm.stopPrank(); + } } } diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index c716131..c610ab0 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -16,6 +16,7 @@ library Mainnet { address public constant TREASURY = 0x6E3fddab68Bf1EBaf9daCF9F7907c7Bc0951D1dc; // Multisig and EOAs + address public constant INITIAL_DEPLOYER = address(0x1001); address public constant GOV_MULTISIG = 0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899; address public constant ARM_MULTISIG = 0xC8F2cF4742C86295653f893214725813B16f7410; address public constant OETH_RELAYER = 0x4b91827516f79d6F6a1F292eD99671663b09169a; @@ -39,6 +40,7 @@ library Mainnet { library Holesky { // Multisig and EOAs + address public constant INITIAL_DEPLOYER = 0x1b94CA50D3Ad9f8368851F8526132272d1a5028C; address public constant RELAYER = 0x3C6B0c7835a2E2E0A45889F64DcE4ee14c1D5CB4; // Tokens @@ -82,6 +84,7 @@ contract AddressResolver { resolver[MAINNET]["LIDO_ARM"] = Mainnet.LIDO_ARM; // Test accounts + resolver[MAINNET]["INITIAL_DEPLOYER"] = address(0x1001); resolver[MAINNET]["WHALE_OETH"] = 0x8E02247D3eE0E6153495c971FFd45Aa131f4D7cB; ///// Holesky ////// @@ -96,6 +99,9 @@ contract AddressResolver { // Contracts resolver[HOLESKY]["OETH_VAULT"] = Holesky.OETH_VAULT; resolver[HOLESKY]["OETH_ARM"] = Mainnet.OETH_ARM; + + // Test accounts + resolver[HOLESKY]["INITIAL_DEPLOYER"] = Holesky.INITIAL_DEPLOYER; } function resolve(string memory name) public view returns (address resolved) { From 8420809b25d93c05b31318eaff0b5b73e0cd1a1c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 1 Oct 2024 21:53:19 +1000 Subject: [PATCH 104/196] Fixed ARM acronym in Natspec --- src/contracts/LidoARM.sol | 2 +- src/contracts/LiquidityProviderController.sol | 2 +- src/contracts/OethARM.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index cd9e368..8d8019d 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -8,7 +8,7 @@ import {AbstractARM} from "./AbstractARM.sol"; import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; /** - * @title Lido (stETH) Application Redemption Manager (ARM) + * @title Lido (stETH) Automated Redemption Manager (ARM) * @dev This implementation supports multiple Liquidity Providers (LPs) with single buy and sell prices. * It also integrates to a LiquidityProviderController contract that caps the amount of assets a liquidity provider * can deposit and caps the ARM's total assets. diff --git a/src/contracts/LiquidityProviderController.sol b/src/contracts/LiquidityProviderController.sol index d9755e9..90dc9bc 100644 --- a/src/contracts/LiquidityProviderController.sol +++ b/src/contracts/LiquidityProviderController.sol @@ -11,7 +11,7 @@ import {ILiquidityProviderARM} from "./Interfaces.sol"; * @author Origin Protocol Inc */ contract LiquidityProviderController is Initializable, OwnableOperable { - /// @notice The address of the linked Application Redemption Manager (ARM). + /// @notice The address of the linked Automated Redemption Manager (ARM). address public immutable arm; /// @notice true if a cap is placed on each liquidity provider's account. diff --git a/src/contracts/OethARM.sol b/src/contracts/OethARM.sol index 2d546ac..5782ac9 100644 --- a/src/contracts/OethARM.sol +++ b/src/contracts/OethARM.sol @@ -9,7 +9,7 @@ import {OwnerLP} from "./OwnerLP.sol"; import {OethLiquidityManager} from "./OethLiquidityManager.sol"; /** - * @title Origin Ether (OETH) Application Redemption Manager (ARM) + * @title Origin Ether (OETH) Automated Redemption Manager (ARM) * @author Origin Protocol Inc */ contract OethARM is Initializable, OwnerLP, PeggedARM, OethLiquidityManager { From 39086afc1eb468332e0ed9922dde3a3bb7cad508 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 1 Oct 2024 21:54:16 +1000 Subject: [PATCH 105/196] Removed postDeploy Hardhat task now the forge script is working --- src/js/tasks/tasks.js | 74 +------------------------------------------ 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 901c44a..3ea16e0 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -1,10 +1,6 @@ const { subtask, task, types } = require("hardhat/config"); -const { formatUnits, parseUnits } = require("ethers"); -const { - parseAddress, - parseDeployedAddress, -} = require("../utils/addressParser"); +const { parseAddress } = require("../utils/addressParser"); const { setAutotaskVars } = require("./autotask"); const { setActionVars } = require("./defender"); const { submitLido, snapLido, swapLido } = require("./lido"); @@ -571,71 +567,3 @@ subtask( task("setActionVars").setAction(async (_, __, runSuper) => { return runSuper(); }); - -subtask( - "postDeploy", - "Used for Testnets after running the Lido deploy script" -).setAction(async () => { - const signer = await getSigner(); - - const wethAddress = await parseAddress("WETH"); - const stethAddress = await parseAddress("STETH"); - const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); - const lidoArmImpl = parseDeployedAddress("LIDO_ARM_IMPL"); - const relayerAddress = await parseAddress("ARM_RELAYER"); - const liquidityProviderController = await parseDeployedAddress( - "LIDO_ARM_LPC" - ); - const feeCollector = await parseAddress("ARM_BUYBACK"); - - const weth = await ethers.getContractAt("IWETH", wethAddress); - const steth = await ethers.getContractAt("IWETH", stethAddress); - const legacyAMM = await ethers.getContractAt("LegacyAMM", lidoArmAddress); - const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); - const lidoProxy = await ethers.getContractAt("Proxy", lidoArmAddress); - - const wethBalance = await weth.balanceOf(lidoArmAddress); - console.log( - `Amount to transfer ${formatUnits(wethBalance)} WETH out of the LidoARM` - ); - await legacyAMM - .connect(signer) - .transferToken(wethAddress, await signer.getAddress(), wethBalance); - - const stethBalance = await steth.balanceOf(lidoArmAddress); - console.log( - `Amount to transfer ${formatUnits(stethBalance)} stETH out of the LidoARM` - ); - await legacyAMM - .connect(signer) - .transferToken(stethAddress, await signer.getAddress(), stethBalance); - - console.log(`Amount to approve the Lido ARM`); - await weth.connect(signer).approve(lidoArmAddress, "1000000000000"); - - const initData = lidoARM.interface.encodeFunctionData( - "initialize(string,string,address,uint256,address,address)", - [ - "Lido ARM", - "ARM-ST", - relayerAddress, - 1500, // 15% performance fee - feeCollector, - liquidityProviderController, - ] - ); - - console.log(`Amount to upgradeToAndCall the Lido ARM`); - await lidoProxy.connect(signer).upgradeToAndCall(lidoArmImpl, initData); - - console.log(`Amount to setPrices on the Lido ARM`); - await lidoARM - .connect(signer) - .setPrices(parseUnits("9994", 32), parseUnits("9999", 32)); - - console.log(`Amount to setOwner on the Lido ARM`); - await lidoProxy.connect(signer).setOwner(await parseAddress("GOV_MULTISIG")); -}); -task("postDeploy").setAction(async (_, __, runSuper) => { - return runSuper(); -}); From d53a75ccd63aeb40ce5565e681f6a66bc07b2cb4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 2 Oct 2024 09:40:52 +1000 Subject: [PATCH 106/196] Simplified deposit to WETH --- src/contracts/LidoLiquidityManager.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 5b27093..324a694 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -77,8 +77,7 @@ abstract contract LidoLiquidityManager is OwnableOperable { outstandingEther -= etherAfter - etherBefore; // Wrap all the received ETH to WETH. - (bool success,) = address(weth).call{value: etherAfter}(new bytes(0)); - require(success, "ARM: ETH transfer failed"); + weth.deposit{value: etherAfter}(); } function _externalWithdrawQueue() internal view virtual returns (uint256 assets) { From 9f999fa27a51454433c99ec3f8cfb84238a5ece0 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 2 Oct 2024 10:01:58 +1000 Subject: [PATCH 107/196] Fixed deploy script when run as a fork --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index dc66778..3c2fb45 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -71,10 +71,10 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { function _fork() internal override { if (tenderlyTestnet) { - vm.startBroadcast(Mainnet.ARM_MULTISIG); - } else { console.log("Broadcasting fork script to Tenderly as: %s", Mainnet.ARM_MULTISIG); vm.startBroadcast(Mainnet.ARM_MULTISIG); + } else { + vm.startPrank(Mainnet.ARM_MULTISIG); } if (lidoARMProxy == Proxy(0x0000000000000000000000000000000000000000)) { From 27416603170a0f2e5c9063ec22c9439336f46994 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 2 Oct 2024 11:48:48 +1000 Subject: [PATCH 108/196] Removed approveStETH as its in the initializer --- src/contracts/LidoLiquidityManager.sol | 7 ------- .../ClaimStETHWithdrawalForWETH.t.sol | 2 -- .../fork/LidoFixedPriceMultiLpARM/Deposit.t.sol | 1 - .../RequestStETHWithdrawalForETH.t.sol | 12 ++++-------- .../LidoFixedPriceMultiLpARM/TotalAssets.t.sol | 3 --- test/fork/utils/Modifiers.sol | 17 ----------------- 6 files changed, 4 insertions(+), 38 deletions(-) diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol index 324a694..168f40f 100644 --- a/src/contracts/LidoLiquidityManager.sol +++ b/src/contracts/LidoLiquidityManager.sol @@ -30,13 +30,6 @@ abstract contract LidoLiquidityManager is OwnableOperable { steth.approve(address(withdrawalQueue), type(uint256).max); } - /** - * @notice Approve the stETH withdrawal contract. Used for redemption requests. - */ - function approveStETH() external onlyOperatorOrOwner { - steth.approve(address(withdrawalQueue), type(uint256).max); - } - /** * @notice Request a stETH for ETH withdrawal. * Reference: https://docs.lido.fi/contracts/withdrawal-queue-erc721/ diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol index 5c7cec1..7ece103 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol @@ -55,7 +55,6 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared function test_ClaimStETHWithdrawalForWETH_SingleRequest() public asOperator - approveStETHOnLidoARM requestStETHWithdrawalForETHOnLidoARM(amounts1) mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT) { @@ -77,7 +76,6 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared function test_ClaimStETHWithdrawalForWETH_MultiRequest() public asOperator - approveStETHOnLidoARM requestStETHWithdrawalForETHOnLidoARM(amounts2) mockCallLidoFindCheckpointHints mockFunctionClaimWithdrawOnLidoARM(amounts2[0] + amounts2[1]) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 8bbebb0..90aa0eb 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -373,7 +373,6 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { function test_Deposit_WithOutStandingWithdrawRequest_BeforeDeposit_ClaimedLidoWithdraw_WithAssetGain() public deal_(address(steth), address(lidoARM), DEFAULT_AMOUNT) - approveStETHOnLidoARM requestStETHWithdrawalForETHOnLidoARM(amounts1) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol index 3c71723..1452ce4 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol @@ -25,11 +25,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared lidoARM.requestStETHWithdrawalForETH(new uint256[](0)); } - function test_RevertWhen_RequestStETHWithdrawalForETH_Because_BalanceExceeded() - public - asOperator - approveStETHOnLidoARM - { + function test_RevertWhen_RequestStETHWithdrawalForETH_Because_BalanceExceeded() public asOperator { // Remove all stETH from the contract deal(address(steth), address(lidoARM), 0); @@ -48,7 +44,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared assertEq(requestIds.length, 0); } - function test_RequestStETHWithdrawalForETH_SingleAmount_1ether() public asOperator approveStETHOnLidoARM { + function test_RequestStETHWithdrawalForETH_SingleAmount_1ether() public asOperator { uint256[] memory amounts = new uint256[](1); amounts[0] = DEFAULT_AMOUNT; @@ -63,7 +59,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared assertGt(requestIds[0], 0); } - function test_RequestStETHWithdrawalForETH_SingleAmount_1000ethers() public asOperator approveStETHOnLidoARM { + function test_RequestStETHWithdrawalForETH_SingleAmount_1000ethers() public asOperator { uint256[] memory amounts = new uint256[](1); amounts[0] = 1_000 ether; @@ -78,7 +74,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared assertGt(requestIds[0], 0); } - function test_RequestStETHWithdrawalForETH_MultipleAmount() public asOperator approveStETHOnLidoARM { + function test_RequestStETHWithdrawalForETH_MultipleAmount() public asOperator { uint256 length = _bound(vm.randomUint(), 2, 10); uint256[] memory amounts = new uint256[](length); for (uint256 i = 0; i < amounts.length; i++) { diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index 04c7a93..02bd8ef 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -24,9 +24,6 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { liquidityProviderController.setLiquidityProviderCaps(providers, type(uint256).max); liquidityProviderController.setTotalAssetsCap(type(uint248).max); - // Approve STETH for Lido - lidoARM.approveStETH(); - deal(address(weth), address(this), 1_000 ether); weth.approve(address(lidoARM), type(uint256).max); } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index ce64605..a4b408d 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -169,23 +169,6 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Approve stETH on LidoARM contract. - modifier approveStETHOnLidoARM() { - // Todo: extend this logic to other modifier if needed - (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); - vm.stopPrank(); - - vm.prank(lidoARM.owner()); - lidoARM.approveStETH(); - - if (mode == VmSafe.CallerMode.Prank) { - vm.prank(_address, _origin); - } else if (mode == VmSafe.CallerMode.RecurrentPrank) { - vm.startPrank(_address, _origin); - } - _; - } - /// @notice Request stETH withdrawal for ETH on LidoARM contract. modifier requestStETHWithdrawalForETHOnLidoARM(uint256[] memory amounts) { // Todo: extend this logic to other modifier if needed From 745bf57af0ced5ba0eed24ac2c5d73cbe309acd3 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 2 Oct 2024 12:01:32 +1000 Subject: [PATCH 109/196] changes to setPrices to make it easier to understand --- src/contracts/AbstractARM.sol | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 6a08caa..4182b81 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -50,15 +50,18 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { * @notice For one `token0` from a Trader, how many `token1` does the pool send. * For example, if `token0` is WETH and `token1` is stETH then * `traderate0` is the WETH/stETH price. - * From a Trader's perspective, this is the stETH/WETH buy price. + * From a Trader's perspective, this is the buy price. + * From the ARM's perspective, this is the sell price. * Rate is to 36 decimals (1e36). + * To convert to a stETH/WETH price, use `PRICE_SCALE * PRICE_SCALE / traderate0`. */ uint256 public traderate0; /** * @notice For one `token1` from a Trader, how many `token0` does the pool send. * For example, if `token0` is WETH and `token1` is stETH then * `traderate1` is the stETH/WETH price. - * From a Trader's perspective, this is the stETH/WETH sell price. + * From a Trader's perspective, this is the sell price. + * From a ARM's perspective, this is the buy price. * Rate is to 36 decimals (1e36). */ uint256 public traderate1; @@ -373,17 +376,18 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { require(sellT1 >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: sell price too low"); require(buyT1 <= PRICE_SCALE + MAX_PRICE_DEVIATION, "ARM: buy price too high"); } - uint256 _traderate0 = PRICE_SCALE * PRICE_SCALE / sellT1; // base (t0) -> token (t1) - uint256 _traderate1 = buyT1; // token (t1) -> base (t0) - _setTraderates(_traderate0, _traderate1); + _setTraderates( + PRICE_SCALE * PRICE_SCALE / sellT1, // base (t0) -> token (t1) + buyT1 // token (t1) -> base (t0) + ); } - function _setTraderates(uint256 _traderate0, uint256 _traderate1) internal { - require((PRICE_SCALE * PRICE_SCALE / (_traderate0)) > _traderate1, "ARM: Price cross"); - traderate0 = _traderate0; - traderate1 = _traderate1; + function _setTraderates(uint256 _baseToTokenRate, uint256 _tokenToBaseRate) internal { + require((PRICE_SCALE * PRICE_SCALE / (_baseToTokenRate)) > _tokenToBaseRate, "ARM: Price cross"); + traderate0 = _baseToTokenRate; + traderate1 = _tokenToBaseRate; - emit TraderateChanged(_traderate0, _traderate1); + emit TraderateChanged(_baseToTokenRate, _tokenToBaseRate); } //////////////////////////////////////////////////// From 5545239c83bb50872b558b051d10d0698328ab00 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 2 Oct 2024 12:19:43 +1000 Subject: [PATCH 110/196] Fix bug in _rawTotalAssets not checking feesAccrued before subtraction --- src/contracts/AbstractARM.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 4182b81..b1098ea 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -583,8 +583,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { uint256 claimedMem = withdrawsClaimed; // If the ARM becomes insolvent enough that the total value in the ARM and external withdrawal queue - // is less than the outstanding withdrawals. - if (assets + claimedMem < queuedMem) { + // is less than the outstanding withdrawals and accrued fees. + if (assets + claimedMem < queuedMem + feesAccrued) { return 0; } From 47fae010df93fe2e0f758ce37721d0f398c0cb4b Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Wed, 2 Oct 2024 16:32:18 +1000 Subject: [PATCH 111/196] Renamed total assets to available assets (#28) * Renamed _rawTotalAssets to _availableAssets Renamed lastTotalAssets to lastAvailableAssets * Generated new LidoARM diagrams * Update Natspec --- docs/LidoARMPublicSquashed.svg | 59 ++++--- docs/LidoARMSquashed.svg | 165 +++++++++--------- src/contracts/AbstractARM.sol | 58 +++--- .../ClaimRedeem.t.sol | 10 +- .../Constructor.t.sol | 2 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 28 +-- .../RequestRedeem.t.sol | 12 +- 7 files changed, 167 insertions(+), 167 deletions(-) diff --git a/docs/LidoARMPublicSquashed.svg b/docs/LidoARMPublicSquashed.svg index 9b0c980..db25cb9 100644 --- a/docs/LidoARMPublicSquashed.svg +++ b/docs/LidoARMPublicSquashed.svg @@ -4,24 +4,25 @@ - - + + UmlClassDiagram - - + + -13 - -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>> +14 + +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>> @@ -34,7 +35,7 @@   feeCollector: address <<AbstractARM>>   fee: uint16 <<AbstractARM>>   feesAccrued: uint112 <<AbstractARM>> -   lastTotalAssets: uint128 <<AbstractARM>> +   lastAvailableAssets: uint128 <<AbstractARM>>   liquidityProviderController: address <<AbstractARM>>   steth: IERC20 <<LidoLiquidityManager>>   weth: IWETH <<LidoLiquidityManager>> @@ -56,18 +57,18 @@    previewRedeem(shares: uint256): (assets: uint256) <<AbstractARM>>    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<AbstractARM>>    claimRedeem(requestId: uint256): (assets: uint256) <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    collectFees(): (fees: uint256) <<AbstractARM>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<AbstractARM>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<AbstractARM>> diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 7d4280a..1a2c8bf 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,93 +4,92 @@ - - + + UmlClassDiagram - + 14 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[42] <<AbstractARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> -   DEAD_ACCOUNT: address <<AbstractARM>> -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: uint128 <<AbstractARM>> -   withdrawsClaimed: uint128 <<AbstractARM>> -   withdrawsClaimable: uint128 <<AbstractARM>> -   nextWithdrawalIndex: uint128 <<AbstractARM>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> -   feeCollector: address <<AbstractARM>> -   fee: uint16 <<AbstractARM>> -   feesAccrued: uint112 <<AbstractARM>> -   lastTotalAssets: uint128 <<AbstractARM>> -   liquidityProviderController: address <<AbstractARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<AbstractARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> -    _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>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<AbstractARM>> -    _updateWithdrawalQueueLiquidity() <<AbstractARM>> -    _liquidityAvailable(): uint256 <<AbstractARM>> -    _rawTotalAssets(): uint256 <<AbstractARM>> -    _externalWithdrawQueue(): uint256 <<LidoARM>> -    _accruePerformanceFee() <<AbstractARM>> -    _setFee(_fee: uint256) <<AbstractARM>> -    _setFeeCollector(_feeCollector: address) <<AbstractARM>> -    _initLidoLiquidityManager() <<LidoLiquidityManager>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    collectFees(): (fees: uint256) <<AbstractARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[42] <<AbstractARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> +   DEAD_ACCOUNT: address <<AbstractARM>> +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: uint128 <<AbstractARM>> +   withdrawsClaimed: uint128 <<AbstractARM>> +   withdrawsClaimable: uint128 <<AbstractARM>> +   nextWithdrawalIndex: uint128 <<AbstractARM>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> +   feeCollector: address <<AbstractARM>> +   fee: uint16 <<AbstractARM>> +   feesAccrued: uint112 <<AbstractARM>> +   lastAvailableAssets: uint128 <<AbstractARM>> +   liquidityProviderController: address <<AbstractARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<AbstractARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> +    _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>> +    _setTraderates(_baseToTokenRate: uint256, _tokenToBaseRate: uint256) <<AbstractARM>> +    _updateWithdrawalQueueLiquidity() <<AbstractARM>> +    _liquidityAvailable(): uint256 <<AbstractARM>> +    _availableAssets(): uint256 <<AbstractARM>> +    _externalWithdrawQueue(): uint256 <<LidoARM>> +    _accruePerformanceFee() <<AbstractARM>> +    _setFee(_fee: uint256) <<AbstractARM>> +    _setFeeCollector(_feeCollector: address) <<AbstractARM>> +    _initLidoLiquidityManager() <<LidoLiquidityManager>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>>    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>>    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoARM>> diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index b1098ea..632129b 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -98,11 +98,11 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// 500 = 5% performance fee uint16 public fee; /// @notice The performance fees accrued but not collected. - /// This is removed from the total assets. + /// This is removed from the available assets. uint112 public feesAccrued; - /// @notice The total assets at the last time performance fees were calculated. + /// @notice The available assets at the last time performance fees were calculated. /// This can only go up so is a high watermark. - uint128 public lastTotalAssets; + uint128 public lastAvailableAssets; address public liquidityProviderController; @@ -166,9 +166,9 @@ 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); - // Initialize the last total assets to the current total assets + // 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 - lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); + lastAvailableAssets = SafeCast.toUint128(_availableAssets()); _setFee(_fee); _setFeeCollector(_feeCollector); @@ -406,12 +406,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @param assets The amount of liquidity assets to deposit /// @return shares The amount of shares that were minted function deposit(uint256 assets) external returns (uint256 shares) { - // Accrue any performance fees based on the increase in total assets before + // Accrue any performance fees based on the increase in available assets before // the liquidity asset from the deposit is transferred into the ARM _accruePerformanceFee(); // Calculate the amount of shares to mint after the performance fees have been accrued - // which reduces the total assets and before new assets are deposited. + // which reduces the available assets, and before new assets are deposited. shares = convertToShares(assets); // Transfer the liquidity asset from the sender to this contract @@ -420,8 +420,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // mint shares _mint(msg.sender, shares); - // Save the new total assets after the performance fee accrued and new assets deposited - lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); + // Save the new available assets after the performance fee accrued and new assets deposited + lastAvailableAssets = SafeCast.toUint128(_availableAssets()); // Check the liquidity provider caps after the new assets have been deposited if (liquidityProviderController != address(0)) { @@ -443,7 +443,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @return requestId The index of the withdrawal request /// @return assets The amount of liquidity assets that will be claimable by the redeemer function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { - // Accrue any performance fees based on the increase in total assets before + // Accrue any performance fees based on the increase in available assets before // the liquidity asset from the redeem is reserved for the ARM withdrawal queue _accruePerformanceFee(); @@ -470,8 +470,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // burn redeemer's shares _burn(msg.sender, shares); - // Save the new total assets after performance fee accrued and withdrawal queue updated - lastTotalAssets = SafeCast.toUint128(_rawTotalAssets()); + // Save the new available assets after performance fee accrued and withdrawal queue updated + lastAvailableAssets = SafeCast.toUint128(_availableAssets()); emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); } @@ -559,22 +559,22 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice The total amount of assets in the ARM and external withdrawal queue, /// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees. function totalAssets() public view virtual returns (uint256) { - uint256 totalAssetsBeforeFees = _rawTotalAssets(); + uint256 availableAssetsBeforeFees = _availableAssets(); - // If the total assets have decreased, then we don't charge a performance fee - if (totalAssetsBeforeFees <= lastTotalAssets) return totalAssetsBeforeFees; + // If the available assets have decreased, then we don't charge a performance fee + if (availableAssetsBeforeFees <= lastAvailableAssets) return availableAssetsBeforeFees; // Calculate the increase in assets since the last time fees were calculated - uint256 assetIncrease = totalAssetsBeforeFees - lastTotalAssets; + uint256 assetIncrease = availableAssetsBeforeFees - lastAvailableAssets; - // Calculate the performance fee and remove from the total assets before new fees are removed - return totalAssetsBeforeFees - ((assetIncrease * fee) / FEE_SCALE); + // Calculate the performance fee and remove from the available assets before new fees are removed + return availableAssetsBeforeFees - ((assetIncrease * fee) / FEE_SCALE); } - /// @dev Calculate the total assets in the ARM, external withdrawal queue, - /// less liquidity assets reserved for the ARM's withdrawal queue and past accrued fees. + /// @dev Calculate the available assets which is the assets in the ARM, external withdrawal queue, + /// less liquidity assets reserved for the ARM's withdrawal queue and accrued fees. /// The accrued fees are from the last time fees were calculated. - function _rawTotalAssets() internal view returns (uint256) { + function _availableAssets() internal view returns (uint256) { // Get the assets in the ARM and external withdrawal queue uint256 assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); @@ -582,7 +582,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { uint256 queuedMem = withdrawsQueued; uint256 claimedMem = withdrawsClaimed; - // If the ARM becomes insolvent enough that the total value in the ARM and external withdrawal queue + // If the ARM becomes insolvent enough that the available assets in the ARM and external withdrawal queue // is less than the outstanding withdrawals and accrued fees. if (assets + claimedMem < queuedMem + feesAccrued) { return 0; @@ -620,23 +620,23 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// Performance Fee Functions //////////////////////////////////////////////////// - /// @dev Accrues the performance fee based on the increase in total assets + /// @dev Accrues the performance fee based on the increase in available assets /// Needs to be called before any action that changes the liquidity provider shares. eg deposit and redeem function _accruePerformanceFee() internal { - uint256 newTotalAssets = _rawTotalAssets(); + uint256 newAvailableAssets = _availableAssets(); - // Do not accrued a performance fee if the total assets has decreased - if (newTotalAssets <= lastTotalAssets) return; + // Do not accrued a performance fee if the available assets has decreased + if (newAvailableAssets <= lastAvailableAssets) return; - uint256 assetIncrease = newTotalAssets - lastTotalAssets; + uint256 assetIncrease = newAvailableAssets - lastAvailableAssets; uint256 newFeesAccrued = (assetIncrease * fee) / FEE_SCALE; // Save the new accrued fees back to storage feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); - // Save the new total assets back to storage less the new accrued fees. + // Save the new available assets back to storage less the new accrued fees. // This is be updated again in the post deposit and post withdraw hooks to include // the assets deposited or withdrawn - lastTotalAssets = SafeCast.toUint128(newTotalAssets - newFeesAccrued); + lastAvailableAssets = SafeCast.toUint128(newAvailableAssets - newFeesAccrued); emit FeeCalculated(newFeesAccrued, assetIncrease); } diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index fc738f4..de65cff 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -121,7 +121,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -142,7 +142,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -186,7 +186,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), 0); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -210,7 +210,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -233,7 +233,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol index debd3a3..7f050d0 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -22,7 +22,7 @@ contract Fork_Concrete_LidoARM_Constructor_Test is Fork_Shared_Test_ { assertEq(lidoARM.operator(), operator); assertEq(lidoARM.feeCollector(), feeCollector); assertEq(lidoARM.fee(), 2000); - assertEq(lidoARM.lastTotalAssets(), 1e12); + assertEq(lidoARM.lastAvailableAssets(), 1e12); assertEq(lidoARM.feesAccrued(), 0); // the 20% performance fee is removed on initialization assertEq(lidoARM.totalAssets(), 1e12); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 90aa0eb..943570c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -108,7 +108,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); @@ -131,7 +131,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.balanceOf(address(this)), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); @@ -154,7 +154,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.balanceOf(address(this)), amount); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); @@ -177,7 +177,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.balanceOf(address(this)), shares * 2); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); @@ -201,7 +201,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.balanceOf(alice), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); @@ -225,7 +225,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.balanceOf(alice), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); @@ -251,7 +251,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); assertEq(lidoARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY, "last available assets before"); assertEq(lidoARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy // 80% of the asset gain goes to the total assets @@ -285,7 +285,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); assertEq(lidoARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees assertEq( - lidoARM.lastTotalAssets(), + lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + (assetGain * 80 / 100) + depositedAssets, "last total assets after" ); @@ -324,7 +324,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before"); - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY, "last total assets before"); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY, "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"); @@ -353,7 +353,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + amount, "last total assets after"); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount, "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"); @@ -381,7 +381,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), DEFAULT_AMOUNT); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100); @@ -419,7 +419,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100 + excessLeftover); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100 + excessLeftover); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used @@ -465,7 +465,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used @@ -511,7 +511,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100); // No perfs so no fees - assertApproxEqAbs(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + deadAddressBenef, STETH_ERROR_ROUNDING); + assertApproxEqAbs(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + deadAddressBenef, STETH_ERROR_ROUNDING); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index dc5b949..053e085 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -33,7 +33,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -57,7 +57,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -76,7 +76,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only @@ -104,7 +104,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastTotalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only @@ -143,7 +143,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); // +perfs assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error + assertApproxEqAbs(lidoARM.lastAvailableAssets(), expectedAssetsDead, 1); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -182,7 +182,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoARM.lastTotalAssets(), expectedAssetsDead, 1); // 1 wei of error + assertApproxEqAbs(lidoARM.lastAvailableAssets(), expectedAssetsDead, 1); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); From c9f25f41033479f36d01e4b972977e58060f2118 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 2 Oct 2024 20:12:11 +1000 Subject: [PATCH 112/196] Added Lido contract dependency diagram --- docs/plantuml/README.md | 22 +++++++++++++++++ docs/plantuml/lidoContracts.png | Bin 0 -> 15524 bytes docs/plantuml/lidoContracts.puml | 39 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 docs/plantuml/README.md create mode 100644 docs/plantuml/lidoContracts.png create mode 100644 docs/plantuml/lidoContracts.puml diff --git a/docs/plantuml/README.md b/docs/plantuml/README.md new file mode 100644 index 0000000..b7fc2a9 --- /dev/null +++ b/docs/plantuml/README.md @@ -0,0 +1,22 @@ +# PlantUML + +[PlantUML](http://plantuml.com) is used for the technical diagrams using [Unified Modeling Language (UML)](https://en.wikipedia.org/wiki/Unified_Modeling_Language) and [Archimate](https://www.itmg-int.com/itmg-int-wp-content/Archimate/An%20Introduction%20to%20Archimate%203.0.pdf). + +The PlantUML files have the `.puml` file extension. + +## VS Code extension + +[Jebbs PlantUML](https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml) extension for VS Code is used to authoring the PlantUML diagrams. + +The following settings are used in VS Code settings.json file: + +```json + "plantuml.exportOutDir": "contracts/docs/plantuml", + "plantuml.exportFormat": "png", + "plantuml.exportIncludeFolderHeirarchy": false, + "plantuml.exportSubFolder": false, +``` + +`Alt-D` on Windows, or `Option-D` on Mac, to start PlantUML preview in VS Code. + +To save the PlantUML diagram as a PNG file, right-click on the diagram and select `Export Current Diagram`. This will save to where the `plantuml.exportOutDir` setting is set to. diff --git a/docs/plantuml/lidoContracts.png b/docs/plantuml/lidoContracts.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f52ac41ebcd4b784623ad21063795c3fcc4ca2 GIT binary patch literal 15524 zcmbWeWmH{Fvo4AzkOW)72@qi6nh@L}xH|;*;1XPeOYq>qJvhPLB_V;}?(Xh(8s6`{ zWA8K0K6l*9kGaNLli9Pos=Df_s;7hHWJFP(;Xi|cfk6=$d#?Zk1NQ_51{MPe4*U{f zd?5}#XzYd6>jR?2Qax>$|-EXm4-xiIb7h#zN1^-oes>!NA(m zvHuq_7>3MLNzMM>-(g_EIL^skO4C;OZ_qu?Ri#XGJ}aT4OB7-Wqca4P$|1)eSd0yK z{V9=dszAp)o#)aHfsmKoY-I9 z2xd8#>)TR&%7|aPfFqUtgD$yN{)YqaBCBhm|I-`0P0SV2&^NPD%8|La8B9aJ1lh>G zi3uVqDkWHmX}}wp8v3Dz%x=~q@R;}hjC6iYP$K%$OzOGd#3-8KyYY3% zk2SMaRTTZad()QAUaU=&us7M}n@lL5&HJcP*4S3bTaIF+6rnP!2co@)I(Z>{%*F3^ z6Fm#D>Kla#dNz(2mZUFqw36i*Sc+r6a^cMzB`oSrU2B#S_Ff$CCG>l(aZF0$q}`1+ zq)FxMYH-orPBA|x;p85@L2yh(Tb{19w_BO-&pxKmULQQRK6Q1XD1TRp8=?D!;q=J} z&8btKXN&rbo`k0>?^G7A$Ey@klt4{})(Nx;9BGa7Nte;4HhNwB3el-S?+tSuR zauZ2XbYXaD`X~QPE+DnHT2xg~2na2mdAd&^u*De@NM5cPXSwGf8*kJy*Ajo>^^8}S z(!~FeBaPXr&r9cpLDo$2mX{e;=V_*idh71wl>Y@v&2%!+I0{u33=HQJ@%I8s&QrT7 zPo1%Zrvu{sWHz*1t>1_-!odv*koL;rP-jReBEVwFCk-jNVj+zDb&H~Tt%V@~i}KpX zCngAK!{qYj&g6)Q^_%7T3cn!7fX5$)lG#b{$I-CQTrpgEk0FRe^5)}jIPejJ@c-*_ z{vL9+H``!3oLgZskaQD>LNHct(8J?)rY2iwzp0Rf>v?x+!Z&m>z;|yEvbfas1x-=A z!Q{(I#ldD?nuw_A{OrX`{P^`Y>l5zF-I{s(fqAccE$*z$OrmtJ`{f8eqIVx7#{*|> z-yDz&t^H19*6$3uzdLW)+q^vNAyzF?I~>!nCOLRY?EcwYtHE&>_Lr=Ih~-@4<;CH$ z*WKm%?Tyd8s`VJ5xS*O^Laym@PejdWPlq87B1dMI&0;$u-%zFJtt&^9YN>W}VXrLT zt^MiNXy;FsdxrH?`sg%H5)zW-?y!b0hm`iHwcKc)S?fPT@tExg{Ga25tVfav5n*1l zSWdlzUi@L3Ke>u*;Cs?+*gI8aUOro_!Isx~b2_e5+H&XmD~7tN_ID}|SJZqESMc8$ z`PVK-EAUTI8*P`no37$tZXikpvC#@Z7f&{ab-Zp*AD$Vn$aOhhvva*V*_4MnT8(*v zg8!O3ggf}F`JGljjyNu}@$aD=Ndi`jAj`(e?)C)_a^$gBHZDYanh>+GBGdX4cqnop z8nH|qoqC3#ztwF0>M4nn*9-UG=o}addf!qPI|3yu(FsPdN_1M9&Bsf`zfur#J2O4b zQ0_;j^?dX7*{(*jyUYFE?N_KcTpBk02X4dYNOWrzAF+1k0q&Rh`1Qtr@F#Cq<8?k% znZ;5o=H6T$3l)Z``%>P4-xySi)C!coOT^IyQx4^X1pdOABW`4JRz~meL41B9RAoNi zjpejI7iKvUC1k*l_@I6fBNXonqrNA^+)H~nu`#6@H8K#*a(!WLWoOy~mEt6F$+%P= zw@g1$645Yx)+4WmefJAX~Aecp6ANV@Nfqu(CP5pkn^D9QDpr;VWY0?Y9}m75ir)IrJLUoqtH@ZN3@x#RZa9J&I6~54}{rs9|&=X#b;aVk0CK?{&KGEP*I)2}GrR2ubbtZ6h zjaV7>Ckbnet&jS{&8ElINt;d_r-RvzM^seQ`9%lDPSwcl0o(#_nEG(OJekEZAJ9Ha zs%aQ)3GA9djO+|88t5l-t@;6-|2=8ujaq*zCDS3uh-)blu zne`ba)obEUT0gqPA#%{rprBUAU+;KZR@kS8U(; z&3kO|L5&*wD^>iu+rD_l>!XM5$7_t_yb>k4a6GE2lla3PDAZ1IY)qi#o*g)u1bP`4 zhD+qFv0{xDPmh%fOF!-1A)s&a)Xm{Mx7(H?*{X3x3u0)C4*&E-k@}*cY4PA#f%5YP zpc-~e!u9?{=5=j}_9!yZ6`8RTtuM3I&6gqrNx<9nR>$mRIQ;m+R2}czj76t*0Nfal z{YIqU&0fR)3^{B=FPs6(@GTIf-RiH5lQ&L#njC%vox+E`_{)Q7UBDHH5$ZTf$4}RN zqIC52_03^V|7LiibwU zc@HwIb}7*rJW1@s&rogfyYWl$UtehRZMxGL%bVJ!9wTV^it`u$o1zNtJYD=pW|Mxp zSiY5xCTs100!cyZxkl5gTnZHP{J!*BKOrU|=U0oqA>U3vL<<1l`)=p%>U4XR;_zvY zDrAO{hFYh_D&<6@mDNr~#(l4Dz3dBq{*H*S@agU}P8BnzO?H{Vb)!rwH(o=2%XkPZ zBKl9fcnjNKCYTcy#(zS>O0*ib0&jxmH)EV^8lB~b zET+YU1effo|A3&OF405reB*?zPP6wc=JI&mfS_89cdv(*4=B@Pdfa5b*@HbKq$L8a zuFqh>Edb_5+SO&Uw};q^j?HK~=g&T0|E3EQ@HwfH-TKo?SO-Fo&zSY{pN_7))YHsL z^VF%Tl+`$J3VxHBPN6MRvVO%W8Rc9Q~KY&xI& zx@Cd}KV4((^bU(+7md=e#-i7D2%)HNsYKu)}i2F}33&eKVaCA;`(w>s;w??)NPxc~d=#p=_R%jOi?_c|}YC zPQlow{8nP(aQ6Fiw_q=QSImPFrg`0QILg2KTk*szqI~cB_@DFrdFv2#VThp>2*)+^ zPmCoC297Qy=uPJuT_!{}e^Mcb5wLZiwR*#o)0}^~`uI{bf`}eV%EIe~1ny_i7 zQDYT*A^j(tVRC7%aVA%qzydf8$@-9MjFw?)Pca`%lamjZe=~%NIdol#+<)FXi~C^q zoTRdfJl{BC0&i^uiR z&qR2YA9nArAXav-#b9+GFbqDszZ~$&_I?(Ti1`*G0r?frIKRo7!sR5Ql5}S`qLQs# z;J7>Gii2c`q8;2r^2#o#-s{y5%4YvYUTRs)bYvcMr}q5(8@tNoxmEfH?&HjH5SQ zZMjz(I;^<6tYcAc3%vE?rh2iDN#9SeRd1ih>jAI|)DdDru0#*3`eW=<)Z4@H>w%0A zno{&3np5f~L%(|FJ7$+>z|$VWO-#L?p}AAdPagaRlULiM#wwYd`I^XT?X>ies+Dq=_4^m zO-OxOddWyKOD-Vzi&fhp0c$gYYSrm`DX=B5FF960d{!<{YP%iFmlt9@`ME)Pe)G-| z%3^!6F}U*c>vF~|K+KK2D|->%jCh$5k~W(GK}&oHLU+970x5me?_1#ueu_z+`>kn-j<##is{Tnaoczeuy~oj?Cb@Xb;s!ET84LxO-|uKQ|- zM@G1R9zzDS%_&_8Xxi3n|L*pDo}|l(edASB8lTtP<_2)$hCk?>Jmq81b9RvyyoYNn z83Wge5Z?`7ygj!F#_{a5c9Uon3CVEwc@d)@GyXz0i0>BlKG!+L?&iXFv(H6=|7YBON;R<)Rw%QNw2H6!43(Lq%{S-fgxHNP; zkF(k9P|FL}vZCEd@$>M@>K+VaHHZuJ_V)9;X>3MqO-?x2mTM;suH$t3WTYCVPiwz% zbll}X^dhI3Nx;>ap8!MzAn;Hc3BOu{BmG{zNEm(u?-kXG0P$N_)q~Qy{kz6z5C2Ny zhyFia1jS$bq5prsK#-yjwi0X?OT+WBzLah=xpU8(2@%a zGN9asR@YvpGt|g_au7HTh>Ff=>Q>1rT{gZeJaT=BpW6u(L-aH3qY51(M3BTm)*fDb-1MWRhFdVu>yT^UbhEB0F;%=1S zRJE5r&oTCR>c<)H2>;Y$l3Ne(H2h0L!xk`Hw(p-*)$` zG7L9|72k1a$Yk&Xnu1OU|8cWzxIUS=Rk3b@N=*|dr%~zIRL_S%y{RGMs%NKF^j`lg z>bk?>RY`0?W|(%N(LmlG_Ux)x>7mza_npX!uaydiWi;-;#X-fcTX1+%e{eZ|WnB-N zN^nzh$+U{%&S72goyhtzn4VRAYCT%=dOrR26VoAc#j}q_X6BYp6(5Cm-C9LZfaR`4 zP$46Ul_|}a@4m%Th}n*7yyR%q6UZwH%^EquDy6BKClr$ZJF92SjUQhjlvEF3W&L&J0ez|`a* zdrjtlDa>;z;pkS4-z=H8C{H?n@-HC&eVrr}|Bnd|WrTdb>Rev@ut}*@oJm=+O(Z6` z@!8hYEg~UgjMySc;v#c85)BiGa?2aK*)!_4^{9T@3tOv z4ON8y)?s#lk^!~J6p?@| zN9s#Ti_t6TL2aEP6m@ihPxC*^;R&gb{~dm2e;?PeVqK9DhE+~p43gbUraoVim!`@R zJO)3SjoBuyY^{`d3)fbCS3H*qYdDZ=G$<^| zA5YhZKW(BCHe;Dn^GZ3XF~74Vhd)xtKn)i}ld(>O9>pjBPb)J?d4$F8$`>jd=}#icrof!~j+h#3 zzs&a#Vv=jWgP0P0{nwlO)4y` zzi%{^F`_BPdAYHs`B7}JE|)NIt#QanGG0-dWkcm4kW<3GsQKY)3;Wy}^KKIQPco-O z?O^E=SJ|^$DKSn@>fs!sv_BU(iJFWSG#*8ph{@|VYQ^}ycSz2U0>)E&e%ls*yVBJ6 zH-O+nbz=b=O%FM+@uxliNX2b4>0DkXF0>;EXwc7X1DFyJyba@QKFgG79*1k5 zsdpee;W;L=%`In^X#eX^GjWhp9w?!#%`=#@UuM^W(x5o3A9to^r{$08(HY_xr~)PFAFEc707fSvI}a~W?SgJnx_>$TG zOk0l}7R&J9hKu!2-;q}Sc|&>NQT&h{&Z$Mq4>3Z2Y8@K|jz$OS;e32wPx+BU`Eilk zbZ38dX2M`-y5yjboza{wQpYtKiE-{Z+ zTeTw7X?u+C)gmH%if974fPSCWdu6$TcZcub6(BE?*=_ZQvc+*2G=VXW1tr6zJQ_Qb z7@UNN_}9XCft&cd{H<|PWMVI3;P6pdU|#t^blrZs3{3cR@j1`zFe=CMa+t*+VE%0{ zD0KZiavm1_*C1I`%y0x~sU4J9KCSdd(W;gJP^1Wf%ph(7d-rSoih#LGY4ydSkFncS zn?X_1gKw8t1%-@X%;4BAM`TNG{$~M6i3+X5$4-gWd56$w1S;=C!B_%X*^$4}JmJ`$ zh4nTowBMVj@ySV!ip-M#^0CQ7c7j4fe`5%#sZD_D?)}}h^*{T}hB8`i^v1>HzATYRDJTU4Ydg1~o9 zUR;hlaHuRhucmL6tQ&k+R%ivp%Zh1N`coP{4rX$ETBDOoZv_QhQ2dk;PtSVrG$psY zEnCckV=aXBE;qRnC*Fh9*LN1$0W_G@3fU5|Gztd20K0!7JXdL|XuhCtnbu-GWlUT-3E5|A`)2i>p@52Jc6x1t_p83@ z3l=Lyu)Xckoai&;K!x@>PQZ(m;0%+2RC#&%@G8HHeus^$W83P^3+-NZas5>N@|Q9* zbqyNBT==ueWFH3YQcJ(TW;eAM*y(iY77qqalR)eGuqtkow11*RGO2mszp49Mur}(X~ zE+=^isJ4>1eh#|`lMLF|3@q!tGTGSrW6<-xsJXxut>vuz95=Lre5;M~g(BgoT#*RW zVY*QHso9;5g)7Cb%H45GFd6b&W9{6`ug+I8DxU^^LW5~FtzepHN3~3j_$ki?7lT>i*1WVUuHj4E7f~Nu zd9{JL{Q@*;ai2<@+@86hH?YHJg%zYCTzA(D!|zE8MoeS9J+E9?av=N`V{{?oS`A`8 z5kQn6Dj{~h67?JaW&*A4(XEv3E+svVD4a0NsbFcz?>1B~gz?sUIgKK}ti@lIhjK<+ zg^ycG*=b6I!u?YSnV`@(dXl8_#46`|GDP3I%L?H@B=Q z?K8#D%FrM8HY?X!T;e|a|GZcpGPA~c0%u9@A{u5u^|k500Hv!e?4DkXORT?FB>hU#}$2!zQgQ8duBKi^bke z$#GQcV6>N42Whlavct!*9N}I+YE4I$yb@M{X7^ zw7&Kf1>gh(@HWd5p<2NlgBE77d*VVMNOVz1QjfX`fxpwl5~-0=rNLO2EJSlnFXkN- zSz!$;bRH6cz}S#pnQdpVkNlaM5xqd>X2DNd_k5*JTyW+=LDLeCauTBwua#VW0%EF@ zG~_0AyG)#I6rI!k``J$6FLy2$#)eMa7Zm2?O;se0mMcm0v9e<`<+Hl(F@A*o$4py` z0@aP>UrR3%7On=qySPuIs?eItOcH1$mQlbMIjT+DCn_gI^yiOZ#I(s5MjF97%cB z@=7a_=xZLkc%1{1RYS&BzB@%VXF`G!Nr7bW9vR`?$=&#EZhkz6_sdUSVuIZ3obNo( zyk~!EDG~lkmHmTL4Ao0okBq1w$M+RZJsm+lY5tb~3T7khP1TsmC}zYpcj%IKYTixQ zcF|>m?!;0+it;7_R2p03Neu;m^T;~G(of}VsE+vP?6CP!sC+Qg-nE1G%aX%*cWd{q z^DGAW;$l2PBUbxN)>+4U`5^S?L4tmsNVv3RLtpSN*i5QSr6`s}+t5j~xf*dx84oS^ zMrV)T_Ay9ERqOD{ZNZvn5;BHFzt)z&E&j~l<3I1}Ucj?^>6sE4?4qlp%-rwrk+7y} zt9sQgJxonw<%~?y?)au)McmTsm0{nLmN*jHmUVqlx)PtUg zwvJ1QPV5hx>@TpQ{HN3_@yKTE zqZ}6fm@B!@Ns;ep`q{Hl|M1C(m)TxiKWR$C#Ws}hVEfas|3dt>tLv$(DA#XZTo!7i`Ky~D$!7xJL3K^<~Q%~y3Vq(^3bzW&l- z{E`@eQU3zWV+nQLbcRHxOl2a~^K~x4X@%|1hb{X8UxX6>kiN2B_Q2Q3LzjAEdMqR_ z;2POGD8as>P1qixBNbsRk64F+V5dlmoN6LqD-d;d+V=!Lfnf6qc>P>u9xuZrld0<%07 zWB8=%#36!+AOmt)tlp?HkM0(P%X?&PC+^jXmNsmc_}_cJTXkYt6A_+L#4 zWgCv{e8harf?bplzpAq?Lf20Z-yI#|BAtydiDt)Aw<+ z9LPddULA{n?wiE1!%EkCRU?0#PPN6RHImvOrvA^1ByUtMa2O-1?5mP0rz@^!w%7W| z4fO$RjPZlHwyE5V%lo^VrQy(}s1R_fw2XEicK*5JLi^2OisG}T@SiKMK$PQ@XosJV z;}->q>;ul-0Eo{1G4_FcHXI})=~-D1RTL^@wCOZLCR*%&{spZWnt+X4yN&VWcL*XF zgi}J0G(tOkpx_B_NxE(lZ2PKaywa{siZcU~3LU^AgYpoFBqVu^uS{#Sb!^yC3t<}$ z{_q2Z_rC+4^Z&QI(}+*pb^goUAM|aNciy{6uqv|g4j5ANE=yHXaH@1jV&cwry%*8& z8ykDMZl>?ZNwlLE{_8(tb8GiOp?4vO48e8dwAcvYQ1qjNLw<3ezN@EPuT)r=Ti3 z6D42D$o3w^u&7TV-cnqhYM-_a!@j_jpgx(&vkhXvuPw@G(`8d~`q_V>do+WHi74@Kbz+5ewEke$VGIUKo_ zy!*#St)`L_7gqKzi}b*<#4@AFL{W_&YPI}eouHzf0YwKA0bjvLj|J|3ZyZK>-d??V z`Tx!kg7AU-r$_3cEE(h@H2V0!W04Yx-20&@{=hII_Xab5;2J&N2-+`uN%JrTr2p$> z356>I0w&i9A6S6#f1c!joBg~VrP9Nz(EqQOP69_k@`t%TMO7vPpa1hL2|+@}5UtB8 z@lZ|=Oa%Odhi377SZL@-&H2tGv-udE@k6N-ol3LT2A0LQ?$dhGO$N#VpVQu}iXgCb zq}LKLRIuCDMYKCJbuW4A>{j*H9;EHvuXko^2j0_Fl2PMPKo zaqs~|ndY-8bI>Pt({DgSMo&+V z!|E%Wwe|&*#>0eRjHW705uamYif;^NJ!O9APatW6v{yTwA9h0kG|IFa4DQW$PIxJgA)mp~oE@9B?{p;d-q7`Bvk6 zsBbaT=<-tHZ`~&os`n8@Jb-4D85kTIx-(f}vOm`ZcrGH321cYb=dzw6pCtn77^R@Q zGx6se+kU!$ZzyaCHho9|JS!M82GBQP;h*>g1ehg_6{<2K{{@}-BK3;1{rMJ<^?|2& z>{faKvnlWe9s~;u(E+qW@gFVP8OC`vR+UD%pc1q-soDSyZXkv0sm48ErftJAXw_q4 zVph7In&Dn8EG#g#+(1&tOLb%u->!mLmV1C^R&Q&Y@6#N7@78=F{uhAD6^P&QQkEMQ z`5$9G=K`z{I^}{9(5@8{6a+n3MB)TbvG$)|DNmWw+>emmUt?KLReF1{RntFE-6lNO z5>4#yL3@&)MkInL%uW$8Cc0BFv?VX*SLARc9ZVm9?ia%8OPzthI&D09|KhyCz9R>1}FZ??l2PXa# zEXeEr_B{Vt41*Rv3))91Q2iJyQdcZgDFyMuH%$5)>mr*@vwM;^ z;B;Zrsz^=6nfy+`c=hTPCgw=G0DOXHlcfu2*#d@y7`_nb4KspS-QBqKb^vXY%RDw; z4$$E~c>%s(Az#rwpd7$un_vJHKG@bn9C+CM4?+hv1LEO>qoP8Fr9*yuO-%*f`vsdG z*n;d!*@xTc>CsH?7YD#TN&v+G^j!f%aI`=(1rc_cY;Bs8>AK&Z2b2hDei{77Uu zzMBIfkBf5C`r+p|3@->d?R&xr?Uq2nh*BoGR=3Ru)S@=t$R;q6@UMyFJZ>Dh@_tWF z!(McY^h|VMLVyLR@#bZ^@$ZCkP=zYHuGz`uYQkc+$7_3F?g1qLc9z9#Jr?W(?@Co2 zF!lgtkWx1F6;eee=$3(|VdO-==Z6}rS+miC9GR4>oOtbe2RFD8@a!+@>fsdqYoO$| zwzhmWpmxMlkPv9aSaq^fD`*<Ap0CSDRrYF(U`p%C=f|IcLAO!!8p;~Y+q$ostXM|HYuB9IT^$YTcDe=_dxu@dutepn@%ei|FDY}0GTaY zHti1Vy$RIh|EvfOi8znT{{v{9n=Ew(zlwi|eF&VuAJA#lS60&e(+d1-yg8HuNSO{W zbL{_v&Di9H67y+OcppfVYBw38t1sz(EBk`ONW^Zl0BjvC*|Zt3zJO`VCbMIdQtS=q z$$A04!{FdxwW!zSYCIrGJsKdDuBM=%V7=pR!L^^iztQi6L*VUxaM><(f=xRcf;XgG zWu~n0RUWXLfFWG!rz&KN4FY3(Uy+gPw>#g$_wJq27ZNI}d~mlXf>_qDBK*sfuGOv4 zLXrCzYDK_$a{~OC?uy8xw?@B!ir+)h-kA=O3WxM&fQ^oz9vSr#unm8IV=GOL=Jjw@ zOa`iLCl2vi0g8Pz?*q9kkubpF-PYO%%n3aKLsq#1r56*p+|CEj85>`xf{8`TH={*D z)7?{OlxY&T;Ut*c&UU~{_-`8HqV^fYWJ@&b*6<=tnHJK3g#y-H4d6(UqEvWp{3YQq z0EtN|Tm=?&#=&N9J+3)CDFDoY1uEMy#r~0Q49F;J~JCTjR}FSi&x6Wq!0CBEv1ct+k4c9F^7tFZ#(*XD{eir8tYbsYF8WYl;@ z9hsze-+!M^mg#kz?M`>d{c;M%AQJ(-gAPM&Jr#*~h7@4jc+AE{YUP5cZ9iJi9Dt71 zD~y?V9%yOFqu&)Nd-=ypv~cLulQ?M)Mb*8qNu*4KTZiJx=6-6GlG* z8&%)&bbn1j8|)#pYCx1${n(l<{xwp?^RCh(2mJE%$eB z7cIyATtB8Wgr4G_1MU(k5jXA-+%e6aXR^-;CWGchujL7KgM;|zO2BBvyQH>!5q-oq z=iE_9%?PcXW#7LhW40E(PN8XzcBNUg>CI`|R+tf1i2M$~4EM^56fH3B%+6}eBV zAyNExWq^)pSbmgm16K5;Dom8v1M|*&tVrN+as;BwJ(#eD#(Tkkk8g~y>DNM+ziBvc zi@Y4oh4kcjivgJ-?=?@7_xZe+&UxTZ290Vu8NU&dwk1YtHim;ugw9&fU{J~OqBSN* zz7YV#wko2`he%U)xd8JOcAG9OI9E0;JPh!e(=quztx4%X1^E2nsQY7OdGcj-fhV7@ zw$!d>l>iZ;!wj^gia|!eE^h_{B3LBaHlRw2xR$|^vjbA=`}cmH^}E5{l0_OdsCQ#{ z+uF{Hh!CDUDi@Vg?gVDD@Og z8nDrYm6(3*sc8K8fP+g`+I(eZV{-spx20_M!@QY%kH}s~fJmO50lS<`QpYj9h34;Mv@4z?cjBL?*vnX>)yJU}< zmh*+)`D9S^bbzaDr{06l@x+>e(rOQS#k_PROj`ARNHk&+deR;#lz9k1iL zY52p4XK-A{Yj&l@beIC#F)=6e0Ad!jATT>IwkeAM{_3)klD3cSwH$U6u-DtIN|+Kl zL?hsxz&HrldIB5)+%Gc=BLTS{*re=JfLf15tI|6vPai3C5XHD(b}_L#+rU<>N7a=2 zv9S|G7(F$;PTwEzyrYK3o0~#Wv!4a1<<7}o*i8=MSd$uG@x5QabQ>C`V~iC zXrcvNVrbYFX9Uj~?hOcceA}$ux$(DvgE!lNgCl79gy85j^ubRF3<5W4$``4HNq_1Y zDF~n`jcN(q^n>x*)!T=^BZ7!KpN^_J2qn{NuwGyIuvJHz3q@k~DGcgQQvB;Ojx;(|mvVV%t+V z#Aj;v!*uFD5=yGT(Ue;%8k=sRUd~6TdM{81>7FuY3hF#u?y+=hSL1NnOCI5V*aM4) z9v>6-DFHj$66}k`cK;!2)_3ph0Y6_sDgK)s=OK7M0G<^L&1*f>ceZyomp~=>EgrU0 zTmbBze}Qr%Sj;unfm8`_-7moTlV3BBS={TIn{Rx+K#c#jDi9ec1;M6MV+ue|Q?Iol z0jOvUf}}_PKLF~7#QI;PjTQ@4`mR7u#Q+Z6pdce7{}7^x?V!H$%W4N)UfYwrG|wMR zoq$3cfE4zezV~YA>hn4P{l?(@j4edy!E_H`B-;V_)?J1$w1Ysg`YYBv)f?nIbegqt z)eZKWnWj^qt83^4Zq)3=(c&`c!uZtvCRhCjxb&R?3-VX8a zGt%$dn~AIIJ<}sL`sOve63GHk2Ko>#0%+N+zqhL`r$bXc)$}F~0c=VJEc ze;61T2i_h#vkf&x1TQ4{Eub#OfkE%?pZF}}2aeeI57#N?N___j1&i`nYc&S;V_Y+XH7CQ?(w`!rvpGvXt(PIFUB4NuDYV=j|$K%OZJ=V*^{vuDej-iQmJG7mU@6Q&O z83#6Cm4Ju^>LgZOMT!7vjmV^M=7>ct0V4*-hT^Uy^eF-!AqXrv2`8s3fC(Uf6*46} z`v_POOMDg+Ogp?sy<}xbpekW=G*fF!<}Rm(8b$(zLcQ*8scUZ4qR_DQpKf#{+Iv?YCE_Lc)50@PCx}5qQBLRHmTBVBDXa*c<$qC_eP|Ja0Ll zor1cB$eVirN0}wy)M{dtk%ZMPKSSojVm2anC6M!!`IuWk zW1u@zRnJ-Tfl<<`6y+=Cp;o({Y>=pcGgek0S-QU3E?op=fiZBBX4;k12ArkStP^#2|WE~{5 zBLov+3v>tsZ}k0kB(#O!zsEIbZum!XwNEXP0nJ3PcTpz55wWUZ~YjeUZj)n`yr}^pJb!4%Y!kFB?k}s|>M#Cm`#z z0{dMWr2?SDAu^y6RQK2N-3hkONZ(yGo(xGMQNRZPG#t1K0!Vj~WbZ2w4Q4Zh0+Se= z#$P_V6&o}tEGTx#{F*K6Vrsa8DF+Kp=5+XlIWhK96UYJ(uP<0GA8?Fo4@ur9OWS7_ zp$B_DCqii;2k&><> #$originColor { +} + +object "LidoARM" as arm <><> #$originColor { + shares: ARM-stETH-WETH + assets: stETH, WETH +} + +object "LiquidityProviderController" as lpc <><> #$originColor { +} + +object "WithdrawalQueueERC721" as lidoQ <><> #$thirdPartyColor { + assets: stETH, WETH +} + +zap ..> arm +arm .> lpc +arm ..> lidoQ + + +@enduml \ No newline at end of file From bf449c2e1dd2c30b67758d86bd30ec2e98321ea0 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 2 Oct 2024 22:22:05 +1000 Subject: [PATCH 113/196] Added Hardhat tasks requestRedeemLido and claimRedeemLido --- src/js/tasks/liquidityProvider.js | 26 ++++++++++++++++++++++++++ src/js/tasks/tasks.js | 21 +++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/js/tasks/liquidityProvider.js b/src/js/tasks/liquidityProvider.js index ed36477..62f6949 100644 --- a/src/js/tasks/liquidityProvider.js +++ b/src/js/tasks/liquidityProvider.js @@ -19,6 +19,30 @@ async function depositLido({ amount }) { await logTxDetails(tx, "deposit"); } +async function requestRedeemLido({ amount }) { + const signer = await getSigner(); + + const amountBn = parseUnits(amount.toString()); + + const lidArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidArmAddress); + + log(`About to request a redeem of ${amount} Lido ARM LP tokens`); + const tx = await lidoARM.connect(signer).requestRedeem(amountBn); + await logTxDetails(tx, "requestRedeem"); +} + +async function claimRedeemLido({ id }) { + const signer = await getSigner(); + + const lidArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidArmAddress); + + log(`About to claim request with id ${id} from the Lido ARM`); + const tx = await lidoARM.connect(signer).claimRedeem(id); + await logTxDetails(tx, "claimRedeem"); +} + async function setLiquidityProviderCaps({ accounts, cap }) { const signer = await getSigner(); @@ -61,6 +85,8 @@ async function setTotalAssetsCap({ cap }) { module.exports = { depositLido, + requestRedeemLido, + claimRedeemLido, setLiquidityProviderCaps, setTotalAssetsCap, }; diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 3ea16e0..7ae6d08 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -14,6 +14,8 @@ const { } = require("./liquidity"); const { depositLido, + requestRedeemLido, + claimRedeemLido, setLiquidityProviderCaps, setTotalAssetsCap, } = require("./liquidityProvider"); @@ -494,6 +496,25 @@ task("depositLido").setAction(async (_, __, runSuper) => { return runSuper(); }); +subtask("requestRedeemLido", "Request redeem from the Lido ARM") + .addParam( + "amount", + "Amount of ARM LP tokens not scaled to 18 decimals", + undefined, + types.float + ) + .setAction(requestRedeemLido); +task("requestRedeemLido").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("claimRedeemLido", "Claim WETH from a previously requested redeem") + .addParam("id", "Request identifier", undefined, types.float) + .setAction(claimRedeemLido); +task("claimRedeemLido").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + subtask("setLiquidityProviderCaps", "Set deposit cap for liquidity providers") .addParam( "cap", From f3358f235992144227859b3dc8bb654ba20824ff Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 2 Oct 2024 22:50:50 +1000 Subject: [PATCH 114/196] Adjust lastAvailableAssets post deposit and redeem so lastAvailableAssets is up only --- src/contracts/AbstractARM.sol | 8 ++++---- .../LidoFixedPriceMultiLpARM/RequestRedeem.t.sol | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 632129b..88855fb 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -420,8 +420,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // mint shares _mint(msg.sender, shares); - // Save the new available assets after the performance fee accrued and new assets deposited - lastAvailableAssets = SafeCast.toUint128(_availableAssets()); + // Add the deposited assets to the last available assets + lastAvailableAssets = SafeCast.toUint128(lastAvailableAssets + assets); // Check the liquidity provider caps after the new assets have been deposited if (liquidityProviderController != address(0)) { @@ -470,8 +470,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // burn redeemer's shares _burn(msg.sender, shares); - // Save the new available assets after performance fee accrued and withdrawal queue updated - lastAvailableAssets = SafeCast.toUint128(_availableAssets()); + // Remove the redeemed assets from the last available assets + lastAvailableAssets = SafeCast.toUint128(lastAvailableAssets - assets); emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 053e085..0e26415 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -169,7 +169,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { uint256 feeAccrued = 0; // No profits uint256 totalAsset = weth.balanceOf(address(lidoARM)); uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + uint256 expectedLastAvailableAssets = lidoARM.lastAvailableAssets() - expectedAssets; vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); @@ -180,11 +180,11 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Assertions After assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); - assertEq(lidoARM.outstandingEther(), 0); - assertEq(lidoARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoARM.lastAvailableAssets(), expectedAssetsDead, 1); // 1 wei of error - assertEq(lidoARM.balanceOf(address(this)), 0); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.outstandingEther(), 0, "outstanding ether"); + assertEq(lidoARM.feesAccrued(), feeAccrued, "fees accrued"); + assertApproxEqAbs(lidoARM.lastAvailableAssets(), expectedLastAvailableAssets, 1, "last available assets"); // 1 wei of error + assertEq(lidoARM.balanceOf(address(this)), 0, "user LP balance"); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssets, 0, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); From 11129657573ec53b23ef3358faced7ed4796f2d8 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 3 Oct 2024 10:05:57 +1000 Subject: [PATCH 115/196] Reordered the storage slots for fee variables --- src/contracts/AbstractARM.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 88855fb..a06b6c9 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -90,8 +90,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice Mapping of withdrawal request indices to the user withdrawal request data mapping(uint256 requestId => WithdrawalRequest) public withdrawalRequests; - /// @notice The account that can collect the performance fee - address public feeCollector; /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent) /// 10,000 = 100% performance fee /// 2,000 = 20% performance fee @@ -103,6 +101,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice The available assets at the last time performance fees were calculated. /// This can only go up so is a high watermark. uint128 public lastAvailableAssets; + /// @notice The account that can collect the performance fee + address public feeCollector; address public liquidityProviderController; From ef6f7e65358995a690a9c10c327a6c3ab822d17a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 3 Oct 2024 11:36:13 +1000 Subject: [PATCH 116/196] Save gas on WETH balanceOf call on swaps of WETH out if no outstanding withdrawals --- src/contracts/AbstractARM.sol | 11 +++++++++-- .../SwapExactTokensForTokens.t.sol | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index a06b6c9..6be4afb 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -539,12 +539,19 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { withdrawsClaimable = SafeCast.toUint128(withdrawsClaimableMem + addedClaimable); } - /// @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. + /// @dev Calculate how much of the liquidity asset (WETH) in the ARM is not reserved for the withdrawal queue. + // That is, the amount of liquidity assets (WETH) that is available to be swapped. + // If there are no outstanding withdrawals, then return the maximum uint256 value. + // The ARM can swap out liquidity assets (WETH) that has been accrued from the performance fee for the fee collector. + // 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 _liquidityAvailable() internal view returns (uint256) { // The amount of 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 type(uint256).max; + // The amount of the liquidity asset is in the ARM uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this)); diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index 543d6f0..d0f697a 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -116,7 +116,7 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test initialBalance = weth.balanceOf(address(lidoARM)); deal(address(steth), address(this), initialBalance * 2); - vm.expectRevert("ARM: Insufficient liquidity"); + vm.expectRevert(); lidoARM.swapExactTokensForTokens( steth, // inToken weth, // outToken From 19b55d7886f5e4bffe25f4c98a0c1a6e64088ca2 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Thu, 3 Oct 2024 21:20:29 +1000 Subject: [PATCH 117/196] Only accrued fees when they are collected (#30) * Only accrue fees when the fees are collected * packed fee and lastAvailableAssets back into a single slot --- src/contracts/AbstractARM.sol | 103 +++++------ .../ClaimRedeem.t.sol | 10 +- .../Constructor.t.sol | 2 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 173 ++++++++++++------ .../RequestRedeem.t.sol | 89 +++++---- 5 files changed, 216 insertions(+), 161 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 6be4afb..f7c0e4a 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -95,12 +95,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// 2,000 = 20% performance fee /// 500 = 5% performance fee uint16 public fee; - /// @notice The performance fees accrued but not collected. - /// This is removed from the available assets. - uint112 public feesAccrued; - /// @notice The available assets at the last time performance fees were calculated. - /// This can only go up so is a high watermark. - uint128 public lastAvailableAssets; + /// @notice The available assets the the last time performance fees were collected and adjusted + /// for liquidity assets (WETH) deposited and redeemed. + /// This can be negative if there were asset gains and then all the liquidity providers redeemed. + int128 public lastAvailableAssets; /// @notice The account that can collect the performance fee address public feeCollector; @@ -118,7 +116,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp ); event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); - event FeeCalculated(uint256 newFeesAccrued, uint256 assetIncrease); event FeeCollected(address indexed feeCollector, uint256 fee); event FeeUpdated(uint256 fee); event FeeCollectorUpdated(address indexed newFeeCollector); @@ -168,7 +165,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // 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.toUint128(_availableAssets()); + lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(_availableAssets())); _setFee(_fee); _setFeeCollector(_feeCollector); @@ -406,10 +403,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @param assets The amount of liquidity assets to deposit /// @return shares The amount of shares that were minted function deposit(uint256 assets) external returns (uint256 shares) { - // Accrue any performance fees based on the increase in available assets before - // the liquidity asset from the deposit is transferred into the ARM - _accruePerformanceFee(); - // Calculate the amount of shares to mint after the performance fees have been accrued // which reduces the available assets, and before new assets are deposited. shares = convertToShares(assets); @@ -421,7 +414,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { _mint(msg.sender, shares); // Add the deposited assets to the last available assets - lastAvailableAssets = SafeCast.toUint128(lastAvailableAssets + assets); + lastAvailableAssets += SafeCast.toInt128(SafeCast.toInt256(assets)); // Check the liquidity provider caps after the new assets have been deposited if (liquidityProviderController != address(0)) { @@ -443,10 +436,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @return requestId The index of the withdrawal request /// @return assets The amount of liquidity assets that will be claimable by the redeemer function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { - // Accrue any performance fees based on the increase in available assets before - // the liquidity asset from the redeem is reserved for the ARM withdrawal queue - _accruePerformanceFee(); - // Calculate the amount of assets to transfer to the redeemer assets = convertToAssets(shares); @@ -471,7 +460,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { _burn(msg.sender, shares); // Remove the redeemed assets from the last available assets - lastAvailableAssets = SafeCast.toUint128(lastAvailableAssets - assets); + lastAvailableAssets -= SafeCast.toInt128(SafeCast.toInt256(assets)); emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); } @@ -566,21 +555,17 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice The total amount of assets in the ARM and external withdrawal queue, /// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees. function totalAssets() public view virtual returns (uint256) { - uint256 availableAssetsBeforeFees = _availableAssets(); - - // If the available assets have decreased, then we don't charge a performance fee - if (availableAssetsBeforeFees <= lastAvailableAssets) return availableAssetsBeforeFees; + (uint256 fees, uint256 newAvailableAssets) = _feesAccrued(); - // Calculate the increase in assets since the last time fees were calculated - uint256 assetIncrease = availableAssetsBeforeFees - lastAvailableAssets; + if (fees > newAvailableAssets) return 0; - // Calculate the performance fee and remove from the available assets before new fees are removed - return availableAssetsBeforeFees - ((assetIncrease * fee) / FEE_SCALE); + // Calculate the performance fee and remove from the available assets + return newAvailableAssets - fees; } /// @dev Calculate the available assets which is the assets in the ARM, external withdrawal queue, - /// less liquidity assets reserved for the ARM's withdrawal queue and accrued fees. - /// The accrued fees are from the last time fees were calculated. + /// less liquidity assets reserved for the ARM's withdrawal queue. + /// This does not exclude any accrued performance fees. function _availableAssets() internal view returns (uint256) { // Get the assets in the ARM and external withdrawal queue uint256 assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); @@ -591,13 +576,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // If the ARM becomes insolvent enough that the available assets in the ARM and external withdrawal queue // is less than the outstanding withdrawals and accrued fees. - if (assets + claimedMem < queuedMem + feesAccrued) { + if (assets + claimedMem < queuedMem) { return 0; } // Need to remove the liquidity assets that have been reserved for the withdrawal queue - // and any accrued fees - return assets + claimedMem - queuedMem - feesAccrued; + return assets + claimedMem - queuedMem; } /// @dev Hook for calculating the amount of assets in an external withdrawal queue like Lido or OETH @@ -627,27 +611,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// Performance Fee Functions //////////////////////////////////////////////////// - /// @dev Accrues the performance fee based on the increase in available assets - /// Needs to be called before any action that changes the liquidity provider shares. eg deposit and redeem - function _accruePerformanceFee() internal { - uint256 newAvailableAssets = _availableAssets(); - - // Do not accrued a performance fee if the available assets has decreased - if (newAvailableAssets <= lastAvailableAssets) return; - - uint256 assetIncrease = newAvailableAssets - lastAvailableAssets; - uint256 newFeesAccrued = (assetIncrease * fee) / FEE_SCALE; - - // Save the new accrued fees back to storage - feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); - // Save the new available assets back to storage less the new accrued fees. - // This is be updated again in the post deposit and post withdraw hooks to include - // the assets deposited or withdrawn - lastAvailableAssets = SafeCast.toUint128(newAvailableAssets - newFeesAccrued); - - emit FeeCalculated(newFeesAccrued, assetIncrease); - } - /// @notice Owner sets the performance fee on increased assets /// @param _fee The performance fee measured in basis points (1/100th of a percent) /// 10,000 = 100% performance fee @@ -664,8 +627,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { function _setFee(uint256 _fee) internal { require(_fee <= FEE_SCALE, "ARM: fee too high"); - // Accrued any performance fees up to this point using the old fee - _accruePerformanceFee(); + // Collect any performance fees up to this point using the old fee + collectFees(); fee = SafeCast.toUint16(_fee); @@ -682,19 +645,37 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice Transfer accrued performance fees to the fee collector /// This requires enough liquidity assets in the ARM to cover the accrued fees. - function collectFees() external returns (uint256 fees) { + function collectFees() public returns (uint256 fees) { + uint256 newAvailableAssets; // Accrue any performance fees up to this point - _accruePerformanceFee(); + (fees, newAvailableAssets) = _feesAccrued(); - // Read the updated accrued fees from storage - fees = feesAccrued; - require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); + if (fee == 0) return fees; - // Reset the accrued fees in storage - feesAccrued = 0; + require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); IERC20(liquidityAsset).transfer(feeCollector, fees); + // Save the new available assets back to storage less the collected fees. + lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(newAvailableAssets) - SafeCast.toInt256(fees)); + emit FeeCollected(feeCollector, fees); } + + /// @notice Calculates the performance fees accrued since the last time fees were collected + function feesAccrued() public view returns (uint256 fees) { + (fees,) = _feesAccrued(); + } + + function _feesAccrued() public view returns (uint256 fees, uint256 newAvailableAssets) { + newAvailableAssets = _availableAssets(); + + // Calculate the increase in assets since the last time fees were calculated + int256 assetIncrease = SafeCast.toInt256(newAvailableAssets) - lastAvailableAssets; + + // Do not accrued a performance fee if the available assets has decreased + if (assetIncrease <= 0) return (0, newAvailableAssets); + + fees = SafeCast.toUint256(assetIncrease) * fee / FEE_SCALE; + } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index de65cff..3845d60 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -121,7 +121,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -142,7 +142,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -186,7 +186,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), 0); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -210,7 +210,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -233,7 +233,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol index 7f050d0..7a3f14f 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -22,7 +22,7 @@ contract Fork_Concrete_LidoARM_Constructor_Test is Fork_Shared_Test_ { assertEq(lidoARM.operator(), operator); assertEq(lidoARM.feeCollector(), feeCollector); assertEq(lidoARM.fee(), 2000); - assertEq(lidoARM.lastAvailableAssets(), 1e12); + assertEq(lidoARM.lastAvailableAssets(), int256(1e12)); assertEq(lidoARM.feesAccrued(), 0); // the 20% performance fee is removed on initialization assertEq(lidoARM.totalAssets(), 1e12); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 943570c..1e55915 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -108,7 +108,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); @@ -131,7 +131,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(address(this)), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); @@ -154,7 +154,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(address(this)), amount); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); @@ -177,7 +177,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount * 2)); assertEq(lidoARM.balanceOf(address(this)), shares * 2); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); @@ -201,7 +201,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(alice), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); @@ -225,7 +225,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount * 2)); assertEq(lidoARM.balanceOf(alice), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); @@ -239,32 +239,32 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_FirstDeposit_WithPerfs() public setTotalAssetsCap(type(uint256).max) // No need to restrict it for this test. - setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT * 20) { // simulate asset gain uint256 balanceBefore = weth.balanceOf(address(lidoARM)); uint256 assetGain = DEFAULT_AMOUNT; deal(address(weth), address(lidoARM), balanceBefore + assetGain); + // 20% of the asset gain goes to the performance fees + uint256 expectedFeesAccrued = assetGain * 20 / 100; + uint256 expectedTotalAssetsBeforeDeposit = balanceBefore + assetGain * 80 / 100; + // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); - assertEq(lidoARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY, "last available assets before"); + assertEq(lidoARM.feesAccrued(), expectedFeesAccrued, "fee accrued before"); // No perfs so no fees + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before"); assertEq(lidoARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy // 80% of the asset gain goes to the total assets - assertEq(lidoARM.totalAssets(), balanceBefore + assetGain * 80 / 100, "Total assets before"); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT, "lp cap before"); + assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit, "Total assets before"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); assertEqQueueMetadata(0, 0, 0, 0); - // 20% of the asset gain goes to the performance fees - uint256 feesAccrued = assetGain * 20 / 100; - uint256 rawTotalAsset = weth.balanceOf(address(lidoARM)) - feesAccrued; // No steth and no externalWithdrawQueue - uint256 depositedAssets = DEFAULT_AMOUNT; - - uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / rawTotalAsset; + uint256 depositedAssets = DEFAULT_AMOUNT * 20; + uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / expectedTotalAssetsBeforeDeposit; // Expected events vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(this), address(lidoARM), depositedAssets); @@ -283,14 +283,11 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(steth.balanceOf(address(lidoARM)), 0, "stETH balance after"); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "WETH balance after"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); - assertEq(lidoARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees - assertEq( - lidoARM.lastAvailableAssets(), - MIN_TOTAL_SUPPLY + (assetGain * 80 / 100) + depositedAssets, - "last total assets after" - ); + assertEq(lidoARM.feesAccrued(), expectedFeesAccrued, "fees accrued after"); // No perfs so no fees + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + depositedAssets), "last total assets after"); assertEq(lidoARM.balanceOf(address(this)), expectedShares, "user shares after"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); + assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit + depositedAssets, "Total assets after"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); } @@ -324,7 +321,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before"); - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY, "last available assets before"); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "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"); @@ -353,7 +350,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount, "last available assets after"); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount), "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"); @@ -364,11 +361,12 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { } /// @notice Test the following scenario: - /// 1. ARM gain assets + /// 1. ARM gain assets in stETH /// 2. Operator request a withdraw from Lido on steth /// 3. User deposit liquidity /// 4. Operator claim the withdrawal on Lido /// 5. User burn shares + /// 6. Operator collects the performance fees /// Checking that amount deposited can be retrieved function test_Deposit_WithOutStandingWithdrawRequest_BeforeDeposit_ClaimedLidoWithdraw_WithAssetGain() public @@ -377,19 +375,22 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // Assertions Before + uint256 expectedTotalSupplyBeforeDeposit = MIN_TOTAL_SUPPLY; + uint256 expectTotalAssetsBeforeDeposit = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100; assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoARM.outstandingEther(), DEFAULT_AMOUNT); - assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.outstandingEther(), DEFAULT_AMOUNT, "stETH in Lido withdrawal queue before deposit"); + assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply before deposit"); + assertEq(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, "total assets before deposit"); + assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued before deposit"); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before deposit"); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); assertEqQueueMetadata(0, 0, 0, 0); - // Expected values - uint256 expectShares = DEFAULT_AMOUNT * MIN_TOTAL_SUPPLY / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100); + // Expected values = 1249998437501 + // shares = assets * total supply / total assets + uint256 expectShares = DEFAULT_AMOUNT * expectedTotalSupplyBeforeDeposit / expectTotalAssetsBeforeDeposit; // Expected events vm.expectEmit({emitter: address(weth)}); @@ -402,30 +403,60 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { requests[0] = requestId; // Main calls - // 1. User mint shares + // 3. User mint shares uint256 shares = lidoARM.deposit(DEFAULT_AMOUNT); - // 2. Lido finalization process is simulated + + assertEq(shares, expectShares, "shares after deposit"); + assertEq(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT, "total assets after deposit"); + assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued after deposit"); + assertEq( + lidoARM.lastAvailableAssets(), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), + "last available assets after deposit" + ); + + // 4. Lido finalization process is simulated lidoARM.totalAssets(); _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); - // 3. Operator claim withdrawal on lido + + // 4. Operator claim withdrawal on lido lidoARM.totalAssets(); lidoARM.claimStETHWithdrawalForWETH(requests); - // 4. User burn shares + + // 5. User burn shares (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); - uint256 excessLeftover = DEFAULT_AMOUNT - receivedAssets; - // Assertions After - assertEq(steth.balanceOf(address(lidoARM)), 0); - assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); - assertEq(lidoARM.outstandingEther(), 0); - assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100 + excessLeftover); - assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + // Assertions after redeem + // This difference comes from the small value of shares, which reduces the precision of the calculation + assertApproxEqRel(receivedAssets, DEFAULT_AMOUNT, 1e6, "received assets from redeem"); // 1e6 -> 0.0000000001%, + assertEq(steth.balanceOf(address(lidoARM)), 0, "ARM stETH balance after redeem"); + assertEq( + weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2, "ARM WETH balance after redeem" + ); + assertEq(lidoARM.outstandingEther(), 0, "stETH in Lido withdrawal queue after redeem"); + assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply after redeem"); + assertApproxEqRel(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, 1e6, "total assets after redeem"); + assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued after redeem"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), 4e6, "last available assets after redeem" + ); + assertEq(lidoARM.balanceOf(address(this)), 0, "User shares after redeem"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "all user cap used"); assertEqQueueMetadata(receivedAssets, 0, 0, 1); - assertApproxEqRel(receivedAssets, DEFAULT_AMOUNT, 1e6, "received assets"); // 1e6 -> 0.0000000001%, - // This difference comes from the small value of shares, which reduces the precision of the calculation + + // 6. collect fees + lidoARM.collectFees(); + + // Assertions after collect fees + assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply after collect fees"); + assertApproxEqRel(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, 1e6, "total assets after collect fees"); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued after collect fees"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + int256(expectTotalAssetsBeforeDeposit), + 4e6, + "last available assets after collect fees" + ); } /// @notice Test the following scenario: @@ -465,7 +496,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used @@ -476,7 +507,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { /// @notice Test the following scenario: /// 1. User deposit liquidity /// 2. ARM asset gain (on steth) - /// 2. Operator request a withdraw from Lido on steth + /// 3. Operator request a withdraw from Lido on steth /// 4. Operator claim the withdrawal on Lido /// 5. User burn shares /// Checking that amount deposited + benefice can be retrieved @@ -489,33 +520,55 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Main calls: // 1. User mint shares + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before deposit"); + uint256 shares = lidoARM.deposit(DEFAULT_AMOUNT); - // Simulate a swap from WETH to stETH + + assertEq(lidoARM.feesAccrued(), 0, "fees accrued after deposit"); + assertEq( + lidoARM.lastAvailableAssets(), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), + "last available assets after deposit" + ); + + // 2. Simulate asset gain (on steth) deal(address(steth), address(lidoARM), DEFAULT_AMOUNT); - // 2. Operator request a claim on withdraw + assertApproxEqAbs( + lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, STETH_ERROR_ROUNDING, "fees accrued before redeem" + ); + + // 3. Operator request a claim on withdraw lidoARM.requestStETHWithdrawalForETH(amounts1); + // 3. We simulate the finalization of the process _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); uint256 requestId = stETHWithdrawal.getLastRequestId(); uint256[] memory requests = new uint256[](1); requests[0] = requestId; + // 4. Operator claim the withdrawal on lido lidoARM.claimStETHWithdrawalForWETH(requests); + // 5. User burn shares (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); uint256 userBenef = (DEFAULT_AMOUNT * 80 / 100) * DEFAULT_AMOUNT / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 deadAddressBenef = (DEFAULT_AMOUNT * 80 / 100) * MIN_TOTAL_SUPPLY / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); // Assertions After + assertEq(receivedAssets, DEFAULT_AMOUNT + userBenef, "received assets"); assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); assertEq(lidoARM.outstandingEther(), 0); - assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100); // No perfs so no fees - assertApproxEqAbs(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + deadAddressBenef, STETH_ERROR_ROUNDING); - assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertApproxEqAbs(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, 2, "fees accrued after redeem"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + // initial assets + user deposit - (user deposit + asset gain less fees) + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT) - int256(DEFAULT_AMOUNT + userBenef), + STETH_ERROR_ROUNDING, + "last available assets after redeem" + ); + assertEq(lidoARM.balanceOf(address(this)), 0, "user shares after"); // Ensure no shares after + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply after"); // Minted to dead on deploy + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 0, 1); - assertEq(receivedAssets, DEFAULT_AMOUNT + userBenef, "received assets"); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 0e26415..0d4d417 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -33,7 +33,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -57,7 +57,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -76,7 +76,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only @@ -104,7 +104,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only @@ -121,34 +121,47 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Not needed as the same as in `test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue` // Simulate assets gain in ARM + uint256 assetsBeforeGain = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT; uint256 assetsGain = DEFAULT_AMOUNT; - deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) + assetsGain); - - // Calculate expected values - uint256 feeAccrued = assetsGain * 20 / 100; // 20% fee - uint256 totalAsset = weth.balanceOf(address(lidoARM)) - feeAccrued; - uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + uint256 assetsAfterGain = assetsBeforeGain + assetsGain; + deal(address(weth), address(lidoARM), assetsAfterGain); - vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.FeeCalculated(feeAccrued, assetsGain); + // Expected Events vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + // Main call - lidoARM.requestRedeem(DEFAULT_AMOUNT); + (, uint256 actualAssetsFromRedeem) = lidoARM.requestRedeem(DEFAULT_AMOUNT); + + // Calculate expected values + uint256 expectedFeeAccrued = assetsGain * 20 / 100; // 20% fee + uint256 expectedTotalAsset = assetsAfterGain - expectedFeeAccrued; + uint256 expectedAssetsFromRedeem = DEFAULT_AMOUNT * expectedTotalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 delay = lidoARM.CLAIM_DELAY(); // Assertions After + assertEq(actualAssetsFromRedeem, expectedAssetsFromRedeem, "Assets from redeem"); assertEq(steth.balanceOf(address(lidoARM)), 0); - assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); // +perfs - assertEq(lidoARM.outstandingEther(), 0); - assertEq(lidoARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoARM.lastAvailableAssets(), expectedAssetsDead, 1); // 1 wei of error + assertEq(weth.balanceOf(address(lidoARM)), assetsAfterGain); + assertEq(lidoARM.outstandingEther(), 0, "stETH in Lido withdrawal queue"); + assertEq(lidoARM.feesAccrued(), expectedFeeAccrued, "fees accrued"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT) - int256(expectedAssetsFromRedeem), + 1, + "last available assets after" + ); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(expectedAssets, 0, 0, 1); - assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); + assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 0, 1); + assertEqUserRequest( + 0, + address(this), + false, + block.timestamp + lidoARM.CLAIM_DELAY(), + expectedAssetsFromRedeem, + expectedAssetsFromRedeem + ); } /// @notice Test the `requestRedeem` function when ARM lost a bit of money before the request. @@ -162,31 +175,39 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Not needed as the same as in `test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue` // Simulate assets loss in ARM + uint256 assetsBeforeLoss = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT; uint256 assetsLoss = DEFAULT_AMOUNT / 10; // 0.1 ether of loss - deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) - assetsLoss); - - // Calculate expected values - uint256 feeAccrued = 0; // No profits - uint256 totalAsset = weth.balanceOf(address(lidoARM)); - uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 expectedLastAvailableAssets = lidoARM.lastAvailableAssets() - expectedAssets; + uint256 assetsAfterLoss = assetsBeforeLoss - assetsLoss; + deal(address(weth), address(lidoARM), assetsAfterLoss); + // Expected Events vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + // Main call - lidoARM.requestRedeem(DEFAULT_AMOUNT); + (, uint256 actualAssetsFromRedeem) = lidoARM.requestRedeem(DEFAULT_AMOUNT); uint256 delay = lidoARM.CLAIM_DELAY(); // Assertions After + uint256 expectedAssetsFromRedeem = DEFAULT_AMOUNT * assetsAfterLoss / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(actualAssetsFromRedeem, expectedAssetsFromRedeem, "Assets from redeem"); assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); - assertEq(lidoARM.outstandingEther(), 0, "outstanding ether"); - assertEq(lidoARM.feesAccrued(), feeAccrued, "fees accrued"); - assertApproxEqAbs(lidoARM.lastAvailableAssets(), expectedLastAvailableAssets, 1, "last available assets"); // 1 wei of error + assertEq(lidoARM.outstandingEther(), 0, "stETH in Lido withdrawal queue"); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + int256(assetsBeforeLoss - expectedAssetsFromRedeem), + 1, + "last available assets" + ); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0, "user LP balance"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply"); + assertEq(lidoARM.totalAssets(), assetsAfterLoss - actualAssetsFromRedeem, "total assets"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(expectedAssets, 0, 0, 1); - assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); + assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 0, 1); + assertEqUserRequest( + 0, address(this), false, block.timestamp + delay, expectedAssetsFromRedeem, expectedAssetsFromRedeem + ); } } From ff4087652054bd7d232130a17bd9c54192614a37 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 3 Oct 2024 21:37:11 +1000 Subject: [PATCH 118/196] Generated latest LidoARM contract diagram --- docs/LidoARMPublicSquashed.svg | 102 ++++++++++----------- docs/LidoARMSquashed.svg | 159 ++++++++++++++++----------------- 2 files changed, 130 insertions(+), 131 deletions(-) diff --git a/docs/LidoARMPublicSquashed.svg b/docs/LidoARMPublicSquashed.svg index db25cb9..05d651d 100644 --- a/docs/LidoARMPublicSquashed.svg +++ b/docs/LidoARMPublicSquashed.svg @@ -32,57 +32,57 @@   withdrawsClaimable: uint128 <<AbstractARM>>   nextWithdrawalIndex: uint128 <<AbstractARM>>   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> -   feeCollector: address <<AbstractARM>> -   fee: uint16 <<AbstractARM>> -   feesAccrued: uint112 <<AbstractARM>> -   lastAvailableAssets: uint128 <<AbstractARM>> -   liquidityProviderController: address <<AbstractARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    collectFees(): (fees: uint256) <<AbstractARM>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<AbstractARM>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>> -    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> -    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> -    totalAssets(): uint256 <<AbstractARM>> -    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> -    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> +   fee: uint16 <<AbstractARM>> +   lastAvailableAssets: int128 <<AbstractARM>> +   feeCollector: address <<AbstractARM>> +   liquidityProviderController: address <<AbstractARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    totalAssets(): uint256 <<AbstractARM>> +    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> +    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>> +    feesAccrued(): (fees: uint256) <<AbstractARM>> +    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 1a2c8bf..eedba41 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,46 +4,45 @@ - - + + UmlClassDiagram - + 14 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[42] <<AbstractARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> -   DEAD_ACCOUNT: address <<AbstractARM>> -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: uint128 <<AbstractARM>> -   withdrawsClaimed: uint128 <<AbstractARM>> -   withdrawsClaimable: uint128 <<AbstractARM>> -   nextWithdrawalIndex: uint128 <<AbstractARM>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> -   feeCollector: address <<AbstractARM>> + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[42] <<AbstractARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> +   DEAD_ACCOUNT: address <<AbstractARM>> +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: uint128 <<AbstractARM>> +   withdrawsClaimed: uint128 <<AbstractARM>> +   withdrawsClaimable: uint128 <<AbstractARM>> +   nextWithdrawalIndex: uint128 <<AbstractARM>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>>   fee: uint16 <<AbstractARM>> -   feesAccrued: uint112 <<AbstractARM>> -   lastAvailableAssets: uint128 <<AbstractARM>> +   lastAvailableAssets: int128 <<AbstractARM>> +   feeCollector: address <<AbstractARM>>   liquidityProviderController: address <<AbstractARM>>   steth: IERC20 <<LidoLiquidityManager>>   weth: IWETH <<LidoLiquidityManager>> @@ -67,51 +66,51 @@    _liquidityAvailable(): uint256 <<AbstractARM>>    _availableAssets(): uint256 <<AbstractARM>>    _externalWithdrawQueue(): uint256 <<LidoARM>> -    _accruePerformanceFee() <<AbstractARM>> -    _setFee(_fee: uint256) <<AbstractARM>> -    _setFeeCollector(_feeCollector: address) <<AbstractARM>> -    _initLidoLiquidityManager() <<LidoLiquidityManager>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    collectFees(): (fees: uint256) <<AbstractARM>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<AbstractARM>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>> -    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> -    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> -    totalAssets(): uint256 <<AbstractARM>> -    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> -    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> +    _setFee(_fee: uint256) <<AbstractARM>> +    _setFeeCollector(_feeCollector: address) <<AbstractARM>> +    _initLidoLiquidityManager() <<LidoLiquidityManager>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    totalAssets(): uint256 <<AbstractARM>> +    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> +    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>> +    feesAccrued(): (fees: uint256) <<AbstractARM>> +    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> From 87ef2f54b905154a6b64c36144acac9a4073656a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 3 Oct 2024 22:36:06 +1000 Subject: [PATCH 119/196] Simplification of claimRedeem --- src/contracts/AbstractARM.sol | 65 +++++-------------- .../ClaimRedeem.t.sol | 12 ++-- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 28 ++++---- .../RequestRedeem.t.sol | 12 ++-- test/fork/utils/Helpers.sol | 11 ++-- 5 files changed, 48 insertions(+), 80 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index f7c0e4a..9a11288 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -67,13 +67,11 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { uint256 public traderate1; /// @notice cumulative total of all withdrawal requests included the ones that have already been claimed - uint128 public withdrawsQueued; + uint120 public withdrawsQueued; /// @notice total of all the withdrawal requests that have been claimed - uint128 public withdrawsClaimed; - /// @notice cumulative total of all the withdrawal requests that can be claimed including the ones already claimed - uint128 public withdrawsClaimable; + uint120 public withdrawsClaimed; /// @notice index of the next withdrawal request starting at 0 - uint128 public nextWithdrawalIndex; + uint16 public nextWithdrawalIndex; struct WithdrawalRequest { address withdrawer; @@ -81,10 +79,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // When the withdrawal can be claimed uint40 claimTimestamp; // Amount of assets to withdraw - uint128 assets; + uint120 assets; // cumulative total of all withdrawal requests including this one. // this request can be claimed when this queued amount is less than or equal to the queue's claimable amount. - uint128 queued; + uint120 queued; } /// @notice Mapping of withdrawal request indices to the user withdrawal request data @@ -440,11 +438,11 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { assets = convertToAssets(shares); requestId = nextWithdrawalIndex; - uint128 queued = SafeCast.toUint128(withdrawsQueued + assets); + uint120 queued = SafeCast.toUint120(withdrawsQueued + assets); uint40 claimTimestamp = uint40(block.timestamp + CLAIM_DELAY); // Store the next withdrawal request - nextWithdrawalIndex = SafeCast.toUint128(requestId + 1); + nextWithdrawalIndex = SafeCast.toUint16(requestId + 1); // Store the updated queued amount which reserves WETH in the withdrawal queue withdrawsQueued = queued; // Store requests @@ -452,7 +450,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { withdrawer: msg.sender, claimed: false, claimTimestamp: claimTimestamp, - assets: SafeCast.toUint128(assets), + assets: SafeCast.toUint120(assets), queued: queued }); @@ -469,15 +467,15 @@ 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) { - // Update the ARM's withdrawal queue's claimable amount - _updateWithdrawalQueueLiquidity(); - // Load the structs from storage into memory WithdrawalRequest memory request = withdrawalRequests[requestId]; require(request.claimTimestamp <= block.timestamp, "Claim delay not met"); - // If there isn't enough reserved liquidity in the queue to claim - require(request.queued <= withdrawsClaimable, "Queue pending liquidity"); + // Is there enough liquidity to claim this request? + require( + request.queued <= withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)), + "Queue pending liquidity" + ); require(request.withdrawer == msg.sender, "Not requester"); require(request.claimed == false, "Already claimed"); @@ -494,38 +492,11 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { IERC20(liquidityAsset).transfer(msg.sender, assets); } - /// @dev Updates the claimable amount in the ARM's withdrawal queue. - /// That's the amount that is used to check if a request can be claimed or not. - function _updateWithdrawalQueueLiquidity() internal { - // Load the claimable amount from storage into memory - uint256 withdrawsClaimableMem = withdrawsClaimable; - - // Check if the claimable amount is less than the queued amount - uint256 queueShortfall = withdrawsQueued - withdrawsClaimableMem; - - // No need to do anything is the withdrawal queue is fully funded - if (queueShortfall == 0) { - return; - } - - uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this)); - - // Of the claimable withdrawal requests, how much is unclaimed? - // That is, the amount of the liquidity assets that is currently allocated for the withdrawal queue - uint256 allocatedLiquidity = withdrawsClaimableMem - withdrawsClaimed; - - // If there is no unallocated liquidity assets then there is nothing to add to the queue - if (liquidityBalance <= allocatedLiquidity) { - return; - } - - uint256 unallocatedLiquidity = liquidityBalance - allocatedLiquidity; - - // the new claimable amount is the smaller of the queue shortfall or unallocated weth - uint256 addedClaimable = queueShortfall < unallocatedLiquidity ? queueShortfall : unallocatedLiquidity; - - // Store the new claimable amount back to storage - withdrawsClaimable = SafeCast.toUint128(withdrawsClaimableMem + addedClaimable); + /// @notice Check if a withdrawal request can be claimed + /// @param requestId The index of the withdrawal request + function isClaimable(uint256 requestId) public view returns (bool) { + return + withdrawalRequests[requestId].queued <= withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)); } /// @dev Calculate how much of the liquidity asset (WETH) in the ARM is not reserved for the withdrawal queue. diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index 3845d60..e76a6a1 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -125,7 +125,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); + assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); // Expected events @@ -146,7 +146,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); } @@ -182,7 +182,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { (uint256 assets) = lidoARM.claimRedeem(0); // Assertions after - assertApproxEqAbs(steth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY, 1); + assertApproxEqAbs(steth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY, 2); assertEq(weth.balanceOf(address(lidoARM)), 0); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees @@ -190,7 +190,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); } @@ -214,7 +214,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2, 2); + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, 2); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); @@ -237,7 +237,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); + assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); assertEqUserRequest(0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT / 2); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 1e55915..a9de41f 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -113,7 +113,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount, "lp cap before"); - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); @@ -136,7 +136,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether } @@ -159,7 +159,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); @@ -182,7 +182,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -206,7 +206,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); assertEq(liquidityProviderController.liquidityProviderCaps(alice), amount); - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); // Expected events vm.expectEmit({emitter: address(weth)}); @@ -230,7 +230,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(liquidityProviderController.liquidityProviderCaps(alice), 0); // All the caps are used - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -261,7 +261,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // 80% of the asset gain goes to the total assets assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit, "Total assets before"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); uint256 depositedAssets = DEFAULT_AMOUNT * 20; uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / expectedTotalAssetsBeforeDeposit; @@ -289,7 +289,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit + depositedAssets, "Total assets after"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); } /// @notice Depositing into the ARM reserves WETH for the withdrawal queue. @@ -326,7 +326,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before"); - assertEqQueueMetadata(assetsRedeem, 0, 0, 1); + assertEqQueueMetadata(assetsRedeem, 0, 1); assertApproxEqAbs(assetsRedeem, DEFAULT_AMOUNT, STETH_ERROR_ROUNDING, "assets redeem before"); uint256 amount = DEFAULT_AMOUNT * 2; @@ -356,7 +356,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used // withdrawal request is now claimable - assertEqQueueMetadata(assetsRedeem, 0, 0, 1); + assertEqQueueMetadata(assetsRedeem, 0, 1); assertApproxEqAbs(shares, amount, STETH_ERROR_ROUNDING, "shares after"); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -386,7 +386,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before deposit"); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); // Expected values = 1249998437501 // shares = assets * total supply / total assets @@ -442,7 +442,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ); assertEq(lidoARM.balanceOf(address(this)), 0, "User shares after redeem"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "all user cap used"); - assertEqQueueMetadata(receivedAssets, 0, 0, 1); + assertEqQueueMetadata(receivedAssets, 0, 1); // 6. collect fees lidoARM.collectFees(); @@ -500,7 +500,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used - assertEqQueueMetadata(receivedAssets, 0, 0, 1); + assertEqQueueMetadata(receivedAssets, 0, 1); assertEq(receivedAssets, DEFAULT_AMOUNT, "received assets"); } @@ -569,6 +569,6 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0, "user shares after"); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply after"); // Minted to dead on deploy assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used - assertEqQueueMetadata(receivedAssets, 0, 0, 1); + assertEqQueueMetadata(receivedAssets, 0, 1); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 0d4d417..37de700 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -37,7 +37,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(0, 0, 0, 0); + assertEqQueueMetadata(0, 0, 0); uint256 delay = lidoARM.CLAIM_DELAY(); @@ -50,7 +50,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Assertions After assertEq(requestId, 0); // First request - assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 0, 1); // One request in the queue + assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 1); // One request in the queue assertEqUserRequest(0, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT, DEFAULT_AMOUNT); // Requested the full amount assertEq(assets, DEFAULT_AMOUNT, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited assertEq(steth.balanceOf(address(lidoARM)), 0); @@ -80,7 +80,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only - assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 0, 1); + assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 1); uint256 delay = lidoARM.CLAIM_DELAY(); @@ -95,7 +95,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Assertions After assertEq(requestId, 1); // Second request - assertEqQueueMetadata(DEFAULT_AMOUNT * 3 / 4, 0, 0, 2); // Two requests in the queue + assertEqQueueMetadata(DEFAULT_AMOUNT * 3 / 4, 0, 2); // Two requests in the queue assertEqUserRequest( 1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT * 3 / 4 ); @@ -153,7 +153,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 0, 1); + assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( 0, address(this), @@ -205,7 +205,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply"); assertEq(lidoARM.totalAssets(), assetsAfterLoss - actualAssetsFromRedeem, "total assets"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 0, 1); + assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( 0, address(this), false, block.timestamp + delay, expectedAssetsFromRedeem, expectedAssetsFromRedeem ); diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index 9223da7..4ef8bee 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -34,14 +34,11 @@ abstract contract Helpers is Base_Test_ { } /// @notice Asserts the equality between value of `withdrawalQueueMetadata()` and the expected values. - function assertEqQueueMetadata( - uint256 expectedQueued, - uint256 expectedClaimable, - uint256 expectedClaimed, - uint256 expectedNextIndex - ) public view { + function assertEqQueueMetadata(uint256 expectedQueued, uint256 expectedClaimed, uint256 expectedNextIndex) + public + view + { assertEq(lidoARM.withdrawsQueued(), expectedQueued, "metadata queued"); - assertEq(lidoARM.withdrawsClaimable(), expectedClaimable, "metadata claimable"); assertEq(lidoARM.withdrawsClaimed(), expectedClaimed, "metadata claimed"); assertEq(lidoARM.nextWithdrawalIndex(), expectedNextIndex, "metadata nextWithdrawalIndex"); } From 533e21e92532933102fa84bf13113492632cfb1b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 3 Oct 2024 22:36:58 +1000 Subject: [PATCH 120/196] Generated latest LidoARM contract diagram --- docs/LidoARMPublicSquashed.svg | 100 +++++++++++----------- docs/LidoARMSquashed.svg | 147 ++++++++++++++++----------------- 2 files changed, 123 insertions(+), 124 deletions(-) diff --git a/docs/LidoARMPublicSquashed.svg b/docs/LidoARMPublicSquashed.svg index 05d651d..d29df4c 100644 --- a/docs/LidoARMPublicSquashed.svg +++ b/docs/LidoARMPublicSquashed.svg @@ -27,56 +27,56 @@   token1: IERC20 <<AbstractARM>>   traderate0: uint256 <<AbstractARM>>   traderate1: uint256 <<AbstractARM>> -   withdrawsQueued: uint128 <<AbstractARM>> -   withdrawsClaimed: uint128 <<AbstractARM>> -   withdrawsClaimable: uint128 <<AbstractARM>> -   nextWithdrawalIndex: uint128 <<AbstractARM>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> -   fee: uint16 <<AbstractARM>> -   lastAvailableAssets: int128 <<AbstractARM>> -   feeCollector: address <<AbstractARM>> -   liquidityProviderController: address <<AbstractARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +   withdrawsQueued: uint120 <<AbstractARM>> +   withdrawsClaimed: uint120 <<AbstractARM>> +   nextWithdrawalIndex: uint16 <<AbstractARM>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<AbstractARM>> +   fee: uint16 <<AbstractARM>> +   lastAvailableAssets: int128 <<AbstractARM>> +   feeCollector: address <<AbstractARM>> +   liquidityProviderController: address <<AbstractARM>> +   steth: IERC20 <<LidoLiquidityManager>> +   weth: IWETH <<LidoLiquidityManager>> +   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> +   outstandingEther: uint256 <<LidoLiquidityManager>> + +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    isClaimable(requestId: uint256): bool <<AbstractARM>>    totalAssets(): uint256 <<AbstractARM>>    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>>    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index eedba41..1c1ba98 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,41 +4,40 @@ - - + + UmlClassDiagram - + 14 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[42] <<AbstractARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> -   DEAD_ACCOUNT: address <<AbstractARM>> -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: uint128 <<AbstractARM>> -   withdrawsClaimed: uint128 <<AbstractARM>> -   withdrawsClaimable: uint128 <<AbstractARM>> -   nextWithdrawalIndex: uint128 <<AbstractARM>> + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[42] <<AbstractARM>> +   _gap: uint256[49] <<LidoLiquidityManager>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> +   DEAD_ACCOUNT: address <<AbstractARM>> +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>> @@ -62,49 +61,49 @@    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<AbstractARM>>    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<AbstractARM>>    _setTraderates(_baseToTokenRate: uint256, _tokenToBaseRate: uint256) <<AbstractARM>> -    _updateWithdrawalQueueLiquidity() <<AbstractARM>> -    _liquidityAvailable(): uint256 <<AbstractARM>> -    _availableAssets(): uint256 <<AbstractARM>> -    _externalWithdrawQueue(): uint256 <<LidoARM>> -    _setFee(_fee: uint256) <<AbstractARM>> -    _setFeeCollector(_feeCollector: address) <<AbstractARM>> -    _initLidoLiquidityManager() <<LidoLiquidityManager>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    _liquidityAvailable(): uint256 <<AbstractARM>> +    _availableAssets(): uint256 <<AbstractARM>> +    _externalWithdrawQueue(): uint256 <<LidoARM>> +    _setFee(_fee: uint256) <<AbstractARM>> +    _setFeeCollector(_feeCollector: address) <<AbstractARM>> +    _initLidoLiquidityManager() <<LidoLiquidityManager>> +External: +    <<payable>> null() <<LidoLiquidityManager>> +    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>> +    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> +    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> +    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> +    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> +    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    isClaimable(requestId: uint256): bool <<AbstractARM>>    totalAssets(): uint256 <<AbstractARM>>    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>>    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> From 67259f567021253a3b12938496e18c796685bf4e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 12:26:30 +1000 Subject: [PATCH 121/196] Aligned the storage slot gap --- src/contracts/AbstractARM.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 9a11288..952eb8a 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -102,7 +102,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { address public liquidityProviderController; - uint256[42] private _gap; + uint256[43] private _gap; //////////////////////////////////////////////////// /// Events From 725fb996c069e4cbf6000150afd16ff34a568f52 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 12:38:54 +1000 Subject: [PATCH 122/196] Changed isClaimable to claimable --- src/contracts/AbstractARM.sol | 9 ++++----- test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol | 4 ++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 952eb8a..23fd81a 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -492,11 +492,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { IERC20(liquidityAsset).transfer(msg.sender, assets); } - /// @notice Check if a withdrawal request can be claimed - /// @param requestId The index of the withdrawal request - function isClaimable(uint256 requestId) public view returns (bool) { - return - withdrawalRequests[requestId].queued <= withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)); + /// @notice Used to work out if an ARM's withdrawal request can be claimed. + /// If the withdrawal request's `queued` amount is less than the returned `claimable` amount, then if can be claimed. + function claimable() public view returns (uint256) { + return withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)); } /// @dev Calculate how much of the liquidity asset (WETH) in the ARM is not reserved for the withdrawal queue. diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index e76a6a1..ad6dfd9 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -127,6 +127,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); + assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); // Expected events vm.expectEmit({emitter: address(lidoARM)}); @@ -149,6 +150,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); + assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); } function test_ClaimRequest_JustEnoughLiquidity_() @@ -217,6 +219,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, 2); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); + assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); // Expected events vm.expectEmit({emitter: address(lidoARM)}); @@ -241,5 +244,6 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEqUserRequest(0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT / 2); + assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); } } From 75eb931f7dbb7cdd80e1ec3a1f48665304885fd1 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 12:41:04 +1000 Subject: [PATCH 123/196] Limit performance fee to 50% --- src/contracts/AbstractARM.sol | 2 +- test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 23fd81a..2e7c4a5 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -595,7 +595,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } function _setFee(uint256 _fee) internal { - require(_fee <= FEE_SCALE, "ARM: fee too high"); + require(_fee <= FEE_SCALE / 2, "ARM: fee too high"); // Collect any performance fees up to this point using the old fee collectFees(); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index dbbb7ad..e5ae8f2 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -63,7 +63,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { function test_PerformanceFee_SetFee_() public asLidoARMOwner { uint256 feeBefore = lidoARM.fee(); - uint256 newFee = _bound(vm.randomUint(), 0, lidoARM.FEE_SCALE()); + uint256 newFee = _bound(vm.randomUint(), 0, lidoARM.FEE_SCALE() / 2); vm.expectEmit({emitter: address(lidoARM)}); emit AbstractARM.FeeUpdated(newFee); From b94977f09b52adea2af3f1e2e564446fa5036e73 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 12:44:08 +1000 Subject: [PATCH 124/196] Fixed visibility of fee functions --- src/contracts/AbstractARM.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 2e7c4a5..d0347bd 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -633,11 +633,11 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } /// @notice Calculates the performance fees accrued since the last time fees were collected - function feesAccrued() public view returns (uint256 fees) { + function feesAccrued() external view returns (uint256 fees) { (fees,) = _feesAccrued(); } - function _feesAccrued() public view returns (uint256 fees, uint256 newAvailableAssets) { + function _feesAccrued() internal view returns (uint256 fees, uint256 newAvailableAssets) { newAvailableAssets = _availableAssets(); // Calculate the increase in assets since the last time fees were calculated From 5d68e7203539badf959a80ec3f66521ae5b3da32 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 12:47:43 +1000 Subject: [PATCH 125/196] Natspec update --- src/contracts/AbstractARM.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index d0347bd..7a7417b 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -585,6 +585,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @param _fee The performance fee measured in basis points (1/100th of a percent) /// 10,000 = 100% performance fee /// 500 = 5% performance fee + /// The max allowed performance fee is 50% (5000) function setFee(uint256 _fee) external onlyOwner { _setFee(_fee); } From 7d8c1f1f277872e0379b338c79b6a12e39d693ff Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 12:49:57 +1000 Subject: [PATCH 126/196] Return 0 on early exit --- src/contracts/AbstractARM.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 7a7417b..5dcdb00 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -621,7 +621,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Accrue any performance fees up to this point (fees, newAvailableAssets) = _feesAccrued(); - if (fee == 0) return fees; + if (fee == 0) return 0; require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); From 2dc008475053c6cd3751fa88e2802222dead8312 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 12:51:00 +1000 Subject: [PATCH 127/196] emit at end of the function --- src/contracts/AbstractARM.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 5dcdb00..12ff954 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -486,10 +486,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { assets = request.assets; - emit RedeemClaimed(msg.sender, requestId, assets); - // transfer the liquidity asset to the withdrawer IERC20(liquidityAsset).transfer(msg.sender, assets); + + emit RedeemClaimed(msg.sender, requestId, assets); } /// @notice Used to work out if an ARM's withdrawal request can be claimed. From 45b3bf0695395feaec34a77abc922df603b6a7a5 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 13:21:20 +1000 Subject: [PATCH 128/196] Collapsed LidoLiquidityManager into LidoARM --- src/contracts/LidoARM.sol | 76 ++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index 8d8019d..3609117 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -5,7 +5,7 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AbstractARM} from "./AbstractARM.sol"; -import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; +import {IERC20, IStETHWithdrawal, IWETH} from "./Interfaces.sol"; /** * @title Lido (stETH) Automated Redemption Manager (ARM) @@ -15,14 +15,24 @@ import {LidoLiquidityManager} from "./LidoLiquidityManager.sol"; * A performance fee is also collected on increases in the ARM's total assets. * @author Origin Protocol Inc */ -contract LidoARM is Initializable, AbstractARM, LidoLiquidityManager { +contract LidoARM is Initializable, AbstractARM { + /// @notice The address of the Lido stETH token + IERC20 public immutable steth; + /// @notice The address of the Wrapped ETH (WETH) token + IWETH public immutable weth; + /// @notice The address of the Lido Withdrawal Queue contract + IStETHWithdrawal public immutable withdrawalQueue; + + uint256 public outstandingEther; + /// @param _steth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract - constructor(address _steth, address _weth, address _lidoWithdrawalQueue) - AbstractARM(_weth, _steth, _weth) - LidoLiquidityManager(_steth, _weth, _lidoWithdrawalQueue) - {} + constructor(address _steth, address _weth, address _lidoWithdrawalQueue) AbstractARM(_weth, _steth, _weth) { + steth = IERC20(_steth); + weth = IWETH(_weth); + withdrawalQueue = IStETHWithdrawal(_lidoWithdrawalQueue); + } /// @notice Initialize the storage variables stored in the proxy contract. /// The deployer that calls initialize has to approve the this ARM's proxy contract to transfer 1e12 WETH. @@ -43,7 +53,9 @@ contract LidoARM is Initializable, AbstractARM, LidoLiquidityManager { address _liquidityProviderController ) external initializer { _initARM(_operator, _name, _symbol, _fee, _feeCollector, _liquidityProviderController); - _initLidoLiquidityManager(); + + // Approve the Lido withdrawal queue contract. Used for redemption requests. + steth.approve(address(withdrawalQueue), type(uint256).max); } /** @@ -59,10 +71,56 @@ contract LidoARM is Initializable, AbstractARM, LidoLiquidityManager { super._transferAsset(asset, to, transferAmount); } + /** + * @notice Request a stETH for ETH withdrawal. + * Reference: https://docs.lido.fi/contracts/withdrawal-queue-erc721/ + * Note: There is a 1k amount limit. Caller should split large withdrawals in chunks of less or equal to 1k each.) + */ + function requestStETHWithdrawalForETH(uint256[] memory amounts) + external + onlyOperatorOrOwner + returns (uint256[] memory requestIds) + { + requestIds = withdrawalQueue.requestWithdrawals(amounts, address(this)); + + // Sum the total amount of stETH being withdraw + uint256 totalAmountRequested = 0; + for (uint256 i = 0; i < amounts.length; i++) { + totalAmountRequested += amounts[i]; + } + + // Increase the Ether outstanding from the Lido Withdrawal Queue + outstandingEther += totalAmountRequested; + } + + /** + * @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 claimStETHWithdrawalForWETH(uint256[] memory requestIds) external onlyOperatorOrOwner { + uint256 etherBefore = address(this).balance; + + // Claim the NFTs for ETH. + uint256 lastIndex = withdrawalQueue.getLastCheckpointIndex(); + uint256[] memory hintIds = withdrawalQueue.findCheckpointHints(requestIds, 1, lastIndex); + withdrawalQueue.claimWithdrawals(requestIds, hintIds); + + uint256 etherAfter = address(this).balance; + + // Reduce the Ether outstanding from the Lido Withdrawal Queue + outstandingEther -= etherAfter - etherBefore; + + // Wrap all the received ETH to WETH. + weth.deposit{value: etherAfter}(); + } + /** * @dev Calculates the amount of stETH in the Lido Withdrawal Queue. */ - function _externalWithdrawQueue() internal view override(AbstractARM, LidoLiquidityManager) returns (uint256) { - return LidoLiquidityManager._externalWithdrawQueue(); + function _externalWithdrawQueue() internal view override returns (uint256) { + return outstandingEther; } + + // This method is necessary for receiving the ETH claimed as part of the withdrawal. + receive() external payable {} } From 80caba03eaabd79332a97e3d5cfd646ab325c308 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 13:24:53 +1000 Subject: [PATCH 129/196] Fixed claimRedeem tests with new event order --- test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index ad6dfd9..d0bee98 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -130,10 +130,10 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); // Expected events - vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(lidoARM), address(this), DEFAULT_AMOUNT); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); // Main call (uint256 assets) = lidoARM.claimRedeem(0); @@ -175,10 +175,10 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { } // Expected events - vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(lidoARM), address(this), DEFAULT_AMOUNT); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); // Main call (uint256 assets) = lidoARM.claimRedeem(0); @@ -222,10 +222,10 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); // Expected events - vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.RedeemClaimed(address(this), 1, DEFAULT_AMOUNT / 2); vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(lidoARM), address(this), DEFAULT_AMOUNT / 2); + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.RedeemClaimed(address(this), 1, DEFAULT_AMOUNT / 2); // Main call skip(delay); From 5d93f21f5f54617a2036cf1f1185869bcd7a6267 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 13:26:09 +1000 Subject: [PATCH 130/196] Removed LidoLiquidityManager as it's now in LidoARM --- src/contracts/LidoLiquidityManager.sol | 82 -------------------------- 1 file changed, 82 deletions(-) delete mode 100644 src/contracts/LidoLiquidityManager.sol diff --git a/src/contracts/LidoLiquidityManager.sol b/src/contracts/LidoLiquidityManager.sol deleted file mode 100644 index 168f40f..0000000 --- a/src/contracts/LidoLiquidityManager.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {OwnableOperable} from "./OwnableOperable.sol"; -import {IERC20, IWETH, IStETHWithdrawal} from "./Interfaces.sol"; - -/** - * @title Manages stETH liquidity against the Lido Withdrawal Queue. - * @author Origin Protocol Inc - */ -abstract contract LidoLiquidityManager is OwnableOperable { - IERC20 public immutable steth; - IWETH public immutable weth; - IStETHWithdrawal public immutable withdrawalQueue; - - uint256 public outstandingEther; - - uint256[49] private _gap; - - constructor(address _steth, address _weth, address _lidoWithdrawalQueue) { - steth = IERC20(_steth); - weth = IWETH(_weth); - withdrawalQueue = IStETHWithdrawal(_lidoWithdrawalQueue); - } - - /** - * @dev Approve the stETH withdrawal contract. Used for redemption requests. - */ - function _initLidoLiquidityManager() internal { - steth.approve(address(withdrawalQueue), type(uint256).max); - } - - /** - * @notice Request a stETH for ETH withdrawal. - * Reference: https://docs.lido.fi/contracts/withdrawal-queue-erc721/ - * Note: There is a 1k amount limit. Caller should split large withdrawals in chunks of less or equal to 1k each.) - */ - function requestStETHWithdrawalForETH(uint256[] memory amounts) - external - onlyOperatorOrOwner - returns (uint256[] memory requestIds) - { - requestIds = withdrawalQueue.requestWithdrawals(amounts, address(this)); - - // Sum the total amount of stETH being withdraw - uint256 totalAmountRequested = 0; - for (uint256 i = 0; i < amounts.length; i++) { - totalAmountRequested += amounts[i]; - } - - // Increase the Ether outstanding from the Lido Withdrawal Queue - outstandingEther += totalAmountRequested; - } - - /** - * @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 claimStETHWithdrawalForWETH(uint256[] memory requestIds) external onlyOperatorOrOwner { - uint256 etherBefore = address(this).balance; - - // Claim the NFTs for ETH. - uint256 lastIndex = withdrawalQueue.getLastCheckpointIndex(); - uint256[] memory hintIds = withdrawalQueue.findCheckpointHints(requestIds, 1, lastIndex); - withdrawalQueue.claimWithdrawals(requestIds, hintIds); - - uint256 etherAfter = address(this).balance; - - // Reduce the Ether outstanding from the Lido Withdrawal Queue - outstandingEther -= etherAfter - etherBefore; - - // Wrap all the received ETH to WETH. - weth.deposit{value: etherAfter}(); - } - - function _externalWithdrawQueue() internal view virtual returns (uint256 assets) { - return outstandingEther; - } - - // This method is necessary for receiving the ETH claimed as part of the withdrawal. - receive() external payable {} -} From 2c342f05c998421419e33688cab39f1cbe0e8f70 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 13:26:21 +1000 Subject: [PATCH 131/196] Generated latest contract diagrams --- docs/LidoARMHierarchy.svg | 78 +++++-------- docs/LidoARMPublicSquashed.svg | 147 ++++++++++++------------ docs/LidoARMSquashed.svg | 200 ++++++++++++++++----------------- 3 files changed, 201 insertions(+), 224 deletions(-) diff --git a/docs/LidoARMHierarchy.svg b/docs/LidoARMHierarchy.svg index 2fcc331..b5e7e70 100644 --- a/docs/LidoARMHierarchy.svg +++ b/docs/LidoARMHierarchy.svg @@ -4,77 +4,57 @@ - + UmlClassDiagram - + 0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol + +<<Abstract>> +AbstractARM +../src/contracts/AbstractARM.sol - + 19 - -OwnableOperable -../src/contracts/OwnableOperable.sol + +OwnableOperable +../src/contracts/OwnableOperable.sol 0->19 - - - - - -13 - -LidoARM -../src/contracts/LidoARM.sol - - - -13->0 - - + + - + 14 - -<<Abstract>> -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol + +LidoARM +../src/contracts/LidoARM.sol - - -13->14 - - - - - -14->19 - - + + +14->0 + + - + 18 - -Ownable -../src/contracts/Ownable.sol + +Ownable +../src/contracts/Ownable.sol - + 19->18 - - + + diff --git a/docs/LidoARMPublicSquashed.svg b/docs/LidoARMPublicSquashed.svg index d29df4c..7cd6256 100644 --- a/docs/LidoARMPublicSquashed.svg +++ b/docs/LidoARMPublicSquashed.svg @@ -4,85 +4,84 @@ - - + + UmlClassDiagram - + 14 - -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>> -   liquidityProviderController: address <<AbstractARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> + +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>> +   liquidityProviderController: address <<AbstractARM>> +   steth: IERC20 <<LidoARM>> +   weth: IWETH <<LidoARM>> +   withdrawalQueue: IStETHWithdrawal <<LidoARM>> +   outstandingEther: 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>> +    setLiquidityProviderController(_liquidityProviderController: 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, _liquidityProviderController: address) <<initializer>> <<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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> -    isClaimable(requestId: uint256): bool <<AbstractARM>> -    totalAssets(): uint256 <<AbstractARM>> -    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> -    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> -    collectFees(): (fees: uint256) <<AbstractARM>> -    feesAccrued(): (fees: uint256) <<AbstractARM>> -    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> +    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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    claimable(): uint256 <<AbstractARM>> +    totalAssets(): uint256 <<AbstractARM>> +    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> +    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>> diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 1c1ba98..2f11869 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,112 +4,110 @@ - - + + UmlClassDiagram - + 14 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[42] <<AbstractARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> -   DEAD_ACCOUNT: address <<AbstractARM>> -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>> -   liquidityProviderController: address <<AbstractARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<AbstractARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> -    _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>> -    _setTraderates(_baseToTokenRate: uint256, _tokenToBaseRate: uint256) <<AbstractARM>> -    _liquidityAvailable(): uint256 <<AbstractARM>> -    _availableAssets(): uint256 <<AbstractARM>> -    _externalWithdrawQueue(): uint256 <<LidoARM>> -    _setFee(_fee: uint256) <<AbstractARM>> -    _setFeeCollector(_feeCollector: address) <<AbstractARM>> -    _initLidoLiquidityManager() <<LidoLiquidityManager>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<AbstractARM>> -    setFee(_fee: uint256) <<onlyOwner>> <<AbstractARM>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<AbstractARM>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[43] <<AbstractARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> +   DEAD_ACCOUNT: address <<AbstractARM>> +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>> +   liquidityProviderController: address <<AbstractARM>> +   steth: IERC20 <<LidoARM>> +   weth: IWETH <<LidoARM>> +   withdrawalQueue: IStETHWithdrawal <<LidoARM>> +   outstandingEther: uint256 <<LidoARM>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<AbstractARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> +    _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>> +    _setTraderates(_baseToTokenRate: uint256, _tokenToBaseRate: uint256) <<AbstractARM>> +    _liquidityAvailable(): uint256 <<AbstractARM>> +    _availableAssets(): uint256 <<AbstractARM>> +    _externalWithdrawQueue(): uint256 <<LidoARM>> +    _setFee(_fee: uint256) <<AbstractARM>> +    _setFeeCollector(_feeCollector: address) <<AbstractARM>> +    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> +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>> +    setLiquidityProviderController(_liquidityProviderController: 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, _liquidityProviderController: address) <<initializer>> <<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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> -    isClaimable(requestId: uint256): bool <<AbstractARM>> -    totalAssets(): uint256 <<AbstractARM>> -    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> -    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> -    collectFees(): (fees: uint256) <<AbstractARM>> -    feesAccrued(): (fees: uint256) <<AbstractARM>> -    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> +    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>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> +    constructor() <<Ownable>> +    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoARM>> +    claimable(): uint256 <<AbstractARM>> +    totalAssets(): uint256 <<AbstractARM>> +    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>> +    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> +    collectFees(): (fees: uint256) <<AbstractARM>> From d89607f8352626a5aa79c4b8fd2a1b63f270c3b4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 13:40:30 +1000 Subject: [PATCH 132/196] Clean up _availableAssets --- src/contracts/AbstractARM.sol | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 12ff954..c5c98bf 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -443,7 +443,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Store the next withdrawal request nextWithdrawalIndex = SafeCast.toUint16(requestId + 1); - // Store the updated queued amount which reserves WETH in the withdrawal queue + // Store the updated queued amount which reserves liquidity assets (WETH) in the withdrawal queue withdrawsQueued = queued; // Store requests withdrawalRequests[requestId] = WithdrawalRequest({ @@ -505,7 +505,7 @@ 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 _liquidityAvailable() internal view returns (uint256) { - // The amount of WETH that is still to be claimed in the withdrawal queue + // 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 @@ -540,18 +540,17 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Get the assets in the ARM and external withdrawal queue uint256 assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); - // Load the queue metadata from storage into memory - uint256 queuedMem = withdrawsQueued; - uint256 claimedMem = withdrawsClaimed; + // The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue + uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; // If the ARM becomes insolvent enough that the available assets in the ARM and external withdrawal queue // is less than the outstanding withdrawals and accrued fees. - if (assets + claimedMem < queuedMem) { + if (assets < outstandingWithdrawals) { return 0; } // Need to remove the liquidity assets that have been reserved for the withdrawal queue - return assets + claimedMem - queuedMem; + return assets - outstandingWithdrawals; } /// @dev Hook for calculating the amount of assets in an external withdrawal queue like Lido or OETH From df8a1f978a9c610141eed9675bacb7112fee0e02 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 13:44:02 +1000 Subject: [PATCH 133/196] cleanup claimRedeem --- src/contracts/AbstractARM.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index c5c98bf..05dfa91 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -479,12 +479,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { require(request.withdrawer == msg.sender, "Not requester"); require(request.claimed == false, "Already claimed"); + assets = request.assets; + // Store the request as claimed withdrawalRequests[requestId].claimed = true; // Store the updated claimed amount - withdrawsClaimed += request.assets; - - assets = request.assets; + withdrawsClaimed += SafeCast.toUint120(assets); // transfer the liquidity asset to the withdrawer IERC20(liquidityAsset).transfer(msg.sender, assets); From 608ccc4d8980c570e24a16eb24ee3161c7a0ee7f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 15:55:54 +1000 Subject: [PATCH 134/196] Renamed LiquidityProviderController to CapManager --- docs/CapManagerHierarchy.svg | 46 ++++++ ...lerSquashed.svg => CapManagerSquashed.svg} | 48 +++--- docs/LidoARMHierarchy.svg | 8 +- docs/LidoARMPublicSquashed.svg | 28 ++-- docs/LidoARMSquashed.svg | 30 ++-- docs/LidoFixedPriceMultiLpARMHierarchy.svg | 154 ------------------ ...LidoFixedPriceMultiLpARMPublicSquashed.svg | 86 ---------- docs/LidoFixedPriceMultiLpARMSquashed.svg | 128 --------------- docs/LiquidityProviderControllerHierarchy.svg | 46 ------ docs/generate.sh | 4 +- .../mainnet/003_UpgradeLidoARMScript.sol | 32 ++-- src/contracts/AbstractARM.sol | 26 +-- ...yProviderController.sol => CapManager.sol} | 5 +- src/contracts/Interfaces.sol | 2 +- src/contracts/LidoARM.sol | 8 +- src/contracts/README.md | 6 +- test/Base.sol | 4 +- .../ClaimRedeem.t.sol | 10 +- .../Constructor.t.sol | 2 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 50 +++--- .../RequestRedeem.t.sol | 12 +- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 120 ++++++-------- .../TotalAssets.t.sol | 4 +- test/fork/shared/Shared.sol | 24 +-- test/fork/utils/Modifiers.sol | 8 +- test/smoke/LidoARMSmokeTest.t.sol | 12 +- 26 files changed, 262 insertions(+), 641 deletions(-) create mode 100644 docs/CapManagerHierarchy.svg rename docs/{LiquidityProviderControllerSquashed.svg => CapManagerSquashed.svg} (77%) delete mode 100644 docs/LidoFixedPriceMultiLpARMHierarchy.svg delete mode 100644 docs/LidoFixedPriceMultiLpARMPublicSquashed.svg delete mode 100644 docs/LidoFixedPriceMultiLpARMSquashed.svg delete mode 100644 docs/LiquidityProviderControllerHierarchy.svg rename src/contracts/{LiquidityProviderController.sol => CapManager.sol} (93%) diff --git a/docs/CapManagerHierarchy.svg b/docs/CapManagerHierarchy.svg new file mode 100644 index 0000000..4954d4f --- /dev/null +++ b/docs/CapManagerHierarchy.svg @@ -0,0 +1,46 @@ + + + + + + +UmlClassDiagram + + + +2 + +CapManager +../src/contracts/CapManager.sol + + + +19 + +OwnableOperable +../src/contracts/OwnableOperable.sol + + + +2->19 + + + + + +18 + +Ownable +../src/contracts/Ownable.sol + + + +19->18 + + + + + diff --git a/docs/LiquidityProviderControllerSquashed.svg b/docs/CapManagerSquashed.svg similarity index 77% rename from docs/LiquidityProviderControllerSquashed.svg rename to docs/CapManagerSquashed.svg index 56543b3..8fbf96f 100644 --- a/docs/LiquidityProviderControllerSquashed.svg +++ b/docs/CapManagerSquashed.svg @@ -4,30 +4,30 @@ - + UmlClassDiagram - - + + -15 - -LiquidityProviderController -../src/contracts/LiquidityProviderController.sol - +2 + +CapManager +../src/contracts/CapManager.sol + Private:   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[48] <<LiquidityProviderController>> +   _gap: uint256[48] <<CapManager>> Internal:   OWNER_SLOT: bytes32 <<Ownable>> Public:   operator: address <<OwnableOperable>> -   arm: address <<LiquidityProviderController>> -   accountCapEnabled: bool <<LiquidityProviderController>> -   totalAssetsCap: uint248 <<LiquidityProviderController>> -   liquidityProviderCaps: mapping(address=>uint256) <<LiquidityProviderController>> - +   arm: address <<CapManager>> +   accountCapEnabled: bool <<CapManager>> +   totalAssetsCap: uint248 <<CapManager>> +   liquidityProviderCaps: mapping(address=>uint256) <<CapManager>> + Internal:    _owner(): (ownerOut: address) <<Ownable>>    _setOwner(newOwner: address) <<Ownable>> @@ -38,21 +38,21 @@    owner(): address <<Ownable>>    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>>    setOperator(newOperator: address) <<onlyOwner>> <<OwnableOperable>> -    initialize(_operator: address) <<initializer>> <<LiquidityProviderController>> -    postDepositHook(liquidityProvider: address, assets: uint256) <<LiquidityProviderController>> -    setLiquidityProviderCaps(_liquidityProviders: address[], cap: uint256) <<onlyOperatorOrOwner>> <<LiquidityProviderController>> -    setTotalAssetsCap(_totalAssetsCap: uint248) <<onlyOperatorOrOwner>> <<LiquidityProviderController>> -    setAccountCapEnabled(_accountCapEnabled: bool) <<onlyOwner>> <<LiquidityProviderController>> +    initialize(_operator: address) <<initializer>> <<CapManager>> +    postDepositHook(liquidityProvider: address, assets: uint256) <<CapManager>> +    setLiquidityProviderCaps(_liquidityProviders: address[], cap: uint256) <<onlyOperatorOrOwner>> <<CapManager>> +    setTotalAssetsCap(_totalAssetsCap: uint248) <<onlyOperatorOrOwner>> <<CapManager>> +    setAccountCapEnabled(_accountCapEnabled: bool) <<onlyOwner>> <<CapManager>> Public:    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>>    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> LiquidityProviderCap(liquidityProvider: address, cap: uint256) <<LiquidityProviderController>> -    <<event>> TotalAssetsCap(cap: uint256) <<LiquidityProviderController>> -    <<event>> AccountCapEnabled(enabled: bool) <<LiquidityProviderController>> +    <<event>> LiquidityProviderCap(liquidityProvider: address, cap: uint256) <<CapManager>> +    <<event>> TotalAssetsCap(cap: uint256) <<CapManager>> +    <<event>> AccountCapEnabled(enabled: bool) <<CapManager>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>> -    constructor(_arm: address) <<LiquidityProviderController>> +    constructor(_arm: address) <<CapManager>> diff --git a/docs/LidoARMHierarchy.svg b/docs/LidoARMHierarchy.svg index b5e7e70..9a92f7e 100644 --- a/docs/LidoARMHierarchy.svg +++ b/docs/LidoARMHierarchy.svg @@ -30,16 +30,16 @@ - + -14 +15 LidoARM ../src/contracts/LidoARM.sol - + -14->0 +15->0 diff --git a/docs/LidoARMPublicSquashed.svg b/docs/LidoARMPublicSquashed.svg index 7cd6256..6801a6f 100644 --- a/docs/LidoARMPublicSquashed.svg +++ b/docs/LidoARMPublicSquashed.svg @@ -4,18 +4,18 @@ - + UmlClassDiagram - - + + -14 - -LidoARM -../src/contracts/LidoARM.sol - +15 + +LidoARM +../src/contracts/LidoARM.sol + Public:   operator: address <<OwnableOperable>>   MAX_PRICE_DEVIATION: uint256 <<AbstractARM>> @@ -34,12 +34,12 @@   fee: uint16 <<AbstractARM>>   lastAvailableAssets: int128 <<AbstractARM>>   feeCollector: address <<AbstractARM>> -   liquidityProviderController: address <<AbstractARM>> +   capManager: address <<AbstractARM>>   steth: IERC20 <<LidoARM>>   weth: IWETH <<LidoARM>>   withdrawalQueue: IStETHWithdrawal <<LidoARM>>   outstandingEther: uint256 <<LidoARM>> - + External:    <<payable>> null() <<LidoARM>>    owner(): address <<Ownable>> @@ -55,11 +55,11 @@    previewRedeem(shares: uint256): (assets: uint256) <<AbstractARM>>    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<AbstractARM>>    claimRedeem(requestId: uint256): (assets: uint256) <<AbstractARM>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<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, _liquidityProviderController: address) <<initializer>> <<LidoARM>> +    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: @@ -72,7 +72,7 @@    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>>    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>>    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> -    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<event>> CapManagerUpdated(capManager: address) <<AbstractARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>> diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 2f11869..9b54e02 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,18 +4,18 @@ - + UmlClassDiagram - - + + -14 - -LidoARM -../src/contracts/LidoARM.sol - +15 + +LidoARM +../src/contracts/LidoARM.sol + Private:   _gap: uint256[49] <<OwnableOperable>>   _gap: uint256[43] <<AbstractARM>> @@ -41,19 +41,19 @@   fee: uint16 <<AbstractARM>>   lastAvailableAssets: int128 <<AbstractARM>>   feeCollector: address <<AbstractARM>> -   liquidityProviderController: address <<AbstractARM>> +   capManager: address <<AbstractARM>>   steth: IERC20 <<LidoARM>>   weth: IWETH <<LidoARM>>   withdrawalQueue: IStETHWithdrawal <<LidoARM>>   outstandingEther: uint256 <<LidoARM>> - + Internal:    _owner(): (ownerOut: address) <<Ownable>>    _setOwner(newOwner: address) <<Ownable>>    _onlyOwner() <<Ownable>>    _initOwnableOperable(_operator: address) <<OwnableOperable>>    _setOperator(newOperator: address) <<OwnableOperable>> -    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<AbstractARM>> +    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _capManager: address) <<AbstractARM>>    _inDeadline(deadline: uint256) <<AbstractARM>>    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>>    _transferAssetFrom(asset: address, from: address, to: address, amount: uint256) <<AbstractARM>> @@ -81,11 +81,11 @@    previewRedeem(shares: uint256): (assets: uint256) <<AbstractARM>>    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<AbstractARM>>    claimRedeem(requestId: uint256): (assets: uint256) <<AbstractARM>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<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, _liquidityProviderController: address) <<initializer>> <<LidoARM>> +    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: @@ -98,7 +98,7 @@    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<AbstractARM>>    <<event>> FeeUpdated(fee: uint256) <<AbstractARM>>    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<AbstractARM>> -    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<AbstractARM>> +    <<event>> CapManagerUpdated(capManager: address) <<AbstractARM>>    <<modifier>> onlyOwner() <<Ownable>>    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>>    constructor() <<Ownable>> diff --git a/docs/LidoFixedPriceMultiLpARMHierarchy.svg b/docs/LidoFixedPriceMultiLpARMHierarchy.svg deleted file mode 100644 index 1c83217..0000000 --- a/docs/LidoFixedPriceMultiLpARMHierarchy.svg +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - -UmlClassDiagram - - - -0 - -<<Abstract>> -AbstractARM -../src/contracts/AbstractARM.sol - - - -22 - -OwnableOperable -../src/contracts/OwnableOperable.sol - - - -0->22 - - - - - -1 - -<<Abstract>> -FixedPriceARM -../src/contracts/FixedPriceARM.sol - - - -1->0 - - - - - -13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - - - -13->1 - - - - - -14 - -<<Abstract>> -LidoLiquidityManager -../src/contracts/LidoLiquidityManager.sol - - - -13->14 - - - - - -16 - -<<Abstract>> -LiquidityProviderControllerARM -../src/contracts/LiquidityProviderControllerARM.sol - - - -13->16 - - - - - -17 - -<<Abstract>> -MultiLP -../src/contracts/MultiLP.sol - - - -13->17 - - - - - -25 - -<<Abstract>> -PerformanceFee -../src/contracts/PerformanceFee.sol - - - -13->25 - - - - - -14->22 - - - - - -16->17 - - - - - -17->0 - - - - - -21 - -Ownable -../src/contracts/Ownable.sol - - - -22->21 - - - - - -25->17 - - - - - diff --git a/docs/LidoFixedPriceMultiLpARMPublicSquashed.svg b/docs/LidoFixedPriceMultiLpARMPublicSquashed.svg deleted file mode 100644 index dea5511..0000000 --- a/docs/LidoFixedPriceMultiLpARMPublicSquashed.svg +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - -UmlClassDiagram - - - -15 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   FEE_SCALE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   liquidityProviderController: address <<LiquidityProviderControllerARM>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   MAX_PRICE_DEVIATION: uint256 <<FixedPriceARM>> -   PRICE_SCALE: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> -    collectFees(): (fees: uint256) <<PerformanceFee>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<LiquidityProviderControllerARM>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> -    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> -    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<LiquidityProviderControllerARM>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> -    constructor(_liquidityAsset: address) <<MultiLP>> -    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> -    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> -    totalAssets(): uint256 <<LidoFixedPriceMultiLpARM>> -    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>> -    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>> -    constructor(_stEth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoFixedPriceMultiLpARM>> - - - diff --git a/docs/LidoFixedPriceMultiLpARMSquashed.svg b/docs/LidoFixedPriceMultiLpARMSquashed.svg deleted file mode 100644 index 525a2dc..0000000 --- a/docs/LidoFixedPriceMultiLpARMSquashed.svg +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - -UmlClassDiagram - - - -13 - -LidoFixedPriceMultiLpARM -../src/contracts/LidoFixedPriceMultiLpARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[50] <<AbstractARM>> -   _gap: uint256[47] <<MultiLP>> -   _gap: uint256[48] <<PerformanceFee>> -   _gap: uint256[49] <<LiquidityProviderControllerARM>> -   _gap: uint256[48] <<FixedPriceARM>> -   _gap: uint256[49] <<LidoLiquidityManager>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<MultiLP>> -   DEAD_ACCOUNT: address <<MultiLP>> -   liquidityAsset: address <<MultiLP>> -Public: -   operator: address <<OwnableOperable>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   CLAIM_DELAY: uint256 <<MultiLP>> -   withdrawsQueued: uint128 <<MultiLP>> -   withdrawsClaimed: uint128 <<MultiLP>> -   withdrawsClaimable: uint128 <<MultiLP>> -   nextWithdrawalIndex: uint128 <<MultiLP>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<MultiLP>> -   FEE_SCALE: uint256 <<PerformanceFee>> -   feeCollector: address <<PerformanceFee>> -   fee: uint16 <<PerformanceFee>> -   feesAccrued: uint112 <<PerformanceFee>> -   lastTotalAssets: uint128 <<PerformanceFee>> -   liquidityProviderController: address <<LiquidityProviderControllerARM>> -   traderate0: uint256 <<FixedPriceARM>> -   traderate1: uint256 <<FixedPriceARM>> -   MAX_PRICE_DEVIATION: uint256 <<FixedPriceARM>> -   PRICE_SCALE: uint256 <<FixedPriceARM>> -   steth: IERC20 <<LidoLiquidityManager>> -   weth: IWETH <<LidoLiquidityManager>> -   withdrawalQueue: IStETHWithdrawal <<LidoLiquidityManager>> -   outstandingEther: uint256 <<LidoLiquidityManager>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<FixedPriceARM>> -    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<FixedPriceARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _transferAsset(asset: address, to: address, amount: uint256) <<LidoFixedPriceMultiLpARM>> -    _transferAssetFrom(asset: address, from: address, to: address, amount: uint256) <<AbstractARM>> -    _initMultiLP(_name: string, _symbol: string) <<MultiLP>> -    _preDepositHook() <<PerformanceFee>> -    _postDepositHook(assets: uint256) <<LidoFixedPriceMultiLpARM>> -    _preWithdrawHook() <<PerformanceFee>> -    _postRequestRedeemHook() <<LidoFixedPriceMultiLpARM>> -    _updateWithdrawalQueueLiquidity() <<MultiLP>> -    _liquidityAvailable(): uint256 <<MultiLP>> -    _externalWithdrawQueue(): uint256 <<LidoFixedPriceMultiLpARM>> -    _initPerformanceFee(_fee: uint256, _feeCollector: address) <<PerformanceFee>> -    _calcFee() <<PerformanceFee>> -    _rawTotalAssets(): uint256 <<PerformanceFee>> -    _setFee(_fee: uint256) <<PerformanceFee>> -    _setFeeCollector(_feeCollector: address) <<PerformanceFee>> -    _initLPControllerARM(_liquidityProviderController: address) <<LiquidityProviderControllerARM>> -    _setTraderates(_traderate0: uint256, _traderate1: uint256) <<FixedPriceARM>> -External: -    <<payable>> null() <<LidoLiquidityManager>> -    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>> -    previewDeposit(assets: uint256): (shares: uint256) <<MultiLP>> -    deposit(assets: uint256): (shares: uint256) <<MultiLP>> -    previewRedeem(shares: uint256): (assets: uint256) <<MultiLP>> -    requestRedeem(shares: uint256): (requestId: uint256, assets: uint256) <<MultiLP>> -    claimRedeem(requestId: uint256): (assets: uint256) <<MultiLP>> -    setFee(_fee: uint256) <<onlyOwner>> <<PerformanceFee>> -    setFeeCollector(_feeCollector: address) <<onlyOwner>> <<PerformanceFee>> -    collectFees(): (fees: uint256) <<PerformanceFee>> -    setLiquidityProviderController(_liquidityProviderController: address) <<onlyOwner>> <<LiquidityProviderControllerARM>> -    setPrices(buyT1: uint256, sellT1: uint256) <<onlyOperatorOrOwner>> <<FixedPriceARM>> -    approveStETH() <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    requestStETHWithdrawalForETH(amounts: uint256[]): (requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    claimStETHWithdrawalForWETH(requestIds: uint256[]) <<onlyOperatorOrOwner>> <<LidoLiquidityManager>> -    initialize(_name: string, _symbol: string, _operator: address, _fee: uint256, _feeCollector: address, _liquidityProviderController: address) <<initializer>> <<LidoFixedPriceMultiLpARM>> -Public: -    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> -    <<event>> OperatorChanged(newAdmin: address) <<OwnableOperable>> -    <<event>> RedeemRequested(withdrawer: address, requestId: uint256, assets: uint256, queued: uint256, claimTimestamp: uint256) <<MultiLP>> -    <<event>> RedeemClaimed(withdrawer: address, requestId: uint256, assets: uint256) <<MultiLP>> -    <<event>> FeeCalculated(newFeesAccrued: uint256, assetIncrease: uint256) <<PerformanceFee>> -    <<event>> FeeCollected(feeCollector: address, fee: uint256) <<PerformanceFee>> -    <<event>> FeeUpdated(fee: uint256) <<PerformanceFee>> -    <<event>> FeeCollectorUpdated(newFeeCollector: address) <<PerformanceFee>> -    <<event>> LiquidityProviderControllerUpdated(liquidityProviderController: address) <<LiquidityProviderControllerARM>> -    <<event>> TraderateChanged(traderate0: uint256, traderate1: uint256) <<FixedPriceARM>> -    <<modifier>> onlyOwner() <<Ownable>> -    <<modifier>> onlyOperatorOrOwner() <<OwnableOperable>> -    constructor() <<Ownable>> -    constructor(_inputToken: address, _outputToken1: address) <<AbstractARM>> -    constructor(_liquidityAsset: address) <<MultiLP>> -    totalAssets(): uint256 <<LidoFixedPriceMultiLpARM>> -    convertToShares(assets: uint256): (shares: uint256) <<MultiLP>> -    convertToAssets(shares: uint256): (assets: uint256) <<MultiLP>> -    constructor(_steth: address, _weth: address, _lidoWithdrawalQueue: address) <<LidoFixedPriceMultiLpARM>> - - - diff --git a/docs/LiquidityProviderControllerHierarchy.svg b/docs/LiquidityProviderControllerHierarchy.svg deleted file mode 100644 index ab3c507..0000000 --- a/docs/LiquidityProviderControllerHierarchy.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - -UmlClassDiagram - - - -15 - -LiquidityProviderController -../src/contracts/LiquidityProviderController.sol - - - -19 - -OwnableOperable -../src/contracts/OwnableOperable.sol - - - -15->19 - - - - - -18 - -Ownable -../src/contracts/Ownable.sol - - - -19->18 - - - - - diff --git a/docs/generate.sh b/docs/generate.sh index 9ca11e9..ac3ee24 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -21,5 +21,5 @@ sol2uml storage ../src/contracts,../lib -c LidoARM -o LidoARMStorage.svg \ -st address,address \ --hideExpand gap,_gap -sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b LiquidityProviderController -o LiquidityProviderControllerHierarchy.svg -sol2uml ../src/contracts -s -d 0 -b LiquidityProviderController -o LiquidityProviderControllerSquashed.svg +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b CapManager -o CapManagerHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b CapManager -o CapManagerSquashed.svg diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 3c2fb45..b61a5bd 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -7,7 +7,7 @@ import {Vm} from "forge-std/Vm.sol"; import {IERC20, IWETH, LegacyAMM} from "contracts/Interfaces.sol"; import {LidoARM} from "contracts/LidoARM.sol"; -import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; +import {CapManager} from "contracts/CapManager.sol"; import {Proxy} from "contracts/Proxy.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; import {GovProposal, GovSixHelper} from "contracts/utils/GovSixHelper.sol"; @@ -22,7 +22,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { bool public constant override proposalExecuted = false; Proxy lidoARMProxy; - Proxy lpcProxy; + Proxy capManProxy; LidoARM lidoARMImpl; function _execute() internal override { @@ -33,31 +33,31 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { _recordDeploy("LIDO_ARM", Mainnet.LIDO_ARM); lidoARMProxy = Proxy(Mainnet.LIDO_ARM); - // 2. Deploy proxy for the Liquidity Provider Controller - lpcProxy = new Proxy(); - _recordDeploy("LIDO_ARM_LPC", address(lpcProxy)); + // 2. Deploy proxy for the CapManager + capManProxy = new Proxy(); + _recordDeploy("LIDO_ARM_CAP_MAN", address(capManProxy)); - // 3. Deploy Liquidity Provider Controller implementation - LiquidityProviderController lpcImpl = new LiquidityProviderController(address(lidoARMProxy)); - _recordDeploy("LIDO_ARM_LPC_IMPL", address(lpcImpl)); + // 3. Deploy CapManager implementation + CapManager capManagerImpl = new CapManager(address(lidoARMProxy)); + _recordDeploy("LIDO_ARM_CAP_IMPL", address(capManagerImpl)); - // 4. Initialize Proxy with LiquidityProviderController implementation and set the owner to the deployer for now + // 4. Initialize Proxy with CapManager implementation and set the owner to the deployer for now bytes memory data = abi.encodeWithSignature("initialize(address)", Mainnet.ARM_RELAYER); - lpcProxy.initialize(address(lpcImpl), deployer, data); - LiquidityProviderController liquidityProviderController = LiquidityProviderController(address(lpcProxy)); + capManProxy.initialize(address(capManagerImpl), deployer, data); + CapManager capManager = CapManager(address(capManProxy)); // 5. Set the liquidity Provider caps - liquidityProviderController.setTotalAssetsCap(10 ether); + capManager.setTotalAssetsCap(10 ether); address[] memory liquidityProviders = new address[](1); liquidityProviders[0] = Mainnet.TREASURY; - liquidityProviderController.setLiquidityProviderCaps(liquidityProviders, 10 ether); + capManager.setLiquidityProviderCaps(liquidityProviders, 10 ether); // 6. Deploy Lido implementation lidoARMImpl = new LidoARM(Mainnet.STETH, Mainnet.WETH, Mainnet.LIDO_WITHDRAWAL); _recordDeploy("LIDO_ARM_IMPL", address(lidoARMImpl)); - // 7. Transfer ownership of LiquidityProviderController to the mainnet 5/8 multisig - lpcProxy.setOwner(Mainnet.GOV_MULTISIG); + // 7. Transfer ownership of CapManager to the mainnet 5/8 multisig + capManProxy.setOwner(Mainnet.GOV_MULTISIG); console.log("Finished deploying", DEPLOY_NAME); @@ -102,7 +102,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { Mainnet.ARM_RELAYER, 1500, // 15% performance fee Mainnet.ARM_BUYBACK, - address(lpcProxy) + address(capManProxy) ); console.log("lidoARM initialize data:"); console.logBytes(data); diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 05dfa91..680351e 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -5,7 +5,7 @@ import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {OwnableOperable} from "./OwnableOperable.sol"; -import {IERC20, ILiquidityProviderController} from "./Interfaces.sol"; +import {IERC20, ICapManager} from "./Interfaces.sol"; abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { //////////////////////////////////////////////////// @@ -100,7 +100,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice The account that can collect the performance fee address public feeCollector; - address public liquidityProviderController; + address public capManager; uint256[43] private _gap; @@ -117,7 +117,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { event FeeCollected(address indexed feeCollector, uint256 fee); event FeeUpdated(uint256 fee); event FeeCollectorUpdated(address indexed newFeeCollector); - event LiquidityProviderControllerUpdated(address indexed liquidityProviderController); + event CapManagerUpdated(address indexed capManager); constructor(address _token0, address _token1, address _liquidityAsset) { require(IERC20(_token0).decimals() == 18); @@ -141,14 +141,14 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// 10,000 = 100% performance fee /// 500 = 5% performance fee /// @param _feeCollector The account that can collect the performance fee - /// @param _liquidityProviderController The address of the Liquidity Provider Controller + /// @param _capManager The address of the CapManager contract function _initARM( address _operator, string calldata _name, string calldata _symbol, uint256 _fee, address _feeCollector, - address _liquidityProviderController + address _capManager ) internal { _initOwnableOperable(_operator); @@ -167,8 +167,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { _setFee(_fee); _setFeeCollector(_feeCollector); - liquidityProviderController = _liquidityProviderController; - emit LiquidityProviderControllerUpdated(_liquidityProviderController); + capManager = _capManager; + emit CapManagerUpdated(_capManager); } //////////////////////////////////////////////////// @@ -415,8 +415,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { lastAvailableAssets += SafeCast.toInt128(SafeCast.toInt256(assets)); // Check the liquidity provider caps after the new assets have been deposited - if (liquidityProviderController != address(0)) { - ILiquidityProviderController(liquidityProviderController).postDepositHook(msg.sender, assets); + if (capManager != address(0)) { + ICapManager(capManager).postDepositHook(msg.sender, assets); } emit Deposit(msg.sender, assets, shares); @@ -568,12 +568,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { assets = (shares * totalAssets()) / totalSupply(); } - /// @notice Set the Liquidity Provider Controller contract address. + /// @notice Set the CapManager contract address. /// Set to a zero address to disable the controller. - function setLiquidityProviderController(address _liquidityProviderController) external onlyOwner { - liquidityProviderController = _liquidityProviderController; + function setCapManager(address _capManager) external onlyOwner { + capManager = _capManager; - emit LiquidityProviderControllerUpdated(_liquidityProviderController); + emit CapManagerUpdated(_capManager); } //////////////////////////////////////////////////// diff --git a/src/contracts/LiquidityProviderController.sol b/src/contracts/CapManager.sol similarity index 93% rename from src/contracts/LiquidityProviderController.sol rename to src/contracts/CapManager.sol index 90dc9bc..4e27895 100644 --- a/src/contracts/LiquidityProviderController.sol +++ b/src/contracts/CapManager.sol @@ -7,10 +7,11 @@ import {OwnableOperable} from "./OwnableOperable.sol"; import {ILiquidityProviderARM} from "./Interfaces.sol"; /** - * @title Controller of ARM liquidity providers. + * @title Manages capital limits of an Automated Redemption Manager (ARM). + * Caps the total assets and individual liquidity provider assets. * @author Origin Protocol Inc */ -contract LiquidityProviderController is Initializable, OwnableOperable { +contract CapManager is Initializable, OwnableOperable { /// @notice The address of the linked Automated Redemption Manager (ARM). address public immutable arm; diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 705f72d..70950ee 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -121,7 +121,7 @@ interface ILiquidityProviderARM { function lastTotalAssets() external returns (uint256 assets); } -interface ILiquidityProviderController { +interface ICapManager { function postDepositHook(address liquidityProvider, uint256 assets) external; } diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index 3609117..e40e7c9 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -10,7 +10,7 @@ import {IERC20, IStETHWithdrawal, IWETH} from "./Interfaces.sol"; /** * @title Lido (stETH) Automated Redemption Manager (ARM) * @dev This implementation supports multiple Liquidity Providers (LPs) with single buy and sell prices. - * It also integrates to a LiquidityProviderController contract that caps the amount of assets a liquidity provider + * It also integrates to a CapManager contract that caps the amount of assets a liquidity provider * can deposit and caps the ARM's total assets. * A performance fee is also collected on increases in the ARM's total assets. * @author Origin Protocol Inc @@ -43,16 +43,16 @@ contract LidoARM is Initializable, AbstractARM { /// 10,000 = 100% performance fee /// 1,500 = 15% performance fee /// @param _feeCollector The account that can collect the performance fee - /// @param _liquidityProviderController The address of the Liquidity Provider Controller + /// @param _capManager The address of the CapManager contract function initialize( string calldata _name, string calldata _symbol, address _operator, uint256 _fee, address _feeCollector, - address _liquidityProviderController + address _capManager ) external initializer { - _initARM(_operator, _name, _symbol, _fee, _feeCollector, _liquidityProviderController); + _initARM(_operator, _name, _symbol, _fee, _feeCollector, _capManager); // Approve the Lido withdrawal queue contract. Used for redemption requests. steth.approve(address(withdrawalQueue), type(uint256).max); diff --git a/src/contracts/README.md b/src/contracts/README.md index d25dce4..9b7fd2e 100644 --- a/src/contracts/README.md +++ b/src/contracts/README.md @@ -40,12 +40,12 @@ ![Lido ARM Storage](../../docs/LidoARMStorage.svg) --> -## Liquidity Provider Controller +## Cap Manager ### Hierarchy -![Liquidity Provider Controller Hierarchy](../../docs/LiquidityProviderControllerHierarchy.svg) +![Cap Manager Hierarchy](../../docs/CapManagerHierarchy.svg) ## Squashed -![Liquidity Provider Controller Squashed](../../docs/LiquidityProviderControllerSquashed.svg) +![Cap Manager Squashed](../../docs/CapManagerSquashed.svg) diff --git a/test/Base.sol b/test/Base.sol index f4b4b57..3bba2ef 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -8,7 +8,7 @@ import {Test} from "forge-std/Test.sol"; import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; import {LidoARM} from "contracts/LidoARM.sol"; -import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; +import {CapManager} from "contracts/CapManager.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; @@ -36,7 +36,7 @@ abstract contract Base_Test_ is Test { Proxy public lidoOwnerProxy; OethARM public oethARM; LidoARM public lidoARM; - LiquidityProviderController public liquidityProviderController; + CapManager public capManager; IERC20 public oeth; IERC20 public weth; diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index d0bee98..bad93ab 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -124,7 +124,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); @@ -146,7 +146,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); @@ -191,7 +191,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); @@ -215,7 +215,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, 2); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); @@ -239,7 +239,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); assertEqUserRequest(0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol index 7a3f14f..03462ef 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -28,6 +28,6 @@ contract Fork_Concrete_LidoARM_Constructor_Test is Fork_Shared_Test_ { assertEq(lidoARM.totalAssets(), 1e12); assertEq(lidoARM.totalSupply(), 1e12); assertEq(weth.balanceOf(address(lidoARM)), 1e12); - assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); + assertEq(capManager.totalAssetsCap(), 100 ether); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index a9de41f..e013db5 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -6,7 +6,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; +import {CapManager} from "contracts/CapManager.sol"; import {IStETHWithdrawal} from "contracts/Interfaces.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; @@ -112,7 +112,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount, "lp cap before"); + assertEq(capManager.liquidityProviderCaps(address(this)), amount, "lp cap before"); assertEqQueueMetadata(0, 0, 0); // Expected events @@ -120,8 +120,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); // Main call uint256 shares = lidoARM.deposit(amount); @@ -135,7 +135,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used + assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether } @@ -158,7 +158,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), amount); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), amount); + assertEq(capManager.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0); // Expected events @@ -166,8 +166,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); // Main call uint256 shares = lidoARM.deposit(amount); @@ -181,7 +181,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), shares * 2); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -205,7 +205,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(alice), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(liquidityProviderController.liquidityProviderCaps(alice), amount); + assertEq(capManager.liquidityProviderCaps(alice), amount); assertEqQueueMetadata(0, 0, 0); // Expected events @@ -213,8 +213,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(alice, address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), alice, amount); // shares == amount here - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.LiquidityProviderCap(alice, 0); + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(alice, 0); vm.prank(alice); // Main call @@ -229,7 +229,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(alice), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(liquidityProviderController.liquidityProviderCaps(alice), 0); // All the caps are used + assertEq(capManager.liquidityProviderCaps(alice), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -260,7 +260,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy // 80% of the asset gain goes to the total assets assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit, "Total assets before"); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); + assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); assertEqQueueMetadata(0, 0, 0); uint256 depositedAssets = DEFAULT_AMOUNT * 20; @@ -270,8 +270,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), depositedAssets); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), expectedShares); - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.LiquidityProviderCap(address(this), 0); + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); // deposit assets uint256 shares = lidoARM.deposit(depositedAssets); @@ -288,7 +288,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), expectedShares, "user shares after"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit + depositedAssets, "Total assets after"); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used + assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0); } @@ -325,7 +325,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { 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"); - assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before"); + assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before"); assertEqQueueMetadata(assetsRedeem, 0, 1); assertApproxEqAbs(assetsRedeem, DEFAULT_AMOUNT, STETH_ERROR_ROUNDING, "assets redeem before"); @@ -336,8 +336,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(alice, address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), alice, amount); // shares == amount here - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); vm.prank(alice); // Main call @@ -354,7 +354,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { 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"); - assertEq(liquidityProviderController.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used + assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used // withdrawal request is now claimable assertEqQueueMetadata(assetsRedeem, 0, 1); assertApproxEqAbs(shares, amount, STETH_ERROR_ROUNDING, "shares after"); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether @@ -385,7 +385,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued before deposit"); assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before deposit"); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); + assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); assertEqQueueMetadata(0, 0, 0); // Expected values = 1249998437501 @@ -441,7 +441,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), 4e6, "last available assets after redeem" ); assertEq(lidoARM.balanceOf(address(this)), 0, "User shares after redeem"); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "all user cap used"); + assertEq(capManager.liquidityProviderCaps(address(this)), 0, "all user cap used"); assertEqQueueMetadata(receivedAssets, 0, 1); // 6. collect fees @@ -499,7 +499,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 1); assertEq(receivedAssets, DEFAULT_AMOUNT, "received assets"); } @@ -568,7 +568,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ); assertEq(lidoARM.balanceOf(address(this)), 0, "user shares after"); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply after"); // Minted to dead on deploy - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used + assertEq(capManager.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 1); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 37de700..c563be8 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -36,7 +36,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(0, 0, 0); uint256 delay = lidoARM.CLAIM_DELAY(); @@ -60,7 +60,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); } /// @notice Test the `requestRedeem` function when there are no profits and the first deposit is made. @@ -79,7 +79,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only + assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 1); uint256 delay = lidoARM.CLAIM_DELAY(); @@ -107,7 +107,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only + assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only } /// @notice Test the `requestRedeem` function when there are profits and the first deposit is already made. @@ -152,7 +152,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { ); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( 0, @@ -204,7 +204,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0, "user LP balance"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply"); assertEq(lidoARM.totalAssets(), assetsAfterLoss - actualAssetsFromRedeem, "total assets"); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); + assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( 0, address(this), false, block.timestamp + delay, expectedAssetsFromRedeem, expectedAssetsFromRedeem diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index e5ae8f2..32bda06 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -7,7 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; import {AbstractARM} from "contracts/AbstractARM.sol"; -import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; +import {CapManager} from "contracts/CapManager.sol"; contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { address[] testProviders; @@ -187,129 +187,117 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- LIQUIIDITY PROVIDER CONTROLLER - REVERTING TESTS ////////////////////////////////////////////////////// - function test_RevertWhen_LiquidityProviderController_SetLiquidityProvider_Because_NotOwner() - public - asRandomAddress - { + function test_RevertWhen_CapManager_SetLiquidityProvider_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - lidoARM.setLiquidityProviderController(address(0)); + lidoARM.setCapManager(address(0)); } - function test_RevertWhen_LiquidityProviderController_SetLiquidityProvider_Because_Operator() public asOperator { + function test_RevertWhen_CapManager_SetLiquidityProvider_Because_Operator() public asOperator { vm.expectRevert("ARM: Only owner can call this function."); - lidoARM.setLiquidityProviderController(address(0)); + lidoARM.setCapManager(address(0)); } ////////////////////////////////////////////////////// /// --- LIQUIIDITY PROVIDER CONTROLLER - PASSING TESTS ////////////////////////////////////////////////////// - function test_LiquidityProviderController_SetLiquidityProvider() public asLidoARMOwner { - address newLiquidityProviderController = vm.randomAddress(); + function test_CapManager_SetLiquidityProvider() public asLidoARMOwner { + address newCapManager = vm.randomAddress(); vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.LiquidityProviderControllerUpdated(newLiquidityProviderController); - lidoARM.setLiquidityProviderController(newLiquidityProviderController); + emit AbstractARM.CapManagerUpdated(newCapManager); + lidoARM.setCapManager(newCapManager); - assertEq(lidoARM.liquidityProviderController(), newLiquidityProviderController); + assertEq(lidoARM.capManager(), newCapManager); } ////////////////////////////////////////////////////// /// --- AccountCapEnabled - REVERTING TEST ////////////////////////////////////////////////////// - function test_RevertWhen_LiquidityProviderController_SetAccountCapEnabled_Because_NotOwner() - public - asRandomAddress - { + function test_RevertWhen_CapManager_SetAccountCapEnabled_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); - liquidityProviderController.setAccountCapEnabled(false); + capManager.setAccountCapEnabled(false); } - function test_RevertWhen_LiquidityProviderController_SetAccountCapEnabled_Because_Operator() public asOperator { + function test_RevertWhen_CapManager_SetAccountCapEnabled_Because_Operator() public asOperator { vm.expectRevert("ARM: Only owner can call this function."); - liquidityProviderController.setAccountCapEnabled(false); + capManager.setAccountCapEnabled(false); } - function test_RevertWhen_LiquidityProviderController_SetAccountCapEnabled_Because_AlreadySet() - public - asLidoARMOwner - { + function test_RevertWhen_CapManager_SetAccountCapEnabled_Because_AlreadySet() public asLidoARMOwner { vm.expectRevert("LPC: Account cap already set"); - liquidityProviderController.setAccountCapEnabled(true); + capManager.setAccountCapEnabled(true); } ////////////////////////////////////////////////////// /// --- AccountCapEnabled - PASSING TESTS ////////////////////////////////////////////////////// - function test_LiquidityProviderController_SetAccountCapEnabled() public asLidoARMOwner { - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.AccountCapEnabled(false); - liquidityProviderController.setAccountCapEnabled(false); + function test_CapManager_SetAccountCapEnabled() public asLidoARMOwner { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.AccountCapEnabled(false); + capManager.setAccountCapEnabled(false); - assertEq(liquidityProviderController.accountCapEnabled(), false); + assertEq(capManager.accountCapEnabled(), false); } ////////////////////////////////////////////////////// /// --- TotalAssetsCap - REVERTING TEST ////////////////////////////////////////////////////// - function test_RevertWhen_LiquidityProviderController_SetTotalAssetsCap_Because_NotOwner() public asRandomAddress { + function test_RevertWhen_CapManager_SetTotalAssetsCap_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); - liquidityProviderController.setTotalAssetsCap(100 ether); + capManager.setTotalAssetsCap(100 ether); } ////////////////////////////////////////////////////// /// --- TotalAssetsCap - PASSING TESTS ////////////////////////////////////////////////////// - function test_LiquidityProviderController_SetTotalAssetsCap_Owner() public asLidoARMOwner { - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.TotalAssetsCap(100 ether); - liquidityProviderController.setTotalAssetsCap(100 ether); + function test_CapManager_SetTotalAssetsCap_Owner() public asLidoARMOwner { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.TotalAssetsCap(100 ether); + capManager.setTotalAssetsCap(100 ether); - assertEq(liquidityProviderController.totalAssetsCap(), 100 ether); + assertEq(capManager.totalAssetsCap(), 100 ether); } - function test_LiquidityProviderController_SetTotalAssetsCap_Operator() public asOperator { - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.TotalAssetsCap(0); - liquidityProviderController.setTotalAssetsCap(0); + function test_CapManager_SetTotalAssetsCap_Operator() public asOperator { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.TotalAssetsCap(0); + capManager.setTotalAssetsCap(0); - assertEq(liquidityProviderController.totalAssetsCap(), 0); + assertEq(capManager.totalAssetsCap(), 0); } ////////////////////////////////////////////////////// /// --- LiquidityProviderCaps - REVERTING TEST ////////////////////////////////////////////////////// - function test_RevertWhen_LiquidityProviderController_SetLiquidityProviderCaps_Because_NotOwner() - public - asRandomAddress - { + function test_RevertWhen_CapManager_SetLiquidityProviderCaps_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); - liquidityProviderController.setLiquidityProviderCaps(testProviders, 50 ether); + capManager.setLiquidityProviderCaps(testProviders, 50 ether); } ////////////////////////////////////////////////////// /// --- LiquidityProviderCaps - PASSING TESTS ////////////////////////////////////////////////////// - function test_LiquidityProviderController_SetLiquidityProviderCaps_Owner() public asLidoARMOwner { - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.LiquidityProviderCap(testProviders[0], 50 ether); - emit LiquidityProviderController.LiquidityProviderCap(testProviders[1], 50 ether); - liquidityProviderController.setLiquidityProviderCaps(testProviders, 50 ether); + function test_CapManager_SetLiquidityProviderCaps_Owner() public asLidoARMOwner { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(testProviders[0], 50 ether); + emit CapManager.LiquidityProviderCap(testProviders[1], 50 ether); + capManager.setLiquidityProviderCaps(testProviders, 50 ether); - assertEq(liquidityProviderController.liquidityProviderCaps(testProviders[0]), 50 ether); - assertEq(liquidityProviderController.liquidityProviderCaps(testProviders[1]), 50 ether); + assertEq(capManager.liquidityProviderCaps(testProviders[0]), 50 ether); + assertEq(capManager.liquidityProviderCaps(testProviders[1]), 50 ether); } - function test_LiquidityProviderController_SetLiquidityProviderCaps_Operator() public asOperator { - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.LiquidityProviderCap(testProviders[0], 50 ether); - emit LiquidityProviderController.LiquidityProviderCap(testProviders[1], 50 ether); - liquidityProviderController.setLiquidityProviderCaps(testProviders, 50 ether); + function test_CapManager_SetLiquidityProviderCaps_Operator() public asOperator { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(testProviders[0], 50 ether); + emit CapManager.LiquidityProviderCap(testProviders[1], 50 ether); + capManager.setLiquidityProviderCaps(testProviders, 50 ether); - assertEq(liquidityProviderController.liquidityProviderCaps(testProviders[0]), 50 ether); - assertEq(liquidityProviderController.liquidityProviderCaps(testProviders[1]), 50 ether); + assertEq(capManager.liquidityProviderCaps(testProviders[0]), 50 ether); + assertEq(capManager.liquidityProviderCaps(testProviders[1]), 50 ether); } - function test_LiquidityProviderController_SetLiquidityProviderCaps_ToZero() + function test_CapManager_SetLiquidityProviderCaps_ToZero() public asOperator setLiquidityProviderCap(testProviders[0], 10 ether) @@ -317,11 +305,11 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { address[] memory providers = new address[](1); providers[0] = testProviders[0]; - vm.expectEmit({emitter: address(liquidityProviderController)}); - emit LiquidityProviderController.LiquidityProviderCap(providers[0], 0); + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(providers[0], 0); - liquidityProviderController.setLiquidityProviderCaps(providers, 0); + capManager.setLiquidityProviderCaps(providers, 0); - assertEq(liquidityProviderController.liquidityProviderCaps(providers[0]), 0); + assertEq(capManager.liquidityProviderCaps(providers[0]), 0); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index 02bd8ef..5ed9187 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -21,8 +21,8 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { // Set Cap to max, as not to interfere with the tests address[] memory providers = new address[](1); providers[0] = address(this); - liquidityProviderController.setLiquidityProviderCaps(providers, type(uint256).max); - liquidityProviderController.setTotalAssetsCap(type(uint248).max); + capManager.setLiquidityProviderCaps(providers, type(uint256).max); + capManager.setTotalAssetsCap(type(uint248).max); deal(address(weth), address(this), 1_000 ether); weth.approve(address(lidoARM), type(uint256).max); diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index ef02344..fac1c6a 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -11,7 +11,7 @@ import {Modifiers} from "test/fork/utils/Modifiers.sol"; import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; import {LidoARM} from "contracts/LidoARM.sol"; -import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; +import {CapManager} from "contracts/CapManager.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; @@ -122,22 +122,22 @@ abstract contract Fork_Shared_Test_ is Modifiers { // Set the Proxy as the OethARM. oethARM = OethARM(address(proxy)); - // --- Deploy LiquidityProviderController implementation --- - // Deploy LiquidityProviderController implementation. - LiquidityProviderController lpcImpl = new LiquidityProviderController(address(lidoProxy)); + // --- Deploy CapManager implementation --- + // Deploy CapManager implementation. + CapManager capManagerImpl = new CapManager(address(lidoProxy)); - // Initialize Proxy with LiquidityProviderController implementation. - lpcProxy.initialize(address(lpcImpl), address(this), data); + // Initialize Proxy with CapManager implementation. + lpcProxy.initialize(address(capManagerImpl), address(this), data); - // Set the Proxy as the LiquidityProviderController. - liquidityProviderController = LiquidityProviderController(payable(address(lpcProxy))); + // Set the Proxy as the CapManager. + capManager = CapManager(payable(address(lpcProxy))); - liquidityProviderController.setTotalAssetsCap(100 ether); + capManager.setTotalAssetsCap(100 ether); address[] memory liquidityProviders = new address[](1); liquidityProviders[0] = address(this); - liquidityProviderController.setLiquidityProviderCaps(liquidityProviders, 20 ether); - liquidityProviderController.setTotalAssetsCap(100 ether); + capManager.setLiquidityProviderCaps(liquidityProviders, 20 ether); + capManager.setTotalAssetsCap(100 ether); // --- Deploy LidoARM implementation --- // Deploy LidoARM implementation. @@ -177,7 +177,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { vm.label(address(proxy), "OETH ARM PROXY"); vm.label(address(lidoARM), "LIDO ARM"); vm.label(address(lidoProxy), "LIDO ARM PROXY"); - vm.label(address(liquidityProviderController), "LIQUIDITY PROVIDER CONTROLLER"); + vm.label(address(capManager), "LIQUIDITY PROVIDER CONTROLLER"); vm.label(operator, "OPERATOR"); vm.label(oethWhale, "WHALE OETH"); vm.label(governor, "GOVERNOR"); diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index a4b408d..4d78006 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -62,18 +62,18 @@ abstract contract Modifiers is Helpers { _; } - /// @notice Set the liquidity provider cap for a given provider on the LiquidityProviderController contract. + /// @notice Set the liquidity provider cap for a given provider on the CapManager contract. modifier setLiquidityProviderCap(address provider, uint256 cap) { address[] memory providers = new address[](1); providers[0] = provider; - liquidityProviderController.setLiquidityProviderCaps(providers, cap); + capManager.setLiquidityProviderCaps(providers, cap); _; } - /// @notice Set the total assets cap on the LiquidityProviderController contract. + /// @notice Set the total assets cap on the CapManager contract. modifier setTotalAssetsCap(uint256 cap) { - liquidityProviderController.setTotalAssetsCap(uint248(cap)); + capManager.setTotalAssetsCap(uint248(cap)); _; } diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 33d69c9..84d0909 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -7,7 +7,7 @@ import {AbstractSmokeTest} from "./AbstractSmokeTest.sol"; import {IERC20} from "contracts/Interfaces.sol"; import {LidoARM} from "contracts/LidoARM.sol"; -import {LiquidityProviderController} from "contracts/LiquidityProviderController.sol"; +import {CapManager} from "contracts/CapManager.sol"; import {Proxy} from "contracts/Proxy.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; import {console} from "forge-std/console.sol"; @@ -19,7 +19,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { IERC20 steth; Proxy proxy; LidoARM lidoARM; - LiquidityProviderController liquidityProviderController; + CapManager capManager; address operator; function setUp() public { @@ -33,7 +33,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { proxy = Proxy(deployManager.getDeployment("LIDO_ARM")); lidoARM = LidoARM(payable(deployManager.getDeployment("LIDO_ARM"))); - liquidityProviderController = LiquidityProviderController(deployManager.getDeployment("LIDO_ARM_LPC")); + capManager = CapManager(deployManager.getDeployment("LIDO_ARM_CAP_MAN")); // Only fuzz from this address. Big speedup on fork. targetSender(address(this)); @@ -52,9 +52,9 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(address(lidoARM.weth()), Mainnet.WETH, "WETH"); assertEq(lidoARM.liquidityAsset(), Mainnet.WETH, "liquidityAsset"); - assertEq(liquidityProviderController.accountCapEnabled(), true, "account cap enabled"); - assertEq(liquidityProviderController.operator(), Mainnet.ARM_RELAYER, "Operator"); - assertEq(liquidityProviderController.arm(), address(lidoARM), "arm"); + assertEq(capManager.accountCapEnabled(), true, "account cap enabled"); + assertEq(capManager.operator(), Mainnet.ARM_RELAYER, "Operator"); + assertEq(capManager.arm(), address(lidoARM), "arm"); } function test_swap_exact_steth_for_weth() external { From faa84950691372b340625d94c6f586c5e7e65242 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 16:03:18 +1000 Subject: [PATCH 135/196] Renamed outstandingEther to lidoWithdrawalQueueAmount --- docs/LidoARMPublicSquashed.svg | 2 +- docs/LidoARMSquashed.svg | 2 +- src/contracts/LidoARM.sol | 9 +++--- .../ClaimRedeem.t.sol | 10 +++---- .../ClaimStETHWithdrawalForWETH.t.sol | 12 ++++---- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 28 +++++++++---------- .../RequestRedeem.t.sol | 12 ++++---- 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/LidoARMPublicSquashed.svg b/docs/LidoARMPublicSquashed.svg index 6801a6f..2bf4713 100644 --- a/docs/LidoARMPublicSquashed.svg +++ b/docs/LidoARMPublicSquashed.svg @@ -38,7 +38,7 @@   steth: IERC20 <<LidoARM>>   weth: IWETH <<LidoARM>>   withdrawalQueue: IStETHWithdrawal <<LidoARM>> -   outstandingEther: uint256 <<LidoARM>> +   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> External:    <<payable>> null() <<LidoARM>> diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 9b54e02..359fde3 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -45,7 +45,7 @@   steth: IERC20 <<LidoARM>>   weth: IWETH <<LidoARM>>   withdrawalQueue: IStETHWithdrawal <<LidoARM>> -   outstandingEther: uint256 <<LidoARM>> +   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> Internal:    _owner(): (ownerOut: address) <<Ownable>> diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index e40e7c9..0a95d07 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -23,7 +23,8 @@ contract LidoARM is Initializable, AbstractARM { /// @notice The address of the Lido Withdrawal Queue contract IStETHWithdrawal public immutable withdrawalQueue; - uint256 public outstandingEther; + /// @notice The amount of stETH in the Lido Withdrawal Queue + uint256 public lidoWithdrawalQueueAmount; /// @param _steth The address of the stETH token /// @param _weth The address of the WETH token @@ -90,7 +91,7 @@ contract LidoARM is Initializable, AbstractARM { } // Increase the Ether outstanding from the Lido Withdrawal Queue - outstandingEther += totalAmountRequested; + lidoWithdrawalQueueAmount += totalAmountRequested; } /** @@ -108,7 +109,7 @@ contract LidoARM is Initializable, AbstractARM { uint256 etherAfter = address(this).balance; // Reduce the Ether outstanding from the Lido Withdrawal Queue - outstandingEther -= etherAfter - etherBefore; + lidoWithdrawalQueueAmount -= etherAfter - etherBefore; // Wrap all the received ETH to WETH. weth.deposit{value: etherAfter}(); @@ -118,7 +119,7 @@ contract LidoARM is Initializable, AbstractARM { * @dev Calculates the amount of stETH in the Lido Withdrawal Queue. */ function _externalWithdrawQueue() internal view override returns (uint256) { - return outstandingEther; + return lidoWithdrawalQueueAmount; } // This method is necessary for receiving the ETH claimed as part of the withdrawal. diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index bad93ab..83f807a 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -119,7 +119,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { // Assertions before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); @@ -141,7 +141,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { // Assertions after assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); @@ -186,7 +186,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { // Assertions after assertApproxEqAbs(steth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY, 2); assertEq(weth.balanceOf(address(lidoARM)), 0); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); @@ -210,7 +210,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { // Assertions before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); @@ -234,7 +234,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { // Assertions after assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol index 7ece103..6411a2c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol @@ -43,13 +43,13 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared requestStETHWithdrawalForETHOnLidoARM(new uint256[](0)) { assertEq(address(lidoARM).balance, 0); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); // Main call lidoARM.claimStETHWithdrawalForWETH(new uint256[](0)); assertEq(address(lidoARM).balance, 0); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); } function test_ClaimStETHWithdrawalForWETH_SingleRequest() @@ -60,7 +60,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared { // Assertions before uint256 balanceBefore = weth.balanceOf(address(lidoARM)); - assertEq(lidoARM.outstandingEther(), DEFAULT_AMOUNT); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), DEFAULT_AMOUNT); stETHWithdrawal.getLastRequestId(); uint256[] memory requests = new uint256[](1); @@ -69,7 +69,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared lidoARM.claimStETHWithdrawalForWETH(requests); // Assertions after - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(weth.balanceOf(address(lidoARM)), balanceBefore + DEFAULT_AMOUNT); } @@ -82,7 +82,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared { // Assertions before uint256 balanceBefore = weth.balanceOf(address(lidoARM)); - assertEq(lidoARM.outstandingEther(), amounts2[0] + amounts2[1]); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), amounts2[0] + amounts2[1]); stETHWithdrawal.getLastRequestId(); uint256[] memory requests = new uint256[](2); @@ -92,7 +92,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared lidoARM.claimStETHWithdrawalForWETH(requests); // Assertions after - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(weth.balanceOf(address(lidoARM)), balanceBefore + amounts2[0] + amounts2[1]); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index e013db5..bb41d4f 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -106,7 +106,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before @@ -129,7 +129,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions After assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(address(this)), shares); @@ -152,7 +152,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(address(this)), amount); @@ -175,7 +175,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions After assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount * 2)); assertEq(lidoARM.balanceOf(address(this)), shares * 2); @@ -199,7 +199,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(alice), 0); @@ -223,7 +223,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions After assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount * 2)); assertEq(lidoARM.balanceOf(alice), shares); @@ -253,7 +253,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain); - assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether before"); assertEq(lidoARM.feesAccrued(), expectedFeesAccrued, "fee accrued before"); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before"); assertEq(lidoARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before @@ -282,7 +282,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions After assertEq(steth.balanceOf(address(lidoARM)), 0, "stETH balance after"); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "WETH balance after"); - assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether after"); assertEq(lidoARM.feesAccrued(), expectedFeesAccrued, "fees accrued after"); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + depositedAssets), "last total assets after"); assertEq(lidoARM.balanceOf(address(this)), expectedShares, "user shares after"); @@ -319,7 +319,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ); uint256 wethBalanceBefore = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - 3 * DEFAULT_AMOUNT / 4; assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); - assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether 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"); assertEq(lidoARM.balanceOf(alice), 0, "alice shares before"); @@ -348,7 +348,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { steth.balanceOf(address(lidoARM)), stethBalanceBefore, STETH_ERROR_ROUNDING, "stETH ARM balance after" ); assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after"); - assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether 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"); assertEq(lidoARM.balanceOf(alice), shares, "alice shares after"); @@ -379,7 +379,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { uint256 expectTotalAssetsBeforeDeposit = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100; assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoARM.outstandingEther(), DEFAULT_AMOUNT, "stETH in Lido withdrawal queue before deposit"); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), DEFAULT_AMOUNT, "stETH in Lido withdrawal queue before deposit"); assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply before deposit"); assertEq(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, "total assets before deposit"); assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued before deposit"); @@ -433,7 +433,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq( weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2, "ARM WETH balance after redeem" ); - assertEq(lidoARM.outstandingEther(), 0, "stETH in Lido withdrawal queue after redeem"); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "stETH in Lido withdrawal queue after redeem"); assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply after redeem"); assertApproxEqRel(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, 1e6, "total assets after redeem"); assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued after redeem"); @@ -494,7 +494,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions After assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after @@ -557,7 +557,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(receivedAssets, DEFAULT_AMOUNT + userBenef, "received assets"); assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertApproxEqAbs(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, 2, "fees accrued after redeem"); assertApproxEqAbs( lidoARM.lastAvailableAssets(), diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index c563be8..8b8a48d 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -31,7 +31,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); @@ -55,7 +55,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(assets, DEFAULT_AMOUNT, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); @@ -74,7 +74,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); @@ -102,7 +102,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(assets, DEFAULT_AMOUNT / 2, "Wrong amount of assets"); // As no profits, assets returned are the same as deposited assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(lidoARM.outstandingEther(), 0); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); @@ -142,7 +142,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(actualAssetsFromRedeem, expectedAssetsFromRedeem, "Assets from redeem"); assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), assetsAfterGain); - assertEq(lidoARM.outstandingEther(), 0, "stETH in Lido withdrawal queue"); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "stETH in Lido withdrawal queue"); assertEq(lidoARM.feesAccrued(), expectedFeeAccrued, "fees accrued"); assertApproxEqAbs( lidoARM.lastAvailableAssets(), @@ -193,7 +193,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(actualAssetsFromRedeem, expectedAssetsFromRedeem, "Assets from redeem"); assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); - assertEq(lidoARM.outstandingEther(), 0, "stETH in Lido withdrawal queue"); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "stETH in Lido withdrawal queue"); assertEq(lidoARM.feesAccrued(), 0, "fees accrued"); assertApproxEqAbs( lidoARM.lastAvailableAssets(), From 5f0b18cd1fc0fa21daaef8c5dbff06776dda4021 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 16:41:41 +1000 Subject: [PATCH 136/196] Updated contract diagram --- docs/plantuml/lidoContracts.png | Bin 15524 -> 18053 bytes docs/plantuml/lidoContracts.puml | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plantuml/lidoContracts.png b/docs/plantuml/lidoContracts.png index c0f52ac41ebcd4b784623ad21063795c3fcc4ca2..731d3c3ecc7d48b9f68dc870e4b9252dcc6130d3 100644 GIT binary patch literal 18053 zcmch<1yogC+ct_cC<0pvK{}*EL=fo^1SB`zNOyO)bR*IYn?_1Hl}5UxLqWPh;>?ZD z`+ev9=l$O^{xSalJcDP*UTZPenseQ8-PgSC{XtGf91D{O69EAM>#c;S0s;aeDgwg2 zNOVMSqvTJ5CIj@02#?=D2ocA~uu(@?S)hi~`=` zNIt@4_(kw0?}skB{R8&Fa5+f6<{>&~{GTIJ6jVu~K_n5R-jJy0I3JS;-)R zQwwT9<_#|A^_MYTw5pri9H_pm`t7v5-g~?|Q%5ydE5s8yY7R_$9DATKTYV?m!_8X#Y2(O`BJ}sKxMfat5xaf%G%IK|M6IL$%Nl!&Z6E8gXC9G z-;946zPs6<>+*)0oRWzKC)$Cbz{Ec?XSjhfAs)YC7#u&rxd!zXY+h9VzF zHfdGu>6NzahK0ueLO>9idMhdfb(!5sLvwj7h7=S`ef(WR_&#}7(1H+7^h1b{mtdO& zMzd55IKq^;;fTWM6i`I)fx#C?7(GD{ogzSz9Gs#7pRyp}lN<*4REYfl zx{O*V-sbq%$b6%-PNUNxg9rGXFi}UV}w`9GntyhpR^UI@Q0cUZ$Kc_ou zkEb?9vRBj&x(Rf6UG|sONd?N-zmVxi;6IBZRj(4)oa0D^eORFE;u7?H|SG~_NmNc=+_@#vpilPde-Othm zyl}K3kcU9mvM^TW5O--K*`AM!o{%Q&J+nPw4UFg95_bhP~9 z=;x}Rz}3D;FxQt0%O&Xmi@92?qxlBM&(7?(e->}QhChj;eMf|w)*VLh?dZ_~C0cbT zT{MFdj6x)c*XxQq%WIw64PkYqH}nyW<`=v5A7AWSuTSTsqCO=uYry6?4Eq9}Msqa2 zFdIjoqw0SoP>J6-_9`dfZ5(Z>PHT(p>er9oM2d$W&@#|WfA#6T-d~X{)M~7qD2j`Q z$Yk)RJvZ!A(RN+m94mNeZFe-+){8FxS{N^Aio>=|!#>F(B<~ANmMQ>-e@=|bIpR2Q0ApmP)_(REs0j5%( z&gXs#{-+Giu6=-s2@a4V#;A4DXtj7e@eL{vSwNoQBO}WQf5Ne1+ibJ!*J;8YZcL5* zQNZh}e)g~+!%M;Zy{(6PBlqoIG4oW3+R^Ep^?NBD9i4qF8{obh4pTUKwP3+X$A`$;YQYT{DtW#^3i$3 zT&-nTFmwfBuSg}0972PKyvrKglsK`*-5 zP)l~vf)An!(2Z4Oz49azq<5P)22)n|z^laZ8(`W{!dH;GhviLs^k`-dW%wEXfuPgw zk5{tkyhGofWlI<~*Tl=XOXb{LH#+a}*u#T`%p3HcwAWn1E!U?S#dPy^))|)t7UpR0 zum5YU8Tf^+FHeAdhq%1t&Fpm8Y}{*6{H9i+>)W~5e_G~LAGRFv_&3`3eUn;wJ=VM$ zK8rLS#|qV)H&}!ue^}J2s3{U`HU^%YWWe1$Y|;C+8J|-;@~Qk0gERJ2k(kAM7oPJ{ zxh!E2&(4Yz3&s-#Zq9SQKq^A$z7uc^%EHQ;gaa{qJejl_prsuGA#p2-6J+9z1hT2z zS$*ebBU=t=*rZ+Ajg5_cGGHkP!q8uQz;{0gFY%SkXZN3f?+NU<>(`=+M)tAK+UCy; z@1aGAZ97pEHdkovKKpfoXtB|hCm9{n;bS3TD`92ctlXe4V@nB zO}(<;(8XOpr(U#ikDlySd0f~HP%$gBp1psZ zLFRo`xx8w>JH+#4}UggI+2FxyD z3_-&Nc{{s*vqCx*y?TF+H*+2arl74jpZBW$V9+~Q>E4o2n97PG`cb~c8E7{J?&~Xz zFT8&~lP*Q`R@ueXmCyTz_l+c*Kv9F8(kL$0qK}l`z(0*NJJ8#RZn3+droZC^c5|vssYT58gF-PJ^_PZQ-r0 zt!|%77P5XT3dUvhE_~SD$>yIIGAo935q1|eV+%)Pch3S3<#CNm`2jpGBwM*uT^Wy1 zU8{~~8`!L6`O8Ux)QfA~LyjU2H#ax+3D&Q|80zVsXS?%5x7xE>J8LYL-dAFzf^#`D zVVKvaw87n+(+=1Uf07y+3vgi0NxS9>zsr>@o+*tVkG4=e~tq( zDw@LWkEeMN{3tIjlDE@9_y_PtUZ-@)>koi7H5N%62d1(Z+>w0^uv0XUY2)rPZQ=15 zhTAe5G>EMBfszgy;Z^ozZ*Xx@ZuL>WA33_==d52rF0I*fFgJ-hYdj-5b0OX4_4vfd=Z;gAkOIxwca_a4Hir+aOd8;QJTl}&;$h1fMNbW16MJX&ML1j z9%)>!f~08~WyH#GJU926>695f;bM!YN~wC@=-X{Pl|1Cs-rCeKM#bgXGkdyhm^k_r ztWQyW2;I>5%4mzl!P)s6lI};^6`k%q%>GTJI_t%?t1KWbQ0?YL|j(kRJv#PkWk6}{gr4W z3Z=w47Pd(&%d)so7TU^q%OvMTv-GZ+v6p_aH}52N=JLpcoDK$BEbK4?oS348!{uAy zC-!Az~prT6MoYx9Elp`B^u;t_7PnKd8?RpKo$dsCwMUDx_YaDq?`Ljar_5 zxlIQ=02I;IlE&f->%uRVru&%A*vpEV546mhX8ayy+o)CkF}0XfxVs*Mj^g-JPplNw zlnLLHW-=(uM;;N`l5^T_vo3q;MdLSi9Vt*1(nNQa1t58c=_iQps&pB9zd_kxrPVg4 zG3HmLqP4(Z?GQa8Jxq&Lhq_0o(%;AD^Y0EO?m{chc{yohh??|wK zdq<*Y^!f)}*+H!zJyjDiP?Bh& z6M8*t*zD(qhVmqLn0kX0cYQ0=sS{Mgg~+k!@!!1y=%A9)vn-=-p(xJ`CH$RpDURo) z1<1z^a2dNx{Oyd6_@wGn62c66SF|zgDXL!D21E4xKj=iZoN-(+#pm+%k<_dNjasK1)MCGP)O4E-iuxl+ z=`PMtzH?I>%q*SQ<8-OyB{AaXXSCahQ+l|rQwNm-g*z~UWu2#-TXe;N&nyx#&eDym zwg?|+u&C_(5pf`8{qaL=pZ|+$d2`{+b2D+vN;Q?338~{``Y8X1{>jK%Dz6DZSNJGPU@sx zTPY2C)XOJA64T!QHiWNoc>K~;03~AIBhD+a~`eDALX z7chDokiiG*3QTCT+Jh4p34(ANz?qWkC=of-ppx!p2;;Fd_+tQQ4hG-)LI|S2!D`Kt zXZAPfOvM)m=D&ww5VP9qWBcGD6S0{`F_v6~umk)4mh1@xskuN)Y(r<4qewNU)LNb6 z492e|c%3SuBwrR&3xgisIoaEA$?wiVq1~GNRRszInVHcY?^?vGc2G|S7pka7oIoYaMd*KvsJd+wrH0?#Qbyhb5#vdA6Bw`&tU+QMhQN>LP%Pa z;SpE;y*%V+4MKT;EsL(bmSdd0F%J@5B0J3#XIJbcJE6ZbnOOJM%wq#;YEbI0UNQ@{ znz5`hw4Hg?Pl!SHGQ zFkcmqA;&L~L;>Tiy#4a{&2OQ@a+sJ+_P-|cJ&dY&Nbbb3zxF!ajL(RmnCayW<Y+9!SLE(0&A;7kus&O zM5b+=TyXNm06)|3IHjNY`E0QfaeIB+g-C;IbCz}sn=8$4Ky*+QkB*^UAqSQ+^nGjptzoFuz_EX?- z=kYkC^oDyPmx(4HrDuz}O_TfWYOAmcie^zmQOI z)wG?>;>~`+@HX(pZ(8+ZTr#WZ92Wl?0gaCSRu*^O!F2J+Ou4c$%{+`w1EtlWQ8Z7Z zKABw*4}I@DH0I)|HRoc7U%t-1vz{x4WngUa)sN{73LIjZVb$eeH7OQW(;cb52xGbla=Z&bwsd-J9?8GB_HdbPhB?^v^Ga;WMn%g! zt@WUk+Ewb~M$`R+S{Ap178A!vhv*e320rwq1^X-}9xwhnLM~)-5ohyxUmf3T%520l zfutuF(ldsWUQ37^H0NjW=5>58G&n6=Q(dnu+3C8TM`()u4gLiIL^scyAI?IWlNy+x z+f3WNB6Ct4`n`Db8xV1LxvvIP0_S6`^I$JIxnJMCT}T*&b}zcO*_ndfNDELWjZeLv zG>vFk$rWX#T8}(qz&(?oM|>8_sUlWjYqy{Jkcy<-?7Yn5!?@MQs}9FqdG@PEf1mg{ z&2bvDMQ^ff8Emorx#lNtR{7>H56IYtV?*5WD#AZ~X!4$~)j!*l-)wAFdQH|J6>0MV z!4g_xjZrQq8($*x_xTT2dJJYgWNFv8;&Q*6C*~WdxA1T0=PA#eGDq()lyd&q>W>W8 z&AEF)*@}M428Xb3BO2sRCPKhcW0N3=Z$8wD=r@JlnRTxm#`b9>h8q<| zuy}h5q^!52$L{?}olhiST>;+aKFlz^pvKJNZASsDTGl8|_m;;q*0AMG+C4 z=>E??L!D7_7~qEgdCaf>C&oP9Tg-@oN12d-cN719(tlTieFY#{TFDA^DR7m9ozczJ z1)v`%@I^#KYA&e6BLe~g*h0ankYbzPlg#lC5?Qp&@E&rJ^YZeh%8P;}{3%$w6}uNE-`l`0f}k&A&YOtC zlM`lzT!{}58bW4x%Egrcl2}k?BqmAl91bmoC?0}P|t#ki;Hp}KLFeA=g{?1JJK8Yj)bl0n+PHlnrf2~ zKY6%CYo_h*+t4k8ClAL2&!nPf^L@NZ#P8J;neMqWBkJiCqDb_ORguDgwKo|U?JGy# z+_s^cYg`}UK|CRPARoz_-L?R3u$mee8oOa!4fpr_Kt4WyOdyg(WL>)yK&Pp^?M0MQ zb7vq4(a8DV%_*f==4 z^SW|(C`$`eZ4`oDxQG)8w+9f-v+nS|=+n7XOzX2X8* zyln~EdQLH`eyt5K>1n57PH^60@1wyhKb@zz@bLP<>))Z_v2_>aj_41He`L`&*47@s z2&~PA6b5jAk`onl1+^31(I+OQV*>{oABYXhoBE?iq-OaR!18gp#&!!1uZP=E4m-yZ z1fC0PN@2V`YD54LI}^e!kFkvy27W0-mVqKRwq=7~7?uO`)q)KkK2!u?U50puKe>oE zAzHKH?@5XvMcrtNCj2JZ|o%H5JlJN_n3xdX0FO5q?&Fpo8nU=4GEv!~^E6gV9@gpXu(Y^kW}&QKowv zjIsIqA*N5O1wT0Su|7?t0qM-3;PEagKyMR&P+oSfMl@*$G3gH_*&*!-Cw>NH7%xXZ zuaVszDrYhsMY5Bx7+6fGu(X|@3%}6%n?q0eEr+V+%EZmFL!bU(DXc={6F7v1F?aQb4?P|{aAW8!U1_S*vNbyR-> zaYaOsK4=UUZw+3?NlHK^B_f+C72t-XD~{2VFlBC?y!c1wOUQ~xqGr|hUd(ez47ygT z(&1#;)3-|++S2pHJLDX#72(x^$b1^RCiByq32gd#KPhn%6s()i6--Lc{#o^ji&(~b2<9feT{0?!5RW_mkptx2a}R=;{CrF z@6%0uQZu71LM9<#(fkOuol7<9Vn0G4w*UQv&&^4T){crgrWT{cyz&BMy+hq5!Z2tM zO8hX`T6Y`rEZb)@n8#MEfrp!wYGd?pB$m*qO~Xg ztwg<$y6i=F7AEVjXRXYYJR`xxYeq0r$ip)0(I3}2g|+upC+h9Te-@}aQpT?;rb@Cg zF|G}!)PdW%}=KmURP@SW83{S?mygi3z#76`aXbu zXa_)3CXEMdsKQGQcw~s5PGED~g0d3hn7F6BYBiLTi{42Tn>%r`;cLw3i+Zi+Fh#ro zk1rsN_14DARp_<@Oe=`g9g4>&m)X|li^QtcV4y6iYWL=Gk*@0|+7G^Le)34IpC8_~ zeipuqG2Ar44FS9{kD1l00f+n9csMd7#6~U&$4=c#B9X=4#XI>56T$5lyHYKqS4`C7 zSTEziyV=-C(&{{IzPbFZr>8fZA>h0fmIJEJ%9qv{9!u7%3lFef^EkA8?jR(YOxDuA zS^=SLIggHFXIgHUzde=vl|UXz-6;*ja!(9ZAKFLuA8o1_DSN$VVt~I((a-)R_a$aq zoZ4QdpB(LS2nUTd@g4fu9SOjCKXtF<`&GrGm;HOOdbpkiUrwE7t{d|hzYe@I;C9-+ z*z<1qnQVa=<8d3O6t#dq>TeF8ib| z#Lj0DZ#8IcWzRp>Lr%)S2K2D6XjB}Zn%DLp4^x(V*9x_2jA8dE?c>9#2V-bN-9)z{+*sQ_ zNJfu+VvO-jKqJ&yzWMpwIE}t>z#LmsHy`;n-EOBgSjJKL*P7mmWJO)rmbuoPR(XKw zpLK>*Pq`37qA#I;EfVVINkcxv{Oj4`goj;wVe1#>M45NhQ{0zR2ba(q36x~**E4Ea zalsRe?F%f^wDu3x8Z*7I^W(`E>GPtSO=oTZg`W@|67M5yCCdu3d-CwN^a26viusdqFFDs?SYaSRW1ulL2hdFlbf{CQ{_c?nm zGK;8qim(@ls{#olVHq1mvqd$gXM2YtkBbF4ix|JLs5`^V%mjqX<}Zs6==uLBwrHO2 zJ;BRvaT{+@*b`96kG1t+|6quowu!bzOo{@r&45%{36{dyk^SU5cI1a{8*jhmwo=>k z8ycsgIb|+h*dt&AuS;5@#PX2Ozj_dy=T9K0eZjg12)|V|V!QW0-@DRGqX_D%dv9NU zzKbn5mY9T=Z|^618P6}#8GFrOyi9+QfJj-4CqAec%v|@NxtK&iT$`T4Qh1BaD9w;m zP0XiXIZVRgfw-IC0*-~J?pf~oR!_C8)axa&ibFK1o?`1h`bbt<+Xz_gdW#3AHSv4k zoageT4F(xO-r|iBomT2*3GaEHke;GAx62}YEs4Od-@{Jcv*mUohDW+MNQu=C6S38g zb;IN3RdsnsgRd)T6r3LK)Z7%FY*r;s>>TS=3yq~(r*9dgj$7cCy6ADoM!n2B$KD{d zWjl`&p&=&`KgHk7JYB{ltQ?IMP=3PPe2K`{sEMlbl3~Fa6lW?p zl!D|y61^B(Kfdt`TdV31Fd0?|pEaI+SzA5DG`1hk=f7^e8l+PARhqO~!YYH=xmu?5 zL{d-~<7-)z&=J%LjROUCmz%%jmX0PCLMZZf*v52Ebgw)@B1>Z_UJgM7{*Q~kX zP0&rm(O9Q|m=S%EY>9Qt3#Zc-HAzIKz-Ptj_|eu0deWWwkbYmBNAt1>h48tbg$*3Y(QV^+8IV=ExjuU;ky z>X#r>uSn223)HaKek=6Mw|p-ZXOtXd{9==g%ud)W&vS7e;F3-ChVZHzoS(w&hHzXm z#3k!UD`TPabaRCN8JBDz7P~wm?HXg4QY;5x*QfLc8~Vy;&#ozcQz*I80K-U6XiKk8 z-SdJw`idY%+PD4ya(B`h9;o}XM`NeSwR@KDJ{c0FkK}fh4k04xb=<4jLs4o-WKUuG zuzD`Rb5_+e;*~N=@O3zX*l(Mj8{WkC&Oa7p4 zk#W6pGc8dAbO^XWzrh}BSX`JMy>A{eLwjCv(zlYhavdmR-*nAzwUXc4&(fV7vnkO8 zwPL?_-?-b3dzU?!;@`HcIjx?(w#89Q!REm+ zw>>>Akf;j-k}kVPbgJy82j^ocyQ)3&OCi#C)3H%^o*%8%-5E~ANH*Ng_G~Przh`Q= z`KjtD&SA~xK}8a8+sD_K=C$FcZ_TfutnWM2IpbANxR`E>jE%ma4|n%>-?{73b4kI= zl|?D(AGD1zMdNZ_qPYvbUD=-bJ80fPgU@%;aM8xSPs;Pl;&LI?L>8Pv9OwbjB`w_Q z7ycF%8;Zg$vv&Eeu->`?V9+oc+P!M%!pgZe%35&NZ1tv9DxJ_|SA1-WV~T|ul)apD zJajee4o|cv#4jmo=2v9OnO=0v*RJQr?QmEUYY@HcuXg@gE3(#aTinn1>lwq#iicM= z{y*xbUPuu`J>xcZ$;4{3Y``7)Xb80_#{KN?aVCacb|^)iUKdLTs)*)yi02e^OswC_ z;Upv_O~$<4T3S}gP#@&CQ1fsOKiOP4)W1VBHkAb8jT!3C=WR!F$12o}q=yM_y`C-e;^ilh zcKFOGpu!##G=!1fy4*%S&A!MFzKK;3>5{+cq^2GZK~W;=zu;7PH21G7>DvvikUH6s zwy~&n!6Hl`ApP_3Z6}?we=?4Kzkw6#EG5V?W}w=wChc+oPXk_{&84_SL#M<)A67(Z z0Lyvz(%@WV+lNPgcaq>Q82q)Yjm`deQEGSr5DGnqY|FGm5djy=xV*mJEdk$=`p-XI zZ!gdGhW}m&-|zgNiMFFZvI7YH=8pe{yi}t4&!d8~U9RW23~;vmKPtn!2)zO%!IVQ# z{{J7dD+J)zWV&`UX1HX%HyNn0_vRZ_G#+4KO>9IxeFcF)I_N0Dsj?1G!pS{XZEz@3 z;{M^Y4~XP9xNwS@LNcDtcU(tDncHQbT=;OUx3dR6R9W|jN6(rl<&}M~4>tzGx;_7# zvfmNp{Ua56BJmg=9v*<3Rs!N5Ka~543`(IRaN)dW%)6AA?Z~i_rJp~4?(8(1j8OjH zXz*H#8KCZFWPA*C(iGwNa8^BgAVZ*4wNhUaW3Tm=|G%klAd6BUBhpL9y&LtDe9jvo zjN6N0Z$dmc+dhGTjU5b`{(wRO^)qL8JzRU+6++7A#s=ALr}zn6$s%DtKAI3%0x&rn zEm8q5UhkVLGOu%Ac_4}a$0TaE=Ns#QUKr?CriAY{0Ko3SCfA>h6zc#S9q(&SL$Gsx zcVRfm>z5j}7MIbFp2S7VZYYHuG{7_TVw9-;*$Ex9+lOlIQm!wgMd|g{7y_OoEciT=taEQ*kW~2T;9@grD5Hi7YKCd zgs_j?wv-$40pnBMWeRFX=m2g*SAdW{=2`g?a<-Z_blKU;BAOC z+=A^CZ|_^6XkWNV0$mHWremnS(^Wv6pTMAW2}JNfY#yl)s9bk-`MX@DMD+kj<%Q)l zrIVl2xwf&FHy!o?o&WFgA`XjbHf7dFaC-W!zmC_%I;dDb&NRDo-CUlZqmzsk$g-+e z7s52O>us`ta5*JK18fvUADcV&gPWcn9v+6|YBf56)&Jsk&2zjD#J`Tx|4cuK{7b9j z??g*(g)KYq8#{UZ!FDhOu@U94NA`(Ad-S7S%%u?jE7|WPdVu5>c4#1nQuHZH9#v= zt1>83E+rbhE_gGa#vftP5I#srN%`<8cQnu>1I_7se-JBRpDm^Q05~Q*+F5Zn23$(rGT+e$#bc@Iw~7u zWVp+d?SVE`jf&Hh;H3dGGj4=yuNc}VHUWo~{M-;)`uuN9seb{MP{4UN_5RK3|J;CR zn&{fw+hLE;gO9f+q&O+V$R4n61L^lOnbdmsGpkssPauUjgG>N=eYy%m|MVp=f{=LP zkB0qV3a~sN)M@`+Y>DW47dJQHdrJ4|flnqfsipI}X5$ygW~2jcazV_y2GFBo_0eFw zOsf%uq9JNo5Yu)STfLj|vXlv&uFh=*1O#wD07t1Yn*ee~vf*AHnJxS@pauTjIhOlY z5$M_t`x7_MIuepVF9zVowpRFfc|ANmCtxj}e;Q^o)n%kVvQdD@X_f@mOpU}Ixc#}% zLD=!`e9B#L*3ANr6%CG6+gTFYb2*g zAy0TITP%!4y9u37I+Yv9l0(gQfDp3b7oBE3TYxRtfIgYn2K_-+Kk6&(e-0s{U@oCE zXvT0D4*^zie)HD0bt+CmKW27zx3IMW^h&KPLM>D9eovNmdT%vl>)YzpHZHkICNh~8 zV#W`TUlq#d*ytT?3@U~Ve&X!R=CI7QUTjHY)~Mz8yqqp0dSyrbF#8g9>f0;czd15Q zA2i@dFbRu@WZcZjKCLS3(dCcW-o951x~d{BB1v9@F#3ibc@zDxlum;@r6{in1Y0H6 zG}H%#nchkfBw%*oosop>l!krr>X4F$=!<|1RXJgb(6j-KCmcotAx%w0y$H~9ZIa&P zPvYrB&*ob^8`VYCCqbrjf4!C9lluIIe(oj`;FA~%5me5-y}k0A-vu#gN=u?3NUDTk zlk%cJc+k~~Dn!{np}qc1pTIaCfsAT4=K7n5iwn1OkayP;Bnm5BBpm-8hQ|ap(sS(I z@Zc$jF#_zo3^X%s?R^FfWF$Neg2F`$QsHC*3;q5546b5T3Sh_H+IkOY#=$$2FBR_d zt1br!0WhP2qeq}~udtcNaSIvCVQ1D67#C<1n*7;UsWMw_YQNrxLRQFR^K&T8{rDF= zu>klJfg&C;0TbCX710dCXW!-v>iZ+nWH@6UZY5w(u+@m7^L;3bK3*qwCd`w1$ z4p8tboy!Vb2kbxLFe>c!@j;;81tIeZXh$3OTT==IbJdhx?g$*d3Y#>n%SSGRHzI|| zDt*f=^Y~G3`_R({^ar`PfmBmlEH_)M_L+8#02lY}#eSr}2Lr{`vqhOUBS zHY>Ggi+LUWdip_c@fX0vwV$Aq!23z?BdHrbE)J)0VVx+&Hje9;_xN}2J!w7cW32sf zGu9JBy^jQX{uZ3@{>H?mK&03QxkFo=Bfu-!K3gyHudM)a{aQGrTB#>mU*`mdwvI92 zeLNx_G|^w1FbeEN5H=5Ba1a);zPJ)19J{(zwix!~YhoMe`TUoT5N>0>vqa{{Hua?5I;jk*N_ zBeINl#KwSE+t5+>Z4}jpn!RIl<3C?Q0yqowKN9as+$Dhu)C+NHI*_A8V8;LksNM_P z!o!IzRb>>SF~02T5yj|PpkxHT`O=_lBFviEP@HJC%CH|GTzD_r6PptCO8vmPI%lv4 za1l96AFpz}ILiQZ^(7IN?^!)n9GU5U4_p&4uSayVJp2t=V%~o3c5Jfa7=39vptz%= zBk(?P{-2wh8}-^LQ4D}u z-X<_4DBpA69HogD+ol)tynO_LWH)<`F0`I)O)!FN4uEJ1hjAqA5sPB}gO}&Kje7&m z;~9t?5;yx9X2xB+J^aP87D6CgR!_Z_n5j9w28ksk6;u&3Up1JVYzvV^y&{M>f$J6V zM+$qa+=?}M9u0Dx##(Tu|N6-OzN7dOnZ`X`Foa#>KuUYpkV{{IY%{9nL@PL zRXjAR8-zezQjEO2HxD{sAkX zXzh4rYLEebIJ=&M*`ro`#J+<}rHt3oEer9w~RvEPoSE}Y6?EE8bR6Rig{ zQ4&5cXnvh3R9ns|M_2`gna)-hC3Y+{I%8v4fmYjbBLLvr7_O%iPo1iHP*_j{iA&80 zUfC*DmYg5Vy63GLL0=0h!bG=xFmSJ=;HBa1RQ!D$R(rv zKj&-A2_!Ad+<)Dx{XR!z%&J}f5(PH zV&&@MC`hMc8bV51CP@nR0exLkt%)|SB9j0*+x|SP3_6YZLr;#5hIXPrB7A4Me{#ghtPDYWlCjTg$Vf;!~A1(53j)Y>yP zeUbrUAUp#A0}7CoV37pywwqcw)o8sh0S=hp+P}QJ6amW2|1W(Xg&I9v>}56b#rw8} zsDA-}!XL+J#4Sk?R7c3>)(cIhE}`O4q{}{++BFlp+3LS*0iZSp+1HCD=cVJR;y-OR?hPBMl3E0W)S69QO?0rX~k zYPJ6v+1U8HR=|EE9MhF5)}@gUUQJN&$L~`^I&FR;m3EZAnPs1uT7yYHG_l^k*9~-I zmS~w#VhrI9yZ}sr;`iGXs8W?$v&`g(s4oGM{PfaNy-DlA9oJMRsakC!|2J2Eq!NF4 zZNmrA<$ z2z3B2rxzO)mhDhh*T5v0}BZW5i#0E7Ch~Ag`NbdsCC zGZ)xvuwDcMytJ794!`G1UfsWGa_i}w9)o6~TvjI!0C3jnn2XKsQ=OeTpumu&LfhB| zu>pwrz`}-f1NU=xbu|@_3Nwz2M$xzm67#(fv*!#9RPj23dokJ&U0;`CZIr0 zHf)vP<>RZ-ODIWDAac!9u=`p%eoZvPB%k+uOUeOV>-#UDeXUvCTy) zuoC~4>ZSQ;?uW~V3;_42GHKS^T9}06-1CyWr#ANKUZiNrU5W ze0+R(XLP{No|x6$`NkmM>3V>-Bx+((LHF_&*aVlj8E^j2>8&Q@Fyw#wO>zmgN3-?GVRl zKwzDug5-7p)=Mn*#rv-hsQW+-=V))Qh%{NQ)u@~|Uh50Hry?kb5b3iYPp=4;R3%Uc z@B^S)K`#N{Q_$>*_JA-Nc%23fp;d_hIexKnshJXcBk=JZa;ZcnA@s+20?(b=@10P9 z`s=F^tlb9W8#d5Xy*T$3LMc8$R@jY8^+q*qH}CKAL?cKdEH5uJEJjol_LfZwZZ9;u z1Ae1+cA5=`*W-L2&`{^*csHw20%l|nXXd7G+=+i;(LTQM2YGGSf6{|j3wqJvhPLB_V;}?(Xh(8s6`{ zWA8K0K6l*9kGaNLli9Pos=Df_s;7hHWJFP(;Xi|cfk6=$d#?Zk1NQ_51{MPe4*U{f zd?5}#XzYd6>jR?2Qax>$|-EXm4-xiIb7h#zN1^-oes>!NA(m zvHuq_7>3MLNzMM>-(g_EIL^skO4C;OZ_qu?Ri#XGJ}aT4OB7-Wqca4P$|1)eSd0yK z{V9=dszAp)o#)aHfsmKoY-I9 z2xd8#>)TR&%7|aPfFqUtgD$yN{)YqaBCBhm|I-`0P0SV2&^NPD%8|La8B9aJ1lh>G zi3uVqDkWHmX}}wp8v3Dz%x=~q@R;}hjC6iYP$K%$OzOGd#3-8KyYY3% zk2SMaRTTZad()QAUaU=&us7M}n@lL5&HJcP*4S3bTaIF+6rnP!2co@)I(Z>{%*F3^ z6Fm#D>Kla#dNz(2mZUFqw36i*Sc+r6a^cMzB`oSrU2B#S_Ff$CCG>l(aZF0$q}`1+ zq)FxMYH-orPBA|x;p85@L2yh(Tb{19w_BO-&pxKmULQQRK6Q1XD1TRp8=?D!;q=J} z&8btKXN&rbo`k0>?^G7A$Ey@klt4{})(Nx;9BGa7Nte;4HhNwB3el-S?+tSuR zauZ2XbYXaD`X~QPE+DnHT2xg~2na2mdAd&^u*De@NM5cPXSwGf8*kJy*Ajo>^^8}S z(!~FeBaPXr&r9cpLDo$2mX{e;=V_*idh71wl>Y@v&2%!+I0{u33=HQJ@%I8s&QrT7 zPo1%Zrvu{sWHz*1t>1_-!odv*koL;rP-jReBEVwFCk-jNVj+zDb&H~Tt%V@~i}KpX zCngAK!{qYj&g6)Q^_%7T3cn!7fX5$)lG#b{$I-CQTrpgEk0FRe^5)}jIPejJ@c-*_ z{vL9+H``!3oLgZskaQD>LNHct(8J?)rY2iwzp0Rf>v?x+!Z&m>z;|yEvbfas1x-=A z!Q{(I#ldD?nuw_A{OrX`{P^`Y>l5zF-I{s(fqAccE$*z$OrmtJ`{f8eqIVx7#{*|> z-yDz&t^H19*6$3uzdLW)+q^vNAyzF?I~>!nCOLRY?EcwYtHE&>_Lr=Ih~-@4<;CH$ z*WKm%?Tyd8s`VJ5xS*O^Laym@PejdWPlq87B1dMI&0;$u-%zFJtt&^9YN>W}VXrLT zt^MiNXy;FsdxrH?`sg%H5)zW-?y!b0hm`iHwcKc)S?fPT@tExg{Ga25tVfav5n*1l zSWdlzUi@L3Ke>u*;Cs?+*gI8aUOro_!Isx~b2_e5+H&XmD~7tN_ID}|SJZqESMc8$ z`PVK-EAUTI8*P`no37$tZXikpvC#@Z7f&{ab-Zp*AD$Vn$aOhhvva*V*_4MnT8(*v zg8!O3ggf}F`JGljjyNu}@$aD=Ndi`jAj`(e?)C)_a^$gBHZDYanh>+GBGdX4cqnop z8nH|qoqC3#ztwF0>M4nn*9-UG=o}addf!qPI|3yu(FsPdN_1M9&Bsf`zfur#J2O4b zQ0_;j^?dX7*{(*jyUYFE?N_KcTpBk02X4dYNOWrzAF+1k0q&Rh`1Qtr@F#Cq<8?k% znZ;5o=H6T$3l)Z``%>P4-xySi)C!coOT^IyQx4^X1pdOABW`4JRz~meL41B9RAoNi zjpejI7iKvUC1k*l_@I6fBNXonqrNA^+)H~nu`#6@H8K#*a(!WLWoOy~mEt6F$+%P= zw@g1$645Yx)+4WmefJAX~Aecp6ANV@Nfqu(CP5pkn^D9QDpr;VWY0?Y9}m75ir)IrJLUoqtH@ZN3@x#RZa9J&I6~54}{rs9|&=X#b;aVk0CK?{&KGEP*I)2}GrR2ubbtZ6h zjaV7>Ckbnet&jS{&8ElINt;d_r-RvzM^seQ`9%lDPSwcl0o(#_nEG(OJekEZAJ9Ha zs%aQ)3GA9djO+|88t5l-t@;6-|2=8ujaq*zCDS3uh-)blu zne`ba)obEUT0gqPA#%{rprBUAU+;KZR@kS8U(; z&3kO|L5&*wD^>iu+rD_l>!XM5$7_t_yb>k4a6GE2lla3PDAZ1IY)qi#o*g)u1bP`4 zhD+qFv0{xDPmh%fOF!-1A)s&a)Xm{Mx7(H?*{X3x3u0)C4*&E-k@}*cY4PA#f%5YP zpc-~e!u9?{=5=j}_9!yZ6`8RTtuM3I&6gqrNx<9nR>$mRIQ;m+R2}czj76t*0Nfal z{YIqU&0fR)3^{B=FPs6(@GTIf-RiH5lQ&L#njC%vox+E`_{)Q7UBDHH5$ZTf$4}RN zqIC52_03^V|7LiibwU zc@HwIb}7*rJW1@s&rogfyYWl$UtehRZMxGL%bVJ!9wTV^it`u$o1zNtJYD=pW|Mxp zSiY5xCTs100!cyZxkl5gTnZHP{J!*BKOrU|=U0oqA>U3vL<<1l`)=p%>U4XR;_zvY zDrAO{hFYh_D&<6@mDNr~#(l4Dz3dBq{*H*S@agU}P8BnzO?H{Vb)!rwH(o=2%XkPZ zBKl9fcnjNKCYTcy#(zS>O0*ib0&jxmH)EV^8lB~b zET+YU1effo|A3&OF405reB*?zPP6wc=JI&mfS_89cdv(*4=B@Pdfa5b*@HbKq$L8a zuFqh>Edb_5+SO&Uw};q^j?HK~=g&T0|E3EQ@HwfH-TKo?SO-Fo&zSY{pN_7))YHsL z^VF%Tl+`$J3VxHBPN6MRvVO%W8Rc9Q~KY&xI& zx@Cd}KV4((^bU(+7md=e#-i7D2%)HNsYKu)}i2F}33&eKVaCA;`(w>s;w??)NPxc~d=#p=_R%jOi?_c|}YC zPQlow{8nP(aQ6Fiw_q=QSImPFrg`0QILg2KTk*szqI~cB_@DFrdFv2#VThp>2*)+^ zPmCoC297Qy=uPJuT_!{}e^Mcb5wLZiwR*#o)0}^~`uI{bf`}eV%EIe~1ny_i7 zQDYT*A^j(tVRC7%aVA%qzydf8$@-9MjFw?)Pca`%lamjZe=~%NIdol#+<)FXi~C^q zoTRdfJl{BC0&i^uiR z&qR2YA9nArAXav-#b9+GFbqDszZ~$&_I?(Ti1`*G0r?frIKRo7!sR5Ql5}S`qLQs# z;J7>Gii2c`q8;2r^2#o#-s{y5%4YvYUTRs)bYvcMr}q5(8@tNoxmEfH?&HjH5SQ zZMjz(I;^<6tYcAc3%vE?rh2iDN#9SeRd1ih>jAI|)DdDru0#*3`eW=<)Z4@H>w%0A zno{&3np5f~L%(|FJ7$+>z|$VWO-#L?p}AAdPagaRlULiM#wwYd`I^XT?X>ies+Dq=_4^m zO-OxOddWyKOD-Vzi&fhp0c$gYYSrm`DX=B5FF960d{!<{YP%iFmlt9@`ME)Pe)G-| z%3^!6F}U*c>vF~|K+KK2D|->%jCh$5k~W(GK}&oHLU+970x5me?_1#ueu_z+`>kn-j<##is{Tnaoczeuy~oj?Cb@Xb;s!ET84LxO-|uKQ|- zM@G1R9zzDS%_&_8Xxi3n|L*pDo}|l(edASB8lTtP<_2)$hCk?>Jmq81b9RvyyoYNn z83Wge5Z?`7ygj!F#_{a5c9Uon3CVEwc@d)@GyXz0i0>BlKG!+L?&iXFv(H6=|7YBON;R<)Rw%QNw2H6!43(Lq%{S-fgxHNP; zkF(k9P|FL}vZCEd@$>M@>K+VaHHZuJ_V)9;X>3MqO-?x2mTM;suH$t3WTYCVPiwz% zbll}X^dhI3Nx;>ap8!MzAn;Hc3BOu{BmG{zNEm(u?-kXG0P$N_)q~Qy{kz6z5C2Ny zhyFia1jS$bq5prsK#-yjwi0X?OT+WBzLah=xpU8(2@%a zGN9asR@YvpGt|g_au7HTh>Ff=>Q>1rT{gZeJaT=BpW6u(L-aH3qY51(M3BTm)*fDb-1MWRhFdVu>yT^UbhEB0F;%=1S zRJE5r&oTCR>c<)H2>;Y$l3Ne(H2h0L!xk`Hw(p-*)$` zG7L9|72k1a$Yk&Xnu1OU|8cWzxIUS=Rk3b@N=*|dr%~zIRL_S%y{RGMs%NKF^j`lg z>bk?>RY`0?W|(%N(LmlG_Ux)x>7mza_npX!uaydiWi;-;#X-fcTX1+%e{eZ|WnB-N zN^nzh$+U{%&S72goyhtzn4VRAYCT%=dOrR26VoAc#j}q_X6BYp6(5Cm-C9LZfaR`4 zP$46Ul_|}a@4m%Th}n*7yyR%q6UZwH%^EquDy6BKClr$ZJF92SjUQhjlvEF3W&L&J0ez|`a* zdrjtlDa>;z;pkS4-z=H8C{H?n@-HC&eVrr}|Bnd|WrTdb>Rev@ut}*@oJm=+O(Z6` z@!8hYEg~UgjMySc;v#c85)BiGa?2aK*)!_4^{9T@3tOv z4ON8y)?s#lk^!~J6p?@| zN9s#Ti_t6TL2aEP6m@ihPxC*^;R&gb{~dm2e;?PeVqK9DhE+~p43gbUraoVim!`@R zJO)3SjoBuyY^{`d3)fbCS3H*qYdDZ=G$<^| zA5YhZKW(BCHe;Dn^GZ3XF~74Vhd)xtKn)i}ld(>O9>pjBPb)J?d4$F8$`>jd=}#icrof!~j+h#3 zzs&a#Vv=jWgP0P0{nwlO)4y` zzi%{^F`_BPdAYHs`B7}JE|)NIt#QanGG0-dWkcm4kW<3GsQKY)3;Wy}^KKIQPco-O z?O^E=SJ|^$DKSn@>fs!sv_BU(iJFWSG#*8ph{@|VYQ^}ycSz2U0>)E&e%ls*yVBJ6 zH-O+nbz=b=O%FM+@uxliNX2b4>0DkXF0>;EXwc7X1DFyJyba@QKFgG79*1k5 zsdpee;W;L=%`In^X#eX^GjWhp9w?!#%`=#@UuM^W(x5o3A9to^r{$08(HY_xr~)PFAFEc707fSvI}a~W?SgJnx_>$TG zOk0l}7R&J9hKu!2-;q}Sc|&>NQT&h{&Z$Mq4>3Z2Y8@K|jz$OS;e32wPx+BU`Eilk zbZ38dX2M`-y5yjboza{wQpYtKiE-{Z+ zTeTw7X?u+C)gmH%if974fPSCWdu6$TcZcub6(BE?*=_ZQvc+*2G=VXW1tr6zJQ_Qb z7@UNN_}9XCft&cd{H<|PWMVI3;P6pdU|#t^blrZs3{3cR@j1`zFe=CMa+t*+VE%0{ zD0KZiavm1_*C1I`%y0x~sU4J9KCSdd(W;gJP^1Wf%ph(7d-rSoih#LGY4ydSkFncS zn?X_1gKw8t1%-@X%;4BAM`TNG{$~M6i3+X5$4-gWd56$w1S;=C!B_%X*^$4}JmJ`$ zh4nTowBMVj@ySV!ip-M#^0CQ7c7j4fe`5%#sZD_D?)}}h^*{T}hB8`i^v1>HzATYRDJTU4Ydg1~o9 zUR;hlaHuRhucmL6tQ&k+R%ivp%Zh1N`coP{4rX$ETBDOoZv_QhQ2dk;PtSVrG$psY zEnCckV=aXBE;qRnC*Fh9*LN1$0W_G@3fU5|Gztd20K0!7JXdL|XuhCtnbu-GWlUT-3E5|A`)2i>p@52Jc6x1t_p83@ z3l=Lyu)Xckoai&;K!x@>PQZ(m;0%+2RC#&%@G8HHeus^$W83P^3+-NZas5>N@|Q9* zbqyNBT==ueWFH3YQcJ(TW;eAM*y(iY77qqalR)eGuqtkow11*RGO2mszp49Mur}(X~ zE+=^isJ4>1eh#|`lMLF|3@q!tGTGSrW6<-xsJXxut>vuz95=Lre5;M~g(BgoT#*RW zVY*QHso9;5g)7Cb%H45GFd6b&W9{6`ug+I8DxU^^LW5~FtzepHN3~3j_$ki?7lT>i*1WVUuHj4E7f~Nu zd9{JL{Q@*;ai2<@+@86hH?YHJg%zYCTzA(D!|zE8MoeS9J+E9?av=N`V{{?oS`A`8 z5kQn6Dj{~h67?JaW&*A4(XEv3E+svVD4a0NsbFcz?>1B~gz?sUIgKK}ti@lIhjK<+ zg^ycG*=b6I!u?YSnV`@(dXl8_#46`|GDP3I%L?H@B=Q z?K8#D%FrM8HY?X!T;e|a|GZcpGPA~c0%u9@A{u5u^|k500Hv!e?4DkXORT?FB>hU#}$2!zQgQ8duBKi^bke z$#GQcV6>N42Whlavct!*9N}I+YE4I$yb@M{X7^ zw7&Kf1>gh(@HWd5p<2NlgBE77d*VVMNOVz1QjfX`fxpwl5~-0=rNLO2EJSlnFXkN- zSz!$;bRH6cz}S#pnQdpVkNlaM5xqd>X2DNd_k5*JTyW+=LDLeCauTBwua#VW0%EF@ zG~_0AyG)#I6rI!k``J$6FLy2$#)eMa7Zm2?O;se0mMcm0v9e<`<+Hl(F@A*o$4py` z0@aP>UrR3%7On=qySPuIs?eItOcH1$mQlbMIjT+DCn_gI^yiOZ#I(s5MjF97%cB z@=7a_=xZLkc%1{1RYS&BzB@%VXF`G!Nr7bW9vR`?$=&#EZhkz6_sdUSVuIZ3obNo( zyk~!EDG~lkmHmTL4Ao0okBq1w$M+RZJsm+lY5tb~3T7khP1TsmC}zYpcj%IKYTixQ zcF|>m?!;0+it;7_R2p03Neu;m^T;~G(of}VsE+vP?6CP!sC+Qg-nE1G%aX%*cWd{q z^DGAW;$l2PBUbxN)>+4U`5^S?L4tmsNVv3RLtpSN*i5QSr6`s}+t5j~xf*dx84oS^ zMrV)T_Ay9ERqOD{ZNZvn5;BHFzt)z&E&j~l<3I1}Ucj?^>6sE4?4qlp%-rwrk+7y} zt9sQgJxonw<%~?y?)au)McmTsm0{nLmN*jHmUVqlx)PtUg zwvJ1QPV5hx>@TpQ{HN3_@yKTE zqZ}6fm@B!@Ns;ep`q{Hl|M1C(m)TxiKWR$C#Ws}hVEfas|3dt>tLv$(DA#XZTo!7i`Ky~D$!7xJL3K^<~Q%~y3Vq(^3bzW&l- z{E`@eQU3zWV+nQLbcRHxOl2a~^K~x4X@%|1hb{X8UxX6>kiN2B_Q2Q3LzjAEdMqR_ z;2POGD8as>P1qixBNbsRk64F+V5dlmoN6LqD-d;d+V=!Lfnf6qc>P>u9xuZrld0<%07 zWB8=%#36!+AOmt)tlp?HkM0(P%X?&PC+^jXmNsmc_}_cJTXkYt6A_+L#4 zWgCv{e8harf?bplzpAq?Lf20Z-yI#|BAtydiDt)Aw<+ z9LPddULA{n?wiE1!%EkCRU?0#PPN6RHImvOrvA^1ByUtMa2O-1?5mP0rz@^!w%7W| z4fO$RjPZlHwyE5V%lo^VrQy(}s1R_fw2XEicK*5JLi^2OisG}T@SiKMK$PQ@XosJV z;}->q>;ul-0Eo{1G4_FcHXI})=~-D1RTL^@wCOZLCR*%&{spZWnt+X4yN&VWcL*XF zgi}J0G(tOkpx_B_NxE(lZ2PKaywa{siZcU~3LU^AgYpoFBqVu^uS{#Sb!^yC3t<}$ z{_q2Z_rC+4^Z&QI(}+*pb^goUAM|aNciy{6uqv|g4j5ANE=yHXaH@1jV&cwry%*8& z8ykDMZl>?ZNwlLE{_8(tb8GiOp?4vO48e8dwAcvYQ1qjNLw<3ezN@EPuT)r=Ti3 z6D42D$o3w^u&7TV-cnqhYM-_a!@j_jpgx(&vkhXvuPw@G(`8d~`q_V>do+WHi74@Kbz+5ewEke$VGIUKo_ zy!*#St)`L_7gqKzi}b*<#4@AFL{W_&YPI}eouHzf0YwKA0bjvLj|J|3ZyZK>-d??V z`Tx!kg7AU-r$_3cEE(h@H2V0!W04Yx-20&@{=hII_Xab5;2J&N2-+`uN%JrTr2p$> z356>I0w&i9A6S6#f1c!joBg~VrP9Nz(EqQOP69_k@`t%TMO7vPpa1hL2|+@}5UtB8 z@lZ|=Oa%Odhi377SZL@-&H2tGv-udE@k6N-ol3LT2A0LQ?$dhGO$N#VpVQu}iXgCb zq}LKLRIuCDMYKCJbuW4A>{j*H9;EHvuXko^2j0_Fl2PMPKo zaqs~|ndY-8bI>Pt({DgSMo&+V z!|E%Wwe|&*#>0eRjHW705uamYif;^NJ!O9APatW6v{yTwA9h0kG|IFa4DQW$PIxJgA)mp~oE@9B?{p;d-q7`Bvk6 zsBbaT=<-tHZ`~&os`n8@Jb-4D85kTIx-(f}vOm`ZcrGH321cYb=dzw6pCtn77^R@Q zGx6se+kU!$ZzyaCHho9|JS!M82GBQP;h*>g1ehg_6{<2K{{@}-BK3;1{rMJ<^?|2& z>{faKvnlWe9s~;u(E+qW@gFVP8OC`vR+UD%pc1q-soDSyZXkv0sm48ErftJAXw_q4 zVph7In&Dn8EG#g#+(1&tOLb%u->!mLmV1C^R&Q&Y@6#N7@78=F{uhAD6^P&QQkEMQ z`5$9G=K`z{I^}{9(5@8{6a+n3MB)TbvG$)|DNmWw+>emmUt?KLReF1{RntFE-6lNO z5>4#yL3@&)MkInL%uW$8Cc0BFv?VX*SLARc9ZVm9?ia%8OPzthI&D09|KhyCz9R>1}FZ??l2PXa# zEXeEr_B{Vt41*Rv3))91Q2iJyQdcZgDFyMuH%$5)>mr*@vwM;^ z;B;Zrsz^=6nfy+`c=hTPCgw=G0DOXHlcfu2*#d@y7`_nb4KspS-QBqKb^vXY%RDw; z4$$E~c>%s(Az#rwpd7$un_vJHKG@bn9C+CM4?+hv1LEO>qoP8Fr9*yuO-%*f`vsdG z*n;d!*@xTc>CsH?7YD#TN&v+G^j!f%aI`=(1rc_cY;Bs8>AK&Z2b2hDei{77Uu zzMBIfkBf5C`r+p|3@->d?R&xr?Uq2nh*BoGR=3Ru)S@=t$R;q6@UMyFJZ>Dh@_tWF z!(McY^h|VMLVyLR@#bZ^@$ZCkP=zYHuGz`uYQkc+$7_3F?g1qLc9z9#Jr?W(?@Co2 zF!lgtkWx1F6;eee=$3(|VdO-==Z6}rS+miC9GR4>oOtbe2RFD8@a!+@>fsdqYoO$| zwzhmWpmxMlkPv9aSaq^fD`*<Ap0CSDRrYF(U`p%C=f|IcLAO!!8p;~Y+q$ostXM|HYuB9IT^$YTcDe=_dxu@dutepn@%ei|FDY}0GTaY zHti1Vy$RIh|EvfOi8znT{{v{9n=Ew(zlwi|eF&VuAJA#lS60&e(+d1-yg8HuNSO{W zbL{_v&Di9H67y+OcppfVYBw38t1sz(EBk`ONW^Zl0BjvC*|Zt3zJO`VCbMIdQtS=q z$$A04!{FdxwW!zSYCIrGJsKdDuBM=%V7=pR!L^^iztQi6L*VUxaM><(f=xRcf;XgG zWu~n0RUWXLfFWG!rz&KN4FY3(Uy+gPw>#g$_wJq27ZNI}d~mlXf>_qDBK*sfuGOv4 zLXrCzYDK_$a{~OC?uy8xw?@B!ir+)h-kA=O3WxM&fQ^oz9vSr#unm8IV=GOL=Jjw@ zOa`iLCl2vi0g8Pz?*q9kkubpF-PYO%%n3aKLsq#1r56*p+|CEj85>`xf{8`TH={*D z)7?{OlxY&T;Ut*c&UU~{_-`8HqV^fYWJ@&b*6<=tnHJK3g#y-H4d6(UqEvWp{3YQq z0EtN|Tm=?&#=&N9J+3)CDFDoY1uEMy#r~0Q49F;J~JCTjR}FSi&x6Wq!0CBEv1ct+k4c9F^7tFZ#(*XD{eir8tYbsYF8WYl;@ z9hsze-+!M^mg#kz?M`>d{c;M%AQJ(-gAPM&Jr#*~h7@4jc+AE{YUP5cZ9iJi9Dt71 zD~y?V9%yOFqu&)Nd-=ypv~cLulQ?M)Mb*8qNu*4KTZiJx=6-6GlG* z8&%)&bbn1j8|)#pYCx1${n(l<{xwp?^RCh(2mJE%$eB z7cIyATtB8Wgr4G_1MU(k5jXA-+%e6aXR^-;CWGchujL7KgM;|zO2BBvyQH>!5q-oq z=iE_9%?PcXW#7LhW40E(PN8XzcBNUg>CI`|R+tf1i2M$~4EM^56fH3B%+6}eBV zAyNExWq^)pSbmgm16K5;Dom8v1M|*&tVrN+as;BwJ(#eD#(Tkkk8g~y>DNM+ziBvc zi@Y4oh4kcjivgJ-?=?@7_xZe+&UxTZ290Vu8NU&dwk1YtHim;ugw9&fU{J~OqBSN* zz7YV#wko2`he%U)xd8JOcAG9OI9E0;JPh!e(=quztx4%X1^E2nsQY7OdGcj-fhV7@ zw$!d>l>iZ;!wj^gia|!eE^h_{B3LBaHlRw2xR$|^vjbA=`}cmH^}E5{l0_OdsCQ#{ z+uF{Hh!CDUDi@Vg?gVDD@Og z8nDrYm6(3*sc8K8fP+g`+I(eZV{-spx20_M!@QY%kH}s~fJmO50lS<`QpYj9h34;Mv@4z?cjBL?*vnX>)yJU}< zmh*+)`D9S^bbzaDr{06l@x+>e(rOQS#k_PROj`ARNHk&+deR;#lz9k1iL zY52p4XK-A{Yj&l@beIC#F)=6e0Ad!jATT>IwkeAM{_3)klD3cSwH$U6u-DtIN|+Kl zL?hsxz&HrldIB5)+%Gc=BLTS{*re=JfLf15tI|6vPai3C5XHD(b}_L#+rU<>N7a=2 zv9S|G7(F$;PTwEzyrYK3o0~#Wv!4a1<<7}o*i8=MSd$uG@x5QabQ>C`V~iC zXrcvNVrbYFX9Uj~?hOcceA}$ux$(DvgE!lNgCl79gy85j^ubRF3<5W4$``4HNq_1Y zDF~n`jcN(q^n>x*)!T=^BZ7!KpN^_J2qn{NuwGyIuvJHz3q@k~DGcgQQvB;Ojx;(|mvVV%t+V z#Aj;v!*uFD5=yGT(Ue;%8k=sRUd~6TdM{81>7FuY3hF#u?y+=hSL1NnOCI5V*aM4) z9v>6-DFHj$66}k`cK;!2)_3ph0Y6_sDgK)s=OK7M0G<^L&1*f>ceZyomp~=>EgrU0 zTmbBze}Qr%Sj;unfm8`_-7moTlV3BBS={TIn{Rx+K#c#jDi9ec1;M6MV+ue|Q?Iol z0jOvUf}}_PKLF~7#QI;PjTQ@4`mR7u#Q+Z6pdce7{}7^x?V!H$%W4N)UfYwrG|wMR zoq$3cfE4zezV~YA>hn4P{l?(@j4edy!E_H`B-;V_)?J1$w1Ysg`YYBv)f?nIbegqt z)eZKWnWj^qt83^4Zq)3=(c&`c!uZtvCRhCjxb&R?3-VX8a zGt%$dn~AIIJ<}sL`sOve63GHk2Ko>#0%+N+zqhL`r$bXc)$}F~0c=VJEc ze;61T2i_h#vkf&x1TQ4{Eub#OfkE%?pZF}}2aeeI57#N?N___j1&i`nYc&S;V_Y+XH7CQ?(w`!rvpGvXt(PIFUB4NuDYV=j|$K%OZJ=V*^{vuDej-iQmJG7mU@6Q&O z83#6Cm4Ju^>LgZOMT!7vjmV^M=7>ct0V4*-hT^Uy^eF-!AqXrv2`8s3fC(Uf6*46} z`v_POOMDg+Ogp?sy<}xbpekW=G*fF!<}Rm(8b$(zLcQ*8scUZ4qR_DQpKf#{+Iv?YCE_Lc)50@PCx}5qQBLRHmTBVBDXa*c<$qC_eP|Ja0Ll zor1cB$eVirN0}wy)M{dtk%ZMPKSSojVm2anC6M!!`IuWk zW1u@zRnJ-Tfl<<`6y+=Cp;o({Y>=pcGgek0S-QU3E?op=fiZBBX4;k12ArkStP^#2|WE~{5 zBLov+3v>tsZ}k0kB(#O!zsEIbZum!XwNEXP0nJ3PcTpz55wWUZ~YjeUZj)n`yr}^pJb!4%Y!kFB?k}s|>M#Cm`#z z0{dMWr2?SDAu^y6RQK2N-3hkONZ(yGo(xGMQNRZPG#t1K0!Vj~WbZ2w4Q4Zh0+Se= z#$P_V6&o}tEGTx#{F*K6Vrsa8DF+Kp=5+XlIWhK96UYJ(uP<0GA8?Fo4@ur9OWS7_ zp$B_DCqii;2k&><> #$originColor { assets: stETH, WETH } -object "LiquidityProviderController" as lpc <><> #$originColor { +object "CapManager" as capMan <><> #$originColor { } object "WithdrawalQueueERC721" as lidoQ <><> #$thirdPartyColor { @@ -32,7 +32,7 @@ object "WithdrawalQueueERC721" as lidoQ <><> #$thirdPartyColor { } zap ..> arm -arm .> lpc +arm .> capMan arm ..> lidoQ From 8d62dfb901b879065554b11c994b09155dbe146f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 16:41:59 +1000 Subject: [PATCH 137/196] Updated _transferAsset to use seth --- src/contracts/LidoARM.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index 0a95d07..00fb9fb 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -67,7 +67,7 @@ contract LidoARM is Initializable, AbstractARM { */ function _transferAsset(address asset, address to, uint256 amount) internal override { // Add 2 wei if transferring stETH - uint256 transferAmount = asset == address(token1) ? amount + 2 : amount; + uint256 transferAmount = asset == address(steth) ? amount + 2 : amount; super._transferAsset(asset, to, transferAmount); } From 0ac44dfcab2cee8ead05a295a3e99f5d3be235cb Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 16:47:17 +1000 Subject: [PATCH 138/196] simplified _transferAsset --- src/contracts/LidoARM.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index 00fb9fb..bb6f2bb 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -67,9 +67,9 @@ contract LidoARM is Initializable, AbstractARM { */ function _transferAsset(address asset, address to, uint256 amount) internal override { // Add 2 wei if transferring stETH - uint256 transferAmount = asset == address(steth) ? amount + 2 : amount; + if (asset == address(steth)) amount += 2; - super._transferAsset(asset, to, transferAmount); + super._transferAsset(asset, to, amount); } /** From 2f522b95026e79f89dbcb62273a055c5f8d33aee Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 17:05:35 +1000 Subject: [PATCH 139/196] Fixed collectFees so it doesn't use WETH reserved for the withdrawal queue --- src/contracts/AbstractARM.sol | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 680351e..47aed5c 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -614,7 +614,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } /// @notice Transfer accrued performance fees to the fee collector - /// This requires enough liquidity assets in the ARM to cover the accrued fees. + /// This requires enough liquidity assets (WETH) in the ARM that are not reserved + /// for the withdrawal queue to cover the accrued fees. function collectFees() public returns (uint256 fees) { uint256 newAvailableAssets; // Accrue any performance fees up to this point @@ -622,7 +623,15 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { if (fee == 0) return 0; - require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); + // Check there is enough liquidity assets (WETH) in the ARM that are not reserved for the withdrawal queue. + // _liquidityAvailable() is optimized for swaps so will return max uint256 if there are no outstanding withdrawals. + // That's why we also need to check the available assets. + // We could try the transfer and let it revert if there are not enough assets, but there is no error message with + // a failed WETH transfer so we spend the extra gas to check and give a meaningful error message. + require( + fees <= _liquidityAvailable() && fees <= IERC20(liquidityAsset).balanceOf(address(this)), + "ARM: insufficient liquidity" + ); IERC20(liquidityAsset).transfer(feeCollector, fees); From 541e2988980061df403d294e61f5d88811d5cfab Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 18:21:19 +1000 Subject: [PATCH 140/196] changed _liquidityAvailable to _requireLiquidityAvailable --- src/contracts/AbstractARM.sol | 42 +++++++++++++++-------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 47aed5c..5a39390 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -297,9 +297,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @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 { - if (asset == liquidityAsset) { - require(amount <= _liquidityAvailable(), "ARM: Insufficient liquidity"); - } + if (asset == liquidityAsset) _requireLiquidityAvailable(amount); IERC20(asset).transfer(to, amount); } @@ -498,28 +496,25 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { return withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)); } - /// @dev Calculate how much of the liquidity asset (WETH) in the ARM is not reserved for the withdrawal queue. - // That is, the amount of liquidity assets (WETH) that is available to be swapped. - // If there are no outstanding withdrawals, then return the maximum uint256 value. + /// @dev Checks if there is enough liquidity asset (WETH) in the ARM is not reserved for the withdrawal queue. + // That is, the amount of liquidity assets (WETH) that is available to be swapped or collected as fees. + // If no outstanding withdrawals, no check will be done of the amount against the balance of the liquidity assets in the ARM. + // This is a gas optimization for swaps. // The ARM can swap out liquidity assets (WETH) that has been accrued from the performance fee for the fee collector. // 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 _liquidityAvailable() internal view returns (uint256) { + 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 type(uint256).max; - - // 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; - } + if (outstandingWithdrawals == 0) return; - return liquidityBalance - outstandingWithdrawals; + // 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)), + "ARM: Insufficient liquidity" + ); } /// @notice The total amount of assets in the ARM and external withdrawal queue, @@ -623,15 +618,14 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { if (fee == 0) return 0; - // Check there is enough liquidity assets (WETH) in the ARM that are not reserved for the withdrawal queue. - // _liquidityAvailable() is optimized for swaps so will return max uint256 if there are no outstanding withdrawals. - // That's why we also need to check the available assets. + // Check there is enough liquidity assets (WETH) that are not reserved for the withdrawal queue + // to cover the fee being collected. + _requireLiquidityAvailable(fees); + // _requireLiquidityAvailable() is optimized for swaps so will not revert if there are no outstanding withdrawals. + // We need to check there is enough liquidity assets to cover the fees being collect from this ARM contract. // We could try the transfer and let it revert if there are not enough assets, but there is no error message with // a failed WETH transfer so we spend the extra gas to check and give a meaningful error message. - require( - fees <= _liquidityAvailable() && fees <= IERC20(liquidityAsset).balanceOf(address(this)), - "ARM: insufficient liquidity" - ); + require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); IERC20(liquidityAsset).transfer(feeCollector, fees); From 123646f8b50dee0d9968a35ef6aff5bf8978ff53 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 21:30:59 +1000 Subject: [PATCH 141/196] Limited prices to crossing 2 basis point past 1.0 --- src/contracts/AbstractARM.sol | 6 +++--- test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol | 10 +++++----- test/smoke/LidoARMSmokeTest.t.sol | 10 ++++------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 5a39390..b11026f 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -13,8 +13,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { //////////////////////////////////////////////////// /// @notice Maximum amount the Operator can set the price from 1 scaled to 36 decimals. - /// 1e33 is a 0.1% deviation, or 10 basis points. - uint256 public constant MAX_PRICE_DEVIATION = 1e33; + /// 2e33 is a 0.02% deviation, or 2 basis points. + uint256 public constant MAX_PRICE_DEVIATION = 2e32; /// @notice Scale of the prices. uint256 public constant PRICE_SCALE = 1e36; /// @notice The delay before a withdrawal request can be claimed in seconds @@ -524,7 +524,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { if (fees > newAvailableAssets) return 0; - // Calculate the performance fee and remove from the available assets + // Remove the performance fee from the available assets return newAvailableAssets - fees; } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 32bda06..a95fb67 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -137,15 +137,15 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { /// --- Set Prices - PASSING TESTS ////////////////////////////////////////////////////// function test_SetPrices_Operator() public asOperator { - // buy price 10 basis points higher than 1.0 - lidoARM.setPrices(1001e33, 1002e33); - // sell price 10 basis points lower than 1.0 - lidoARM.setPrices(9980e32, 9991e32); + // buy price 2 basis points higher than 1.0 + lidoARM.setPrices(1002e32, 10004e32); + // sell price 2 basis points lower than 1.0 + lidoARM.setPrices(9980e32, 99998e32); // 2% of one basis point spread lidoARM.setPrices(999999e30, 1000001e30); lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - lidoARM.setPrices(1001 * 1e33, 1004 * 1e33); + lidoARM.setPrices(10002 * 1e32, 1004 * 1e33); lidoARM.setPrices(992 * 1e33, 2000 * 1e33); // Check the traderates diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 84d0909..5a9361a 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -71,10 +71,8 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { // trader buys stETH and sells WETH, the ARM sells stETH at a // 1 bps discount _swapExactTokensForTokens(weth, steth, 9999e32, 10 ether); - // 5 bps discount - _swapExactTokensForTokens(weth, steth, 9995e32, 100 ether); - // 10 bps discount - _swapExactTokensForTokens(weth, steth, 9990e32, 100 ether); + // 2 bps discount + _swapExactTokensForTokens(weth, steth, 9998e32, 100 ether); } function test_swapTokensForExactTokens() external { @@ -108,7 +106,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { expectedOut = amountIn * price / 1e36; vm.prank(Mainnet.ARM_RELAYER); - uint256 sellPrice = price < 9988e32 ? 9990e32 : price + 2e32; + uint256 sellPrice = price < 9996e32 ? 9998e32 : price + 2e32; lidoARM.setPrices(price, sellPrice); } // Approve the ARM to transfer the input token of the swap. @@ -144,7 +142,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { expectedIn = amountOut * 1e36 / price; vm.prank(Mainnet.ARM_RELAYER); - uint256 sellPrice = price < 9988e32 ? 9990e32 : price + 2e32; + uint256 sellPrice = price < 9996e32 ? 9998e32 : price + 2e32; lidoARM.setPrices(price, sellPrice); } // Approve the ARM to transfer the input token of the swap. From 9d70f655a6dafe3a7a400c4d03dfd06f23f27033 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 4 Oct 2024 23:31:43 +1000 Subject: [PATCH 142/196] Changed so buy price has to be below 1 --- src/contracts/AbstractARM.sol | 2 +- test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index b11026f..043e5e5 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -367,7 +367,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Limit funds and loss when called by the Operator if (msg.sender == operator) { require(sellT1 >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: sell price too low"); - require(buyT1 <= PRICE_SCALE + MAX_PRICE_DEVIATION, "ARM: buy price too high"); + require(buyT1 < PRICE_SCALE, "ARM: buy price too high"); } _setTraderates( PRICE_SCALE * PRICE_SCALE / sellT1, // base (t0) -> token (t1) diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index a95fb67..593c56e 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -137,15 +137,13 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { /// --- Set Prices - PASSING TESTS ////////////////////////////////////////////////////// function test_SetPrices_Operator() public asOperator { - // buy price 2 basis points higher than 1.0 - lidoARM.setPrices(1002e32, 10004e32); // sell price 2 basis points lower than 1.0 lidoARM.setPrices(9980e32, 99998e32); // 2% of one basis point spread lidoARM.setPrices(999999e30, 1000001e30); lidoARM.setPrices(992 * 1e33, 1001 * 1e33); - lidoARM.setPrices(10002 * 1e32, 1004 * 1e33); + lidoARM.setPrices(99999e31, 1004 * 1e33); lidoARM.setPrices(992 * 1e33, 2000 * 1e33); // Check the traderates From 61e95abbd75f1680e8928d6f178e5bcc8ab49c57 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 7 Oct 2024 21:23:57 +1100 Subject: [PATCH 143/196] Increases initial total cap --- .../mainnet/003_UpgradeLidoARMScript.sol | 2 +- src/js/tasks/lido.js | 4 +++- src/js/tasks/liquidityProvider.js | 20 ++++++------------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index b61a5bd..cc4acf1 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -47,7 +47,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { CapManager capManager = CapManager(address(capManProxy)); // 5. Set the liquidity Provider caps - capManager.setTotalAssetsCap(10 ether); + capManager.setTotalAssetsCap(1000 ether); address[] memory liquidityProviders = new address[](1); liquidityProviders[0] = Mainnet.TREASURY; capManager.setLiquidityProviderCaps(liquidityProviders, 10 ether); diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index fad6e07..7da2b01 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -37,7 +37,9 @@ const snapLido = async ({ block }) => { const steth = await resolveAsset("STETH"); const liquiditySteth = await steth.balanceOf(armAddress, { blockTag }); - const liquidityLidoWithdraws = await lidoARM.outstandingEther({ blockTag }); + const liquidityLidoWithdraws = await lidoARM.lidoWithdrawalQueueAmount({ + blockTag, + }); const total = liquidityWeth + liquiditySteth + liquidityLidoWithdraws; const wethPercent = total == 0 ? 0 : (liquidityWeth * 10000n) / total; diff --git a/src/js/tasks/liquidityProvider.js b/src/js/tasks/liquidityProvider.js index 62f6949..3405cb3 100644 --- a/src/js/tasks/liquidityProvider.js +++ b/src/js/tasks/liquidityProvider.js @@ -50,16 +50,13 @@ async function setLiquidityProviderCaps({ accounts, cap }) { const liquidityProviders = accounts.split(","); - const lpcAddress = await parseDeployedAddress("LIDO_ARM_LPC"); - const liquidityProviderController = await ethers.getContractAt( - "LiquidityProviderController", - lpcAddress - ); + const lpcAddress = await parseDeployedAddress("LIDO_ARM_CAP_MAN"); + const capManager = await ethers.getContractAt("CapManager", lpcAddress); log( `About to set deposit cap of ${cap} WETH for liquidity providers ${liquidityProviders}` ); - const tx = await liquidityProviderController + const tx = await capManager .connect(signer) .setLiquidityProviderCaps(liquidityProviders, capBn); await logTxDetails(tx, "setLiquidityProviderCaps"); @@ -70,16 +67,11 @@ async function setTotalAssetsCap({ cap }) { const capBn = parseUnits(cap.toString()); - const lpcAddress = await parseDeployedAddress("LIDO_ARM_LPC"); - const liquidityProviderController = await ethers.getContractAt( - "LiquidityProviderController", - lpcAddress - ); + const lpcAddress = await parseDeployedAddress("LIDO_ARM_CAP_MAN"); + const capManager = await ethers.getContractAt("CapManager", lpcAddress); log(`About to set total asset cap of ${cap} WETH`); - const tx = await liquidityProviderController - .connect(signer) - .setTotalAssetsCap(capBn); + const tx = await capManager.connect(signer).setTotalAssetsCap(capBn); await logTxDetails(tx, "setTotalAssetsCap"); } From 89f71465559185af62e02a0f4cc4f1bbd77ff35d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 8 Oct 2024 12:46:47 +1100 Subject: [PATCH 144/196] Made claim delay immutable and only 1 minute for Testnet --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 3 ++- src/contracts/AbstractARM.sol | 10 ++++++---- src/contracts/Interfaces.sol | 2 +- src/contracts/LidoARM.sol | 5 ++++- src/contracts/OethARM.sol | 2 +- test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol | 2 +- test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol | 8 ++++---- test/fork/shared/Shared.sol | 2 +- test/smoke/LidoARMSmokeTest.t.sol | 3 ++- 9 files changed, 22 insertions(+), 15 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index cc4acf1..b5d58c8 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -53,7 +53,8 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { capManager.setLiquidityProviderCaps(liquidityProviders, 10 ether); // 6. Deploy Lido implementation - lidoARMImpl = new LidoARM(Mainnet.STETH, Mainnet.WETH, Mainnet.LIDO_WITHDRAWAL); + uint256 claimDelay = tenderlyTestnet ? 1 minutes : 10 minutes; + lidoARMImpl = new LidoARM(Mainnet.STETH, Mainnet.WETH, Mainnet.LIDO_WITHDRAWAL, claimDelay); _recordDeploy("LIDO_ARM_IMPL", address(lidoARMImpl)); // 7. Transfer ownership of CapManager to the mainnet 5/8 multisig diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 043e5e5..d6cb810 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -17,8 +17,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { uint256 public constant MAX_PRICE_DEVIATION = 2e32; /// @notice Scale of the prices. uint256 public constant PRICE_SCALE = 1e36; - /// @notice The delay before a withdrawal request can be claimed in seconds - uint256 public constant CLAIM_DELAY = 10 minutes; /// @dev The amount of shares that are minted to a dead address on initalization uint256 internal constant MIN_TOTAL_SUPPLY = 1e12; /// @dev The address with no known private key that the initial shares are minted to @@ -41,6 +39,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// From a User perspective, this is the token being bought. /// token1 is also compatible with the Uniswap V2 Router interface. IERC20 public immutable token1; + /// @notice The delay before a withdrawal request can be claimed in seconds + uint256 public immutable claimDelay; //////////////////////////////////////////////////// /// Storage Variables @@ -119,13 +119,15 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { event FeeCollectorUpdated(address indexed newFeeCollector); event CapManagerUpdated(address indexed capManager); - constructor(address _token0, address _token1, address _liquidityAsset) { + constructor(address _token0, address _token1, address _liquidityAsset, uint256 _claimDelay) { require(IERC20(_token0).decimals() == 18); require(IERC20(_token1).decimals() == 18); token0 = IERC20(_token0); token1 = IERC20(_token1); + claimDelay = _claimDelay; + _setOwner(address(0)); // Revoke owner for implementation contract at deployment require(_liquidityAsset == address(token0) || _liquidityAsset == address(token1), "invalid liquidity asset"); @@ -437,7 +439,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { requestId = nextWithdrawalIndex; uint120 queued = SafeCast.toUint120(withdrawsQueued + assets); - uint40 claimTimestamp = uint40(block.timestamp + CLAIM_DELAY); + uint40 claimTimestamp = uint40(block.timestamp + claimDelay); // Store the next withdrawal request nextWithdrawalIndex = SafeCast.toUint16(requestId + 1); diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 70950ee..d9b1618 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -160,7 +160,7 @@ interface IOETHVault { view returns (address withdrawer, bool claimed, uint40 timestamp, uint128 amount, uint128 queued); - function CLAIM_DELAY() external view returns (uint256); + function claimDelay() external view returns (uint256); } interface IGovernance { diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index bb6f2bb..ebac059 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -29,7 +29,10 @@ contract LidoARM is Initializable, AbstractARM { /// @param _steth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract - constructor(address _steth, address _weth, address _lidoWithdrawalQueue) AbstractARM(_weth, _steth, _weth) { + /// @param _claimDelay The delay in seconds before a user can claim a redeem from the request + constructor(address _steth, address _weth, address _lidoWithdrawalQueue, uint256 _claimDelay) + AbstractARM(_weth, _steth, _weth, _claimDelay) + { steth = IERC20(_steth); weth = IWETH(_weth); withdrawalQueue = IStETHWithdrawal(_lidoWithdrawalQueue); diff --git a/src/contracts/OethARM.sol b/src/contracts/OethARM.sol index 5782ac9..37d217c 100644 --- a/src/contracts/OethARM.sol +++ b/src/contracts/OethARM.sol @@ -17,7 +17,7 @@ contract OethARM is Initializable, OwnerLP, PeggedARM, OethLiquidityManager { /// @param _weth The address of the WETH token that is being swapped out of this contract. /// @param _oethVault The address of the OETH Vault proxy. constructor(address _oeth, address _weth, address _oethVault) - AbstractARM(_oeth, _weth, _weth) + AbstractARM(_oeth, _weth, _weth, 10 minutes) PeggedARM(false) OethLiquidityManager(_oeth, _oethVault) {} diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index 83f807a..8e866e4 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -17,7 +17,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { function setUp() public override { super.setUp(); - delay = lidoARM.CLAIM_DELAY(); + delay = lidoARM.claimDelay(); deal(address(weth), address(this), 1_000 ether); } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 8b8a48d..bef9214 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -39,7 +39,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(0, 0, 0); - uint256 delay = lidoARM.CLAIM_DELAY(); + uint256 delay = lidoARM.claimDelay(); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); @@ -82,7 +82,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 1); - uint256 delay = lidoARM.CLAIM_DELAY(); + uint256 delay = lidoARM.claimDelay(); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT / 2); @@ -158,7 +158,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { 0, address(this), false, - block.timestamp + lidoARM.CLAIM_DELAY(), + block.timestamp + lidoARM.claimDelay(), expectedAssetsFromRedeem, expectedAssetsFromRedeem ); @@ -187,7 +187,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Main call (, uint256 actualAssetsFromRedeem) = lidoARM.requestRedeem(DEFAULT_AMOUNT); - uint256 delay = lidoARM.CLAIM_DELAY(); + uint256 delay = lidoARM.claimDelay(); // Assertions After uint256 expectedAssetsFromRedeem = DEFAULT_AMOUNT * assetsAfterLoss / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(actualAssetsFromRedeem, expectedAssetsFromRedeem, "Assets from redeem"); diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index fac1c6a..50e4d57 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -141,7 +141,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { // --- Deploy LidoARM implementation --- // Deploy LidoARM implementation. - LidoARM lidoImpl = new LidoARM(address(steth), address(weth), Mainnet.LIDO_WITHDRAWAL); + LidoARM lidoImpl = new LidoARM(address(steth), address(weth), Mainnet.LIDO_WITHDRAWAL, 10 minutes); // Deployer will need WETH to initialize the ARM. deal(address(weth), address(this), 1e12); diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 5a9361a..ebff091 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -50,7 +50,8 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(address(lidoARM.withdrawalQueue()), Mainnet.LIDO_WITHDRAWAL, "Lido withdrawal queue"); assertEq(address(lidoARM.steth()), Mainnet.STETH, "stETH"); assertEq(address(lidoARM.weth()), Mainnet.WETH, "WETH"); - assertEq(lidoARM.liquidityAsset(), Mainnet.WETH, "liquidityAsset"); + assertEq(lidoARM.liquidityAsset(), Mainnet.WETH, "liquidity asset"); + assertEq(lidoARM.claimDelay(), 10 minutes, "claim delay"); assertEq(capManager.accountCapEnabled(), true, "account cap enabled"); assertEq(capManager.operator(), Mainnet.ARM_RELAYER, "Operator"); From de025028bba5065575cf25e098761b1d263dcb56 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 8 Oct 2024 12:47:34 +1100 Subject: [PATCH 145/196] Generate latest LidoARM diagram --- docs/LidoARMSquashed.svg | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 359fde3..5c74b0d 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -27,11 +27,11 @@   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>> +   FEE_SCALE: uint256 <<AbstractARM>> +   liquidityAsset: address <<AbstractARM>> +   token0: IERC20 <<AbstractARM>> +   token1: IERC20 <<AbstractARM>> +   claimDelay: uint256 <<AbstractARM>>   traderate0: uint256 <<AbstractARM>>   traderate1: uint256 <<AbstractARM>>   withdrawsQueued: uint120 <<AbstractARM>> @@ -60,7 +60,7 @@    _swapExactTokensForTokens(inToken: IERC20, outToken: IERC20, amountIn: uint256, to: address): (amountOut: uint256) <<AbstractARM>>    _swapTokensForExactTokens(inToken: IERC20, outToken: IERC20, amountOut: uint256, to: address): (amountIn: uint256) <<AbstractARM>>    _setTraderates(_baseToTokenRate: uint256, _tokenToBaseRate: uint256) <<AbstractARM>> -    _liquidityAvailable(): uint256 <<AbstractARM>> +    _requireLiquidityAvailable(amount: uint256) <<AbstractARM>>    _availableAssets(): uint256 <<AbstractARM>>    _externalWithdrawQueue(): uint256 <<LidoARM>>    _setFee(_fee: uint256) <<AbstractARM>> @@ -102,7 +102,7 @@    <<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>> From d3a12368eef749b7869d30e1a326a400d4763708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Tue, 8 Oct 2024 08:56:41 +0200 Subject: [PATCH 146/196] Add Zapper for LidoARM (#31) * feat: create Zapper contract. * test: add tests for Zapper. * test: add extra test for rescue tokens. * Added assets (ETH) to Zapper's Zap event * Added ZapperLidoARM to deploy script * Updated deploy script for Testnet --------- Co-authored-by: Nicholas Addison --- .../mainnet/003_UpgradeLidoARMScript.sol | 33 +++++++++-- src/contracts/Interfaces.sol | 2 +- src/contracts/ZapperLidoARM.sol | 58 +++++++++++++++++++ test/Base.sol | 2 + test/fork/Zapper/Deposit.t.sol | 43 ++++++++++++++ test/fork/Zapper/RescueToken.sol | 29 ++++++++++ test/fork/shared/Shared.sol | 5 ++ 7 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/contracts/ZapperLidoARM.sol create mode 100644 test/fork/Zapper/Deposit.t.sol create mode 100644 test/fork/Zapper/RescueToken.sol diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index b5d58c8..4e37773 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -9,6 +9,7 @@ import {IERC20, IWETH, LegacyAMM} from "contracts/Interfaces.sol"; import {LidoARM} from "contracts/LidoARM.sol"; import {CapManager} from "contracts/CapManager.sol"; import {Proxy} from "contracts/Proxy.sol"; +import {ZapperLidoARM} from "contracts/ZapperLidoARM.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; import {GovProposal, GovSixHelper} from "contracts/utils/GovSixHelper.sol"; import {AbstractDeployScript} from "../AbstractDeployScript.sol"; @@ -24,6 +25,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { Proxy lidoARMProxy; Proxy capManProxy; LidoARM lidoARMImpl; + CapManager capManager; function _execute() internal override { console.log("Deploy:", DEPLOY_NAME); @@ -44,13 +46,13 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // 4. Initialize Proxy with CapManager implementation and set the owner to the deployer for now bytes memory data = abi.encodeWithSignature("initialize(address)", Mainnet.ARM_RELAYER); capManProxy.initialize(address(capManagerImpl), deployer, data); - CapManager capManager = CapManager(address(capManProxy)); + capManager = CapManager(address(capManProxy)); // 5. Set the liquidity Provider caps - capManager.setTotalAssetsCap(1000 ether); + capManager.setTotalAssetsCap(100 ether); address[] memory liquidityProviders = new address[](1); liquidityProviders[0] = Mainnet.TREASURY; - capManager.setLiquidityProviderCaps(liquidityProviders, 10 ether); + capManager.setLiquidityProviderCaps(liquidityProviders, 100 ether); // 6. Deploy Lido implementation uint256 claimDelay = tenderlyTestnet ? 1 minutes : 10 minutes; @@ -60,6 +62,11 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // 7. Transfer ownership of CapManager to the mainnet 5/8 multisig capManProxy.setOwner(Mainnet.GOV_MULTISIG); + // 8. Deploy the Zapper + ZapperLidoARM zapper = new ZapperLidoARM(Mainnet.WETH, Mainnet.LIDO_ARM); + zapper.setOwner(Mainnet.STRATEGIST); + _recordDeploy("LIDO_ARM_ZAPPER", address(zapper)); + console.log("Finished deploying", DEPLOY_NAME); // Post deploy @@ -121,7 +128,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { lidoARMProxy.upgradeToAndCall(address(lidoARMImpl), data); // Set the buy price with a 8 basis point discount. The sell price is 1.0 - LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(9994e32, 1e36); + LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9994e36, 0.9998e36); // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); @@ -133,5 +140,23 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { } else { vm.stopPrank(); } + + if (tenderlyTestnet) { + console.log("Broadcasting fork script to Tenderly as: %s", Mainnet.ARM_RELAYER); + vm.startBroadcast(Mainnet.ARM_RELAYER); + } else { + vm.startPrank(Mainnet.ARM_RELAYER); + } + + // Add some test liquidity providers + address[] memory testProviders = new address[](1); + testProviders[0] = 0x3bB354a1E0621F454c5D5CE98f6ea21a53bf2d7d; + capManager.setLiquidityProviderCaps(testProviders, 100 ether); + + if (tenderlyTestnet) { + vm.stopBroadcast(); + } else { + vm.stopPrank(); + } } } diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index d9b1618..e453356 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -107,7 +107,7 @@ interface IOethARM { function claimWithdrawals(uint256[] calldata requestIds) external; } -interface ILiquidityProviderARM { +interface ILiquidityProviderARM is IERC20 { function previewDeposit(uint256 assets) external returns (uint256 shares); function deposit(uint256 assets) external returns (uint256 shares); diff --git a/src/contracts/ZapperLidoARM.sol b/src/contracts/ZapperLidoARM.sol new file mode 100644 index 0000000..d127740 --- /dev/null +++ b/src/contracts/ZapperLidoARM.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +// Contracts +import {Ownable} from "./Ownable.sol"; + +// Interfaces +import {IWETH} from "./Interfaces.sol"; +import {IERC20} from "./Interfaces.sol"; +import {ILiquidityProviderARM} from "./Interfaces.sol"; + +/** + * @title Zapper contract for the Lido (stETH) Automated Redemption Manager (ARM) + * Converts ETH to WETH and deposits it to the Lido ARM to receive ARM LP shares. + * @author Origin Protocol Inc + */ +contract ZapperLidoARM is Ownable { + IWETH public immutable weth; + /// @notice The address of the Lido ARM contract + ILiquidityProviderARM public immutable lidoArm; + + event Zap(address indexed sender, uint256 assets, uint256 shares); + + constructor(address _weth, address _lidoArm) { + weth = IWETH(_weth); + lidoArm = ILiquidityProviderARM(_lidoArm); + + weth.approve(_lidoArm, type(uint256).max); + } + + /// @notice Deposit ETH to LidoARM and receive ARM LP shares + receive() external payable { + deposit(); + } + + /// @notice Deposit ETH to LidoARM and receive shares + /// @return shares The amount of ARM LP shares sent to the depositor + function deposit() public payable returns (uint256 shares) { + // Wrap all ETH to WETH + uint256 ethBalance = address(this).balance; + weth.deposit{value: ethBalance}(); + + // Deposit all WETH to LidoARM + shares = lidoArm.deposit(ethBalance); + + // Transfer received ARM LP shares to msg.sender + lidoArm.transfer(msg.sender, shares); + + // Emit event + emit Zap(msg.sender, ethBalance, shares); + } + + /// @notice Rescue ERC20 tokens + /// @param token The address of the ERC20 token + function rescueERC20(address token, uint256 amount) external onlyOwner { + IERC20(token).transfer(msg.sender, amount); + } +} diff --git a/test/Base.sol b/test/Base.sol index 3bba2ef..32ac957 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -9,6 +9,7 @@ import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; import {LidoARM} from "contracts/LidoARM.sol"; import {CapManager} from "contracts/CapManager.sol"; +import {ZapperLidoARM} from "contracts/ZapperLidoARM.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; @@ -37,6 +38,7 @@ abstract contract Base_Test_ is Test { OethARM public oethARM; LidoARM public lidoARM; CapManager public capManager; + ZapperLidoARM public zapperLidoARM; IERC20 public oeth; IERC20 public weth; diff --git a/test/fork/Zapper/Deposit.t.sol b/test/fork/Zapper/Deposit.t.sol new file mode 100644 index 0000000..7d8449e --- /dev/null +++ b/test/fork/Zapper/Deposit.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {ZapperLidoARM} from "contracts/ZapperLidoARM.sol"; + +contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { + function setUp() public override { + super.setUp(); + + vm.deal(address(this), DEFAULT_AMOUNT); + } + + function test_Deposit_ViaFunction() public setLiquidityProviderCap(address(zapperLidoARM), DEFAULT_AMOUNT) { + assertEq(lidoARM.balanceOf(address(this)), 0); + uint256 expectedShares = lidoARM.previewDeposit(DEFAULT_AMOUNT); + + vm.expectEmit({emitter: address(zapperLidoARM)}); + emit ZapperLidoARM.Zap(address(this), DEFAULT_AMOUNT, expectedShares); + // Deposit + zapperLidoARM.deposit{value: DEFAULT_AMOUNT}(); + + // Check balance + assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); + } + + function test_Deposit_ViaCall() public setLiquidityProviderCap(address(zapperLidoARM), DEFAULT_AMOUNT) { + assertEq(lidoARM.balanceOf(address(this)), 0); + uint256 expectedShares = lidoARM.previewDeposit(DEFAULT_AMOUNT); + + vm.expectEmit({emitter: address(zapperLidoARM)}); + emit ZapperLidoARM.Zap(address(this), DEFAULT_AMOUNT, expectedShares); + // Deposit + (bool success,) = address(zapperLidoARM).call{value: DEFAULT_AMOUNT}(""); + assertTrue(success); + + // Check balance + assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); + } +} diff --git a/test/fork/Zapper/RescueToken.sol b/test/fork/Zapper/RescueToken.sol new file mode 100644 index 0000000..22aac8d --- /dev/null +++ b/test/fork/Zapper/RescueToken.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {ZapperLidoARM} from "contracts/ZapperLidoARM.sol"; + +contract Fork_Concrete_ZapperLidoARM_RescueToken_Test_ is Fork_Shared_Test_ { + function test_RevertWhen_RescueToken_CalledByNonOwner() public asRandomAddress { + vm.expectRevert("ARM: Only owner can call this function."); + zapperLidoARM.rescueERC20(address(badToken), DEFAULT_AMOUNT); + } + + function test_RescueToken() public { + deal(address(weth), address(zapperLidoARM), DEFAULT_AMOUNT); + assertEq(weth.balanceOf(address(zapperLidoARM)), DEFAULT_AMOUNT); + assertEq(weth.balanceOf(address(this)), 0); + + // Rescue the tokens + vm.prank(zapperLidoARM.owner()); + zapperLidoARM.rescueERC20(address(weth), DEFAULT_AMOUNT); + + // Check balance + assertEq(weth.balanceOf(address(zapperLidoARM)), 0); + assertEq(weth.balanceOf(address(this)), DEFAULT_AMOUNT); + } +} diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 50e4d57..bb6073d 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -12,6 +12,7 @@ import {Proxy} from "contracts/Proxy.sol"; import {OethARM} from "contracts/OethARM.sol"; import {LidoARM} from "contracts/LidoARM.sol"; import {CapManager} from "contracts/CapManager.sol"; +import {ZapperLidoARM} from "contracts/ZapperLidoARM.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; @@ -165,6 +166,9 @@ abstract contract Fork_Shared_Test_ is Modifiers { // set prices lidoARM.setPrices(992 * 1e33, 1001 * 1e33); + + // --- Deploy ZapperLidoARM --- + zapperLidoARM = new ZapperLidoARM(address(weth), address(lidoProxy)); } function _label() internal { @@ -178,6 +182,7 @@ abstract contract Fork_Shared_Test_ is Modifiers { vm.label(address(lidoARM), "LIDO ARM"); vm.label(address(lidoProxy), "LIDO ARM PROXY"); vm.label(address(capManager), "LIQUIDITY PROVIDER CONTROLLER"); + vm.label(address(zapperLidoARM), "ZAPPER LIDO ARM"); vm.label(operator, "OPERATOR"); vm.label(oethWhale, "WHALE OETH"); vm.label(governor, "GOVERNOR"); From ebdf3e215fdfa8b26457ae270427af064e99f597 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 8 Oct 2024 21:06:17 +1100 Subject: [PATCH 147/196] updated snapLido Hardhat task --- src/js/tasks/lido.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index 7da2b01..f5a27ae 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -3,7 +3,10 @@ const { formatUnits, parseUnits, MaxInt256 } = require("ethers"); const { getBlock } = require("../utils/block"); const { getSigner } = require("../utils/signers"); const { logTxDetails } = require("../utils/txLogger"); -const { parseAddress } = require("../utils/addressParser"); +const { + parseAddress, + parseDeployedAddress, +} = require("../utils/addressParser"); const { resolveAddress, resolveAsset } = require("../utils/assets"); const log = require("../utils/logger")("task:lido"); @@ -31,6 +34,11 @@ const snapLido = async ({ block }) => { const armAddress = await parseAddress("LIDO_ARM"); const lidoARM = await ethers.getContractAt("LidoARM", armAddress); + const capManagerAddress = await parseDeployedAddress("LIDO_ARM_CAP_MAN"); + const capManager = await ethers.getContractAt( + "CapManager", + capManagerAddress + ); const weth = await resolveAsset("WETH"); const liquidityWeth = await weth.balanceOf(armAddress, { blockTag }); @@ -46,6 +54,11 @@ const snapLido = async ({ block }) => { const stethWithdrawsPercent = total == 0 ? 0 : (liquidityLidoWithdraws * 10000n) / total; const oethPercent = total == 0 ? 0 : (liquiditySteth * 10000n) / total; + const totalAssets = await lidoARM.totalAssets({ blockTag }); + const feesAccrued = await lidoARM.feesAccrued({ blockTag }); + const totalAssetsCap = await capManager.totalAssetsCap({ blockTag }); + const capRemaining = totalAssetsCap - totalAssets; + const capUsedPercent = (totalAssets * 10000n) / totalAssetsCap; console.log( `${formatUnits(liquidityWeth, 18)} WETH ${formatUnits(wethPercent, 2)}%` @@ -60,6 +73,14 @@ const snapLido = async ({ block }) => { )} Lido withdrawal requests ${formatUnits(stethWithdrawsPercent, 2)}%` ); console.log(`${formatUnits(total, 18)} total WETH and stETH`); + console.log(`${formatUnits(totalAssets, 18)} total assets`); + console.log( + `\n${formatUnits(totalAssetsCap, 18)} total assets cap, ${formatUnits( + capUsedPercent, + 2 + )}% used, ${formatUnits(capRemaining, 18)} remaining` + ); + console.log(`${formatUnits(feesAccrued, 18)} in accrued fees`); }; const swapLido = async ({ from, to, amount }) => { From 72415d15c763946e30e6923550351534e27a53f5 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 8 Oct 2024 21:25:29 +1100 Subject: [PATCH 148/196] Added collectFees Hardhat task --- src/js/tasks/lido.js | 12 ++++++++++++ src/js/tasks/tasks.js | 10 +++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index f5a27ae..d79bb43 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -11,6 +11,17 @@ const { resolveAddress, resolveAsset } = require("../utils/assets"); const log = require("../utils/logger")("task:lido"); +async function collectFees() { + const signer = await getSigner(); + + const lidArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidArmAddress); + + log(`About to collect fees from the Lido ARM`); + const tx = await lidoARM.connect(signer).collectFees(); + await logTxDetails(tx, "collectFees"); +} + const submitLido = async ({ amount }) => { const signer = await getSigner(); @@ -143,6 +154,7 @@ const swapLido = async ({ from, to, amount }) => { }; module.exports = { + collectFees, submitLido, swapLido, snapLido, diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 7ae6d08..0c22f88 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -3,7 +3,7 @@ const { subtask, task, types } = require("hardhat/config"); const { parseAddress } = require("../utils/addressParser"); const { setAutotaskVars } = require("./autotask"); const { setActionVars } = require("./defender"); -const { submitLido, snapLido, swapLido } = require("./lido"); +const { submitLido, snapLido, swapLido, collectFees } = require("./lido"); const { autoRequestWithdraw, autoClaimWithdraw, @@ -547,6 +547,14 @@ task("setTotalAssetsCap").setAction(async (_, __, runSuper) => { // Lido +subtask( + "collectFees", + "Collect the performance fees from the Lido ARM" +).setAction(collectFees); +task("collectFees").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + subtask("snapLido", "Take a snapshot of the Lido ARM") .addOptionalParam( "block", From 4fbb80c1ce2a1e18b3e1de781c1ce65b3219d2aa Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 8 Oct 2024 22:12:24 +1100 Subject: [PATCH 149/196] Update Natspec --- src/contracts/AbstractARM.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index d6cb810..ce9fb0e 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -493,7 +493,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } /// @notice Used to work out if an ARM's withdrawal request can be claimed. - /// If the withdrawal request's `queued` amount is less than the returned `claimable` amount, then if can be claimed. + /// If the withdrawal request's `queued` amount is less than the returned `claimable` amount, then it can be claimed. function claimable() public view returns (uint256) { return withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)); } From 5ce2a037c26fb634943d79688bef68b38930df92 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 8 Oct 2024 22:12:41 +1100 Subject: [PATCH 150/196] Updated contract diagrams --- docs/ZapperLidoARMHierarchy.svg | 33 ++++++++++++++++++++++++ docs/ZapperLidoARMSquashed.svg | 43 +++++++++++++++++++++++++++++++ docs/generate.sh | 3 +++ docs/plantuml/lidoContracts.png | Bin 18053 -> 18208 bytes docs/plantuml/lidoContracts.puml | 2 +- 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 docs/ZapperLidoARMHierarchy.svg create mode 100644 docs/ZapperLidoARMSquashed.svg diff --git a/docs/ZapperLidoARMHierarchy.svg b/docs/ZapperLidoARMHierarchy.svg new file mode 100644 index 0000000..0136bf7 --- /dev/null +++ b/docs/ZapperLidoARMHierarchy.svg @@ -0,0 +1,33 @@ + + + + + + +UmlClassDiagram + + + +18 + +Ownable +../src/contracts/Ownable.sol + + + +23 + +ZapperLidoARM +../src/contracts/ZapperLidoARM.sol + + + +23->18 + + + + + diff --git a/docs/ZapperLidoARMSquashed.svg b/docs/ZapperLidoARMSquashed.svg new file mode 100644 index 0000000..c270069 --- /dev/null +++ b/docs/ZapperLidoARMSquashed.svg @@ -0,0 +1,43 @@ + + + + + + +UmlClassDiagram + + + +23 + +ZapperLidoARM +../src/contracts/ZapperLidoARM.sol + +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +Public: +   weth: IWETH <<ZapperLidoARM>> +   lidoArm: ILiquidityProviderARM <<ZapperLidoARM>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +External: +    <<payable>> null() <<ZapperLidoARM>> +    owner(): address <<Ownable>> +    setOwner(newOwner: address) <<onlyOwner>> <<Ownable>> +    rescueERC20(token: address, amount: uint256) <<onlyOwner>> <<ZapperLidoARM>> +Public: +    <<payable>> deposit(): (shares: uint256) <<ZapperLidoARM>> +    <<event>> AdminChanged(previousAdmin: address, newAdmin: address) <<Ownable>> +    <<event>> Zap(sender: address, assets: uint256, shares: uint256) <<ZapperLidoARM>> +    <<modifier>> onlyOwner() <<Ownable>> +    constructor() <<Ownable>> +    constructor(_weth: address, _lidoArm: address) <<ZapperLidoARM>> + + + diff --git a/docs/generate.sh b/docs/generate.sh index ac3ee24..3c607ab 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -23,3 +23,6 @@ sol2uml storage ../src/contracts,../lib -c LidoARM -o LidoARMStorage.svg \ sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b CapManager -o CapManagerHierarchy.svg sol2uml ../src/contracts -s -d 0 -b CapManager -o CapManagerSquashed.svg + +sol2uml ../src/contracts -v -hv -hf -he -hs -hl -hi -b ZapperLidoARM -o ZapperLidoARMHierarchy.svg +sol2uml ../src/contracts -s -d 0 -b ZapperLidoARM -o ZapperLidoARMSquashed.svg \ No newline at end of file diff --git a/docs/plantuml/lidoContracts.png b/docs/plantuml/lidoContracts.png index 731d3c3ecc7d48b9f68dc870e4b9252dcc6130d3..0f400546be8fa504f0ae550b5bcd12ee3456d637 100644 GIT binary patch literal 18208 zcmcJ%bzIb6*Dfqbh%`t_!_Xy?(j^K=cXvyNw4^lBDc#*2f^;L@(t?B_oo5gJp67nw z_uS9@o_`LX(b3^M-`RWZwXe0-wbwPlaxxOAPYItsdh`hO?He(LM~`3;9zBBj_yiWb zqOQeX4xZ>8#nl`QZERhwj7=OLNf=uj+v__T8<82fl9@R=+B&>qVX?K+w{~=TZ^dkA z^WJ%6fE3(@!(2(t@xNYw1Ox8#A+=jc+h(5indfoHw;#?wgHh&vH&t`=(SG!lXM4W)EzL`<6uw{r_dhb`m`~>vE5H9n zx(eFa^KvKH?i=}G?m~d%S1qwm2EL?`{stZnt^eFP6$UTdh^@J@G4re<{Y zb6y;G!mDqGd9^fZg*E$0tnSPw4JnSQ@;hO)0cgf+2R7TbarY8E66J#o6avQg)&9Cf zS2!@FI;-s@`&3)s8G9(zvPZ1kY9(7WY?MYqMVa)17z#Kt=T8Wg%dcRX{O*XX+Pgwj zW5%1%k_KucF@lE7DhKxAX)B+%ZLtpLVj{We6|be1by`&*>XXfgFeGKL?`>JPXu{15 z3@I2du9;+e(4h>;O&Hm7nRwm|{F%j=QuXk~PJ^E>C=GCJ|VzzI^gq1$b?|(u3@T~j((@*^#Zh5scyCba!!2ks>4S_gpqj<8!a( zaByF7F&PZIh{O>#OPuS$Vx!T?zW3Em{$oV6 zuv027LDW>b&+y2&VFWL?(!K9q2&NUQmX~SPTd#Emt6jv%3Uss`EHx*D4Jhi$H9POW z^1M0$mnFq||9HEba4IeN`}`bF#6KW_U~Ao|N*5kNk=5vR>&6#h{%g9_;*^BnjoT*5 za;9vl(SgQJlF6(2B^YBGVOm;RW@hH7-8GUnEcLc%%lLrNa*M0R7MQE@){hRq7^$1& zm!@cFA5MObSEg-j<@>yZuL9GScG>yfJ>GG1I-^VZI_ymX^Op*03JQuSZ_Lpu;S^t3 z^Y1wXta@L8wR9)zQ-uV-`d=7Bg0Psi8f6pbEcH1hU$q>1K)uS*X9)jy=RF_XpNJZ{#ZEZ;}O?yU9hmu}a;)d$;P-5s^ z{a*cM(Vy;h^>#SC_l&r7?WVmvUaQied%>nX91@Ddke3yvk2IT#UKxL%%wgH&^edi5 zHf?vR*!%8sn|fPkzQ%lMaZ%1xDgP7rc?ZXSlRV_IVTPk5rCDbc?2Wq_=v}SVXg}Kt zY-Pwax;dDT&1d(04bke4E*w1ksu`E{;?JSC)?S3%>SPJb+MT_?f1U?2?GZYR%ds;4 z`QTwkz3tbgMih5R>V5OVFnqF`b3+j`#01c!i1g(;^Ki-;BS>)x@V?NCB^t@ zZ;tL@2(5?PX!pF;BRE4@T2;$+E(X|)`aYJku(2J(>vD^3psV`>>K<&GSv- zm^3-Lxz(n+rJai+_$;SO5{tb^3ObAjV&C2hz^r9!VC73~4#Xj%5d|?(3bL|dBn37r z-V4+%=Dfw0enzkKMtg{Z4p%}_D2|ni3T}d>^(0#7ligBVwsPwI`BZdbz)A(Zs8Sb( zB8t@4Esvnun-%QB@gLI_gcY_va2sx>gjS_(oL&|4z=~~E-Rk-buV2G7S-BWuB%^7E2GHfe7Jvv zT5qEJEP|r+*2HqEC?ImGNZD_^^1v4!StkceCY3w$E@n7|YkmEE#k(A57(m}-P0`Xf zr(MeU7mLI{1lb;&FLg;K``fAUE7881!W= zcFIPA7cY{CCu{y1di$8}MBN*Xxv~0%AkvkdgkF#SM1bHldnF zmCpMbDeFo6?wrTtePi>!Zfm#QJgTh_w(X7X zs;O4q_oi>RG*;lg_-WkQa! z>L`XrXnl6A0DrQTVncCYv(%(~8*|~iUIh2&OA+Ntm2s$>HOvI9dhHtTVT)g+nNLyV z_Q;oCOU+ywrUgF=1|n83UpZ_^dx%}Ps<0P)EQo#qoNzPjse74LBkOphgXwz}zPdxR zIPTkP&Z!u;)7@3Wg}V2K%Dyi*(Vy#I^@Km)O++RAE+=K+q@1!)Ynf0)ydWkdp&Qd5 zfugG3^5I1uX6RJ+EqB~@FR8cH#UCEWpBP(NtA`tJt?dc9b;%y-#;E8{|Jlh;FY+Gv z9aZUA6%G84%loU*p>U{!_h@@boR`2Uk~q z*JCOfN!FFZSN65UIM~R~Xfsr-R@z!|v5(#t#*@hy#2vmtBK16pvhZ@Az{J5ZR$!#Y zi#R)2DlRYXvAK>=vAbJwabzkm9*nP~??^gUm9pry%7T-ILqI5E7~Can5`azico*Re zVDI!NT2|@PAUk?l33UGql8{CKxX#B^Ts)|$j2PnKgdA4ZdS>TMQzl$ivwGe&M|W#O zqlhu(Z}-P1CMLG-4ESO*rsiDsW>;^{bH7ZMz9Hk)edT?l?)C9Z(?RLk z)#f|S;)|90yL@Z8Rd&U=FnIs0L!` zvfGN@#!wedVCtsb!k4@L+0w@)tUE)`X*=q9o@hN)gp)U}LaSPqYB6dd`2-bJxjIie z$+wf^!DjpOwY&7JBT}TKq_wXAjO++ zSy1S@l!yN5?J!Tztcnq$MEsZvO8ai{ z+KhLdSJ5=HxtZ7NQQ!;Ad-^t2HJS8A<3Ze7+z_k#yIb*4T;=nU8jWZLST8;hRg{%m zC?z5i50!{70gBg3wSvUN0#}B`gNVyIwMdr3Y8FHD>#pbZnK(RRwPYN@kT$ra?y|bQu*val^sPJ!K8%zJ7ISv z`DYtU;d&R1XI1?jQ(H_+Q)W>1#R*0GU{SCH zTe*tm1{&PW7;GA2@=1TROF@a(x+wkmqI=bOXj=gZRE<=H4CK4I@>N-VnqnSjF487G z(QCE1_G{m84E7LSScIF+ztd4vAf3_>jUNaUKWBa6R(fIUI@CGeb<(<4Dv;&vKYNS! z_Y;e-u10XRX+ne6votaiD|PFTu7>M1GIpP5DNu$!XEVn0n(E@EWF!^s=?@Mh7K=~K zIu&xweZqeDo&yiN@i?^eF3Vbz$7Ndh#hxN2pH@j5J^eIZ^L-9<(r>=YiCjlzIpVzV zs_t87*doSZy6*qc_RCvk@Yyl8lW*5FB1K%vMf*8Z((d{Ou|WqCPvvjkq~JR7WJiPF zG%p;c#Qe?-MzLLWg|+>BQP@l{tI>k0YQ41(*{#cMF{ZPOOVTWi zaPY;uPqSBV%z(%8JUwe2e|3ww(|It`$ye-j>ji@aX^JE6T&vjVjy)N#;e*2 zp@63ELKQMUW#;Ohkn}7JLYKdvR{xhL=3RJd;#uS7TqdXA#;+$yRY;NS$IeG3kK-Yp zx>bwJb(akrB`s1~MO8ofgmnUU*{3bayh@|XgEBgfb3X;F^b@~Z8KjATu9Jd-ufB)_ zjJ*3q`G`P$5jB{2slRys#Tgsa%)*kk0-3=d&%|nlFn=doDLcv{+(T)KI3Wm?^!$;U z&b_Nmna6L(oyfiQAh1;$DzIkviNQNFu>iLL9*_q^_)7-qS}ZRdV-Y>PH|;IbFR!Ru z{#mM9hOXfBo1*>%94j`4No^zLS4 zk#||<{J5Yq{2U!O=9PouurE;KaibU2;vfAYbgxn-Tqi^M^DnsmprrOhl=525L6Tnv zUuyrL&)pJH6q>4xXW0y0@L0qxzcglXD#EiAg%otx#G?wB3{ZbE`R3E1pNyr)(dA|} zKp9S}BIAEhHQ#sA8lOpMi_decaT<`X+W9%xzA~!V&?K&m-l0aOj>a0hP@&)BVRFXw z`bXvjuQAtaqEM0@F^Am-$cHRyqhaheN8@ko%R_4j#6~=-xH7A1HyPjIQ&$r@$NDsE zJGvs##-(_iGMdDlRH^d9X-z?*GO;QWr*oZSMid_VEOiIPRjpHI(7{wM@^5sf!yp?4 zN&HB2r!t}B4m?RSxTYLDebLIw4zX&Xg#Xy>d_(fGSRYZIN+<#@-@d~0o-oTq>S1^g zdh*?spD6`}8=p-BwIx6LkoJmg6D`{h8J%Lr*>xHJQMa^F3lCSE)jgBRtbfyWq;>!? zlN9O;MAz|Nh;H8wW=~S^ZBo`*XBsSd=d$Uo)T%zDIYz+NZx&(x0d~^Tv~~g|Nj&(e zJ+c`-YGh3^Jj6dNq1c}^W}pl9a5NhVSqcRJ52KFC%()iQb^;mWaCRlbH?-h{ub=W_ zP1`%*X?nd($wlb*VIq4>HQ7StTl9ku~nzq*Mbo(-7wSxJ=|LOcyR{Y%$Iw=s;lL9SG|m-0~fp_4-@E+2Sm z57q$Bk_o%kMrosZBUbtPd@GrbyrMa^`7&*;gwA~fuITKxlL-mtm6zm`B?uSC1@>cz z|3OU?Q<%R-yJ|;%WaTu@8H$d9`)a``W73LUSSi-z^f6w{y?7k;G3A4A!p6xlvAVSY z3NY!S0M)U2D>-#q_jU&HXl*l>XUMd)k-2lA7vlt&FO6E%?BpHsA>XVQGAl=m z?juk--XrwWXqgg+6>dGJhf%&ROz+OAyL1lWHMn{UT z?9$y^l5qKj<_*NmrBB&6>od~o9G_jy3F2jEvJIWmEtYKur^J1OY=u{Zu|Ws#rHX!w7ut$zdKZ$o9*p$>p{6P=KPF^lI8pF^m_%m z#mxg}2`+o4=7*?^f-Ls}>(58z@B0SD+|#)6=l=+wjNg3|fC72nA4OmNs`knCA;vR* zNdV9a8CXUxqL0`3LZSHx8E`P^htL`KRqO#cF(5gl1Uqvt!{{;SP?l{ou1 zdr5qNZ+f5L|7-t^N`4quba1A=C^c34Bx{AMJ>MQC_QODqTaJ#84agseaOi0N$Hy8K zV2l|bLd}0<(u1FSJc)zma>yRa;n3&*{McRZVyh9wDQ_Ne+ zu7mc!dyc#?udP~qrk`T1p#YcGWG~^puh*Ya+=8pXg2}$MKab>G{o6E zB@Jq(u8Bo#*t%$ZNaR_6Fls&1<$Q1QJrbp0f;mlOVrrGPbolvr9_u@+mk{He^xYI9{}IhN&L%cbKc)`3U=)?^i3I8~s?m z{M6!vg1{7C?R-!7Dgx8`fWoT&Mvdl;k_}09)>-9S)2Jn>lT_w)XZG;~aW4=K(pA2eEEZYB9l5T3(`Us+@%V@ie z?KK%@r34%Inbo}4(Din^weh%PB3H0VNJ|Q-|4-v1dLQs^fc4Hk;viF3g2j&e%3J|9 zijU-nypwUfDuho;$2juEaUOz6@@bN9=(P3Z!Bue>z0cTW+FEeyG#!!SSU9g|^nzTp zAvq2Iyf?_OXnFCNQi5Jy_)N@57g32tkn0elaZ=mTTydEe249V?u98|MagqdiUGmH? zq06fAd?*o*xWGTB+&^{}9v4_s4#}<%X^qnBz93H@78U-+K=7(F1MOm~xe7Aw_Hi$^ zb4gSnyvVKntHFh9c3A>lO5$a%*4O3=V;1bXC!-}*5lP>hh2N1&zRCQrAtxl0d8Pk| zLpXMmpGP_NG%Wp|KLvxA9#1IOK2_ko$>>!4dp+j(Jx9&+s;%gKq`5jZW`$g=1(Jc8 zl^+N-SXvp+C!b~cKb!y8@UnQ&F{XR1KW8rHea{z|wlZ%_&LNSJjgeV)k|Sl{ikEUu zGP?|-&ukJ3ix(bei(iPQYkfW$>##}-`N!TOS{Al=!JoS}1*Fv!<~?tvC=i?8Pr z)f|?-Qwv>_iRLAqb*0fL;DhS9eV5r6+0ifAKK3(K^Vcj;V9}E^a|qwI8edDMJ2yTO z{NdEzr)yJLRaNLSbd<6=9C)caNDaHzef;MIR{_jfRvCAr5LW@+jdHEg^1o)`hEubs zIZc!D>PJg9#i#lbCW(l1nok&8F|>|)@(3z z>SA{57&SW|8x}>VLkU4xD@8+N3;h3kWFT4j=L0|vqKBvUa1r01_p8hCTr-IU{;^a4 zOyRzN#bTVTF6kn;?WXAHxNssw<@U?c#qpnGrW^xfROgV-KC#qa_SoM##g?Z}xn)cp z(|0`s#UmB@zZ~{L_O1xI-71BEZwTaLvPcEfbXKnzJ7pa3K0kgj?JaR;?=$u>BV!nL zIiMuohKS;&d{horbP$$;!9N4zmh*jk{y0!_Y-`?#qAGuo)_b88arQvTGT9NI3GXMV zel8sAy`|L1LHkA=BCrh|`G2{9?_)@YiVjU5Tk9|HW5YbFW5oNBC#zAfUL!T%$5xUE zDn+tqsqtVo4O&o6oNK(E7ZDnbn%U(l`TrDqp^vfa`z@dJY#a0|e zJbQ$@aO6J3s6-VC*XQG*j8=E3E2`s=ffXH#rJ(iC#Jlp~Zl10B&8$S2k7SeN$9`){`bWw{^6AZsVmHp_pU*eohUR{DK&So#Fra$Ug43VO6*< z{BpVkgsPh+LP6!fZw-noBQr0TCSpQX>QYP5Mn? zRoxl;><(zI;`sZ1(Kw}UuM{NJymZP)*u``bKE~)#(|3Tbfsvua7wcE2zxt(--ZmgJ zJ^-JAqR3BXf@DN*gAD(%FDmr`Q{B4dzOv`s|NCpRdwT3ed4eLZt1oT zNU<~?CBkFtxlQ2=Hp4S`*X0DQA9px#sFwQ|cn5ep&7sMu=AKIa#I?W-cC+-6WTjwZ z`_7OIsPaaG458agqgy)j>a)jEk-a<%DK>e3qS-*(UJ=QNI)b!YmIIbazJ+gr5? zqf)2b^E|Y2R2Xbr88FKH+$F|hB+2!BI57JNZC)AAcxGmaTu;|ms7`> zx#W=|7M_C4j7hCR4JanTV;PsTv zC|(wq+{02*?L5>&6UXOy&Uq|)X7P=P*Rh>w3UN3ncoc1l4c4zzlbdBiTwP!QqY&nL zqvYNRH*n+`ENN>5udAzDgF}Yk?R^hURZxkzf3J0iK5TP2B&4I*KEY$Q@iZDVlGh$& zmM2KRcjYAa*J;_=CkN4t=F?3^Sa_@#xvxQMgDY43`rMJ4z+!Ou=xGnz12Fo~b=P5q z;IV*~7ngSkAYIh0wTM$^(CLj@1Qj}4{ckS+sFrHr zQ2#zVvmVRnI9l&DY)ruLk{00jOdfLqh@8#G{2<~T;s_2B7jCS)#CeER(sUcal%%BT z%-T(ht?o00G!aGc1A6c8sBI_Pd$d&v&MY=PMwRU5OI{Z9U3UPy>vWJ8xr&NP`2O@UXY?O_#UvZ&J%mdPLdL8^^6_zcUa$I-<~WXoq27?C^t*iC`_JV<2E1wN z#`Puh{8+UKfLuz@Lc5yI10W2_y=Y4YXbZ^d>M9N6T7E9r<{3tvTpK?2!){hMs+^Kt!N~M(bPa==jt)DYP4I z>Y}aRxC|_s0=Nt`&B9?4qe^EfPUjdgb%Q$`Gg(d?BUW)7Go`O)M0kaMxmfO_7Gt_U zy$=02Q{sVZmwFD3h#**uL8jimEsrE3*%%Qtirn!vJ7q|DS`|cjNwLeu39J_t9BDwTilNJD2Y>p-)RDKl}tzgbOse%;YmdCbIl8Gg)Vm_wh)dizkz#y?S zF1Vv~PwdKrOe!cq_sM$t)lu%>kE4<46?->0I+z~bmOy^XTkyvGd;h0}f*b~C!;GhJ%;w+C1tIS{c=n8l0}1@y2)byqg}M%r>d^R()plMn}-+G9YwUT+vpkqhUQq_#;G5z5Nd zqH=^K+v48KGT-t?VQS`ajn}g3hrv#JoB&teP(fx>8&uw{H?*f0rf-_P-PS$qiVoT{ zo45QDxw~GaGPTb-Z{bh0tt;zPC0)c-ZJ~pYwvYi~zwan{Tp4zPQH+JrE8i+kG)bZb zBO0%Jc=J?8xLkuk?1RadAah{aM0 z;;>y(aT}2a!%k;*Vh}222pK!sQ1}9^ZZSbU-QA(-e7n`o&synk{Ab7sk@n9?$m{%U zTW+IT1dv5s2xwtW&7M38?bc#+UOQEa5g50Mz&NT=Q>UZakY2P zBnx#7>nC1$w}bK2<;H{u<2l|QeMsYy?rqIFyu}Ui;{TNAzfz<= z!;3gEi;PK;0R4WkAN}2o`$Oz0ESX)^OWvAZDqSB4ZhF3%(s}#C4|>c&-w_u(W*3G6 z$MdJZUbNPd&CXMB;;yN*-!duCo!#87xb>^ho7s%BmW&X~B!|9u?z+f^LQ)mT^o(Nt za>A5RivK1tab_!)hh*OfmeO|(nl1U+bBHU7hFftYRBa@o}hID4Q2BH$%q;)Y`8k3y>GGom{fDxp!Ym zZCRq9@%(YyJ!mf>8J9=OoEyia{<&`)<3~{6e|*5afp=P711qQcr)Jsx1B*{7AW8UU zXd>>SxIQf{GGUUJ{p-GjdCIDchqxs-bJ23@J@oU{3&~g*47bID&O#SlBW-EBLuOyD zC`*n@7+a z?U>+R;H~I41a0vwSDm{cM}6@TFcs{Pc<>2BeSo1bGn)`2U z*SIXDXYeghezOleZ`|jbIK7u!Lk=q$gD3`~5LQsOi>%R; z%RFA<<|&AHmoH>T%=78LFP@dOWv7Z*72*m^3KZR(jpV`3k8+- z7eqgMSSM@?h`zlrO(p%;n}1UrLS|8d4Y|X@JoEbHcsOTV*VygR5G(E8LL7;)aoK(Q z=h*GBK&Hg!0^uga>b1ouxR#E|w6~)8kotP$?TxUNG}@Lpy21J@C?WhIQ<9Yct9M+8 zi!+HDjAcc~*=r-Ui?pj}b=!u@%!?3{EB@74g(c%nWZb|<$dfS`d6KOOaO;M6$ zO1{VpK5uikTEzyv7!m1xpp?plx54c@Tu|aZp%397-SmyGPgCtCO8?e=h_x`}h6h5A-It9)R{(l~JB!EhVlF$FKG2Jc@JrfIjsDV#;5hnD*|Knqg?;kGyKX$yY zp^%C|z*M_nb(Jaq?Uet%*cIfkJ(5J*VZsa@B3%-X!`9w>t<*I>0YTwAQf^x^KE70O zF(`Q*0O;v#V+>48{TOsEvisxBK}HHFIV}=Pul#h}`}%BOV+2%opJP~b1#r;-n{USy zLBz9$6J7=D@rdBZIw&U8xuLA}rrB(T{@u-`r-w%%8nH&sn7kmK@3%1 zw^yK)DXtS4&k}EKZ3UV2kii4B-3L0f2I3eJxNLNc13CkcDL-{XZJ-qk1|7?+4Gspo zf-o7Ox`4H&#dh!ZH&GP3!j#h0LeTq4y|kKTrOniCc4mAEuE3*+#m|go{qS24H-KDU zU;kzw97e#i*{yZ;b_IdX=xPk24)Sd%Gugn5PAZTJN?>%so3B|Jz~h7yzO%9`M}2qPyK8_z+-O&%y7a3`bd*l^?ip8knxy7 zF-$K={C6Ieqf`H#e9vFgGGPiO0br&+H~xriX)=xX&o~uN=*C zCF7<`)E{H-t+YdzJm?fOrWe+I1wF)|$(+-AF-Yj};K1wV!p>;9-CIz*$q^irBnmk1 z;j{mSHtL$d;l6$j%hs35^HKw)y!DLm*@JU~H0uk@V+62hZlG%q2p2$8KfFE0Ri=Nn z>8Qov-eMy*kK9AU`$buMJye-scvV`z{Qa0zz~hN>7^xu1gqg)N1{IWKAF5c^f4K7< zECK@1@9V7e$6<3IR|DbfJYJsAOh|7nPE`jSl7s@P52ygZ!WG^q7poZeeO?7V1Z*;# zE_e?tZ!uj$fQ7a8)Fl&oz#)g!pH}yN!FhMm9Q2F_UAr7EqoSac=(Jvi=Px$9l!H6; zbw!6i=PFaHT&rL4YW{F!P(xqiK3Lv<=LWhQy{>j>*x>;gFn0XW5FGSWnNQ?t)LNht zvIo^C00052*8zI2?0N74ze1)3|KXnwy(bQZ)S#P%*A* z-3PDF56_Q}kAw1njDgE$xy63->B;Y(DXscejb|b@(9~0+yPKs~+r5{)hu?UZ7M^ zE>aR+^tPIOe762`nXzIP9BXA~WmTCBH<^x-Fv=Ckk=G^wRfb7_v@sA`l#R)A{@qRL z2D9YOOHEF?o~KhN1Z*dVEB8ke0+m3BQL6G{59avN+bBt)>7S{m6NI9^1l^*rQ9=39m!^)j-|@ zmQYFU@pergFj<;;8Zhj+e|^0LR2@~ugJ+uwx>>=W!Nvx#XTeo4F85Vu5{CwFhc@Le zwlG#6VrVXDPG_G6msT9YfEg~SSDPqnepJ;wt2XMl-}w%aZkCal`|ALB zo&6*g`pAHA0nT>Z3O)O!@0i*F+g?cMV^gs-H1^0LF%N@IW!W#UHhu58|3^2HLmHJ- zQ%l^1qfl`>`I)IhAw`6Oz6SsaU|k~st}5hVa5#FmTUeULlk|X~40Qt^#CTPYD3Wj}%1w3X)M`gvKtTz# zxN6s!MR_q1xz1G?E9Oe(qAh>>maSH4U;%_lt&4kdHZozvymdl8uoiKQ>Q%E=4nMy7 zT!T`9Kb??mnQx<2BnmOy0(1ad{{F_)XC4s16Uu74FMir`1_Mzz#)~3iTFOS zzDs8B10CddbJeDqp8$KNS|BGptX*fNPD2bDb^}<;blc0)l5c-zN0#ZdHoae{1F8-K zWP05wUYCREQqA>We;uj|8r75p%e5MP&AZT)3xQ5+b^WLH54q-4Hsg?CdH^7vJGluc zl;zVcW7)>96nwo(CC0lb=jY1{1S__NQ`T3M4mlZAN;LxRCtbz6*T3$mb{qk$o|nTh zOE28f{}#CV)nqiSRDjd`I~?g3R|%*HfyaL1ClCVMmSl>4GN3H7vGq*)hW%hUfBZV@ zr4T(#0)op6b3wa;tSsM4?xt$Qp;sVG^?5Qk!D3$$Ap@5|NM zesQ}}I1cEY4jD!xV50&kqVYuKxxWpdj?3NRUo$mWFExQ9@YmWp;YN>&R7yi^^YY~L zdt~aZ=CTAR;!l5$e;W^RgJk4|c@MO;X9LfHih>O2_snF+%(44!Bhbe0BDuAhf|<1$blRMUs(=hYQxDK zqCe+A7||#OF?vi0G$JH0svi$9l>fBcRAT^oKoGZm(3Nl+Kr=7^GtEi^-~mh(8^AS` zXQ#?iim&W}-?H8UO;`uaOOqknnykA-JH9?rUgwhG`-sP{+nSs=wzp>*>_18gWd%$CSsdzfPQ5D-lAh=n>GFtiU1R{&hQ*66$}YxMET93i{W(T{y)InR z^*Vz{dNh?sQQVcCo6Te>Xu4;Rr9JEhxNGK>l>?1XJD8&2LI1t;q^|*_gJ;6`b|-JJ zE_}hRe=i7Hdq6dzXWIco{&-wnn>v}-g_+v^JxaZ&km3! z5G?D|wbOZ?A83p11Gah+Xt5ZIaK8c%oD4VxsleI?Rv2upHV+;uDlyF^Tk%`wI;%Nf zk86@&03i4`?l0COsgFPt{eI$W%YGKZt}(EG|Bf4-kYA8boC99T3+OzKWcBrbsyC0- z0Bxq#Q{g#2i{h+lxa{q3=z@+eE|Eb&tXj}w35*G@C>w!q0&SvcTUH(N zCeCejT?ptVx324KUBtwg{J>6A49pW`8bHjxO{0p@jGx=`~r14BE^Pjx|*kb-S*&}UOyEIWUC4s@+X!^u;iIR*su-85U!7=wFp(0n+gI(Z0-{lxo1 zwwB+(Nhy7bvhD|njnmABWL1_&w;+k5O$8`Co1n?360D3wdt)(Glq>T^gP$nixa&UD zp4aQ9A^US_4>s`&1OIn-Kxk@Pve(YP8#*rU*>s};Vp3OC*xeiJUsU1 zs%i2tX$`gI+e1OrBCDVXf=V)H%Ctq15vFzl(qucA53Rj?(lDNd;L%`e|FsY zF1{ovM=D=CndYZ72~?`!1{PO9ep-}PMiMbOsvvps`x|<#P%5vB^@hPCAH8B0Nby>g3Pc)!wRJEiMgZ;-^W8Zo4!t{NsZ^N73h~NQx;m6OaQZ=JJm;dtN)& z%uXJUJ+{iM=Qz^=Y2NiAJIK^HujiYbiaGI6xo;Vt5&uo~?lN?O^@x8NeC`4P7d73# zavM}AMVm(;D-P-%Q&Yyzpkm@^tG6x*i z*+fME!5IK3rFSm7lW(8A0O`P~-R;$BWcXFy!AvLC zbLmvtlE62iVPPIb{3;GWd=Dzo$p#@%AgPMRJg7Xtc`vA*_oho1LU0%~YRoc8gW+M2 zJ6wPm79h~XYkRIC6g0HgKhi*79ngMaFi&NeJn@EsfmvQ&RBx*@LBD1uOwz81}yBB3t~rs@wyf5D^JZr^Us_dR*&A7u!5}?d8 zQTsl(9>WYoUSP-sv0RXE6KMKI#8!bCUTgdaU~dm$!1c+FUtlN5>L!h|K+;--`SW|e zEXY!C#02o6r6+l2tSvb0R@#L|8jCv#QqLS!ShXg{!uEDSHjv5h&?;(M^q!RDo8cH; zFAN4gcl5vQWZ5`llk+pHGoPS#KidPzzAw)2N>IIAg#JwL`^N)gv7jj^{xcGih@C&% zqlOwwNMk~%t)TJ-BI7IW+|d@>RS{6Ufo~hoBb9`G{J0j^?-E$Ax?&gso9U?`^PL3a zbb&nODUnhHSKw*!2%uwW)vDgp2!(5&aO z(S8#uuGgo=MSJ>GWb!pPuiJ^4lG3xz^!W<_3l$xp%3W!3ofvPLEx&C>j=-I^kqc1L z)m?f2s)jsv9WMZUHNtxv8=HNEW|ESCZjcma3lvp7MM4_4xx6Gnf%2liVGkfR)=Tr0 zgkq~8ho+%z+ZbGz7pI&UC?{k zME{$#Ef9}^dUUP)-Cd;MT>(G$hMBe~5mU*^k3aCe=z`GX@Rkso4A^QzFvLMgTAObU zIuW5oW^SY74i>V{L+S$N{=n$|8*<|F{T(ud>HvKG#ZkQ|z}bJ>m>lhU;f*RN-=FwYm8L=`EJ>UNY%OicT literal 18053 zcmch<1yogC+ct_cC<0pvK{}*EL=fo^1SB`zNOyO)bR*IYn?_1Hl}5UxLqWPh;>?ZD z`+ev9=l$O^{xSalJcDP*UTZPenseQ8-PgSC{XtGf91D{O69EAM>#c;S0s;aeDgwg2 zNOVMSqvTJ5CIj@02#?=D2ocA~uu(@?S)hi~`=` zNIt@4_(kw0?}skB{R8&Fa5+f6<{>&~{GTIJ6jVu~K_n5R-jJy0I3JS;-)R zQwwT9<_#|A^_MYTw5pri9H_pm`t7v5-g~?|Q%5ydE5s8yY7R_$9DATKTYV?m!_8X#Y2(O`BJ}sKxMfat5xaf%G%IK|M6IL$%Nl!&Z6E8gXC9G z-;946zPs6<>+*)0oRWzKC)$Cbz{Ec?XSjhfAs)YC7#u&rxd!zXY+h9VzF zHfdGu>6NzahK0ueLO>9idMhdfb(!5sLvwj7h7=S`ef(WR_&#}7(1H+7^h1b{mtdO& zMzd55IKq^;;fTWM6i`I)fx#C?7(GD{ogzSz9Gs#7pRyp}lN<*4REYfl zx{O*V-sbq%$b6%-PNUNxg9rGXFi}UV}w`9GntyhpR^UI@Q0cUZ$Kc_ou zkEb?9vRBj&x(Rf6UG|sONd?N-zmVxi;6IBZRj(4)oa0D^eORFE;u7?H|SG~_NmNc=+_@#vpilPde-Othm zyl}K3kcU9mvM^TW5O--K*`AM!o{%Q&J+nPw4UFg95_bhP~9 z=;x}Rz}3D;FxQt0%O&Xmi@92?qxlBM&(7?(e->}QhChj;eMf|w)*VLh?dZ_~C0cbT zT{MFdj6x)c*XxQq%WIw64PkYqH}nyW<`=v5A7AWSuTSTsqCO=uYry6?4Eq9}Msqa2 zFdIjoqw0SoP>J6-_9`dfZ5(Z>PHT(p>er9oM2d$W&@#|WfA#6T-d~X{)M~7qD2j`Q z$Yk)RJvZ!A(RN+m94mNeZFe-+){8FxS{N^Aio>=|!#>F(B<~ANmMQ>-e@=|bIpR2Q0ApmP)_(REs0j5%( z&gXs#{-+Giu6=-s2@a4V#;A4DXtj7e@eL{vSwNoQBO}WQf5Ne1+ibJ!*J;8YZcL5* zQNZh}e)g~+!%M;Zy{(6PBlqoIG4oW3+R^Ep^?NBD9i4qF8{obh4pTUKwP3+X$A`$;YQYT{DtW#^3i$3 zT&-nTFmwfBuSg}0972PKyvrKglsK`*-5 zP)l~vf)An!(2Z4Oz49azq<5P)22)n|z^laZ8(`W{!dH;GhviLs^k`-dW%wEXfuPgw zk5{tkyhGofWlI<~*Tl=XOXb{LH#+a}*u#T`%p3HcwAWn1E!U?S#dPy^))|)t7UpR0 zum5YU8Tf^+FHeAdhq%1t&Fpm8Y}{*6{H9i+>)W~5e_G~LAGRFv_&3`3eUn;wJ=VM$ zK8rLS#|qV)H&}!ue^}J2s3{U`HU^%YWWe1$Y|;C+8J|-;@~Qk0gERJ2k(kAM7oPJ{ zxh!E2&(4Yz3&s-#Zq9SQKq^A$z7uc^%EHQ;gaa{qJejl_prsuGA#p2-6J+9z1hT2z zS$*ebBU=t=*rZ+Ajg5_cGGHkP!q8uQz;{0gFY%SkXZN3f?+NU<>(`=+M)tAK+UCy; z@1aGAZ97pEHdkovKKpfoXtB|hCm9{n;bS3TD`92ctlXe4V@nB zO}(<;(8XOpr(U#ikDlySd0f~HP%$gBp1psZ zLFRo`xx8w>JH+#4}UggI+2FxyD z3_-&Nc{{s*vqCx*y?TF+H*+2arl74jpZBW$V9+~Q>E4o2n97PG`cb~c8E7{J?&~Xz zFT8&~lP*Q`R@ueXmCyTz_l+c*Kv9F8(kL$0qK}l`z(0*NJJ8#RZn3+droZC^c5|vssYT58gF-PJ^_PZQ-r0 zt!|%77P5XT3dUvhE_~SD$>yIIGAo935q1|eV+%)Pch3S3<#CNm`2jpGBwM*uT^Wy1 zU8{~~8`!L6`O8Ux)QfA~LyjU2H#ax+3D&Q|80zVsXS?%5x7xE>J8LYL-dAFzf^#`D zVVKvaw87n+(+=1Uf07y+3vgi0NxS9>zsr>@o+*tVkG4=e~tq( zDw@LWkEeMN{3tIjlDE@9_y_PtUZ-@)>koi7H5N%62d1(Z+>w0^uv0XUY2)rPZQ=15 zhTAe5G>EMBfszgy;Z^ozZ*Xx@ZuL>WA33_==d52rF0I*fFgJ-hYdj-5b0OX4_4vfd=Z;gAkOIxwca_a4Hir+aOd8;QJTl}&;$h1fMNbW16MJX&ML1j z9%)>!f~08~WyH#GJU926>695f;bM!YN~wC@=-X{Pl|1Cs-rCeKM#bgXGkdyhm^k_r ztWQyW2;I>5%4mzl!P)s6lI};^6`k%q%>GTJI_t%?t1KWbQ0?YL|j(kRJv#PkWk6}{gr4W z3Z=w47Pd(&%d)so7TU^q%OvMTv-GZ+v6p_aH}52N=JLpcoDK$BEbK4?oS348!{uAy zC-!Az~prT6MoYx9Elp`B^u;t_7PnKd8?RpKo$dsCwMUDx_YaDq?`Ljar_5 zxlIQ=02I;IlE&f->%uRVru&%A*vpEV546mhX8ayy+o)CkF}0XfxVs*Mj^g-JPplNw zlnLLHW-=(uM;;N`l5^T_vo3q;MdLSi9Vt*1(nNQa1t58c=_iQps&pB9zd_kxrPVg4 zG3HmLqP4(Z?GQa8Jxq&Lhq_0o(%;AD^Y0EO?m{chc{yohh??|wK zdq<*Y^!f)}*+H!zJyjDiP?Bh& z6M8*t*zD(qhVmqLn0kX0cYQ0=sS{Mgg~+k!@!!1y=%A9)vn-=-p(xJ`CH$RpDURo) z1<1z^a2dNx{Oyd6_@wGn62c66SF|zgDXL!D21E4xKj=iZoN-(+#pm+%k<_dNjasK1)MCGP)O4E-iuxl+ z=`PMtzH?I>%q*SQ<8-OyB{AaXXSCahQ+l|rQwNm-g*z~UWu2#-TXe;N&nyx#&eDym zwg?|+u&C_(5pf`8{qaL=pZ|+$d2`{+b2D+vN;Q?338~{``Y8X1{>jK%Dz6DZSNJGPU@sx zTPY2C)XOJA64T!QHiWNoc>K~;03~AIBhD+a~`eDALX z7chDokiiG*3QTCT+Jh4p34(ANz?qWkC=of-ppx!p2;;Fd_+tQQ4hG-)LI|S2!D`Kt zXZAPfOvM)m=D&ww5VP9qWBcGD6S0{`F_v6~umk)4mh1@xskuN)Y(r<4qewNU)LNb6 z492e|c%3SuBwrR&3xgisIoaEA$?wiVq1~GNRRszInVHcY?^?vGc2G|S7pka7oIoYaMd*KvsJd+wrH0?#Qbyhb5#vdA6Bw`&tU+QMhQN>LP%Pa z;SpE;y*%V+4MKT;EsL(bmSdd0F%J@5B0J3#XIJbcJE6ZbnOOJM%wq#;YEbI0UNQ@{ znz5`hw4Hg?Pl!SHGQ zFkcmqA;&L~L;>Tiy#4a{&2OQ@a+sJ+_P-|cJ&dY&Nbbb3zxF!ajL(RmnCayW<Y+9!SLE(0&A;7kus&O zM5b+=TyXNm06)|3IHjNY`E0QfaeIB+g-C;IbCz}sn=8$4Ky*+QkB*^UAqSQ+^nGjptzoFuz_EX?- z=kYkC^oDyPmx(4HrDuz}O_TfWYOAmcie^zmQOI z)wG?>;>~`+@HX(pZ(8+ZTr#WZ92Wl?0gaCSRu*^O!F2J+Ou4c$%{+`w1EtlWQ8Z7Z zKABw*4}I@DH0I)|HRoc7U%t-1vz{x4WngUa)sN{73LIjZVb$eeH7OQW(;cb52xGbla=Z&bwsd-J9?8GB_HdbPhB?^v^Ga;WMn%g! zt@WUk+Ewb~M$`R+S{Ap178A!vhv*e320rwq1^X-}9xwhnLM~)-5ohyxUmf3T%520l zfutuF(ldsWUQ37^H0NjW=5>58G&n6=Q(dnu+3C8TM`()u4gLiIL^scyAI?IWlNy+x z+f3WNB6Ct4`n`Db8xV1LxvvIP0_S6`^I$JIxnJMCT}T*&b}zcO*_ndfNDELWjZeLv zG>vFk$rWX#T8}(qz&(?oM|>8_sUlWjYqy{Jkcy<-?7Yn5!?@MQs}9FqdG@PEf1mg{ z&2bvDMQ^ff8Emorx#lNtR{7>H56IYtV?*5WD#AZ~X!4$~)j!*l-)wAFdQH|J6>0MV z!4g_xjZrQq8($*x_xTT2dJJYgWNFv8;&Q*6C*~WdxA1T0=PA#eGDq()lyd&q>W>W8 z&AEF)*@}M428Xb3BO2sRCPKhcW0N3=Z$8wD=r@JlnRTxm#`b9>h8q<| zuy}h5q^!52$L{?}olhiST>;+aKFlz^pvKJNZASsDTGl8|_m;;q*0AMG+C4 z=>E??L!D7_7~qEgdCaf>C&oP9Tg-@oN12d-cN719(tlTieFY#{TFDA^DR7m9ozczJ z1)v`%@I^#KYA&e6BLe~g*h0ankYbzPlg#lC5?Qp&@E&rJ^YZeh%8P;}{3%$w6}uNE-`l`0f}k&A&YOtC zlM`lzT!{}58bW4x%Egrcl2}k?BqmAl91bmoC?0}P|t#ki;Hp}KLFeA=g{?1JJK8Yj)bl0n+PHlnrf2~ zKY6%CYo_h*+t4k8ClAL2&!nPf^L@NZ#P8J;neMqWBkJiCqDb_ORguDgwKo|U?JGy# z+_s^cYg`}UK|CRPARoz_-L?R3u$mee8oOa!4fpr_Kt4WyOdyg(WL>)yK&Pp^?M0MQ zb7vq4(a8DV%_*f==4 z^SW|(C`$`eZ4`oDxQG)8w+9f-v+nS|=+n7XOzX2X8* zyln~EdQLH`eyt5K>1n57PH^60@1wyhKb@zz@bLP<>))Z_v2_>aj_41He`L`&*47@s z2&~PA6b5jAk`onl1+^31(I+OQV*>{oABYXhoBE?iq-OaR!18gp#&!!1uZP=E4m-yZ z1fC0PN@2V`YD54LI}^e!kFkvy27W0-mVqKRwq=7~7?uO`)q)KkK2!u?U50puKe>oE zAzHKH?@5XvMcrtNCj2JZ|o%H5JlJN_n3xdX0FO5q?&Fpo8nU=4GEv!~^E6gV9@gpXu(Y^kW}&QKowv zjIsIqA*N5O1wT0Su|7?t0qM-3;PEagKyMR&P+oSfMl@*$G3gH_*&*!-Cw>NH7%xXZ zuaVszDrYhsMY5Bx7+6fGu(X|@3%}6%n?q0eEr+V+%EZmFL!bU(DXc={6F7v1F?aQb4?P|{aAW8!U1_S*vNbyR-> zaYaOsK4=UUZw+3?NlHK^B_f+C72t-XD~{2VFlBC?y!c1wOUQ~xqGr|hUd(ez47ygT z(&1#;)3-|++S2pHJLDX#72(x^$b1^RCiByq32gd#KPhn%6s()i6--Lc{#o^ji&(~b2<9feT{0?!5RW_mkptx2a}R=;{CrF z@6%0uQZu71LM9<#(fkOuol7<9Vn0G4w*UQv&&^4T){crgrWT{cyz&BMy+hq5!Z2tM zO8hX`T6Y`rEZb)@n8#MEfrp!wYGd?pB$m*qO~Xg ztwg<$y6i=F7AEVjXRXYYJR`xxYeq0r$ip)0(I3}2g|+upC+h9Te-@}aQpT?;rb@Cg zF|G}!)PdW%}=KmURP@SW83{S?mygi3z#76`aXbu zXa_)3CXEMdsKQGQcw~s5PGED~g0d3hn7F6BYBiLTi{42Tn>%r`;cLw3i+Zi+Fh#ro zk1rsN_14DARp_<@Oe=`g9g4>&m)X|li^QtcV4y6iYWL=Gk*@0|+7G^Le)34IpC8_~ zeipuqG2Ar44FS9{kD1l00f+n9csMd7#6~U&$4=c#B9X=4#XI>56T$5lyHYKqS4`C7 zSTEziyV=-C(&{{IzPbFZr>8fZA>h0fmIJEJ%9qv{9!u7%3lFef^EkA8?jR(YOxDuA zS^=SLIggHFXIgHUzde=vl|UXz-6;*ja!(9ZAKFLuA8o1_DSN$VVt~I((a-)R_a$aq zoZ4QdpB(LS2nUTd@g4fu9SOjCKXtF<`&GrGm;HOOdbpkiUrwE7t{d|hzYe@I;C9-+ z*z<1qnQVa=<8d3O6t#dq>TeF8ib| z#Lj0DZ#8IcWzRp>Lr%)S2K2D6XjB}Zn%DLp4^x(V*9x_2jA8dE?c>9#2V-bN-9)z{+*sQ_ zNJfu+VvO-jKqJ&yzWMpwIE}t>z#LmsHy`;n-EOBgSjJKL*P7mmWJO)rmbuoPR(XKw zpLK>*Pq`37qA#I;EfVVINkcxv{Oj4`goj;wVe1#>M45NhQ{0zR2ba(q36x~**E4Ea zalsRe?F%f^wDu3x8Z*7I^W(`E>GPtSO=oTZg`W@|67M5yCCdu3d-CwN^a26viusdqFFDs?SYaSRW1ulL2hdFlbf{CQ{_c?nm zGK;8qim(@ls{#olVHq1mvqd$gXM2YtkBbF4ix|JLs5`^V%mjqX<}Zs6==uLBwrHO2 zJ;BRvaT{+@*b`96kG1t+|6quowu!bzOo{@r&45%{36{dyk^SU5cI1a{8*jhmwo=>k z8ycsgIb|+h*dt&AuS;5@#PX2Ozj_dy=T9K0eZjg12)|V|V!QW0-@DRGqX_D%dv9NU zzKbn5mY9T=Z|^618P6}#8GFrOyi9+QfJj-4CqAec%v|@NxtK&iT$`T4Qh1BaD9w;m zP0XiXIZVRgfw-IC0*-~J?pf~oR!_C8)axa&ibFK1o?`1h`bbt<+Xz_gdW#3AHSv4k zoageT4F(xO-r|iBomT2*3GaEHke;GAx62}YEs4Od-@{Jcv*mUohDW+MNQu=C6S38g zb;IN3RdsnsgRd)T6r3LK)Z7%FY*r;s>>TS=3yq~(r*9dgj$7cCy6ADoM!n2B$KD{d zWjl`&p&=&`KgHk7JYB{ltQ?IMP=3PPe2K`{sEMlbl3~Fa6lW?p zl!D|y61^B(Kfdt`TdV31Fd0?|pEaI+SzA5DG`1hk=f7^e8l+PARhqO~!YYH=xmu?5 zL{d-~<7-)z&=J%LjROUCmz%%jmX0PCLMZZf*v52Ebgw)@B1>Z_UJgM7{*Q~kX zP0&rm(O9Q|m=S%EY>9Qt3#Zc-HAzIKz-Ptj_|eu0deWWwkbYmBNAt1>h48tbg$*3Y(QV^+8IV=ExjuU;ky z>X#r>uSn223)HaKek=6Mw|p-ZXOtXd{9==g%ud)W&vS7e;F3-ChVZHzoS(w&hHzXm z#3k!UD`TPabaRCN8JBDz7P~wm?HXg4QY;5x*QfLc8~Vy;&#ozcQz*I80K-U6XiKk8 z-SdJw`idY%+PD4ya(B`h9;o}XM`NeSwR@KDJ{c0FkK}fh4k04xb=<4jLs4o-WKUuG zuzD`Rb5_+e;*~N=@O3zX*l(Mj8{WkC&Oa7p4 zk#W6pGc8dAbO^XWzrh}BSX`JMy>A{eLwjCv(zlYhavdmR-*nAzwUXc4&(fV7vnkO8 zwPL?_-?-b3dzU?!;@`HcIjx?(w#89Q!REm+ zw>>>Akf;j-k}kVPbgJy82j^ocyQ)3&OCi#C)3H%^o*%8%-5E~ANH*Ng_G~Przh`Q= z`KjtD&SA~xK}8a8+sD_K=C$FcZ_TfutnWM2IpbANxR`E>jE%ma4|n%>-?{73b4kI= zl|?D(AGD1zMdNZ_qPYvbUD=-bJ80fPgU@%;aM8xSPs;Pl;&LI?L>8Pv9OwbjB`w_Q z7ycF%8;Zg$vv&Eeu->`?V9+oc+P!M%!pgZe%35&NZ1tv9DxJ_|SA1-WV~T|ul)apD zJajee4o|cv#4jmo=2v9OnO=0v*RJQr?QmEUYY@HcuXg@gE3(#aTinn1>lwq#iicM= z{y*xbUPuu`J>xcZ$;4{3Y``7)Xb80_#{KN?aVCacb|^)iUKdLTs)*)yi02e^OswC_ z;Upv_O~$<4T3S}gP#@&CQ1fsOKiOP4)W1VBHkAb8jT!3C=WR!F$12o}q=yM_y`C-e;^ilh zcKFOGpu!##G=!1fy4*%S&A!MFzKK;3>5{+cq^2GZK~W;=zu;7PH21G7>DvvikUH6s zwy~&n!6Hl`ApP_3Z6}?we=?4Kzkw6#EG5V?W}w=wChc+oPXk_{&84_SL#M<)A67(Z z0Lyvz(%@WV+lNPgcaq>Q82q)Yjm`deQEGSr5DGnqY|FGm5djy=xV*mJEdk$=`p-XI zZ!gdGhW}m&-|zgNiMFFZvI7YH=8pe{yi}t4&!d8~U9RW23~;vmKPtn!2)zO%!IVQ# z{{J7dD+J)zWV&`UX1HX%HyNn0_vRZ_G#+4KO>9IxeFcF)I_N0Dsj?1G!pS{XZEz@3 z;{M^Y4~XP9xNwS@LNcDtcU(tDncHQbT=;OUx3dR6R9W|jN6(rl<&}M~4>tzGx;_7# zvfmNp{Ua56BJmg=9v*<3Rs!N5Ka~543`(IRaN)dW%)6AA?Z~i_rJp~4?(8(1j8OjH zXz*H#8KCZFWPA*C(iGwNa8^BgAVZ*4wNhUaW3Tm=|G%klAd6BUBhpL9y&LtDe9jvo zjN6N0Z$dmc+dhGTjU5b`{(wRO^)qL8JzRU+6++7A#s=ALr}zn6$s%DtKAI3%0x&rn zEm8q5UhkVLGOu%Ac_4}a$0TaE=Ns#QUKr?CriAY{0Ko3SCfA>h6zc#S9q(&SL$Gsx zcVRfm>z5j}7MIbFp2S7VZYYHuG{7_TVw9-;*$Ex9+lOlIQm!wgMd|g{7y_OoEciT=taEQ*kW~2T;9@grD5Hi7YKCd zgs_j?wv-$40pnBMWeRFX=m2g*SAdW{=2`g?a<-Z_blKU;BAOC z+=A^CZ|_^6XkWNV0$mHWremnS(^Wv6pTMAW2}JNfY#yl)s9bk-`MX@DMD+kj<%Q)l zrIVl2xwf&FHy!o?o&WFgA`XjbHf7dFaC-W!zmC_%I;dDb&NRDo-CUlZqmzsk$g-+e z7s52O>us`ta5*JK18fvUADcV&gPWcn9v+6|YBf56)&Jsk&2zjD#J`Tx|4cuK{7b9j z??g*(g)KYq8#{UZ!FDhOu@U94NA`(Ad-S7S%%u?jE7|WPdVu5>c4#1nQuHZH9#v= zt1>83E+rbhE_gGa#vftP5I#srN%`<8cQnu>1I_7se-JBRpDm^Q05~Q*+F5Zn23$(rGT+e$#bc@Iw~7u zWVp+d?SVE`jf&Hh;H3dGGj4=yuNc}VHUWo~{M-;)`uuN9seb{MP{4UN_5RK3|J;CR zn&{fw+hLE;gO9f+q&O+V$R4n61L^lOnbdmsGpkssPauUjgG>N=eYy%m|MVp=f{=LP zkB0qV3a~sN)M@`+Y>DW47dJQHdrJ4|flnqfsipI}X5$ygW~2jcazV_y2GFBo_0eFw zOsf%uq9JNo5Yu)STfLj|vXlv&uFh=*1O#wD07t1Yn*ee~vf*AHnJxS@pauTjIhOlY z5$M_t`x7_MIuepVF9zVowpRFfc|ANmCtxj}e;Q^o)n%kVvQdD@X_f@mOpU}Ixc#}% zLD=!`e9B#L*3ANr6%CG6+gTFYb2*g zAy0TITP%!4y9u37I+Yv9l0(gQfDp3b7oBE3TYxRtfIgYn2K_-+Kk6&(e-0s{U@oCE zXvT0D4*^zie)HD0bt+CmKW27zx3IMW^h&KPLM>D9eovNmdT%vl>)YzpHZHkICNh~8 zV#W`TUlq#d*ytT?3@U~Ve&X!R=CI7QUTjHY)~Mz8yqqp0dSyrbF#8g9>f0;czd15Q zA2i@dFbRu@WZcZjKCLS3(dCcW-o951x~d{BB1v9@F#3ibc@zDxlum;@r6{in1Y0H6 zG}H%#nchkfBw%*oosop>l!krr>X4F$=!<|1RXJgb(6j-KCmcotAx%w0y$H~9ZIa&P zPvYrB&*ob^8`VYCCqbrjf4!C9lluIIe(oj`;FA~%5me5-y}k0A-vu#gN=u?3NUDTk zlk%cJc+k~~Dn!{np}qc1pTIaCfsAT4=K7n5iwn1OkayP;Bnm5BBpm-8hQ|ap(sS(I z@Zc$jF#_zo3^X%s?R^FfWF$Neg2F`$QsHC*3;q5546b5T3Sh_H+IkOY#=$$2FBR_d zt1br!0WhP2qeq}~udtcNaSIvCVQ1D67#C<1n*7;UsWMw_YQNrxLRQFR^K&T8{rDF= zu>klJfg&C;0TbCX710dCXW!-v>iZ+nWH@6UZY5w(u+@m7^L;3bK3*qwCd`w1$ z4p8tboy!Vb2kbxLFe>c!@j;;81tIeZXh$3OTT==IbJdhx?g$*d3Y#>n%SSGRHzI|| zDt*f=^Y~G3`_R({^ar`PfmBmlEH_)M_L+8#02lY}#eSr}2Lr{`vqhOUBS zHY>Ggi+LUWdip_c@fX0vwV$Aq!23z?BdHrbE)J)0VVx+&Hje9;_xN}2J!w7cW32sf zGu9JBy^jQX{uZ3@{>H?mK&03QxkFo=Bfu-!K3gyHudM)a{aQGrTB#>mU*`mdwvI92 zeLNx_G|^w1FbeEN5H=5Ba1a);zPJ)19J{(zwix!~YhoMe`TUoT5N>0>vqa{{Hua?5I;jk*N_ zBeINl#KwSE+t5+>Z4}jpn!RIl<3C?Q0yqowKN9as+$Dhu)C+NHI*_A8V8;LksNM_P z!o!IzRb>>SF~02T5yj|PpkxHT`O=_lBFviEP@HJC%CH|GTzD_r6PptCO8vmPI%lv4 za1l96AFpz}ILiQZ^(7IN?^!)n9GU5U4_p&4uSayVJp2t=V%~o3c5Jfa7=39vptz%= zBk(?P{-2wh8}-^LQ4D}u z-X<_4DBpA69HogD+ol)tynO_LWH)<`F0`I)O)!FN4uEJ1hjAqA5sPB}gO}&Kje7&m z;~9t?5;yx9X2xB+J^aP87D6CgR!_Z_n5j9w28ksk6;u&3Up1JVYzvV^y&{M>f$J6V zM+$qa+=?}M9u0Dx##(Tu|N6-OzN7dOnZ`X`Foa#>KuUYpkV{{IY%{9nL@PL zRXjAR8-zezQjEO2HxD{sAkX zXzh4rYLEebIJ=&M*`ro`#J+<}rHt3oEer9w~RvEPoSE}Y6?EE8bR6Rig{ zQ4&5cXnvh3R9ns|M_2`gna)-hC3Y+{I%8v4fmYjbBLLvr7_O%iPo1iHP*_j{iA&80 zUfC*DmYg5Vy63GLL0=0h!bG=xFmSJ=;HBa1RQ!D$R(rv zKj&-A2_!Ad+<)Dx{XR!z%&J}f5(PH zV&&@MC`hMc8bV51CP@nR0exLkt%)|SB9j0*+x|SP3_6YZLr;#5hIXPrB7A4Me{#ghtPDYWlCjTg$Vf;!~A1(53j)Y>yP zeUbrUAUp#A0}7CoV37pywwqcw)o8sh0S=hp+P}QJ6amW2|1W(Xg&I9v>}56b#rw8} zsDA-}!XL+J#4Sk?R7c3>)(cIhE}`O4q{}{++BFlp+3LS*0iZSp+1HCD=cVJR;y-OR?hPBMl3E0W)S69QO?0rX~k zYPJ6v+1U8HR=|EE9MhF5)}@gUUQJN&$L~`^I&FR;m3EZAnPs1uT7yYHG_l^k*9~-I zmS~w#VhrI9yZ}sr;`iGXs8W?$v&`g(s4oGM{PfaNy-DlA9oJMRsakC!|2J2Eq!NF4 zZNmrA<$ z2z3B2rxzO)mhDhh*T5v0}BZW5i#0E7Ch~Ag`NbdsCC zGZ)xvuwDcMytJ794!`G1UfsWGa_i}w9)o6~TvjI!0C3jnn2XKsQ=OeTpumu&LfhB| zu>pwrz`}-f1NU=xbu|@_3Nwz2M$xzm67#(fv*!#9RPj23dokJ&U0;`CZIr0 zHf)vP<>RZ-ODIWDAac!9u=`p%eoZvPB%k+uOUeOV>-#UDeXUvCTy) zuoC~4>ZSQ;?uW~V3;_42GHKS^T9}06-1CyWr#ANKUZiNrU5W ze0+R(XLP{No|x6$`NkmM>3V>-Bx+((LHF_&*aVlj8E^j2>8&Q@Fyw#wO>zmgN3-?GVRl zKwzDug5-7p)=Mn*#rv-hsQW+-=V))Qh%{NQ)u@~|Uh50Hry?kb5b3iYPp=4;R3%Uc z@B^S)K`#N{Q_$>*_JA-Nc%23fp;d_hIexKnshJXcBk=JZa;ZcnA@s+20?(b=@10P9 z`s=F^tlb9W8#d5Xy*T$3LMc8$R@jY8^+q*qH}CKAL?cKdEH5uJEJjol_LfZwZZ9;u z1Ae1+cA5=`*W-L2&`{^*csHw20%l|nXXd7G+=+i;(LTQM2YGGSf6{|j3w><> #$originColor { +object "ZapperLidoARM" as zap <> #$originColor { } object "LidoARM" as arm <><> #$originColor { From 2e7f29473bfeccf5b6be31c9b63caaa6e0157f9b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 11:35:35 +1100 Subject: [PATCH 151/196] Added crossPrice to check when Operator sets prices --- docs/LidoARMSquashed.svg | 171 +++++++++--------- src/contracts/AbstractARM.sol | 19 +- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 41 ++++- 3 files changed, 144 insertions(+), 87 deletions(-) diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 5c74b0d..cac20f2 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,94 +4,97 @@ - - + + UmlClassDiagram - + 15 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[43] <<AbstractARM>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> -   DEAD_ACCOUNT: address <<AbstractARM>> -Public: -   operator: address <<OwnableOperable>> -   MAX_PRICE_DEVIATION: uint256 <<AbstractARM>> -   PRICE_SCALE: uint256 <<AbstractARM>> -   FEE_SCALE: uint256 <<AbstractARM>> -   liquidityAsset: address <<AbstractARM>> -   token0: IERC20 <<AbstractARM>> -   token1: IERC20 <<AbstractARM>> -   claimDelay: uint256 <<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>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _capManager: address) <<AbstractARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> -    _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>> -    _setTraderates(_baseToTokenRate: uint256, _tokenToBaseRate: uint256) <<AbstractARM>> -    _requireLiquidityAvailable(amount: uint256) <<AbstractARM>> -    _availableAssets(): uint256 <<AbstractARM>> -    _externalWithdrawQueue(): uint256 <<LidoARM>> -    _setFee(_fee: uint256) <<AbstractARM>> -    _setFeeCollector(_feeCollector: address) <<AbstractARM>> -    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> -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>> + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[43] <<AbstractARM>> +Internal: +   OWNER_SLOT: bytes32 <<Ownable>> +   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> +   DEAD_ACCOUNT: address <<AbstractARM>> +Public: +   operator: address <<OwnableOperable>> +   MAX_PRICE_DEVIATION: uint256 <<AbstractARM>> +   PRICE_SCALE: uint256 <<AbstractARM>> +   FEE_SCALE: uint256 <<AbstractARM>> +   liquidityAsset: 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>> +   steth: IERC20 <<LidoARM>> +   weth: IWETH <<LidoARM>> +   withdrawalQueue: IStETHWithdrawal <<LidoARM>> +   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _capManager: address) <<AbstractARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> +    _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>> +    _setTraderates(_baseToTokenRate: uint256, _tokenToBaseRate: uint256) <<AbstractARM>> +    _requireLiquidityAvailable(amount: uint256) <<AbstractARM>> +    _availableAssets(): uint256 <<AbstractARM>> +    _externalWithdrawQueue(): uint256 <<LidoARM>> +    _setFee(_fee: uint256) <<AbstractARM>> +    _setFeeCollector(_feeCollector: address) <<AbstractARM>> +    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> +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(_crossPrice: uint256) <<onlyOwner>> <<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>> 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>> diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index ce9fb0e..b4db783 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -66,6 +66,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { */ uint256 public traderate1; + uint256 public crossPrice; + /// @notice cumulative total of all withdrawal requests included the ones that have already been claimed uint120 public withdrawsQueued; /// @notice total of all the withdrawal requests that have been claimed @@ -109,6 +111,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { //////////////////////////////////////////////////// event TraderateChanged(uint256 traderate0, uint256 traderate1); + event CrossPriceUpdated(uint256 crossPrice); event Deposit(address indexed owner, uint256 assets, uint256 shares); event RedeemRequested( address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp @@ -171,6 +174,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { capManager = _capManager; emit CapManagerUpdated(_capManager); + + crossPrice = PRICE_SCALE; + emit CrossPriceUpdated(PRICE_SCALE); } //////////////////////////////////////////////////// @@ -368,8 +374,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { function setPrices(uint256 buyT1, uint256 sellT1) external onlyOperatorOrOwner { // Limit funds and loss when called by the Operator if (msg.sender == operator) { - require(sellT1 >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: sell price too low"); - require(buyT1 < PRICE_SCALE, "ARM: buy price too high"); + require(sellT1 >= crossPrice, "ARM: sell price too low"); + require(buyT1 < crossPrice, "ARM: buy price too high"); } _setTraderates( PRICE_SCALE * PRICE_SCALE / sellT1, // base (t0) -> token (t1) @@ -385,6 +391,15 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { emit TraderateChanged(_baseToTokenRate, _tokenToBaseRate); } + function setCrossPrice(uint256 _crossPrice) external onlyOwner { + require(_crossPrice >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: cross price too low"); + require(_crossPrice <= PRICE_SCALE, "ARM: cross price too high"); + + crossPrice = _crossPrice; + + emit CrossPriceUpdated(_crossPrice); + } + //////////////////////////////////////////////////// /// Liquidity Provider Functions //////////////////////////////////////////////////// diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 593c56e..af68ae0 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -151,7 +151,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.traderate1(), 992 * 1e33); } - function test_SetPrices_Owner() public { + function test_SetPrices_Owner() public asLidoARMOwner { // buy price 11 basis points higher than 1.0 lidoARM.setPrices(10011e32, 10020e32); @@ -159,6 +159,45 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setPrices(9980e32, 9989e32); } + ////////////////////////////////////////////////////// + /// --- Set Prices - 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 { + // 3 basis points lower than 1.0 + vm.expectRevert("ARM: cross price too low"); + lidoARM.setCrossPrice(0.9997e36); + + // 1 basis points higher than 1.0 + vm.expectRevert("ARM: cross price too high"); + lidoARM.setCrossPrice(1.0001e36); + } + + ////////////////////////////////////////////////////// + /// --- Set Prices - PASSING TESTS + ////////////////////////////////////////////////////// + + function test_SetCrossPrice_Owner() public { + // at 1.0 + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.CrossPriceUpdated(1e36); + lidoARM.setCrossPrice(1e36); + + // 2 basis points lower than 1.0 + vm.expectEmit({emitter: address(lidoARM)}); + emit AbstractARM.CrossPriceUpdated(0.9998e36); + lidoARM.setCrossPrice(0.9998e36); + } + ////////////////////////////////////////////////////// /// --- OWNABLE - REVERTING TESTS ////////////////////////////////////////////////////// From b339fc50261ae02c45084743424334792a6b7127 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 11:41:08 +1100 Subject: [PATCH 152/196] Set cross price in fork section fo deploy script --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 3 +++ test/smoke/LidoARMSmokeTest.t.sol | 1 + 2 files changed, 4 insertions(+) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 4e37773..c4de208 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -127,6 +127,9 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // upgrade and initialize the Lido ARM lidoARMProxy.upgradeToAndCall(address(lidoARMImpl), data); + // Set the price that buy and sell prices can not cross + LidoARM(payable(Mainnet.LIDO_ARM)).setCrossPrice(0.9998e36); + // Set the buy price with a 8 basis point discount. The sell price is 1.0 LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9994e36, 0.9998e36); diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index ebff091..0196123 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -52,6 +52,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(address(lidoARM.weth()), Mainnet.WETH, "WETH"); assertEq(lidoARM.liquidityAsset(), Mainnet.WETH, "liquidity asset"); assertEq(lidoARM.claimDelay(), 10 minutes, "claim delay"); + assertEq(lidoARM.crossPrice(), 0.9998e36, "cross price"); assertEq(capManager.accountCapEnabled(), true, "account cap enabled"); assertEq(capManager.operator(), Mainnet.ARM_RELAYER, "Operator"); From 322ec2f15f633003c05ce0fe947cd2116de2575b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 15:43:46 +1100 Subject: [PATCH 153/196] Safety check in setCrossPrice so no stETH in ARM when cross price reduced --- src/contracts/AbstractARM.sol | 18 ++++++++---- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 29 ++++++++++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index b4db783..90987d4 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -391,13 +391,21 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { emit TraderateChanged(_baseToTokenRate, _tokenToBaseRate); } - function setCrossPrice(uint256 _crossPrice) external onlyOwner { - require(_crossPrice >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: cross price too low"); - require(_crossPrice <= PRICE_SCALE, "ARM: cross price too high"); + function setCrossPrice(uint256 newCrossPrice) external onlyOwner { + require(newCrossPrice >= PRICE_SCALE - MAX_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 (newCrossPrice < crossPrice) { + // The base asset, eg stETH, is not the liquidity asset, eg WETH + address baseAsset = liquidityAsset == address(token0) ? address(token1) : address(token0); + // 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"); + } - crossPrice = _crossPrice; + crossPrice = newCrossPrice; - emit CrossPriceUpdated(_crossPrice); + emit CrossPriceUpdated(newCrossPrice); } //////////////////////////////////////////////////// diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index af68ae0..55e3456 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -89,7 +89,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- Set Prices - REVERTING TESTS ////////////////////////////////////////////////////// - function test_RevertWhen_SetPrices_Because_PriceCross() public { + function test_RevertWhen_SetPrices_Because_PriceCross() public asLidoARMOwner { vm.expectRevert("ARM: Price cross"); lidoARM.setPrices(90 * 1e33, 89 * 1e33); @@ -160,7 +160,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { } ////////////////////////////////////////////////////// - /// --- Set Prices - REVERTING TESTS + /// --- Set Cross Price - REVERTING TESTS ////////////////////////////////////////////////////// function test_RevertWhen_SetCrossPrice_Because_NotOwner() public asRandomAddress { vm.expectRevert("ARM: Only owner can call this function."); @@ -182,11 +182,20 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { 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 Prices - PASSING TESTS + /// --- Set Cross Price - PASSING TESTS ////////////////////////////////////////////////////// - function test_SetCrossPrice_Owner() public { + 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); @@ -198,6 +207,18 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setCrossPrice(0.9998e36); } + 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 ////////////////////////////////////////////////////// From db0b95aba14c0240ab5d96fdf42863abf99d42b7 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 15:59:49 +1100 Subject: [PATCH 154/196] Allow the cross price to be 20 basis points below 1 --- src/contracts/AbstractARM.sol | 17 +++++++++++++---- .../fork/LidoFixedPriceMultiLpARM/Setters.t.sol | 10 +++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 90987d4..d6ca721 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -12,9 +12,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// Constants //////////////////////////////////////////////////// - /// @notice Maximum amount the Operator can set the price from 1 scaled to 36 decimals. - /// 2e33 is a 0.02% deviation, or 2 basis points. - uint256 public constant MAX_PRICE_DEVIATION = 2e32; + /// @notice Maximum amount the Owner can set the cross price below 1 scaled to 36 decimals. + /// 20e32 is a 0.2% deviation, or 20 basis points. + 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 @@ -391,8 +391,17 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { emit TraderateChanged(_baseToTokenRate, _tokenToBaseRate); } + /** + * @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. + * 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. + */ function setCrossPrice(uint256 newCrossPrice) external onlyOwner { - require(newCrossPrice >= PRICE_SCALE - MAX_PRICE_DEVIATION, "ARM: cross price too low"); + 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 diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 55e3456..20964a3 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -173,9 +173,9 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { } function test_RevertWhen_SetCrossPrice_Because_PriceRange() public asLidoARMOwner { - // 3 basis points lower than 1.0 + // 21 basis points lower than 1.0 vm.expectRevert("ARM: cross price too low"); - lidoARM.setCrossPrice(0.9997e36); + lidoARM.setCrossPrice(0.9979e36); // 1 basis points higher than 1.0 vm.expectRevert("ARM: cross price too high"); @@ -201,10 +201,10 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { emit AbstractARM.CrossPriceUpdated(1e36); lidoARM.setCrossPrice(1e36); - // 2 basis points lower than 1.0 + // 20 basis points lower than 1.0 vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.CrossPriceUpdated(0.9998e36); - lidoARM.setCrossPrice(0.9998e36); + emit AbstractARM.CrossPriceUpdated(0.9980e36); + lidoARM.setCrossPrice(0.9980e36); } function test_SetCrossPrice_With_StETH_PriceUp_Owner() public { From fecb2c08feec789dd8b55a3df3002f3322a970a6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 19:09:35 +1100 Subject: [PATCH 155/196] totalAssets returns MIN_TOTAL_SUPPLY if fees > available assets setPrice enforcement of crossPrice also applies to Owner to ensure no asset losses remove old Price cross check as that's replaced by the crossPrice check --- src/contracts/AbstractARM.sol | 30 ++++++------- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 3 +- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 43 +++++++------------ .../SwapExactTokensForTokens.t.sol | 4 +- .../SwapTokensForExactTokens.t.sol | 2 +- .../TotalAssets.t.sol | 2 +- 6 files changed, 34 insertions(+), 50 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index d6ca721..245613a 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -372,23 +372,14 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { * From the Trader's perspective, this is the buy price. */ function setPrices(uint256 buyT1, uint256 sellT1) external onlyOperatorOrOwner { - // Limit funds and loss when called by the Operator - if (msg.sender == operator) { - require(sellT1 >= crossPrice, "ARM: sell price too low"); - require(buyT1 < crossPrice, "ARM: buy price too high"); - } - _setTraderates( - PRICE_SCALE * PRICE_SCALE / sellT1, // base (t0) -> token (t1) - buyT1 // token (t1) -> base (t0) - ); - } + // Ensure buy price is always below past sell prices + require(sellT1 >= crossPrice, "ARM: sell price too low"); + require(buyT1 < crossPrice, "ARM: buy price too high"); - function _setTraderates(uint256 _baseToTokenRate, uint256 _tokenToBaseRate) internal { - require((PRICE_SCALE * PRICE_SCALE / (_baseToTokenRate)) > _tokenToBaseRate, "ARM: Price cross"); - traderate0 = _baseToTokenRate; - traderate1 = _tokenToBaseRate; + traderate0 = PRICE_SCALE * PRICE_SCALE / sellT1; // base (t0) -> token (t1); + traderate1 = buyT1; // token (t1) -> base (t0) - emit TraderateChanged(_baseToTokenRate, _tokenToBaseRate); + emit TraderateChanged(traderate0, traderate1); } /** @@ -556,7 +547,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { function totalAssets() public view virtual returns (uint256) { (uint256 fees, uint256 newAvailableAssets) = _feesAccrued(); - if (fees > newAvailableAssets) return 0; + // total assets should only go up from the initial deposit amount that is burnt + // but in case of something unforeseen, return MIN_TOTAL_SUPPLY if fees is greater than the available assets + if (fees > newAvailableAssets) return MIN_TOTAL_SUPPLY; // Remove the performance fee from the available assets return newAvailableAssets - fees; @@ -587,12 +580,13 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { function _externalWithdrawQueue() internal view virtual returns (uint256 assets); /// @notice Calculates the amount of shares for a given amount of liquidity assets + /// @dev Total assets can't be zero. The lowest it can be is MIN_TOTAL_SUPPLY function convertToShares(uint256 assets) public view returns (uint256 shares) { - uint256 totalAssetsMem = totalAssets(); - shares = (totalAssetsMem == 0) ? assets : (assets * totalSupply()) / totalAssetsMem; + shares = assets * totalSupply() / totalAssets(); } /// @notice Calculates the amount of liquidity assets for a given amount of shares + /// @dev Total supply can't be zero. The lowest it can be is MIN_TOTAL_SUPPLY function convertToAssets(uint256 shares) public view returns (uint256 assets) { assets = (shares * totalAssets()) / totalSupply(); } diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index bb41d4f..dff508b 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -302,7 +302,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { depositInLidoARM(address(this), DEFAULT_AMOUNT) { // set stETH/WETH buy price to 1 - lidoARM.setPrices(1e36, 1e36 + 1); + lidoARM.setCrossPrice(1e36); + lidoARM.setPrices(1e36 - 1, 1e36); // User Swap stETH for 3/4 of WETH in the ARM deal(address(steth), address(this), DEFAULT_AMOUNT); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 20964a3..540582b 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -89,25 +89,10 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- Set Prices - REVERTING TESTS ////////////////////////////////////////////////////// - function test_RevertWhen_SetPrices_Because_PriceCross() public asLidoARMOwner { - vm.expectRevert("ARM: Price cross"); - lidoARM.setPrices(90 * 1e33, 89 * 1e33); - - vm.expectRevert("ARM: Price cross"); - lidoARM.setPrices(72, 70); - - vm.expectRevert("ARM: Price cross"); - lidoARM.setPrices(1005 * 1e33, 1000 * 1e33); - - // Both set to 1.0 - vm.expectRevert("ARM: Price cross"); - lidoARM.setPrices(1e36, 1e36); - } - - function test_RevertWhen_SetPrices_Because_PriceRange() public asOperator { - // buy price 11 basis points higher than 1.0 + function test_RevertWhen_SetPrices_Because_PriceRange_Operator() public asOperator { + // buy price 1 basis points higher than 1.0 vm.expectRevert("ARM: buy price too high"); - lidoARM.setPrices(1.0011 * 1e36, 1.002 * 1e36); + lidoARM.setPrices(1.0001 * 1e36, 1.002 * 1e36); // sell price 11 basis points lower than 1.0 vm.expectRevert("ARM: sell price too low"); @@ -118,6 +103,16 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { lidoARM.setPrices(1e18, 1e18); } + function test_RevertWhen_SetPrices_Because_PriceRange_Owner() public asLidoARMOwner { + // buy price 1 basis points higher than 1.0 + vm.expectRevert("ARM: buy price too high"); + lidoARM.setPrices(1.0001 * 1e36, 1.002 * 1e36); + + // sell price 11 basis points lower than 1.0 + vm.expectRevert("ARM: sell price too low"); + lidoARM.setPrices(0.998 * 1e36, 0.9989 * 1e36); + } + function test_RevertWhen_SetPrices_Because_NotOwnerOrOperator() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); lidoARM.setPrices(0, 0); @@ -151,14 +146,6 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.traderate1(), 992 * 1e33); } - function test_SetPrices_Owner() public asLidoARMOwner { - // buy price 11 basis points higher than 1.0 - lidoARM.setPrices(10011e32, 10020e32); - - // sell price 11 basis points lower than 1.0 - lidoARM.setPrices(9980e32, 9989e32); - } - ////////////////////////////////////////////////////// /// --- Set Cross Price - REVERTING TESTS ////////////////////////////////////////////////////// @@ -203,8 +190,8 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { // 20 basis points lower than 1.0 vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.CrossPriceUpdated(0.9980e36); - lidoARM.setCrossPrice(0.9980e36); + emit AbstractARM.CrossPriceUpdated(0.998e36); + lidoARM.setCrossPrice(0.998e36); } function test_SetCrossPrice_With_StETH_PriceUp_Owner() public { diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index d0f697a..93c9798 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -11,7 +11,7 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test /// --- CONSTANTS ////////////////////////////////////////////////////// uint256 private constant MIN_PRICE0 = 980e33; // 0.98 - uint256 private constant MAX_PRICE0 = 1_000e33; // 1.00 + uint256 private constant MAX_PRICE0 = 1_000e33 - 1; // just under 1.00 uint256 private constant MIN_PRICE1 = 1_000e33; // 1.00 uint256 private constant MAX_PRICE1 = 1_020e33; // 1.02 uint256 private constant MAX_WETH_RESERVE = 1_000_000 ether; // 1M WETH, no limit, but need to be consistent. @@ -306,6 +306,7 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test // Use random stETH/WETH sell price between 0.98 and 1, // the buy price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); + lidoARM.setCrossPrice(1e36); lidoARM.setPrices(MIN_PRICE0, price); // Set random amount of stETH in the ARM @@ -369,6 +370,7 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test // Use random stETH/WETH buy price between MIN_PRICE0 and MAX_PRICE0, // the sell price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); + lidoARM.setCrossPrice(1e36); lidoARM.setPrices(price, MAX_PRICE1); // Set random amount of WETH in the ARM diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol index 6541bb5..5029dd4 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -11,7 +11,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test /// --- CONSTANTS ////////////////////////////////////////////////////// uint256 private constant MIN_PRICE0 = 980e33; // 0.98 - uint256 private constant MAX_PRICE0 = 1_000e33; // 1.00 + uint256 private constant MAX_PRICE0 = 1_000e33 - 1; // just under 1.00 uint256 private constant MIN_PRICE1 = 1_000e33; // 1.00 uint256 private constant MAX_PRICE1 = 1_020e33; // 1.02 uint256 private constant MAX_WETH_RESERVE = 1_000_000 ether; // 1M WETH, no limit, but need to be consistent. diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index 5ed9187..686c7af 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -146,6 +146,6 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { simulateAssetGainInLidoARM(DEFAULT_AMOUNT * 2, address(weth), false) { // vm.expectRevert(stdError.arithmeticError); - assertEq(lidoARM.totalAssets(), 0); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY); } } From c88e741b8761b9a63dea705938038ad1970754bb Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 20:30:52 +1100 Subject: [PATCH 156/196] Generated latest LidoARM contract diagram --- docs/LidoARMSquashed.svg | 107 +++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index cac20f2..86977a5 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,63 +4,62 @@ - - + + UmlClassDiagram - + 15 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[43] <<AbstractARM>> -Internal: -   OWNER_SLOT: bytes32 <<Ownable>> -   MIN_TOTAL_SUPPLY: uint256 <<AbstractARM>> -   DEAD_ACCOUNT: address <<AbstractARM>> -Public: -   operator: address <<OwnableOperable>> -   MAX_PRICE_DEVIATION: uint256 <<AbstractARM>> -   PRICE_SCALE: uint256 <<AbstractARM>> -   FEE_SCALE: uint256 <<AbstractARM>> -   liquidityAsset: 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>> -   steth: IERC20 <<LidoARM>> -   weth: IWETH <<LidoARM>> -   withdrawalQueue: IStETHWithdrawal <<LidoARM>> -   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _capManager: address) <<AbstractARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> -    _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>> -    _setTraderates(_baseToTokenRate: uint256, _tokenToBaseRate: uint256) <<AbstractARM>> + +LidoARM +../src/contracts/LidoARM.sol + +Private: +   _gap: uint256[49] <<OwnableOperable>> +   _gap: uint256[43] <<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>> +   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>> +   steth: IERC20 <<LidoARM>> +   weth: IWETH <<LidoARM>> +   withdrawalQueue: IStETHWithdrawal <<LidoARM>> +   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _capManager: address) <<AbstractARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> +    _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>>    _requireLiquidityAvailable(amount: uint256) <<AbstractARM>>    _availableAssets(): uint256 <<AbstractARM>>    _externalWithdrawQueue(): uint256 <<LidoARM>> @@ -77,7 +76,7 @@    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(_crossPrice: uint256) <<onlyOwner>> <<AbstractARM>> +    setCrossPrice(newCrossPrice: uint256) <<onlyOwner>> <<AbstractARM>>    previewDeposit(assets: uint256): (shares: uint256) <<AbstractARM>>    deposit(assets: uint256): (shares: uint256) <<AbstractARM>>    previewRedeem(shares: uint256): (assets: uint256) <<AbstractARM>> From 711ba467dc9a8cfc1d3941ba655d3992a056065c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 20:41:43 +1100 Subject: [PATCH 157/196] Updated Natspec --- src/contracts/AbstractARM.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 245613a..899509a 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -65,7 +65,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { * Rate is to 36 decimals (1e36). */ uint256 public traderate1; - + /// @notice The price that buy and sell prices can not cross scaled to 36 decimals. uint256 public crossPrice; /// @notice cumulative total of all withdrawal requests included the ones that have already been claimed @@ -101,7 +101,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { int128 public lastAvailableAssets; /// @notice The account that can collect the performance fee address public feeCollector; - + /// @notice The address of the CapManager contract used to manage the ARM's liquidity provider and total assets caps address public capManager; uint256[43] private _gap; From 66c0bbf7dfa03f794bd83ab369ac1778d68234d2 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 22:41:16 +1100 Subject: [PATCH 158/196] Now pricing the base assets, stETH, using the crossPrice --- src/contracts/AbstractARM.sol | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 899509a..2ff723f 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -31,6 +31,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice The address of the asset that is used to add and remove liquidity. eg WETH address public immutable liquidityAsset; + /// @notice The base asset is the asset being purchased by the ARM and put in the withdrawal queue. eg stETH + address public immutable baseAsset; /// @notice The swap input token that is transferred to this contract. /// From a User perspective, this is the token being sold. /// token0 is also compatible with the Uniswap V2 Router interface. @@ -135,6 +137,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { require(_liquidityAsset == address(token0) || _liquidityAsset == address(token1), "invalid liquidity asset"); liquidityAsset = _liquidityAsset; + // The base asset, eg stETH, is not the liquidity asset, eg WETH + baseAsset = _liquidityAsset == address(token0) ? address(token1) : address(token0); } /// @notice Initialize the contract. @@ -397,8 +401,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // If the new cross price is lower than the current cross price if (newCrossPrice < crossPrice) { - // The base asset, eg stETH, is not the liquidity asset, eg WETH - address baseAsset = liquidityAsset == address(token0) ? address(token1) : address(token0); // 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"); } @@ -559,8 +561,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// less liquidity assets reserved for the ARM's withdrawal queue. /// This does not exclude any accrued performance fees. function _availableAssets() internal view returns (uint256) { - // Get the assets in the ARM and external withdrawal queue - uint256 assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); + // Get the base assets, eg stETH, in the ARM and external withdrawal queue + uint256 baseAssets = IERC20(baseAsset).balanceOf(address(this)) + _externalWithdrawQueue(); + + // Liquidity assets, eg WETH, are priced at 1.0 + // Base assets, eg stETH, are priced at the cross price which is a discounted price + uint256 assets = IERC20(liquidityAsset).balanceOf(address(this)) + baseAssets * crossPrice / PRICE_SCALE; // The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; From 4f6310403a45960f27b06c5cccded32916d85cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 9 Oct 2024 14:09:49 +0200 Subject: [PATCH 159/196] fix: make proxy falback payable. --- script/deploy/holesky/002_UpgradeScript.sol | 2 +- script/deploy/mainnet/002_UpgradeScript.sol | 2 +- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 4 ++-- src/contracts/Proxy.sol | 2 +- test/smoke/LidoARMSmokeTest.t.sol | 2 +- test/smoke/OethARMSmokeTest.t.sol | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/script/deploy/holesky/002_UpgradeScript.sol b/script/deploy/holesky/002_UpgradeScript.sol index 15894b4..4f6172c 100644 --- a/script/deploy/holesky/002_UpgradeScript.sol +++ b/script/deploy/holesky/002_UpgradeScript.sol @@ -33,7 +33,7 @@ contract UpgradeHoleskyScript is AbstractDeployScript { function _fork() internal override { // Upgrade the proxy - Proxy proxy = Proxy(deployManager.getDeployment("OETH_ARM")); + Proxy proxy = Proxy(payable(deployManager.getDeployment("OETH_ARM"))); vm.prank(Holesky.RELAYER); proxy.upgradeTo(newImpl); diff --git a/script/deploy/mainnet/002_UpgradeScript.sol b/script/deploy/mainnet/002_UpgradeScript.sol index 53a1fd8..7d13bc1 100644 --- a/script/deploy/mainnet/002_UpgradeScript.sol +++ b/script/deploy/mainnet/002_UpgradeScript.sol @@ -33,7 +33,7 @@ contract UpgradeMainnetScript is AbstractDeployScript { function _fork() internal override { // Upgrade the proxy - Proxy proxy = Proxy(deployManager.getDeployment("OETH_ARM")); + Proxy proxy = Proxy(payable(deployManager.getDeployment("OETH_ARM"))); vm.prank(Mainnet.TIMELOCK); proxy.upgradeTo(newImpl); diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index c4de208..e76de12 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -33,7 +33,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // 1. Record the proxy address used for AMM v1 _recordDeploy("LIDO_ARM", Mainnet.LIDO_ARM); - lidoARMProxy = Proxy(Mainnet.LIDO_ARM); + lidoARMProxy = Proxy(payable(Mainnet.LIDO_ARM)); // 2. Deploy proxy for the CapManager capManProxy = new Proxy(); @@ -85,7 +85,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { vm.startPrank(Mainnet.ARM_MULTISIG); } - if (lidoARMProxy == Proxy(0x0000000000000000000000000000000000000000)) { + if (lidoARMProxy == Proxy(payable(address(0)))) { revert("Lido ARM proxy not found"); } diff --git a/src/contracts/Proxy.sol b/src/contracts/Proxy.sol index 7e596f5..bad4745 100644 --- a/src/contracts/Proxy.sol +++ b/src/contracts/Proxy.sol @@ -88,7 +88,7 @@ contract Proxy is Ownable { * @notice Fallback function. * Implemented entirely in `_delegate`. */ - fallback() external { + fallback() external payable { _delegate(_implementation()); } diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 0196123..3d2f69b 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -31,7 +31,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { vm.label(address(steth), "stETH"); vm.label(address(operator), "OPERATOR"); - proxy = Proxy(deployManager.getDeployment("LIDO_ARM")); + proxy = Proxy(payable(deployManager.getDeployment("LIDO_ARM"))); lidoARM = LidoARM(payable(deployManager.getDeployment("LIDO_ARM"))); capManager = CapManager(deployManager.getDeployment("LIDO_ARM_CAP_MAN")); diff --git a/test/smoke/OethARMSmokeTest.t.sol b/test/smoke/OethARMSmokeTest.t.sol index 8483403..abbbc2d 100644 --- a/test/smoke/OethARMSmokeTest.t.sol +++ b/test/smoke/OethARMSmokeTest.t.sol @@ -28,7 +28,7 @@ contract Fork_OethARM_Smoke_Test is AbstractSmokeTest { vm.label(address(oeth), "OETH"); vm.label(address(operator), "OPERATOR"); - proxy = Proxy(deployManager.getDeployment("OETH_ARM")); + proxy = Proxy(payable(deployManager.getDeployment("OETH_ARM"))); oethARM = OethARM(deployManager.getDeployment("OETH_ARM")); _dealWETH(address(oethARM), 100 ether); From 59c27016e671cd35a543df856d653c48af8270c1 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 9 Oct 2024 23:35:53 +1100 Subject: [PATCH 160/196] Price stETH in the withdrawal queue at 1.0 as it can't be sold for any less --- src/contracts/AbstractARM.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 2ff723f..056d2df 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -561,12 +561,11 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// less liquidity assets reserved for the ARM's withdrawal queue. /// This does not exclude any accrued performance fees. function _availableAssets() internal view returns (uint256) { - // Get the base assets, eg stETH, in the ARM and external withdrawal queue - uint256 baseAssets = IERC20(baseAsset).balanceOf(address(this)) + _externalWithdrawQueue(); - // Liquidity assets, eg WETH, are priced at 1.0 - // Base assets, eg stETH, are priced at the cross price which is a discounted price - uint256 assets = IERC20(liquidityAsset).balanceOf(address(this)) + baseAssets * crossPrice / PRICE_SCALE; + // Base assets, eg stETH, in the withdrawal queue are also priced at 1.0 + // Base assets, eg stETH, in the ARM are priced at the cross price which is a discounted price + uint256 assets = IERC20(liquidityAsset).balanceOf(address(this)) + _externalWithdrawQueue() + + IERC20(baseAsset).balanceOf(address(this)) * crossPrice / PRICE_SCALE; // The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; From 450524d55d0b80484fbd7c4e9fc8c8995a5f5aca Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 08:07:06 +1100 Subject: [PATCH 161/196] Fixed when fees = available assets in totalAssets --- src/contracts/AbstractARM.sol | 5 +++-- test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 056d2df..19c00ec 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -550,8 +550,9 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { (uint256 fees, uint256 newAvailableAssets) = _feesAccrued(); // total assets should only go up from the initial deposit amount that is burnt - // but in case of something unforeseen, return MIN_TOTAL_SUPPLY if fees is greater than the available assets - if (fees > newAvailableAssets) return MIN_TOTAL_SUPPLY; + // but in case of something unforeseen, return MIN_TOTAL_SUPPLY if fees is + // greater than or equal the available assets + if (fees >= newAvailableAssets) return MIN_TOTAL_SUPPLY; // Remove the performance fee from the available assets return newAvailableAssets - fees; diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index 686c7af..f202042 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -135,7 +135,7 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { // Simulate a loss of assets deal(address(weth), address(lidoARM), DEFAULT_AMOUNT - 1); - assertEq(lidoARM.totalAssets(), 0); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY); } function test_RevertWhen_TotalAssets_Because_MathError() From 6b991c0eb10eb18b9a1dbb14beaaf309163aff12 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 10:10:44 +1100 Subject: [PATCH 162/196] Natspec updates --- src/contracts/AbstractARM.sol | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 19c00ec..ba65891 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -31,7 +31,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice The address of the asset that is used to add and remove liquidity. eg WETH address public immutable liquidityAsset; - /// @notice The base asset is the asset being purchased by the ARM and put in the withdrawal queue. eg stETH + /// @notice The asset being purchased by the ARM and put in the withdrawal queue. eg stETH address public immutable baseAsset; /// @notice The swap input token that is transferred to this contract. /// From a User perspective, this is the token being sold. @@ -41,7 +41,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// From a User perspective, this is the token being bought. /// token1 is also compatible with the Uniswap V2 Router interface. IERC20 public immutable token1; - /// @notice The delay before a withdrawal request can be claimed in seconds + /// @notice The delay before a withdrawal request can be claimed in seconds. eg 600 is 10 minutes. uint256 public immutable claimDelay; //////////////////////////////////////////////////// @@ -68,13 +68,14 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { */ uint256 public traderate1; /// @notice The price that buy and sell prices can not cross scaled to 36 decimals. + /// 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 included the ones that have already been claimed + /// @notice Cumulative total of all withdrawal requests including the ones that have already been claimed. uint120 public withdrawsQueued; - /// @notice total of all the withdrawal requests that have been claimed + /// @notice Total of all the withdrawal requests that have been claimed. uint120 public withdrawsClaimed; - /// @notice index of the next withdrawal request starting at 0 + /// @notice Index of the next withdrawal request starting at 0. uint16 public nextWithdrawalIndex; struct WithdrawalRequest { @@ -82,28 +83,27 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { bool claimed; // When the withdrawal can be claimed uint40 claimTimestamp; - // Amount of assets to withdraw + // Amount of liquidity assets to withdraw. eg WETH uint120 assets; - // cumulative total of all withdrawal requests including this one. - // this request can be claimed when this queued amount is less than or equal to the queue's claimable amount. + // Cumulative total of all withdrawal requests including this one when the redeem request was made. uint120 queued; } - /// @notice Mapping of withdrawal request indices to the user withdrawal request data + /// @notice Mapping of withdrawal request indices to the user withdrawal request data. mapping(uint256 requestId => WithdrawalRequest) public withdrawalRequests; - /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent) + /// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent). /// 10,000 = 100% performance fee /// 2,000 = 20% performance fee /// 500 = 5% performance fee uint16 public fee; - /// @notice The available assets the the last time performance fees were collected and adjusted + /// @notice The available assets the last time the performance fees were collected and adjusted /// for liquidity assets (WETH) deposited and redeemed. /// This can be negative if there were asset gains and then all the liquidity providers redeemed. int128 public lastAvailableAssets; - /// @notice The account that can collect the performance fee + /// @notice The account or contract that can collect the performance fee. address public feeCollector; - /// @notice The address of the CapManager contract used to manage the ARM's liquidity provider and total assets caps + /// @notice The address of the CapManager contract used to manage the ARM's liquidity provider and total assets caps. address public capManager; uint256[43] private _gap; From 299be9d6a06cc04fdb0504f9aecd35bddb3cd957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Thu, 10 Oct 2024 02:11:40 +0200 Subject: [PATCH 163/196] Add Zap compliant with Liquidity Provider Cap (#32) * feat: add zap to deposit. * Updated Natspec --------- Co-authored-by: Nicholas Addison --- src/contracts/AbstractARM.sol | 34 +++++++++++++++++++++++++++++---- src/contracts/Interfaces.sol | 1 + src/contracts/ZapperLidoARM.sol | 5 +---- test/fork/Zapper/Deposit.t.sol | 13 +++++++++++-- test/fork/shared/Shared.sol | 3 +++ 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index ba65891..d07ef7b 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -105,8 +105,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { address public feeCollector; /// @notice The address of the CapManager contract used to manage the ARM's liquidity provider and total assets caps. address public capManager; + /// @notice The address of the Zapper contract that converts ETH to WETH before ARM deposits + address public zap; - uint256[43] private _gap; + uint256[41] private _gap; //////////////////////////////////////////////////// /// Events @@ -123,6 +125,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { event FeeUpdated(uint256 fee); event FeeCollectorUpdated(address indexed newFeeCollector); event CapManagerUpdated(address indexed capManager); + event ZapUpdated(address indexed zap); constructor(address _token0, address _token1, address _liquidityAsset, uint256 _claimDelay) { require(IERC20(_token0).decimals() == 18); @@ -426,6 +429,22 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @param assets The amount of liquidity assets to deposit /// @return shares The amount of shares that were minted function deposit(uint256 assets) external returns (uint256 shares) { + shares = _deposit(assets, msg.sender); + } + + /// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares. + /// This function is restricted to the Zap contract. + /// @param assets The amount of liquidity assets to deposit + /// @param liquidityProvider The address of the liquidity provider + /// @return shares The amount of shares that were minted + function deposit(uint256 assets, address liquidityProvider) external returns (uint256 shares) { + require(msg.sender == zap, "Only Zap"); + + shares = _deposit(assets, liquidityProvider); + } + + /// @dev Internal logic for depositing liquidity assets in exchange for liquidity provider (LP) shares. + function _deposit(uint256 assets, address liquidityProvider) internal returns (uint256 shares) { // Calculate the amount of shares to mint after the performance fees have been accrued // which reduces the available assets, and before new assets are deposited. shares = convertToShares(assets); @@ -434,17 +453,17 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { IERC20(liquidityAsset).transferFrom(msg.sender, address(this), assets); // mint shares - _mint(msg.sender, shares); + _mint(liquidityProvider, shares); // Add the deposited assets to the last available assets lastAvailableAssets += SafeCast.toInt128(SafeCast.toInt256(assets)); // Check the liquidity provider caps after the new assets have been deposited if (capManager != address(0)) { - ICapManager(capManager).postDepositHook(msg.sender, assets); + ICapManager(capManager).postDepositHook(liquidityProvider, assets); } - emit Deposit(msg.sender, assets, shares); + emit Deposit(liquidityProvider, assets, shares); } /// @notice Preview the amount of assets that would be received for burning a given amount of shares @@ -605,6 +624,13 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { emit CapManagerUpdated(_capManager); } + /// @notice Set the Zap contract address. + function setZap(address _zap) external onlyOwner { + zap = _zap; + + emit ZapUpdated(_zap); + } + //////////////////////////////////////////////////// /// Performance Fee Functions //////////////////////////////////////////////////// diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index e453356..408f0f6 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -110,6 +110,7 @@ interface IOethARM { interface ILiquidityProviderARM is IERC20 { function previewDeposit(uint256 assets) external returns (uint256 shares); function deposit(uint256 assets) external returns (uint256 shares); + function deposit(uint256 assets, address liquidityProvider) external returns (uint256 shares); function previewRedeem(uint256 shares) external returns (uint256 assets); function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets); diff --git a/src/contracts/ZapperLidoARM.sol b/src/contracts/ZapperLidoARM.sol index d127740..945f46f 100644 --- a/src/contracts/ZapperLidoARM.sol +++ b/src/contracts/ZapperLidoARM.sol @@ -41,10 +41,7 @@ contract ZapperLidoARM is Ownable { weth.deposit{value: ethBalance}(); // Deposit all WETH to LidoARM - shares = lidoArm.deposit(ethBalance); - - // Transfer received ARM LP shares to msg.sender - lidoArm.transfer(msg.sender, shares); + shares = lidoArm.deposit(ethBalance, msg.sender); // Emit event emit Zap(msg.sender, ethBalance, shares); diff --git a/test/fork/Zapper/Deposit.t.sol b/test/fork/Zapper/Deposit.t.sol index 7d8449e..eb54f1c 100644 --- a/test/fork/Zapper/Deposit.t.sol +++ b/test/fork/Zapper/Deposit.t.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.23; import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts +import {CapManager} from "src/contracts/CapManager.sol"; import {ZapperLidoARM} from "contracts/ZapperLidoARM.sol"; contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { @@ -14,10 +15,13 @@ contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { vm.deal(address(this), DEFAULT_AMOUNT); } - function test_Deposit_ViaFunction() public setLiquidityProviderCap(address(zapperLidoARM), DEFAULT_AMOUNT) { + function test_Deposit_ViaFunction() public { assertEq(lidoARM.balanceOf(address(this)), 0); uint256 expectedShares = lidoARM.previewDeposit(DEFAULT_AMOUNT); + uint256 capBefore = capManager.liquidityProviderCaps(address(this)); + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), capBefore - DEFAULT_AMOUNT); vm.expectEmit({emitter: address(zapperLidoARM)}); emit ZapperLidoARM.Zap(address(this), DEFAULT_AMOUNT, expectedShares); // Deposit @@ -25,12 +29,16 @@ contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Check balance assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); + assertEq(capManager.liquidityProviderCaps(address(this)), capBefore - DEFAULT_AMOUNT); } - function test_Deposit_ViaCall() public setLiquidityProviderCap(address(zapperLidoARM), DEFAULT_AMOUNT) { + function test_Deposit_ViaCall() public { assertEq(lidoARM.balanceOf(address(this)), 0); uint256 expectedShares = lidoARM.previewDeposit(DEFAULT_AMOUNT); + uint256 capBefore = capManager.liquidityProviderCaps(address(this)); + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), capBefore - DEFAULT_AMOUNT); vm.expectEmit({emitter: address(zapperLidoARM)}); emit ZapperLidoARM.Zap(address(this), DEFAULT_AMOUNT, expectedShares); // Deposit @@ -39,5 +47,6 @@ contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Check balance assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); + assertEq(capManager.liquidityProviderCaps(address(this)), capBefore - DEFAULT_AMOUNT); } } diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index bb6073d..915c9bd 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -169,6 +169,9 @@ abstract contract Fork_Shared_Test_ is Modifiers { // --- Deploy ZapperLidoARM --- zapperLidoARM = new ZapperLidoARM(address(weth), address(lidoProxy)); + + // Set zap address in LidoARM. + lidoARM.setZap(address(zapperLidoARM)); } function _label() internal { From 720013845bd07f709bc3bd45c02a1f575e199b98 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 12:57:53 +1100 Subject: [PATCH 164/196] Generated latest contract diagrams --- docs/LidoARMSquashed.svg | 192 ++++++++++++++++++++------------------- src/js/tasks/lido.js | 46 +++++++++- 2 files changed, 144 insertions(+), 94 deletions(-) diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 86977a5..0571c08 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -4,103 +4,109 @@ - - + + UmlClassDiagram - + 15 - -LidoARM -../src/contracts/LidoARM.sol - -Private: -   _gap: uint256[49] <<OwnableOperable>> -   _gap: uint256[43] <<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>> -   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>> -   steth: IERC20 <<LidoARM>> -   weth: IWETH <<LidoARM>> -   withdrawalQueue: IStETHWithdrawal <<LidoARM>> -   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> - -Internal: -    _owner(): (ownerOut: address) <<Ownable>> -    _setOwner(newOwner: address) <<Ownable>> -    _onlyOwner() <<Ownable>> -    _initOwnableOperable(_operator: address) <<OwnableOperable>> -    _setOperator(newOperator: address) <<OwnableOperable>> -    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _capManager: address) <<AbstractARM>> -    _inDeadline(deadline: uint256) <<AbstractARM>> -    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> -    _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>> -    _requireLiquidityAvailable(amount: uint256) <<AbstractARM>> -    _availableAssets(): uint256 <<AbstractARM>> -    _externalWithdrawQueue(): uint256 <<LidoARM>> -    _setFee(_fee: uint256) <<AbstractARM>> -    _setFeeCollector(_feeCollector: address) <<AbstractARM>> -    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> -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>> -    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>> 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>> + +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>> +   steth: IERC20 <<LidoARM>> +   weth: IWETH <<LidoARM>> +   withdrawalQueue: IStETHWithdrawal <<LidoARM>> +   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> + +Internal: +    _owner(): (ownerOut: address) <<Ownable>> +    _setOwner(newOwner: address) <<Ownable>> +    _onlyOwner() <<Ownable>> +    _initOwnableOperable(_operator: address) <<OwnableOperable>> +    _setOperator(newOperator: address) <<OwnableOperable>> +    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _capManager: address) <<AbstractARM>> +    _inDeadline(deadline: uint256) <<AbstractARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> +    _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>> +    _requireLiquidityAvailable(amount: uint256) <<AbstractARM>> +    _availableAssets(): uint256 <<AbstractARM>> +    _externalWithdrawQueue(): uint256 <<LidoARM>> +    _setFee(_fee: uint256) <<AbstractARM>> +    _setFeeCollector(_feeCollector: address) <<AbstractARM>> +    _feesAccrued(): (fees: uint256, newAvailableAssets: uint256) <<AbstractARM>> +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, liquidityProvider: 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>> +    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>> +    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>> 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>> diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index d79bb43..ef5c447 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -71,6 +71,8 @@ const snapLido = async ({ block }) => { const capRemaining = totalAssetsCap - totalAssets; const capUsedPercent = (totalAssets * 10000n) / totalAssetsCap; + await armRates(lidoARM, blockTag); + console.log( `${formatUnits(liquidityWeth, 18)} WETH ${formatUnits(wethPercent, 2)}%` ); @@ -91,7 +93,49 @@ const snapLido = async ({ block }) => { 2 )}% used, ${formatUnits(capRemaining, 18)} remaining` ); - console.log(`${formatUnits(feesAccrued, 18)} in accrued fees`); + console.log(`${formatUnits(feesAccrued, 18)} in accrued performance fees`); +}; + +const armRates = async (arm, blockTag) => { + // The rate of 1 WETH for stETH to 36 decimals from the perspective of the AMM. ie WETH/stETH + // from the trader's perspective, this is the stETH/WETH buy price + const OWethStEthRate = await arm.traderate0({ blockTag }); + console.log(`traderate0: ${formatUnits(OWethStEthRate, 36)} WETH/stETH`); + + // convert from WETH/stETH rate with 36 decimals to stETH/WETH rate with 18 decimals + const buyPrice = BigInt(1e54) / BigInt(OWethStEthRate); + + // The rate of 1 stETH for WETH to 36 decimals. ie stETH/WETH + const OStEthWethRate = await arm.traderate1({ blockTag }); + console.log(`traderate1: ${formatUnits(OStEthWethRate, 36)} stETH/WETH`); + // Convert back to 18 decimals + const sellPrice = BigInt(OStEthWethRate) / BigInt(1e18); + + const midPrice = (buyPrice + sellPrice) / 2n; + + const crossPrice = await arm.crossPrice({ blockTag }); + + console.log(`buy : ${formatUnits(buyPrice, 18).padEnd(20)} stETH/WETH`); + if (crossPrice > buyPrice) { + console.log(`cross : ${formatUnits(crossPrice, 18).padEnd(20)} stETH/WETH`); + console.log(`mid : ${formatUnits(midPrice, 18).padEnd(20)} stETH/WETH`); + } else { + console.log(`mid : ${formatUnits(midPrice, 18).padEnd(20)} stETH/WETH`); + console.log(`cross : ${formatUnits(crossPrice, 18).padEnd(20)} stETH/WETH`); + } + console.log(`sell : ${formatUnits(sellPrice, 18).padEnd(20)} stETH/WETH`); + + const spread = BigInt(buyPrice) - BigInt(sellPrice); + // Origin rates are to 36 decimals + console.log(`spread: ${formatUnits(spread, 14)} bps\n`); + + return { + buyPrice, + sellPrice, + midPrice, + crossPrice, + spread, + }; }; const swapLido = async ({ from, to, amount }) => { From eea9445cbc6e75a04e08e62d0f7809a10ee289c5 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 13:09:42 +1100 Subject: [PATCH 165/196] standardizes deal in smoke tests --- test/smoke/LidoARMSmokeTest.t.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 3d2f69b..7fdf5b0 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -92,7 +92,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { if (inToken == weth) { // Trader is buying stETH and selling WETH // the ARM is selling stETH and buying WETH - _dealWETH(address(this), 1000 ether); + deal(address(weth), address(this), 1000 ether); _dealStETH(address(lidoARM), 1000 ether); expectedOut = amountIn * 1e36 / price; @@ -103,7 +103,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { // Trader is selling stETH and buying WETH // the ARM is buying stETH and selling WETH _dealStETH(address(this), 1000 ether); - _dealWETH(address(lidoARM), 1000 ether); + deal(address(weth), address(lidoARM), 1000 ether); expectedOut = amountIn * price / 1e36; @@ -128,7 +128,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { if (inToken == weth) { // Trader is buying stETH and selling WETH // the ARM is selling stETH and buying WETH - _dealWETH(address(this), 1000 ether); + deal(address(weth), address(this), 1000 ether); _dealStETH(address(lidoARM), 1000 ether); expectedIn = amountOut * price / 1e36; @@ -139,7 +139,8 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { // Trader is selling stETH and buying WETH // the ARM is buying stETH and selling WETH _dealStETH(address(this), 1000 ether); - _dealWETH(address(lidoARM), 1000 ether); + deal(address(weth), address(lidoARM), 1000 ether); + // _dealWETH(address(lidoARM), 1000 ether); expectedIn = amountOut * 1e36 / price; @@ -183,13 +184,11 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { lidoARM.setOwner(RANDOM_ADDRESS); } + // TODO replace _dealStETH with just deal function _dealStETH(address to, uint256 amount) internal { vm.prank(0xEB9c1CE881F0bDB25EAc4D74FccbAcF4Dd81020a); steth.transfer(to, amount + 2); - } - - function _dealWETH(address to, uint256 amount) internal { - deal(address(weth), to, amount); + // deal(address(steth), to, amount); } /* Operator Tests */ From 90c543b2ed449750a52eb66efdd4de684c45227c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 13:28:02 +1100 Subject: [PATCH 166/196] Renamed request and claim Lido withdrawals --- docs/LidoARMSquashed.svg | 4 +-- src/contracts/LidoARM.sol | 4 +-- .../ClaimStETHWithdrawalForWETH.t.sol | 20 +++++++------- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 12 ++++----- .../RequestStETHWithdrawalForETH.t.sol | 26 +++++++++---------- .../TotalAssets.t.sol | 2 +- test/fork/utils/Modifiers.sol | 4 +-- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 0571c08..ec6ecc1 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -92,8 +92,8 @@    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>> +    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>> diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index ebac059..1231ba8 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -80,7 +80,7 @@ contract LidoARM is Initializable, AbstractARM { * Reference: https://docs.lido.fi/contracts/withdrawal-queue-erc721/ * Note: There is a 1k amount limit. Caller should split large withdrawals in chunks of less or equal to 1k each.) */ - function requestStETHWithdrawalForETH(uint256[] memory amounts) + function requestLidoWithdrawals(uint256[] memory amounts) external onlyOperatorOrOwner returns (uint256[] memory requestIds) @@ -101,7 +101,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 claimStETHWithdrawalForWETH(uint256[] memory requestIds) external onlyOperatorOrOwner { + function claimLidoWithdrawals(uint256[] memory requestIds) external onlyOperatorOrOwner { uint256 etherBefore = address(this).balance; // Claim the NFTs for ETH. diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol index 6411a2c..d73d32c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol @@ -9,7 +9,7 @@ import {IERC20} from "contracts/Interfaces.sol"; import {IStETHWithdrawal} from "contracts/Interfaces.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; -contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ { uint256[] amounts0; uint256[] amounts1; uint256[] amounts2; @@ -37,25 +37,25 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_ClaimStETHWithdrawalForWETH_EmptyList() + function test_ClaimLidoWithdrawals_EmptyList() public asOperator - requestStETHWithdrawalForETHOnLidoARM(new uint256[](0)) + requestLidoWithdrawalsOnLidoARM(new uint256[](0)) { assertEq(address(lidoARM).balance, 0); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); // Main call - lidoARM.claimStETHWithdrawalForWETH(new uint256[](0)); + lidoARM.claimLidoWithdrawals(new uint256[](0)); assertEq(address(lidoARM).balance, 0); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); } - function test_ClaimStETHWithdrawalForWETH_SingleRequest() + function test_ClaimLidoWithdrawals_SingleRequest() public asOperator - requestStETHWithdrawalForETHOnLidoARM(amounts1) + requestLidoWithdrawalsOnLidoARM(amounts1) mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT) { // Assertions before @@ -66,17 +66,17 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared uint256[] memory requests = new uint256[](1); requests[0] = stETHWithdrawal.getLastRequestId(); // Main call - lidoARM.claimStETHWithdrawalForWETH(requests); + lidoARM.claimLidoWithdrawals(requests); // Assertions after assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(weth.balanceOf(address(lidoARM)), balanceBefore + DEFAULT_AMOUNT); } - function test_ClaimStETHWithdrawalForWETH_MultiRequest() + function test_ClaimLidoWithdrawals_MultiRequest() public asOperator - requestStETHWithdrawalForETHOnLidoARM(amounts2) + requestLidoWithdrawalsOnLidoARM(amounts2) mockCallLidoFindCheckpointHints mockFunctionClaimWithdrawOnLidoARM(amounts2[0] + amounts2[1]) { @@ -89,7 +89,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared requests[0] = stETHWithdrawal.getLastRequestId() - 1; requests[1] = stETHWithdrawal.getLastRequestId(); // Main call - lidoARM.claimStETHWithdrawalForWETH(requests); + lidoARM.claimLidoWithdrawals(requests); // Assertions after assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index dff508b..97bfec8 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -372,7 +372,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { function test_Deposit_WithOutStandingWithdrawRequest_BeforeDeposit_ClaimedLidoWithdraw_WithAssetGain() public deal_(address(steth), address(lidoARM), DEFAULT_AMOUNT) - requestStETHWithdrawalForETHOnLidoARM(amounts1) + requestLidoWithdrawalsOnLidoARM(amounts1) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // Assertions Before @@ -422,7 +422,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // 4. Operator claim withdrawal on lido lidoARM.totalAssets(); - lidoARM.claimStETHWithdrawalForWETH(requests); + lidoARM.claimLidoWithdrawals(requests); // 5. User burn shares (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); @@ -481,14 +481,14 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { deal(address(weth), address(lidoARM), MIN_TOTAL_SUPPLY); deal(address(steth), address(lidoARM), DEFAULT_AMOUNT); // 2. Operator request a claim on withdraw - lidoARM.requestStETHWithdrawalForETH(amounts1); + lidoARM.requestLidoWithdrawals(amounts1); // 3. We simulate the finalization of the process _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); uint256 requestId = stETHWithdrawal.getLastRequestId(); uint256[] memory requests = new uint256[](1); requests[0] = requestId; // 4. Operator claim the withdrawal on lido - lidoARM.claimStETHWithdrawalForWETH(requests); + lidoARM.claimLidoWithdrawals(requests); // 5. User burn shares (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); @@ -539,7 +539,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ); // 3. Operator request a claim on withdraw - lidoARM.requestStETHWithdrawalForETH(amounts1); + lidoARM.requestLidoWithdrawals(amounts1); // 3. We simulate the finalization of the process _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); @@ -548,7 +548,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { requests[0] = requestId; // 4. Operator claim the withdrawal on lido - lidoARM.claimStETHWithdrawalForWETH(requests); + lidoARM.claimLidoWithdrawals(requests); // 5. User burn shares (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol index 1452ce4..33a0863 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol @@ -7,7 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts import {IERC20} from "contracts/Interfaces.sol"; -contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared_Test_ { +contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -20,12 +20,12 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared ////////////////////////////////////////////////////// /// --- REVERTING TESTS ////////////////////////////////////////////////////// - function test_RevertWhen_RequestStETHWithdrawalForETH_NotOperator() public asRandomAddress { + function test_RevertWhen_RequestLidoWithdrawals_NotOperator() public asRandomAddress { vm.expectRevert("ARM: Only operator or owner can call this function."); - lidoARM.requestStETHWithdrawalForETH(new uint256[](0)); + lidoARM.requestLidoWithdrawals(new uint256[](0)); } - function test_RevertWhen_RequestStETHWithdrawalForETH_Because_BalanceExceeded() public asOperator { + function test_RevertWhen_RequestLidoWithdrawals_Because_BalanceExceeded() public asOperator { // Remove all stETH from the contract deal(address(steth), address(lidoARM), 0); @@ -33,18 +33,18 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared amounts[0] = DEFAULT_AMOUNT; vm.expectRevert("BALANCE_EXCEEDED"); - lidoARM.requestStETHWithdrawalForETH(amounts); + lidoARM.requestLidoWithdrawals(amounts); } ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_RequestStETHWithdrawalForETH_EmptyList() public asOperator { - uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(new uint256[](0)); + function test_RequestLidoWithdrawals_EmptyList() public asOperator { + uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(new uint256[](0)); assertEq(requestIds.length, 0); } - function test_RequestStETHWithdrawalForETH_SingleAmount_1ether() public asOperator { + function test_RequestLidoWithdrawals_SingleAmount_1ether() public asOperator { uint256[] memory amounts = new uint256[](1); amounts[0] = DEFAULT_AMOUNT; @@ -53,13 +53,13 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared emit IERC20.Transfer(address(lidoARM), address(lidoARM.withdrawalQueue()), amounts[0]); // Main call - uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(amounts); + uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(amounts); assertEq(requestIds.length, 1); assertGt(requestIds[0], 0); } - function test_RequestStETHWithdrawalForETH_SingleAmount_1000ethers() public asOperator { + function test_RequestLidoWithdrawals_SingleAmount_1000ethers() public asOperator { uint256[] memory amounts = new uint256[](1); amounts[0] = 1_000 ether; @@ -68,13 +68,13 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared emit IERC20.Transfer(address(lidoARM), address(lidoARM.withdrawalQueue()), amounts[0]); // Main call - uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(amounts); + uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(amounts); assertEq(requestIds.length, 1); assertGt(requestIds[0], 0); } - function test_RequestStETHWithdrawalForETH_MultipleAmount() public asOperator { + function test_RequestLidoWithdrawals_MultipleAmount() public asOperator { uint256 length = _bound(vm.randomUint(), 2, 10); uint256[] memory amounts = new uint256[](length); for (uint256 i = 0; i < amounts.length; i++) { @@ -82,7 +82,7 @@ contract Fork_Concrete_LidoARM_RequestStETHWithdrawalForETH_Test_ is Fork_Shared } // Main call - uint256[] memory requestIds = lidoARM.requestStETHWithdrawalForETH(amounts); + uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(amounts); uint256 initialRequestId = requestIds[0]; assertGt(initialRequestId, 0); diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index f202042..3bd9f5d 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -107,7 +107,7 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { // Request a redeem on Lido uint256[] memory amounts = new uint256[](1); amounts[0] = swapAmount; - lidoARM.requestStETHWithdrawalForETH(amounts); + lidoARM.requestLidoWithdrawals(amounts); // Check total assets after withdrawal is the same as before assertApproxEqAbs(lidoARM.totalAssets(), totalAssetsBefore, STETH_ERROR_ROUNDING); diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index 4d78006..5081900 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -170,13 +170,13 @@ abstract contract Modifiers is Helpers { } /// @notice Request stETH withdrawal for ETH on LidoARM contract. - modifier requestStETHWithdrawalForETHOnLidoARM(uint256[] memory amounts) { + modifier requestLidoWithdrawalsOnLidoARM(uint256[] memory amounts) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); vm.stopPrank(); vm.prank(lidoARM.owner()); - lidoARM.requestStETHWithdrawalForETH(amounts); + lidoARM.requestLidoWithdrawals(amounts); if (mode == VmSafe.CallerMode.Prank) { vm.prank(_address, _origin); From 8b814a87ccb3d146d6a8ec39fecb36a059329a85 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 13:37:09 +1100 Subject: [PATCH 167/196] Natspec update --- src/contracts/LidoARM.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index 1231ba8..21c5890 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -125,6 +125,6 @@ contract LidoARM is Initializable, AbstractARM { return lidoWithdrawalQueueAmount; } - // This method is necessary for receiving the ETH claimed as part of the withdrawal. + /// @notice This payable method is necessary for receiving ETH claimed from the Lido withdrawal queue. receive() external payable {} } From 2a547b448af6a9f7176b77790f194f64a41fd38d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 18:11:31 +1100 Subject: [PATCH 168/196] More info to snapLido Hardhat task --- src/js/tasks/lido.js | 109 +++++++++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 29 deletions(-) diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index ef5c447..f62636f 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -51,12 +51,66 @@ const snapLido = async ({ block }) => { capManagerAddress ); + await logRates(lidoARM, blockTag); + const { totalAssets, totalSupply, liquidityWeth } = await logAssets( + lidoARM, + blockTag + ); + await logWithdrawalQueue(lidoARM, blockTag, liquidityWeth); + await logUser(lidoARM, capManager, blockTag, totalSupply); + + const feesAccrued = await lidoARM.feesAccrued({ blockTag }); + const totalAssetsCap = await capManager.totalAssetsCap({ blockTag }); + const capRemaining = totalAssetsCap - totalAssets; + const capUsedPercent = (totalAssets * 10000n) / totalAssetsCap; + + console.log(`\nCaps`); + console.log( + `${formatUnits(totalAssetsCap, 18)} total assets cap, ${formatUnits( + capUsedPercent, + 2 + )}% used, ${formatUnits(capRemaining, 18)} remaining` + ); + console.log(`${formatUnits(feesAccrued, 18)} in accrued performance fees`); +}; + +const logUser = async (arm, capManager, blockTag, totalSupply) => { + const user = await getSigner(); + console.log(`\nUser ${await user.getAddress()}`); + + const shares = await arm.balanceOf(user.getAddress(), { blockTag }); + const sharesPercentage = (shares * 10000n) / totalSupply; + const userCap = await capManager.liquidityProviderCaps(user.getAddress(), { + blockTag, + }); + + console.log( + `${formatUnits(shares, 18)} shares (${formatUnits(sharesPercentage, 2)}%)` + ); + console.log(`${formatUnits(userCap, 18)} cap remaining`); +}; + +const logWithdrawalQueue = async (arm, blockTag, liquidityWeth) => { + const queue = await arm.withdrawsQueued({ + blockTag, + }); + const claimed = await arm.withdrawsClaimed({ blockTag }); + const outstanding = queue - claimed; + const shortfall = + liquidityWeth < outstanding ? liquidityWeth - outstanding : 0; + + console.log(`\nARM Withdrawal Queue`); + console.log(`${formatUnits(outstanding, 18)} outstanding`); + console.log(`${formatUnits(shortfall, 18)} shortfall`); +}; + +const logAssets = async (arm, blockTag) => { const weth = await resolveAsset("WETH"); - const liquidityWeth = await weth.balanceOf(armAddress, { blockTag }); + const liquidityWeth = await weth.balanceOf(arm.getAddress(), { blockTag }); const steth = await resolveAsset("STETH"); - const liquiditySteth = await steth.balanceOf(armAddress, { blockTag }); - const liquidityLidoWithdraws = await lidoARM.lidoWithdrawalQueueAmount({ + const liquiditySteth = await steth.balanceOf(arm.getAddress(), { blockTag }); + const liquidityLidoWithdraws = await arm.lidoWithdrawalQueueAmount({ blockTag, }); @@ -65,14 +119,13 @@ const snapLido = async ({ block }) => { const stethWithdrawsPercent = total == 0 ? 0 : (liquidityLidoWithdraws * 10000n) / total; const oethPercent = total == 0 ? 0 : (liquiditySteth * 10000n) / total; - const totalAssets = await lidoARM.totalAssets({ blockTag }); - const feesAccrued = await lidoARM.feesAccrued({ blockTag }); - const totalAssetsCap = await capManager.totalAssetsCap({ blockTag }); - const capRemaining = totalAssetsCap - totalAssets; - const capUsedPercent = (totalAssets * 10000n) / totalAssetsCap; - - await armRates(lidoARM, blockTag); + const totalAssets = await arm.totalAssets({ blockTag }); + const totalSupply = await arm.totalSupply({ blockTag }); + const assetPerShare = await arm.convertToAssets(parseUnits("1"), { + blockTag, + }); + console.log(`\nAssets`); console.log( `${formatUnits(liquidityWeth, 18)} WETH ${formatUnits(wethPercent, 2)}%` ); @@ -87,51 +140,49 @@ const snapLido = async ({ block }) => { ); console.log(`${formatUnits(total, 18)} total WETH and stETH`); console.log(`${formatUnits(totalAssets, 18)} total assets`); - console.log( - `\n${formatUnits(totalAssetsCap, 18)} total assets cap, ${formatUnits( - capUsedPercent, - 2 - )}% used, ${formatUnits(capRemaining, 18)} remaining` - ); - console.log(`${formatUnits(feesAccrued, 18)} in accrued performance fees`); + console.log(`${formatUnits(totalSupply, 18)} total supply`); + console.log(`${formatUnits(assetPerShare, 18)} asset per share`); + + return { totalAssets, totalSupply, liquidityWeth }; }; -const armRates = async (arm, blockTag) => { +const logRates = async (arm, blockTag) => { + console.log(`\nPrices`); // The rate of 1 WETH for stETH to 36 decimals from the perspective of the AMM. ie WETH/stETH // from the trader's perspective, this is the stETH/WETH buy price const OWethStEthRate = await arm.traderate0({ blockTag }); console.log(`traderate0: ${formatUnits(OWethStEthRate, 36)} WETH/stETH`); // convert from WETH/stETH rate with 36 decimals to stETH/WETH rate with 18 decimals - const buyPrice = BigInt(1e54) / BigInt(OWethStEthRate); + const sellPrice = BigInt(1e54) / BigInt(OWethStEthRate); // The rate of 1 stETH for WETH to 36 decimals. ie stETH/WETH const OStEthWethRate = await arm.traderate1({ blockTag }); console.log(`traderate1: ${formatUnits(OStEthWethRate, 36)} stETH/WETH`); // Convert back to 18 decimals - const sellPrice = BigInt(OStEthWethRate) / BigInt(1e18); + const buyPrice = BigInt(OStEthWethRate) / BigInt(1e18); - const midPrice = (buyPrice + sellPrice) / 2n; + const midPrice = (sellPrice + buyPrice) / 2n; const crossPrice = await arm.crossPrice({ blockTag }); - console.log(`buy : ${formatUnits(buyPrice, 18).padEnd(20)} stETH/WETH`); - if (crossPrice > buyPrice) { - console.log(`cross : ${formatUnits(crossPrice, 18).padEnd(20)} stETH/WETH`); + console.log(`sell : ${formatUnits(sellPrice, 18).padEnd(20)} stETH/WETH`); + if (crossPrice > sellPrice) { + console.log(`cross : ${formatUnits(crossPrice, 36).padEnd(20)} stETH/WETH`); console.log(`mid : ${formatUnits(midPrice, 18).padEnd(20)} stETH/WETH`); } else { console.log(`mid : ${formatUnits(midPrice, 18).padEnd(20)} stETH/WETH`); console.log(`cross : ${formatUnits(crossPrice, 18).padEnd(20)} stETH/WETH`); } - console.log(`sell : ${formatUnits(sellPrice, 18).padEnd(20)} stETH/WETH`); + console.log(`buy : ${formatUnits(buyPrice, 18).padEnd(20)} stETH/WETH`); - const spread = BigInt(buyPrice) - BigInt(sellPrice); + const spread = BigInt(sellPrice) - BigInt(buyPrice); // Origin rates are to 36 decimals - console.log(`spread: ${formatUnits(spread, 14)} bps\n`); + console.log(`spread: ${formatUnits(spread, 14)} bps`); return { - buyPrice, - sellPrice, + buyPrice: sellPrice, + sellPrice: buyPrice, midPrice, crossPrice, spread, From 3191670a97f8fd82344f3ac977aee7f8a6d39fae Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 18:28:23 +1100 Subject: [PATCH 169/196] Added requestLidoWithdrawals, lidoClaimWithdraw and lidoWithdrawStatus Hardhat tasks --- src/js/tasks/lido.js | 45 +++++++++++++++++++++++++++++-- src/js/tasks/liquidityProvider.js | 12 ++++----- src/js/tasks/tasks.js | 34 ++++++++++++++++++++++- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index f62636f..16b242e 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -14,14 +14,52 @@ const log = require("../utils/logger")("task:lido"); async function collectFees() { const signer = await getSigner(); - const lidArmAddress = await parseDeployedAddress("LIDO_ARM"); - const lidoARM = await ethers.getContractAt("LidoARM", lidArmAddress); + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); log(`About to collect fees from the Lido ARM`); const tx = await lidoARM.connect(signer).collectFees(); await logTxDetails(tx, "collectFees"); } +async function requestLidoWithdrawals({ amount }) { + const signer = await getSigner(); + + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); + + const amountBI = parseUnits(amount.toString(), 18); + + log(`About to request the withdrawal of ${amount} stETH from Lido`); + const tx = await lidoARM.connect(signer).requestLidoWithdrawals([amountBI]); + await logTxDetails(tx, "requestLidoWithdrawals"); +} + +async function claimLidoWithdrawals({ id }) { + const signer = await getSigner(); + + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); + + log(`About to claim the withdrawal with ${id} from Lido`); + const tx = await lidoARM.connect(signer).claimLidoWithdrawals([id]); + await logTxDetails(tx, "claimLidoWithdrawals"); +} + +const lidoWithdrawStatus = async ({ id }) => { + const lidoWithdrawalQueueAddress = await parseAddress("LIDO_WITHDRAWAL"); + const stEthWithdrawQueue = await hre.ethers.getContractAt( + "IStETHWithdrawal", + lidoWithdrawalQueueAddress + ); + + const status = await stEthWithdrawQueue.getWithdrawalStatus([id]); + + console.log( + `Withdrawal request ${id} is finalized ${status[0].isFinalized} and claimed ${status[0].isClaimed}` + ); +}; + const submitLido = async ({ amount }) => { const signer = await getSigner(); @@ -250,6 +288,9 @@ const swapLido = async ({ from, to, amount }) => { module.exports = { collectFees, + requestLidoWithdrawals, + claimLidoWithdrawals, + lidoWithdrawStatus, submitLido, swapLido, snapLido, diff --git a/src/js/tasks/liquidityProvider.js b/src/js/tasks/liquidityProvider.js index 3405cb3..3d7ad43 100644 --- a/src/js/tasks/liquidityProvider.js +++ b/src/js/tasks/liquidityProvider.js @@ -11,8 +11,8 @@ async function depositLido({ amount }) { const amountBn = parseUnits(amount.toString()); - const lidArmAddress = await parseDeployedAddress("LIDO_ARM"); - const lidoARM = await ethers.getContractAt("LidoARM", lidArmAddress); + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); log(`About to deposit ${amount} WETH to the Lido ARM`); const tx = await lidoARM.connect(signer).deposit(amountBn); @@ -24,8 +24,8 @@ async function requestRedeemLido({ amount }) { const amountBn = parseUnits(amount.toString()); - const lidArmAddress = await parseDeployedAddress("LIDO_ARM"); - const lidoARM = await ethers.getContractAt("LidoARM", lidArmAddress); + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); log(`About to request a redeem of ${amount} Lido ARM LP tokens`); const tx = await lidoARM.connect(signer).requestRedeem(amountBn); @@ -35,8 +35,8 @@ async function requestRedeemLido({ amount }) { async function claimRedeemLido({ id }) { const signer = await getSigner(); - const lidArmAddress = await parseDeployedAddress("LIDO_ARM"); - const lidoARM = await ethers.getContractAt("LidoARM", lidArmAddress); + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); log(`About to claim request with id ${id} from the Lido ARM`); const tx = await lidoARM.connect(signer).claimRedeem(id); diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 0c22f88..9cdac6b 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -3,7 +3,15 @@ const { subtask, task, types } = require("hardhat/config"); const { parseAddress } = require("../utils/addressParser"); const { setAutotaskVars } = require("./autotask"); const { setActionVars } = require("./defender"); -const { submitLido, snapLido, swapLido, collectFees } = require("./lido"); +const { + submitLido, + snapLido, + swapLido, + collectFees, + requestLidoWithdrawals, + claimLidoWithdrawals, + lidoWithdrawStatus, +} = require("./lido"); const { autoRequestWithdraw, autoClaimWithdraw, @@ -547,6 +555,30 @@ task("setTotalAssetsCap").setAction(async (_, __, runSuper) => { // Lido +subtask( + "requestLidoWithdrawals", + "Collect the performance fees from the Lido ARM" +) + .addParam("amount", "stETH withdraw amount", undefined, types.float) + .setAction(requestLidoWithdrawals); +task("requestLidoWithdrawals").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("lidoClaimWithdraw", "Claim a requested withdrawal from Lido (stETH)") + .addParam("id", "Request identifier", undefined, types.string) + .setAction(claimLidoWithdrawals); +task("lidoClaimWithdraw").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("lidoWithdrawStatus", "Get the status of a Lido withdrawal request") + .addParam("id", "Request identifier", undefined, types.string) + .setAction(lidoWithdrawStatus); +task("lidoWithdrawStatus").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + subtask( "collectFees", "Collect the performance fees from the Lido ARM" From 730b8dc28b9c8eb6636760d4eefd6acbd846fd6e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 18:43:53 +1100 Subject: [PATCH 170/196] Added setZap to deploy script --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index e76de12..1054563 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -26,6 +26,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { Proxy capManProxy; LidoARM lidoARMImpl; CapManager capManager; + ZapperLidoARM zapper; function _execute() internal override { console.log("Deploy:", DEPLOY_NAME); @@ -63,7 +64,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { capManProxy.setOwner(Mainnet.GOV_MULTISIG); // 8. Deploy the Zapper - ZapperLidoARM zapper = new ZapperLidoARM(Mainnet.WETH, Mainnet.LIDO_ARM); + zapper = new ZapperLidoARM(Mainnet.WETH, Mainnet.LIDO_ARM); zapper.setOwner(Mainnet.STRATEGIST); _recordDeploy("LIDO_ARM_ZAPPER", address(zapper)); @@ -133,6 +134,9 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // Set the buy price with a 8 basis point discount. The sell price is 1.0 LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9994e36, 0.9998e36); + // Set the Zapper contract on the Lido ARM + LidoARM(payable(Mainnet.LIDO_ARM)).setZap(zapper); + // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); From 3d48ae209589c34e3fcc42a93cdddb277acfbeaa Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 18:44:58 +1100 Subject: [PATCH 171/196] depositLido now supports depositing ETH using the Zapper Added setZapper Hardhat task --- src/js/tasks/lido.js | 14 ++++++++++++++ src/js/tasks/liquidityProvider.js | 23 ++++++++++++++++------- src/js/tasks/tasks.js | 14 ++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index 16b242e..5ab05c8 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -46,6 +46,19 @@ async function claimLidoWithdrawals({ id }) { await logTxDetails(tx, "claimLidoWithdrawals"); } +async function setZapper() { + const signer = await getSigner(); + + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); + + const zapperAddress = await parseDeployedAddress("LIDO_ARM_ZAPPER"); + + log(`About to set the Zapper contract on the Lido ARM to ${zapperAddress}`); + const tx = await lidoARM.connect(signer).setZap(zapperAddress); + await logTxDetails(tx, "setZap"); +} + const lidoWithdrawStatus = async ({ id }) => { const lidoWithdrawalQueueAddress = await parseAddress("LIDO_WITHDRAWAL"); const stEthWithdrawQueue = await hre.ethers.getContractAt( @@ -294,4 +307,5 @@ module.exports = { submitLido, swapLido, snapLido, + setZapper, }; diff --git a/src/js/tasks/liquidityProvider.js b/src/js/tasks/liquidityProvider.js index 3d7ad43..c9034e3 100644 --- a/src/js/tasks/liquidityProvider.js +++ b/src/js/tasks/liquidityProvider.js @@ -6,17 +6,26 @@ const { logTxDetails } = require("../utils/txLogger"); const log = require("../utils/logger")("task:lpCap"); -async function depositLido({ amount }) { +async function depositLido({ amount, asset }) { const signer = await getSigner(); const amountBn = parseUnits(amount.toString()); - const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); - const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); - - log(`About to deposit ${amount} WETH to the Lido ARM`); - const tx = await lidoARM.connect(signer).deposit(amountBn); - await logTxDetails(tx, "deposit"); + if (asset == "WETH") { + const lidoArmAddress = await parseDeployedAddress("LIDO_ARM"); + const lidoARM = await ethers.getContractAt("LidoARM", lidoArmAddress); + + log(`About to deposit ${amount} WETH to the Lido ARM`); + const tx = await lidoARM.connect(signer).deposit(amountBn); + await logTxDetails(tx, "deposit"); + } else if (asset == "ETH") { + const zapperAddress = await parseDeployedAddress("LIDO_ARM_ZAPPER"); + const zapper = await ethers.getContractAt("ZapperLidoARM", zapperAddress); + + log(`About to deposit ${amount} ETH to the Lido ARM via the Zapper`); + const tx = await zapper.connect(signer).deposit({ value: amountBn }); + await logTxDetails(tx, "zap deposit"); + } } async function requestRedeemLido({ amount }) { diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 9cdac6b..79c6e66 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -11,6 +11,7 @@ const { requestLidoWithdrawals, claimLidoWithdrawals, lidoWithdrawStatus, + setZapper, } = require("./lido"); const { autoRequestWithdraw, @@ -499,6 +500,12 @@ subtask( undefined, types.float ) + .addOptionalParam( + "asset", + "Symbol of the asset to deposit. eg ETH or WETH", + "WETH", + types.string + ) .setAction(depositLido); task("depositLido").setAction(async (_, __, runSuper) => { return runSuper(); @@ -587,6 +594,13 @@ task("collectFees").setAction(async (_, __, runSuper) => { return runSuper(); }); +subtask("setZapper", "Set the Zapper contract on the Lido ARM").setAction( + setZapper +); +task("setZapper").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + subtask("snapLido", "Take a snapshot of the Lido ARM") .addOptionalParam( "block", From c6c29fbb549bf6d20849baeecfc05e9aa1854a6d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 19:48:40 +1100 Subject: [PATCH 172/196] fix deploy script --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 1054563..f4930b9 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -135,7 +135,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9994e36, 0.9998e36); // Set the Zapper contract on the Lido ARM - LidoARM(payable(Mainnet.LIDO_ARM)).setZap(zapper); + LidoARM(payable(Mainnet.LIDO_ARM)).setZap(address(zapper)); // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); From cf916aaa7e5b7a2b7b92e5d7b372cfd89a3f0f59 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 20:06:10 +1100 Subject: [PATCH 173/196] Added more logs to the deploy script --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index f4930b9..2991caa 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -93,12 +93,12 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // remove all liquidity from the old AMM v1 contract uint256 wethLegacyBalance = IERC20(Mainnet.WETH).balanceOf(Mainnet.LIDO_ARM); if (wethLegacyBalance > 0) { - console.log("Withdrawing WETH from legacy Lido ARM"); + console.log("About to withdraw WETH from legacy Lido ARM"); LegacyAMM(Mainnet.LIDO_ARM).transferToken(Mainnet.WETH, Mainnet.ARM_MULTISIG, wethLegacyBalance); } uint256 stethLegacyBalance = IERC20(Mainnet.STETH).balanceOf(Mainnet.LIDO_ARM); if (stethLegacyBalance > 0) { - console.log("Withdrawing stETH from legacy Lido ARM"); + console.log("About to withdraw stETH from legacy Lido ARM"); LegacyAMM(Mainnet.LIDO_ARM).transferToken(Mainnet.STETH, Mainnet.ARM_MULTISIG, stethLegacyBalance); } // TODO need to also remove anything in the Lido withdrawal queue @@ -113,7 +113,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { Mainnet.ARM_BUYBACK, address(capManProxy) ); - console.log("lidoARM initialize data:"); + console.log("LidoARM initialize data:"); console.logBytes(data); uint256 tinyMintAmount = 1e12; @@ -126,18 +126,23 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { IERC20(Mainnet.WETH).approve(address(lidoARMProxy), tinyMintAmount); // upgrade and initialize the Lido ARM + console.log("About to upgrade the ARM contract"); lidoARMProxy.upgradeToAndCall(address(lidoARMImpl), data); // Set the price that buy and sell prices can not cross + console.log("About to set the cross price on the ARM contract"); LidoARM(payable(Mainnet.LIDO_ARM)).setCrossPrice(0.9998e36); // Set the buy price with a 8 basis point discount. The sell price is 1.0 + console.log("About to set prices on the ARM contract"); LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9994e36, 0.9998e36); // Set the Zapper contract on the Lido ARM + console.log("About to set the Zapper on the ARM contract"); LidoARM(payable(Mainnet.LIDO_ARM)).setZap(address(zapper)); // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig + console.log("About to set ARM owner to", Mainnet.GOV_MULTISIG); lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); console.log("Finished running initializing Lido ARM as ARM_MULTISIG"); From 1e80d467dcd33ba0992a27a47f84e16a27f92b93 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 20:06:30 +1100 Subject: [PATCH 174/196] Made claimable external as it's not used internally --- src/contracts/AbstractARM.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index d07ef7b..4baaeec 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -538,7 +538,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice Used to work out if an ARM's withdrawal request can be claimed. /// If the withdrawal request's `queued` amount is less than the returned `claimable` amount, then it can be claimed. - function claimable() public view returns (uint256) { + function claimable() external view returns (uint256) { return withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)); } From c8467af270df599e5ace0b3cb935292e2e20485b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 20:17:21 +1100 Subject: [PATCH 175/196] Renamed withdrawalQueue to lidoWithdrawalQueue so its not confused with the ARM withdrawal queue --- docs/LidoARMSquashed.svg | 54 +++++++++---------- src/contracts/LidoARM.sol | 14 ++--- .../RequestStETHWithdrawalForETH.t.sol | 4 +- test/smoke/LidoARMSmokeTest.t.sol | 2 +- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index ec6ecc1..881bbad 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -47,7 +47,7 @@   zap: address <<AbstractARM>>   steth: IERC20 <<LidoARM>>   weth: IWETH <<LidoARM>> -   withdrawalQueue: IStETHWithdrawal <<LidoARM>> +   lidoWithdrawalQueue: IStETHWithdrawal <<LidoARM>>   lidoWithdrawalQueueAmount: uint256 <<LidoARM>> Internal: @@ -86,32 +86,32 @@    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>> -    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>> -    claimable(): 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>>    totalAssets(): uint256 <<AbstractARM>>    convertToShares(assets: uint256): (shares: uint256) <<AbstractARM>>    convertToAssets(shares: uint256): (assets: uint256) <<AbstractARM>> diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index 21c5890..dabbe78 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -21,7 +21,7 @@ contract LidoARM is Initializable, AbstractARM { /// @notice The address of the Wrapped ETH (WETH) token IWETH public immutable weth; /// @notice The address of the Lido Withdrawal Queue contract - IStETHWithdrawal public immutable withdrawalQueue; + IStETHWithdrawal public immutable lidoWithdrawalQueue; /// @notice The amount of stETH in the Lido Withdrawal Queue uint256 public lidoWithdrawalQueueAmount; @@ -35,7 +35,7 @@ contract LidoARM is Initializable, AbstractARM { { steth = IERC20(_steth); weth = IWETH(_weth); - withdrawalQueue = IStETHWithdrawal(_lidoWithdrawalQueue); + lidoWithdrawalQueue = IStETHWithdrawal(_lidoWithdrawalQueue); } /// @notice Initialize the storage variables stored in the proxy contract. @@ -59,7 +59,7 @@ contract LidoARM is Initializable, AbstractARM { _initARM(_operator, _name, _symbol, _fee, _feeCollector, _capManager); // Approve the Lido withdrawal queue contract. Used for redemption requests. - steth.approve(address(withdrawalQueue), type(uint256).max); + steth.approve(address(lidoWithdrawalQueue), type(uint256).max); } /** @@ -85,7 +85,7 @@ contract LidoARM is Initializable, AbstractARM { onlyOperatorOrOwner returns (uint256[] memory requestIds) { - requestIds = withdrawalQueue.requestWithdrawals(amounts, address(this)); + requestIds = lidoWithdrawalQueue.requestWithdrawals(amounts, address(this)); // Sum the total amount of stETH being withdraw uint256 totalAmountRequested = 0; @@ -105,9 +105,9 @@ contract LidoARM is Initializable, AbstractARM { uint256 etherBefore = address(this).balance; // Claim the NFTs for ETH. - uint256 lastIndex = withdrawalQueue.getLastCheckpointIndex(); - uint256[] memory hintIds = withdrawalQueue.findCheckpointHints(requestIds, 1, lastIndex); - withdrawalQueue.claimWithdrawals(requestIds, hintIds); + uint256 lastIndex = lidoWithdrawalQueue.getLastCheckpointIndex(); + uint256[] memory hintIds = lidoWithdrawalQueue.findCheckpointHints(requestIds, 1, lastIndex); + lidoWithdrawalQueue.claimWithdrawals(requestIds, hintIds); uint256 etherAfter = address(this).balance; diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol index 33a0863..a5646c7 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol @@ -50,7 +50,7 @@ contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ // Expected events vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoARM), address(lidoARM.withdrawalQueue()), amounts[0]); + emit IERC20.Transfer(address(lidoARM), address(lidoARM.lidoWithdrawalQueue()), amounts[0]); // Main call uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(amounts); @@ -65,7 +65,7 @@ contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ // Expected events vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoARM), address(lidoARM.withdrawalQueue()), amounts[0]); + emit IERC20.Transfer(address(lidoARM), address(lidoARM.lidoWithdrawalQueue()), amounts[0]); // Main call uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(amounts); diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 7fdf5b0..83b25e0 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -47,7 +47,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(lidoARM.feeCollector(), Mainnet.ARM_BUYBACK, "Fee collector"); assertEq((100 * uint256(lidoARM.fee())) / lidoARM.FEE_SCALE(), 15, "Performance fee as a percentage"); // LidoLiquidityManager - assertEq(address(lidoARM.withdrawalQueue()), Mainnet.LIDO_WITHDRAWAL, "Lido withdrawal queue"); + assertEq(address(lidoARM.lidoWithdrawalQueue()), Mainnet.LIDO_WITHDRAWAL, "Lido withdrawal queue"); assertEq(address(lidoARM.steth()), Mainnet.STETH, "stETH"); assertEq(address(lidoARM.weth()), Mainnet.WETH, "WETH"); assertEq(lidoARM.liquidityAsset(), Mainnet.WETH, "liquidity asset"); From 7aa40092f658c35748d9d8e8d39912ad30f692c2 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 20:21:58 +1100 Subject: [PATCH 176/196] Updated contract dependency diagram --- docs/plantuml/lidoContracts.png | Bin 18208 -> 18562 bytes docs/plantuml/lidoContracts.puml | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plantuml/lidoContracts.png b/docs/plantuml/lidoContracts.png index 0f400546be8fa504f0ae550b5bcd12ee3456d637..1d284ef492ab0c32a647293f2f74964a2193625f 100644 GIT binary patch literal 18562 zcmc({bySsK*DkDxz?N=l0qGP)LAq1vmhSHE?vf5kr8X%g9nzgr(xpgubJoV+^PKaI z_nhbb=R4z!!O$J|z3#QA$;=S!2?tYabfuf58x0UJb37X1P4A* z`@mBM{-bvgQFYL_wsEyEG;(+#W@u$-r|V#7@J!G3nTdmgjs05|78?s)D+fnQ3ub+5 zOQ)eeGB6CgsiLaGzdnEP5RBuJ-l?c*J}Iil8iysv`be4;)i# z>N=@LYNs_0R^^?Ry;+=xN?h*}Y7TtthEF&*XS*e6UVcd94kd6udT)yPWJaN4Y#(wN z@_XCMonW(PXvx%>0ODUMJWq*9Wp#Qk9uQkM7V^=CZ-uQxXYs1oQsP@^02;MHqo-)p zfF6n*!f`o;JuUXAx`HP{b^>(>qh?7u^}}c_c&lPO^3Asoj0Kr{Ni(8*6K&{1XmXT!?CX}Oo z;QJWw`{$3C?c`b2F7Nct@RVmF?!E~BvlcRG_O9x(*)s_3xSyi*#gWgq^#PQPsdcKG zI=o@FR}*&o?l$MK`qBB(SN3_$40kvSMYkX_Im%)+fd?z#zzLcvl&rO?-)q zNv5^Zg0cI2V~nwjQZ;wT!mC=cN!?m;C|rn1CxoGZJ$v?;P^s+lVZHw?u|-Qqm`dDe zJz7d%bu>oEfJsH)4lZp4QS%1t;1^7Yn+|j(o%y>(Iil{fDM5ymtXJC`R*f3)Q+)&S zhCf$~ay{r!24u$#Y~C1oUiTeMV@#^}#$+RgUzA_RM*ez~pg>FN8L280>$@Odk9>;k z^0d`Y>Px!kg9jpG62bzCF0(tI5M6LO?}CG$ACG9jL9zo#v5OqwKl_Wu@I9j%OMCT2 zJeEQeE6f2aCMw$*?jfddDs9%YESy;ScPJv>Ve5A(0~IqrcPyEG1D>C`X1X435B?a~ z@mx${dbz?Uh(!LJu#Mtb0EQqHh9FWl77}@YECjqOfCpbG!jT7H(EEZf68OOvlK=HG ztq?3UlDAd%n?wCcEE?<4gzQE7-BF0>q*a>hKT0%Gaxd1Cw6ScYz3*<$hD@v$8dFmH zW*b!%IT=oO=A8Eynl>A5I=a?8#97r-xLx-7`>$`#7L_(Gjy4rWYVUsMXQC0XJs*nu zdHu@fGd~{l2M(*B-@I=xy02YIH5+PlzX$ck(daLKkxJ6jKHFQA5m+x|Dd_3xd1*X! zeX*I@dx4ly?4>sR8Q$C5n_O62JVclGJ@uTDECod>(HpDhOUX6Xi>1nY^9{ADcy(Sk zm-7wIad{J1HC#t}a_fc5ySt>s#Mi{NnbTA{cD&D@mPl_5XODHfJ%?+nM6$>hD6vu@!z%P}3z2~_9NsIe$`$oW+= zS2VJmyNFvt!3;9@iBdB8mFd`QwK-Trgz2@(NkW^!7x+0WR!*`EO-Qx*RL!j86Z!Uo z)$Z}UcN$eDqutSD-L4fJm|!S32X~m_VFD5 zfRRX1WVB4X<$EwX_}8n|=b`D?XMy+qiA)M9SXiq`3ABW^#CMFaMO4 zXw(j+yfn@h3|unv99@cl?Y2}rZ7g`o*cYkQh*zd7*$07(=w7Vng*wP!q;aFk5)FKJ zZkweCd84AI;RvW}(Y%*Pk?f4=Z>+V}mT=xI7QQ{SoU18()6KLzB0@wNIUK#YP-bG%oGf=Kb-S(-#U>+n^fbQ z{pS1h^*XzC$#|Og>eoIcx8Y7Zv&$xLtQWR>?$tx}SrUtOS2vqs-_szbZ9@A|Sysj) zcn@3ZP5)-q?Su<5@9P(46STxkr@wY~<}38YzN2;#By~lTmGv!;WDD7PY1bG#rbC}* zh(m545?T$`&{}w_oxTodX|i5yKJNqL29&XIb6-E!<`8UDwtRE_VBdQyJWnEyI=RdF zXWl!a^uc7Sxtfxu`FAO7kCC_M8}QcyLshg~Mbg%P{|-$j;d0WsoG4T1cPkyBZp>@Vj-JB4~7N7g|Dw~&#RxG zF3&2~jRsS7b8ME|xAaOSDy9|SiUKqnoH`u}<-Z)ijo3TgDOovhDNF2Nybe+#$e{>I z)>DO3-V{n-kJiRKE%mxcpjTq!Jk#>NalHP->-pABHl?$wNYD&(WZ#@^F}6u*|Efdm z`Wct=?uRV?hu!_yBA<&E)IX1xjum*{0lN~FzP6F?>;D+;aC101-MZ!GN0B(NH6^!n za}EBG+ZE{{M9+$8279H9EO6#giCznH5?QeheLJ3aUM$=hNPELFwAX)H9A%ID9h{gbUpU5DZZusr<2fH)eNqC0~>KT6Kef#ot6vEN@3U@tK{ zv-xxxVu<35*n&$ZzqaUo$K$k}tbW){=KV?qjyDJCcu@N2QxRpEY&M)3SvVe(eC0m6 zUOtE6$=v8rXRcUu(d*_*lj??>tFzTyC=^=s-p$o@nUd{YSf>_eu31+5E9!DLzglbS z1ecpDGl?ovj0~^ai%nuqI};^!C|3DfGGaRKiK0@irm)8oj1x863B6&s4Ewcq>wP_K z+ONzPTsKKP;j7NrQ?`3-63Ut{C7CMCrKBtWoFt zu0GCr9CGq!N`quLozp%hS~Tbf-bCCI%h19_77N~Ywy9GS$CQ32f7>pqc$yVPwqP_8 z!p$>7cjfN7upPczMn3+zk{O04H!S@ImEeHLNAksg4{vEgfkGd79{-A+WM_BON3|qS zOkzdWMarkN8?mXJPomFyv*7Z`*8A#oXDhP zvC2K&p&fq~Yl|{onT$Ljo2#`Z%f|vjn?@Q3$5pZ0;yflu9SaJb{a|45xrjtVgx_5| zt~UxrMeVBrBo8xuvh&6zVUs4)L+|{E)8m|VBWHPUNy5sGkVEU)1MN6v?H@hfGh*iZGL9 z_IPI&y}P@se1?ym1z$4M3~Xh03(HQilMU|8|Dqs=)M=eBvR)~^gFF5;x++4y+#ZO$ z#nF6wY3?CB$(fIG>!!fp zaF3r9h;?Gal;b%+r08vKbzd+#nUw~xPX3hZWGqw#Tgj)NBYELcJ>H!<8CWL(Z(o?N zY}0%L4!?SJf9zpZKD>pPlIG+UqV4^`@qe5ndvFFESB*?oBCVX@e9p+?LhIhIHh)B5 zi3Wxd(g6=-TWzizO(@$Sa+ELqCQYpUc&Rj<{X<&C3$<|&l zs0_Qi+4J5)z^^KL{=)n(tPx}5Wutsf;&9ys5eHkllYnXg;W z%U`+C6Td@PhEM&bt}l*WAfsx`A(wvt!VD+GH-3+iiz%{t zrwpvcsG*7lvI6d}ag^NIQI$H>GN_{?p$#3tsM@bRf5VAN%!pA^>T}n*?}DK`F^o9t zve$Gbi*3Eo$VsvK&2|;*x-%scomxAtMl68!8lMBHqz*)^aZIhhQuThfV9$l*)YNn3;qm}H~VNX>+HTlYG3yERNRanVeWd}E|tal+wTA9;vOL4MD>EP~1k&+bjxUe>J zpkLwcSt0JC=NSk@f7^`R9(0odZ3RA)@^Ma6lFs2%ilRG=pvy}|l%fxHUuQ+yo4;`Y zO=asQon&;BfR|ga@u#+hXg?w3!N@VaO;NcNG}v0#BIy;2W|=|lh`;}MWUT*OQ8o}_*SQI%9MuEIde1C`3upA zIb@X-Gn=@L2a>b3_@(?*A}3A|PsAHIW#>UCj7kAo^r{2XR{i7oyUR77dP|M31`;RFFvRzd=4;T8vYgW4 z@7jU#F1YXUI0mQh7w2L1*yn;k8CXGu;g=i`SOVa!2_nNJEjNhUh!%k-^Yd*d?ZcnQ z!)E_6m-;^@S^rHgCjWv=`U%?gvj93%`H^Id3_Fm)^(OJI3UzGWy`Xw`NF)qWJ1SWS zMBM!jBzW0aA@&TqN16xf=*w!V-|8Wm@_IcKtDPGpxXp&0L*0-RDcJQfI9UPv{S;!n za&;A|`_-zRBJhzoI_-G=RQR3@ZZg#kBu8~1KU9R1*p-~VDx+1CSYx5*z2c&-)R8!l zl;S*SqkrKQcW(2^r76EL?=x>vbr4GuYPc4;-nQF8TVHZubtu7AN7_wi7Hh>73lI#w z?{gI@lw?XbX-r)j;S+uU3s_*Ie84dGH7g74d&g+i^HJek3Ew)p#$)@f!24Ib&%dtY zSqAkmh-8VhsM^1X?Uuqhz(1Mmud754jm-?plGEE^mn+PkzM%oxYXbkrV63-Mn1)4R zp&BB*WH0WLP1t1-&U)@T)$__T3#Q&%ct0Ea7INo4{IQVt{R|ymMm%Bejvdt;xRTs> z_3IgGwzko%7D0C~3qILi5_a0I>S1+s^_`4jfpPB#H)SJcUXqt8Ey}-aId6W;*wx=4 zEo%x&ijPC?C#>}utIHBc%|4XMSopM2ZyO;+p{0p-iO4)94Ar3FVZzPAxPg>l{}ivX zB{doZ&h0^|K6J z|BfCsx__v*q2miQ#42vhuz-10e{ix)>CkZ0D%s<=?x?e!*vr#{P)dM1d`yqmNgnyY zKe2EK%^fZ&3aubAV9-4{~IM}m5+8?}~m=f3rkP}%*p*SS=E^b*y%C~79+Id?Iwy{q!VJ}zB5FxS(tq-CgI6|9U zzIB4cHK-vQwKl$Q8A}(Aa+aAMmhzYzxm&&2R3$~N{5fp9VkB9JcWxEVeb_ z-buSF46ISHAsTajWf!7Kwpf&foSsJYWRO=zY*->_FbJ5ZIuJ78KgGe5g6OnO$sljB z%=o{#A1XCr#5MxOo`kpD!^69xG2kO-+IGeJNUbcTm_OBVnB&l7lrqP;9@0N6>D7PG zXKsio3f-MX?f3`IIn5HhDh*g1i}1~bs0uo(5>a`LddY*0zWcW6ref)^ceq*fF@2&{ z)`A!7n(c}3Ow1;pEuBZQ_Yk*$i-G<303z-G-vCpv=uq_bWQf&YD zk}ZOw%aoGf)gR{6vsj_zNmHgrD_faN7wg)%zh8#_d~9GXxW3h=mdYw^#Jr6(#XBykAly)EBCxp&v9aaDGE6| zz3yJi=o`859`a$7hWH0UyrH2(b=>R8y<3#@t0Daxl|L3E*k8ZL^q%1gT%gdUS^1Wf zMfZ@Lh}Y$WN$NI(X3RhmvZd@=@`kCh=4afU+~CFwYXRdvid@dTWynaDQ1K_DK6**T zcn$vVCW_!)tMVzTOHF@8M6cBDQ^&T@v@l+8T)*6J7+7zI&518CQqvw4 zQ2pUUXU*b(QK5jH4sy=}h@;JpRmmj1ExqokKwY789??%N<*C3Cj8P+8ddkKyjXbg9 z@@srnVk|UjA)^_+5vNr4ByJsIjb6!$u0Ul;Rab%11*>n{>qPJUQh>)P2v95tsGc%- z%<24aCcHW^boLGX;f!5>(M`kAKs?5@)X==!qQ#F19y>UtE8pzIfCrygso=%>-l1xk zWd$s0R#+iZX8+ie`}JGD_#}|&d!m{)#}=Q)hBK*g34AqsnciVb!K}2`XPEJn&&Dc3 z6pdDCI+P&HF_`yh7RiS6=_*R0hqQm|H^6ho?pzthRPR#H)~hJ z%6C`OCw3JJ3QSy+En`1dbCaF%S&_v`dAy1uClJ=p<8$seW-5>}uIhIb%KDbwldYbu z8p-Vokd@9Az#;2K=LBw3|v zlH7<>Ix}P;z)8|8`fXd8*fbi;!7Sx-TzcP704hsnVI}|7xSW6p{r35*09GoezVl^R zxJ2(;0#987n@a&a_5FKEo%^Z%-*_t?TETu3(RA-1T!*C5&3<@>;eh)G0|(b zPLhjXy??(gl}=`TDEVuymL=Q|Op=2=nw018`3zkD7rr3(wcuoVCV0`gjSVTDGQ0~(o(a%tGdFR?H&<%(5btUt20-UE4BaBy&? z>irjEjjl(6flpjrUD*Nt4d8xRK>(4dG6ETP{N`{y{A@;MvDi%F4IuC`UQ!;n_&_1x z@Y0b1>b?F*S|83k0e8eoXBmd^)2wtw5{29Xz+Q#T@t0MfzzYg1TV10p<|6}Q)gNQdk5-c+0adVL<`2Roug{Un?- zKrQ|I9m&tNUG37+(pp$pNTyYXJ`ODJ`Ne4+n$5 zCi0)0;zj413si6j6ZokYNYh#JQ$<#Ef2U}rXDgJL-d-+caC%*mGndub5Zv7Ag&jq7|IN0%_Jea`p_U zXYqcsh3HD@Zxn@!l5}R!95(yaBRnQB7@{^J!&%=7s}cHVc!JMP-$i1RIzC%cREGbb~@%Pf!`{#bWhJn+Hetx>60$;hhR<#^_r z!xd(GLqL2?9`;`Y>Jn@EQ#6)6N!3o&V3?uHDu(l9E*!48BC&9qzlKP5W{>Ax&GRac z9sNn89Dx|B%01~=^2)}+ED*7?(@O86VP~dexi+mc1s|5FTS?sqy^8JOGLdil=Gs@^ z^tLwrL3jQ06h)K57zD2lq%~zpST78i7<-Hdu9AfuNNdvcBuxoAg=e%Kb{gHOqZ|xD0~DwJ{WZIW zs@Ix0hyXG|EdKaxt#9Iaiu6BBbjGh)NUG2o((MmZI19P0?EE#UH~lr4=`0QRDCbc^ z%ymKxVMA*UY*ysF>;fJgdt{TZq_eVpw70vppH4hnbP=n0K8uq0N&Z`XZ@!$;9=<9H zpY_BE5Oig8JhBU3#kS$t>*Wm1A1@T#-xL%ILkg>q8R>n&idGb`JNB>}e~N3dNM=ej zTNf)`R=r*N_)`ASR&D*6HmB4){q8Ia@Z@=j;P?nlQ-WUmcY~e zLSRpzB~Nhj6#)WJ6!b9jGZ}gmZ~{_O+S;Py!su!ZwNmQB;PG+87-y1+Zu)oYALnT_ zcylBRm@#q9tUOYx&;QH)vfkD+HDmdj%&q7y-&qc) zG>X$L%%0h=D7#kbwoQEv^>L{<6dXV`^Jywzc>jI`)r{U-i7C+ef3AWwknt?>q_+r- z<1V>poz6U59Mx~_%Nj5|gL0GG7;HNDC&l$12o;r$(8hzr^-dKai3Cq#s?C4aAf8^E z_(L=LDmI%0U1Y)U&{VNGX(>QbJW*3e4rH$h`z!L{&4+y-Zw*!G+g#&Yt@dC`r&CTX z7b&jqsZ9d<3)`RM>1+`*MZqU~fAtR*KK(O5t1wmhKbx3wQDei`pJpx%^0BRd6!mV+ zda}0Ik=iSVWs+5k*V1j&Ehd*p+f|@BP_a0@?L8)2%$Q-!C+fne!;{Kcl+fzNCof>h zZ?8C{u=JmCZ{u3FC|J~t|H*mbiX5Ln(Zb8e8y~OpN1!eyHhp;Z>KUy`rH}ElYX6%Z z`j>LCpRqoMV#(|Ob8x2R{3OmE1xb!)$g!&x7N+bxrTnbGuqB%8G!|04t*Z40;}J`6^9fYY(GSewksp5 z0(~+Vyo(phwIJV<`a8RdBU6;}QU^wv(WDp1%gCQ)N{TDvN6BmcdtPw#db1(4gY^=q z@TVWqed9|hyuYMkvGz~C)4C>Tlnru$-*pw!e?9iCvatx`Kpg%w(foH{HXVT6=pHcE z9GxYr6^P@Z#yq+xBDPp;No?HUr+Usn^|}6E8Y~4lr)x(nYsMW<7vp(P_t3HF6?4vy zv_v(HT?wiQ3)1n>cX^C#srrN&4^ZEB%$nvEeSCwvgJX3G?mOT5hj~eU4yr~b>jT+^b#!^qX$jP`>>`!1I<$f96+{3M+DlG_) zKNE$VHPdG;Dq&I48k5m>%2Zf+`wuq?I=TwOYm*V3`Fcm03~mW3Nu6Lk*qLjO_bIxm z8rB=TOslYBNrF=D9iAjpXj8U+yqxP#Nb6C%G3xCW*{Vvc?6Qo%n5UMYYK6ZfQ8J}} zIi*?awPJFCekdT6Id-_4qXk(`S|5b$*eMh#w1Zl2BD1DC;CIM>kxXDz!_xHxks*Ww zcT~A2lg}GaDkPJbNiZ;2?KcJr3TW>Sc0oV@EY(~8lr6QE>(!%zJa8$`>-B`qFhK^7 z+{0XQdOzGl1J{?xz#$1e8+zUDb!aODC5hBz!B?<`>aAX?q`G_H9PPpZM1}b1yzn#t z62GcI??}50QUh#iXuEQp=SIPzjbdq#r#caV$h!o?E2@HRH2#at@B^(+;7$osY}Q z%Qg{~yr;O`2iqQuWV;3hFk|B#9#VL5-h+bQ$g5(Hrd5le|bda0e zPR44~fHc_6ewb{h4j!q+Tnfm>Xohxu>cdG3AH%yWeCX-E1}1?&0fD-YPutI z)-;@kCam%(bedO26(4RR$^fr6ECM0u4rn-o=<3;V8(!N~H=SYqDAm*eOLM8lMI+@d z@L)9^dk*cE8H_Lg9o49%c&pTaiTy4(iKsWJt8%j%#V%{_!b~>VL;Jaqkz8$LW8|94 zu;NArF?4)G1F1*PBTnUr)c4wFN=&b zR7szjt6`3z1U@hR@)VXgjDG&+@cUygTbE?AyKvWTDfSMjLFI0aE7?UrvRk>Td-78} ziA;$#=4ai4v??k?W9?OxJPIimuNCkJ+CbRgWCL##umJ@qx3<0MPc^hs*&3%}#N1d2 z*>MWFd$-go#lok2uJZ_hAs_pG+Q7UIbEJo+xrFtA5nChQn#&Uc7m+PTf2m}tIZ)i9ioRQ+V0O_FuePAD`Hj~Zq^k{nbkoG@v?^nT85>u+VS*2)NP*9$%z1c7sR*bT6+T*iR z?tWnemA+!UJU1qE_{wHkmj2jU070B!UVqm`gM|C-Sx9uZdB6ql*YPykA#>^JD&XYzXkADlKj(5 z+cag9j6CKOi$9I}iMD4AiEq|jJQbO=1iH)--Sg~hDnVkc@wne$m~xN#SD8N@Rgbx( zslgOo%^g0N%X3Pqtk}8mC%TfaSE=ClK4!l(XP)WaDUz}qU{frRYfEz6FEa@6ED26- z)KoO1%}+Z6616g2M}1U0_FIT9;gqsCL8r`^{%DYpzRe&3fd68|Hl3Gt6C^ir?|~yu zs@y+_Q?UDGRD!JyrY$ZiX|v=YJyvz-cRlmVonWlRKP;Zv-{V|H>GmNMnJ-S?*MBm#5bmv1wybxqw`#rM+$t2?p*y*)G>n&C^Os;2`XxQb zP_|Et(cvlC@BYY4`Q|jycNoq++^nqsF~UGadKUMb4gmpOq7G!1TXO~_07 zSP!W9TC1bQys0Jk0r3;3`3-c>g*F-<`T$A#kB3SN{=KC2zGTTIs+Rxa`hy>Jh!@Y3 zv67TW@vKNa^w@1{geo!Hxq4^w;3L);R?hcd?1`p|xDU}ZJcUPIZ2ur~6e?fOnE#Xr zrPR;F<606azebdLANj^W63kmGx5%=Yfn{(maXOFdtH*?f=GRA1Z~aeQe@ZoW>AT+$ zSe+86w5dE0EsIL1yOL4c3$@>V4U-`SmFlN&6{%eZ-h2vt{Ru6&Oy@zW!>NBktkqa;<2OFZ-lAL1{6?LSv- z`Qp-)X?pebOVbn^>e)1dFz;(CmjE-L0bk^`Ll`*UrdS3KFUe|T$*m<*on#S>krqVNETw`J6(i!kTVv7d-zdt^nqN_ z*K4`rhBn@)f`GLm_yB}}I!M~OIXiaE^XnVwxg@5YrbN?k-M-m}a#r6dJDf10C`o(& zR6gSvmPC%uV(s~=>18*4Tn;bJ4GmWQe(oGQK{1RG+DhlDTtf`)bP|EIlSj?|P zOi_-5!wB2s7+l>>`lpbyiEOt=|Z3BcmqCEms(E-ntc08tS<(i$|Yw z)Vbok?huBE7pNo_Kd1S+8JpCZ>KG?ey?3jy5UjY9Ak6#pUg^>$AfLK>`C}SB#{PBqgdsvS<;9H0+hJw1*tlip)Vw|xI zYP9TpVshhmyCb~0Gur25D3fDdbD=r#t~47|b*g8*iP1%C)%ev|7ymq$n(93MBPJfs zK@$E?%f)nu{d)~X)$XsV?SAdQ-f0W{&ah2G|GIpLxIwFQ&b?@zMDd1UkcT(S)wwNp zN@0htZ67HcZyULw;==@Xsaw^iwjqud9!Vg2XH@&-8iIZkD>viYCwGM1(b_y_>_$0XAe^+L%QYT=xBG$#I&9CQntZK_+1>^9 zRWvnipZ9P|@P$P7#y0+f%@`D}Q1XXW5%fHdnl3h=R?I~CrK%d2n+#x}*rBUizTM&Sy7kAw+ zoy80ztuJ!@xJatk5`zeam5~?5!=?r1e%h7zf|$PBY#`;I*)N?h=kv&%J5a(Z%QVVx zagt1zVakux&=lcdwTisEjxG&G5O3YbC)j_>&!M2&0dk2}uyq(9b0=UW?{I&Btlq|F;DFe_`GS{yf=O;1PPhWkoPd z|3Ck&aCjgk{P(y%>i;`jKLis{c;3kQO@4qiC&<491&zpvhzZ`H;9&D{(0_n}ifZ^z z@d9KuCIou!gM_Py9@S7Z}NBbu7TBA&_tT0LL^A;80z|EE=R9Gmm*Pr%rwrHUZsNqr1kMtV{~K|zj4 zcoAYG>tF2t{a{ZlWp)7Qu_&j#2j_M40_5Avb-%0P1O)^H?AHo`I&BX~naQvzDJUpZ zkxrLl>|t1EdJ-Tm4t?S}3nuf@kpmbl_)MNDHZ~UI)+%LMlYiF?DAd33xolSgFjms6 zo3l}Xwg972m0p+P|gOlZ(H?@Fv z%?y~h)8)EA4T3dy7h7chazN;OFeyb9OFy`R{GGK83{v``&QBHANn(clv)-}4v(xZs zV-V@Mw6qj3LivqAX(R}RpuN4F)${t?)?^7<)+|4W#2E1!-XMnET)FD0mg_n$?R~`x z0LWaRZG%}MZ{YIe&;0y+%lQiO+OuOJ2p~j`C(cxxD($tUI$+`hUc3`1Cz&XjoF!^| zo=k#L;O+6ajL8#tc=)XKhut_#>JFe)!*OeZ;H?9#H48ROHR$}cK|{t;n}70jojuL% z<@q@h2_V`s$mImo*TP8HDk>`Y_(`IXgyhHOXNl;<97`bO$9p$dV`&X`B#}gdyMmL=89KP}2Nn zPP;eXG4d5V816vzlKF+&IyWR}a_|yP&AAuxSCQVtq%e_%l_e4PYSY6gE`CSHBL%c`L3? z{^Tpa*nUVf4jPQ?9UVm)$brc)ktwJVNquEne%a)HwuDfHmHs)`;$CWkec!c$r;+{u zN~I{RO1P;fU|pTMwITokw0{JvQy^=(=TYZAWP0T7eFv;0w^P)==YS680FH)vb@C6I zn)Y%;i%Dw!s|QY_v)E=1Bpw~wj`(d7Z~OB8mi@#nxH^99GMSrr>M>Zjw~B7mJN~g- z>p1}3UOAyL%$oIpL~Qz|U{tnMYXX;s<~Qv>qN4z!)0o%zk$VzaQoi#Lu!!dY?xv#Q zqoR|vF_;Do^!M}oYS`~|acC&bB_lTTR}E;|+epiGkwID9bUg2%>AvjNHuD>p7Jh!8 zdZ_f@qyK&ooCeT9wG(j1@n7j)|5m19Nzezj1yAuFW?8f-qzF;ax51+UzS!h>Yh!Z7gOiR-M8b49o?0Ho` zowbnul+Vip)QczSfudHvd5|$jy0R8};ZM3I3{TO*wBJCiSY;mcj)~g!;s1D!3l$T zjZ!qCRa9|t>5?s&=C9;lzz7v>6rh*tVBBj(Rz&3Sk*$LMu4@vS6 z%7WS)OJB1*JXhdUOPJrHK4s9W+qZyD&URf^?XBz{qyjjWnPj3Yt0do%Q*bMMf#bpWlCBW=%^e3V7 z$|^0v!jA13d!Twu8Z{jsfp=8URcWv)-RcL}cIPp0JP3#b>73&WVVC+{2hY5(&kvOE zxX_+nY|X}IW@-a}qI4_6cN1{^dpsW+4I0ppb`|oZVP0s$Z@&mE^+{Cs)YRFULKSlZ zI~6~ZQjwy7Ymqz3RuA9x{&x;fo6BQU7q`>x8L;qKs9GiU8a^(r2u#nC=^iSAhBd&Y zN$vrY1g%UM-Q)Q(-gmb*zC1bU#_GA;{y2bjvWz%~$YZ2PiJ|)G z3Z`D776h;>-g=yu?;Jc+)}xd~%QGHK{gT5?NDMk4Ma@^gBxwoW0-q%7SyPXlr7h<@ z04P9{r9r>jY$>!+3rty>_3)GNHRSRLldK8G@CtKi;*UmeUzHt z+TA?JJoSkHondA~$7EvB&wW`;c4n(LIvF$V-6Gv(m3lp96Ha=`TjMK?D$ z2ifw0z9;BuD(y1pVg@yYajVu0K5tS>Om|7y;k$sKATyQBsy}<+v;sHLP+NNjEYAeb zvX9RL#(iK}Q_2b!A5&x8?q{00cWf2#Ld7iFq*$6hYc2>FOs!mj5KfK*c;&B5weZDQ z`B<{&z?Fe`qr{El$$ID={<_mj2gIFRBa-H=+p!7s`V*J)3hVLN94uOh`IhhiBma06 zWeTc-PS)VGWA^k6sJ+m@{5RX&dM+~=$krFc>Bo~7Kr=4R&sFEOelRSDvoyE<1YX%< zx?Es~WDt%WnRBa;i-MQ;^!GU8I8wZAX~AMAXqm-O#q8j_yFLQ3NbX~QGew zcG&$%a9|58`(Dc(ulmbN_fDXT{@e&?#4f-R0wI{~WWX2utqO2%j~pBg^J(zdIvlR` zg2v}~Mm6~+yW-u{tcSso9-uAQNLV1yQnLO^wljD=$V=vVtA!Hu= zzIY+PKZU0=s(l^!#D)J*T*1$aNdyFO6Tgf&lEHyC!#o9+3F+H#wR6dOgAP6A4uWFO5vB_sO^QuFOh+g`jr9C5{e> zq&Ha06|eRu;Pc`(GL1eG6&B9w%u5U=1$C;DiZc-A&3WYm=2*?_6tJ|_btbL9jkACK zT?_>AL=yfF2fqJv(6^$0{yc(7rSyXwQ#1%a1h}JG2sh5dKt^-7T{#R|ic3ku0C|vz z%^;+x2S)PvF$f62QHcZL|J~`F zH6!ZZaN0G!{Prc701SV-52p)7ARx9+6At;#sNI|n3u`qX0pvtT?-5Q0;E zrl(RYQJ2Nwwd#?elkt9N_NYtU9s3fFp$Ec&EpV=h$cRqf z@z^Z!Ga8NA%7W%Z$wbD&JdjGPZ*7IGgD}6)PAiu^Mr~!tc!K=KpN@+jdha# z!Olq^hdN8-&X0+QXTAk=W65|5E-RYilJG$;C&wev8&_GEG}nAwCClqoWC%| z$NGkbe5oWfB@1?+%H{ev>UZ=SHN`t}Gt3qZZJ$Hi%|fd7_xHO;_1QNE)22E*MIwAGVD*hj8(-b$MBdcR(lEZD$D22-v_d?p26>epa#Tm zILq>=WMotSX@L&JTDl6OT%`X_LSdC~G3B%#c$_4Sl zhTQpI=MPrrPRzq<42sRbKWSFziFgqIjR0!apK}0&8|YK$F4eOba#Hf8qG*sVLFICT zCPR>FQo!YY2#OsZ#5}|muTMWuI+A`zo&9d33moUAfKD+&ZY_RbAm)bLHxRJl(kr?U z^Uw|gPYcWIc|9*dJ%6f1L#0~f;~{`$0jejg*bXcR9+Ub6NOvcGoaoPhUy*o12H=wD zkgUift26>kOkZNf#G4#jXXmLP!*0F;&CN~h3~=@CDO>VfdB%Jq&}VZCT0oXO7WiJ|wMo8&!j&LU1Dfms(2ioCXb?np zYYX2!ht&jt#46&;jRpxjvPMQ^rM17=JRIM8?}K3W3;aHUlcVEgH=XAOF#Y47CB_Tn zDCcCco<1$8emd~R1RWY!@;cc-?y3C&pI!y^yVQcstbe&832#Zq;EEH7Q$< zOM$*xJiGuo8xTNgAD{cF1wa1-HjqL9FaUHu8Vn?lDno*h|GWu_1~FVk{fh>@&L@_X zAmtWD!}+qvrrd^b!s(BI+OD@1xQu{GsB?L>*HX(wg%efpoyChr#{1LEdHCk98UeW4 z1`wH*_Cy&)rzf>A8UP{)AcbPQ*8}i>%_h#R`ug4ZYV@qX(CH8Mj)nG1R9wN{l z*1lqg@mZG|C7*?X9K=*+9P8e2`YG6hOcW0g`Cyjw9(A|}F8&KPad(3+Eq)DwK$fim kQ}%!V`zg8#nl`QZERhwj7=OLNf=uj+v__T8<82fl9@R=+B&>qVX?K+w{~=TZ^dkA z^WJ%6fE3(@!(2(t@xNYw1Ox8#A+=jc+h(5indfoHw;#?wgHh&vH&t`=(SG!lXM4W)EzL`<6uw{r_dhb`m`~>vE5H9n zx(eFa^KvKH?i=}G?m~d%S1qwm2EL?`{stZnt^eFP6$UTdh^@J@G4re<{Y zb6y;G!mDqGd9^fZg*E$0tnSPw4JnSQ@;hO)0cgf+2R7TbarY8E66J#o6avQg)&9Cf zS2!@FI;-s@`&3)s8G9(zvPZ1kY9(7WY?MYqMVa)17z#Kt=T8Wg%dcRX{O*XX+Pgwj zW5%1%k_KucF@lE7DhKxAX)B+%ZLtpLVj{We6|be1by`&*>XXfgFeGKL?`>JPXu{15 z3@I2du9;+e(4h>;O&Hm7nRwm|{F%j=QuXk~PJ^E>C=GCJ|VzzI^gq1$b?|(u3@T~j((@*^#Zh5scyCba!!2ks>4S_gpqj<8!a( zaByF7F&PZIh{O>#OPuS$Vx!T?zW3Em{$oV6 zuv027LDW>b&+y2&VFWL?(!K9q2&NUQmX~SPTd#Emt6jv%3Uss`EHx*D4Jhi$H9POW z^1M0$mnFq||9HEba4IeN`}`bF#6KW_U~Ao|N*5kNk=5vR>&6#h{%g9_;*^BnjoT*5 za;9vl(SgQJlF6(2B^YBGVOm;RW@hH7-8GUnEcLc%%lLrNa*M0R7MQE@){hRq7^$1& zm!@cFA5MObSEg-j<@>yZuL9GScG>yfJ>GG1I-^VZI_ymX^Op*03JQuSZ_Lpu;S^t3 z^Y1wXta@L8wR9)zQ-uV-`d=7Bg0Psi8f6pbEcH1hU$q>1K)uS*X9)jy=RF_XpNJZ{#ZEZ;}O?yU9hmu}a;)d$;P-5s^ z{a*cM(Vy;h^>#SC_l&r7?WVmvUaQied%>nX91@Ddke3yvk2IT#UKxL%%wgH&^edi5 zHf?vR*!%8sn|fPkzQ%lMaZ%1xDgP7rc?ZXSlRV_IVTPk5rCDbc?2Wq_=v}SVXg}Kt zY-Pwax;dDT&1d(04bke4E*w1ksu`E{;?JSC)?S3%>SPJb+MT_?f1U?2?GZYR%ds;4 z`QTwkz3tbgMih5R>V5OVFnqF`b3+j`#01c!i1g(;^Ki-;BS>)x@V?NCB^t@ zZ;tL@2(5?PX!pF;BRE4@T2;$+E(X|)`aYJku(2J(>vD^3psV`>>K<&GSv- zm^3-Lxz(n+rJai+_$;SO5{tb^3ObAjV&C2hz^r9!VC73~4#Xj%5d|?(3bL|dBn37r z-V4+%=Dfw0enzkKMtg{Z4p%}_D2|ni3T}d>^(0#7ligBVwsPwI`BZdbz)A(Zs8Sb( zB8t@4Esvnun-%QB@gLI_gcY_va2sx>gjS_(oL&|4z=~~E-Rk-buV2G7S-BWuB%^7E2GHfe7Jvv zT5qEJEP|r+*2HqEC?ImGNZD_^^1v4!StkceCY3w$E@n7|YkmEE#k(A57(m}-P0`Xf zr(MeU7mLI{1lb;&FLg;K``fAUE7881!W= zcFIPA7cY{CCu{y1di$8}MBN*Xxv~0%AkvkdgkF#SM1bHldnF zmCpMbDeFo6?wrTtePi>!Zfm#QJgTh_w(X7X zs;O4q_oi>RG*;lg_-WkQa! z>L`XrXnl6A0DrQTVncCYv(%(~8*|~iUIh2&OA+Ntm2s$>HOvI9dhHtTVT)g+nNLyV z_Q;oCOU+ywrUgF=1|n83UpZ_^dx%}Ps<0P)EQo#qoNzPjse74LBkOphgXwz}zPdxR zIPTkP&Z!u;)7@3Wg}V2K%Dyi*(Vy#I^@Km)O++RAE+=K+q@1!)Ynf0)ydWkdp&Qd5 zfugG3^5I1uX6RJ+EqB~@FR8cH#UCEWpBP(NtA`tJt?dc9b;%y-#;E8{|Jlh;FY+Gv z9aZUA6%G84%loU*p>U{!_h@@boR`2Uk~q z*JCOfN!FFZSN65UIM~R~Xfsr-R@z!|v5(#t#*@hy#2vmtBK16pvhZ@Az{J5ZR$!#Y zi#R)2DlRYXvAK>=vAbJwabzkm9*nP~??^gUm9pry%7T-ILqI5E7~Can5`azico*Re zVDI!NT2|@PAUk?l33UGql8{CKxX#B^Ts)|$j2PnKgdA4ZdS>TMQzl$ivwGe&M|W#O zqlhu(Z}-P1CMLG-4ESO*rsiDsW>;^{bH7ZMz9Hk)edT?l?)C9Z(?RLk z)#f|S;)|90yL@Z8Rd&U=FnIs0L!` zvfGN@#!wedVCtsb!k4@L+0w@)tUE)`X*=q9o@hN)gp)U}LaSPqYB6dd`2-bJxjIie z$+wf^!DjpOwY&7JBT}TKq_wXAjO++ zSy1S@l!yN5?J!Tztcnq$MEsZvO8ai{ z+KhLdSJ5=HxtZ7NQQ!;Ad-^t2HJS8A<3Ze7+z_k#yIb*4T;=nU8jWZLST8;hRg{%m zC?z5i50!{70gBg3wSvUN0#}B`gNVyIwMdr3Y8FHD>#pbZnK(RRwPYN@kT$ra?y|bQu*val^sPJ!K8%zJ7ISv z`DYtU;d&R1XI1?jQ(H_+Q)W>1#R*0GU{SCH zTe*tm1{&PW7;GA2@=1TROF@a(x+wkmqI=bOXj=gZRE<=H4CK4I@>N-VnqnSjF487G z(QCE1_G{m84E7LSScIF+ztd4vAf3_>jUNaUKWBa6R(fIUI@CGeb<(<4Dv;&vKYNS! z_Y;e-u10XRX+ne6votaiD|PFTu7>M1GIpP5DNu$!XEVn0n(E@EWF!^s=?@Mh7K=~K zIu&xweZqeDo&yiN@i?^eF3Vbz$7Ndh#hxN2pH@j5J^eIZ^L-9<(r>=YiCjlzIpVzV zs_t87*doSZy6*qc_RCvk@Yyl8lW*5FB1K%vMf*8Z((d{Ou|WqCPvvjkq~JR7WJiPF zG%p;c#Qe?-MzLLWg|+>BQP@l{tI>k0YQ41(*{#cMF{ZPOOVTWi zaPY;uPqSBV%z(%8JUwe2e|3ww(|It`$ye-j>ji@aX^JE6T&vjVjy)N#;e*2 zp@63ELKQMUW#;Ohkn}7JLYKdvR{xhL=3RJd;#uS7TqdXA#;+$yRY;NS$IeG3kK-Yp zx>bwJb(akrB`s1~MO8ofgmnUU*{3bayh@|XgEBgfb3X;F^b@~Z8KjATu9Jd-ufB)_ zjJ*3q`G`P$5jB{2slRys#Tgsa%)*kk0-3=d&%|nlFn=doDLcv{+(T)KI3Wm?^!$;U z&b_Nmna6L(oyfiQAh1;$DzIkviNQNFu>iLL9*_q^_)7-qS}ZRdV-Y>PH|;IbFR!Ru z{#mM9hOXfBo1*>%94j`4No^zLS4 zk#||<{J5Yq{2U!O=9PouurE;KaibU2;vfAYbgxn-Tqi^M^DnsmprrOhl=525L6Tnv zUuyrL&)pJH6q>4xXW0y0@L0qxzcglXD#EiAg%otx#G?wB3{ZbE`R3E1pNyr)(dA|} zKp9S}BIAEhHQ#sA8lOpMi_decaT<`X+W9%xzA~!V&?K&m-l0aOj>a0hP@&)BVRFXw z`bXvjuQAtaqEM0@F^Am-$cHRyqhaheN8@ko%R_4j#6~=-xH7A1HyPjIQ&$r@$NDsE zJGvs##-(_iGMdDlRH^d9X-z?*GO;QWr*oZSMid_VEOiIPRjpHI(7{wM@^5sf!yp?4 zN&HB2r!t}B4m?RSxTYLDebLIw4zX&Xg#Xy>d_(fGSRYZIN+<#@-@d~0o-oTq>S1^g zdh*?spD6`}8=p-BwIx6LkoJmg6D`{h8J%Lr*>xHJQMa^F3lCSE)jgBRtbfyWq;>!? zlN9O;MAz|Nh;H8wW=~S^ZBo`*XBsSd=d$Uo)T%zDIYz+NZx&(x0d~^Tv~~g|Nj&(e zJ+c`-YGh3^Jj6dNq1c}^W}pl9a5NhVSqcRJ52KFC%()iQb^;mWaCRlbH?-h{ub=W_ zP1`%*X?nd($wlb*VIq4>HQ7StTl9ku~nzq*Mbo(-7wSxJ=|LOcyR{Y%$Iw=s;lL9SG|m-0~fp_4-@E+2Sm z57q$Bk_o%kMrosZBUbtPd@GrbyrMa^`7&*;gwA~fuITKxlL-mtm6zm`B?uSC1@>cz z|3OU?Q<%R-yJ|;%WaTu@8H$d9`)a``W73LUSSi-z^f6w{y?7k;G3A4A!p6xlvAVSY z3NY!S0M)U2D>-#q_jU&HXl*l>XUMd)k-2lA7vlt&FO6E%?BpHsA>XVQGAl=m z?juk--XrwWXqgg+6>dGJhf%&ROz+OAyL1lWHMn{UT z?9$y^l5qKj<_*NmrBB&6>od~o9G_jy3F2jEvJIWmEtYKur^J1OY=u{Zu|Ws#rHX!w7ut$zdKZ$o9*p$>p{6P=KPF^lI8pF^m_%m z#mxg}2`+o4=7*?^f-Ls}>(58z@B0SD+|#)6=l=+wjNg3|fC72nA4OmNs`knCA;vR* zNdV9a8CXUxqL0`3LZSHx8E`P^htL`KRqO#cF(5gl1Uqvt!{{;SP?l{ou1 zdr5qNZ+f5L|7-t^N`4quba1A=C^c34Bx{AMJ>MQC_QODqTaJ#84agseaOi0N$Hy8K zV2l|bLd}0<(u1FSJc)zma>yRa;n3&*{McRZVyh9wDQ_Ne+ zu7mc!dyc#?udP~qrk`T1p#YcGWG~^puh*Ya+=8pXg2}$MKab>G{o6E zB@Jq(u8Bo#*t%$ZNaR_6Fls&1<$Q1QJrbp0f;mlOVrrGPbolvr9_u@+mk{He^xYI9{}IhN&L%cbKc)`3U=)?^i3I8~s?m z{M6!vg1{7C?R-!7Dgx8`fWoT&Mvdl;k_}09)>-9S)2Jn>lT_w)XZG;~aW4=K(pA2eEEZYB9l5T3(`Us+@%V@ie z?KK%@r34%Inbo}4(Din^weh%PB3H0VNJ|Q-|4-v1dLQs^fc4Hk;viF3g2j&e%3J|9 zijU-nypwUfDuho;$2juEaUOz6@@bN9=(P3Z!Bue>z0cTW+FEeyG#!!SSU9g|^nzTp zAvq2Iyf?_OXnFCNQi5Jy_)N@57g32tkn0elaZ=mTTydEe249V?u98|MagqdiUGmH? zq06fAd?*o*xWGTB+&^{}9v4_s4#}<%X^qnBz93H@78U-+K=7(F1MOm~xe7Aw_Hi$^ zb4gSnyvVKntHFh9c3A>lO5$a%*4O3=V;1bXC!-}*5lP>hh2N1&zRCQrAtxl0d8Pk| zLpXMmpGP_NG%Wp|KLvxA9#1IOK2_ko$>>!4dp+j(Jx9&+s;%gKq`5jZW`$g=1(Jc8 zl^+N-SXvp+C!b~cKb!y8@UnQ&F{XR1KW8rHea{z|wlZ%_&LNSJjgeV)k|Sl{ikEUu zGP?|-&ukJ3ix(bei(iPQYkfW$>##}-`N!TOS{Al=!JoS}1*Fv!<~?tvC=i?8Pr z)f|?-Qwv>_iRLAqb*0fL;DhS9eV5r6+0ifAKK3(K^Vcj;V9}E^a|qwI8edDMJ2yTO z{NdEzr)yJLRaNLSbd<6=9C)caNDaHzef;MIR{_jfRvCAr5LW@+jdHEg^1o)`hEubs zIZc!D>PJg9#i#lbCW(l1nok&8F|>|)@(3z z>SA{57&SW|8x}>VLkU4xD@8+N3;h3kWFT4j=L0|vqKBvUa1r01_p8hCTr-IU{;^a4 zOyRzN#bTVTF6kn;?WXAHxNssw<@U?c#qpnGrW^xfROgV-KC#qa_SoM##g?Z}xn)cp z(|0`s#UmB@zZ~{L_O1xI-71BEZwTaLvPcEfbXKnzJ7pa3K0kgj?JaR;?=$u>BV!nL zIiMuohKS;&d{horbP$$;!9N4zmh*jk{y0!_Y-`?#qAGuo)_b88arQvTGT9NI3GXMV zel8sAy`|L1LHkA=BCrh|`G2{9?_)@YiVjU5Tk9|HW5YbFW5oNBC#zAfUL!T%$5xUE zDn+tqsqtVo4O&o6oNK(E7ZDnbn%U(l`TrDqp^vfa`z@dJY#a0|e zJbQ$@aO6J3s6-VC*XQG*j8=E3E2`s=ffXH#rJ(iC#Jlp~Zl10B&8$S2k7SeN$9`){`bWw{^6AZsVmHp_pU*eohUR{DK&So#Fra$Ug43VO6*< z{BpVkgsPh+LP6!fZw-noBQr0TCSpQX>QYP5Mn? zRoxl;><(zI;`sZ1(Kw}UuM{NJymZP)*u``bKE~)#(|3Tbfsvua7wcE2zxt(--ZmgJ zJ^-JAqR3BXf@DN*gAD(%FDmr`Q{B4dzOv`s|NCpRdwT3ed4eLZt1oT zNU<~?CBkFtxlQ2=Hp4S`*X0DQA9px#sFwQ|cn5ep&7sMu=AKIa#I?W-cC+-6WTjwZ z`_7OIsPaaG458agqgy)j>a)jEk-a<%DK>e3qS-*(UJ=QNI)b!YmIIbazJ+gr5? zqf)2b^E|Y2R2Xbr88FKH+$F|hB+2!BI57JNZC)AAcxGmaTu;|ms7`> zx#W=|7M_C4j7hCR4JanTV;PsTv zC|(wq+{02*?L5>&6UXOy&Uq|)X7P=P*Rh>w3UN3ncoc1l4c4zzlbdBiTwP!QqY&nL zqvYNRH*n+`ENN>5udAzDgF}Yk?R^hURZxkzf3J0iK5TP2B&4I*KEY$Q@iZDVlGh$& zmM2KRcjYAa*J;_=CkN4t=F?3^Sa_@#xvxQMgDY43`rMJ4z+!Ou=xGnz12Fo~b=P5q z;IV*~7ngSkAYIh0wTM$^(CLj@1Qj}4{ckS+sFrHr zQ2#zVvmVRnI9l&DY)ruLk{00jOdfLqh@8#G{2<~T;s_2B7jCS)#CeER(sUcal%%BT z%-T(ht?o00G!aGc1A6c8sBI_Pd$d&v&MY=PMwRU5OI{Z9U3UPy>vWJ8xr&NP`2O@UXY?O_#UvZ&J%mdPLdL8^^6_zcUa$I-<~WXoq27?C^t*iC`_JV<2E1wN z#`Puh{8+UKfLuz@Lc5yI10W2_y=Y4YXbZ^d>M9N6T7E9r<{3tvTpK?2!){hMs+^Kt!N~M(bPa==jt)DYP4I z>Y}aRxC|_s0=Nt`&B9?4qe^EfPUjdgb%Q$`Gg(d?BUW)7Go`O)M0kaMxmfO_7Gt_U zy$=02Q{sVZmwFD3h#**uL8jimEsrE3*%%Qtirn!vJ7q|DS`|cjNwLeu39J_t9BDwTilNJD2Y>p-)RDKl}tzgbOse%;YmdCbIl8Gg)Vm_wh)dizkz#y?S zF1Vv~PwdKrOe!cq_sM$t)lu%>kE4<46?->0I+z~bmOy^XTkyvGd;h0}f*b~C!;GhJ%;w+C1tIS{c=n8l0}1@y2)byqg}M%r>d^R()plMn}-+G9YwUT+vpkqhUQq_#;G5z5Nd zqH=^K+v48KGT-t?VQS`ajn}g3hrv#JoB&teP(fx>8&uw{H?*f0rf-_P-PS$qiVoT{ zo45QDxw~GaGPTb-Z{bh0tt;zPC0)c-ZJ~pYwvYi~zwan{Tp4zPQH+JrE8i+kG)bZb zBO0%Jc=J?8xLkuk?1RadAah{aM0 z;;>y(aT}2a!%k;*Vh}222pK!sQ1}9^ZZSbU-QA(-e7n`o&synk{Ab7sk@n9?$m{%U zTW+IT1dv5s2xwtW&7M38?bc#+UOQEa5g50Mz&NT=Q>UZakY2P zBnx#7>nC1$w}bK2<;H{u<2l|QeMsYy?rqIFyu}Ui;{TNAzfz<= z!;3gEi;PK;0R4WkAN}2o`$Oz0ESX)^OWvAZDqSB4ZhF3%(s}#C4|>c&-w_u(W*3G6 z$MdJZUbNPd&CXMB;;yN*-!duCo!#87xb>^ho7s%BmW&X~B!|9u?z+f^LQ)mT^o(Nt za>A5RivK1tab_!)hh*OfmeO|(nl1U+bBHU7hFftYRBa@o}hID4Q2BH$%q;)Y`8k3y>GGom{fDxp!Ym zZCRq9@%(YyJ!mf>8J9=OoEyia{<&`)<3~{6e|*5afp=P711qQcr)Jsx1B*{7AW8UU zXd>>SxIQf{GGUUJ{p-GjdCIDchqxs-bJ23@J@oU{3&~g*47bID&O#SlBW-EBLuOyD zC`*n@7+a z?U>+R;H~I41a0vwSDm{cM}6@TFcs{Pc<>2BeSo1bGn)`2U z*SIXDXYeghezOleZ`|jbIK7u!Lk=q$gD3`~5LQsOi>%R; z%RFA<<|&AHmoH>T%=78LFP@dOWv7Z*72*m^3KZR(jpV`3k8+- z7eqgMSSM@?h`zlrO(p%;n}1UrLS|8d4Y|X@JoEbHcsOTV*VygR5G(E8LL7;)aoK(Q z=h*GBK&Hg!0^uga>b1ouxR#E|w6~)8kotP$?TxUNG}@Lpy21J@C?WhIQ<9Yct9M+8 zi!+HDjAcc~*=r-Ui?pj}b=!u@%!?3{EB@74g(c%nWZb|<$dfS`d6KOOaO;M6$ zO1{VpK5uikTEzyv7!m1xpp?plx54c@Tu|aZp%397-SmyGPgCtCO8?e=h_x`}h6h5A-It9)R{(l~JB!EhVlF$FKG2Jc@JrfIjsDV#;5hnD*|Knqg?;kGyKX$yY zp^%C|z*M_nb(Jaq?Uet%*cIfkJ(5J*VZsa@B3%-X!`9w>t<*I>0YTwAQf^x^KE70O zF(`Q*0O;v#V+>48{TOsEvisxBK}HHFIV}=Pul#h}`}%BOV+2%opJP~b1#r;-n{USy zLBz9$6J7=D@rdBZIw&U8xuLA}rrB(T{@u-`r-w%%8nH&sn7kmK@3%1 zw^yK)DXtS4&k}EKZ3UV2kii4B-3L0f2I3eJxNLNc13CkcDL-{XZJ-qk1|7?+4Gspo zf-o7Ox`4H&#dh!ZH&GP3!j#h0LeTq4y|kKTrOniCc4mAEuE3*+#m|go{qS24H-KDU zU;kzw97e#i*{yZ;b_IdX=xPk24)Sd%Gugn5PAZTJN?>%so3B|Jz~h7yzO%9`M}2qPyK8_z+-O&%y7a3`bd*l^?ip8knxy7 zF-$K={C6Ieqf`H#e9vFgGGPiO0br&+H~xriX)=xX&o~uN=*C zCF7<`)E{H-t+YdzJm?fOrWe+I1wF)|$(+-AF-Yj};K1wV!p>;9-CIz*$q^irBnmk1 z;j{mSHtL$d;l6$j%hs35^HKw)y!DLm*@JU~H0uk@V+62hZlG%q2p2$8KfFE0Ri=Nn z>8Qov-eMy*kK9AU`$buMJye-scvV`z{Qa0zz~hN>7^xu1gqg)N1{IWKAF5c^f4K7< zECK@1@9V7e$6<3IR|DbfJYJsAOh|7nPE`jSl7s@P52ygZ!WG^q7poZeeO?7V1Z*;# zE_e?tZ!uj$fQ7a8)Fl&oz#)g!pH}yN!FhMm9Q2F_UAr7EqoSac=(Jvi=Px$9l!H6; zbw!6i=PFaHT&rL4YW{F!P(xqiK3Lv<=LWhQy{>j>*x>;gFn0XW5FGSWnNQ?t)LNht zvIo^C00052*8zI2?0N74ze1)3|KXnwy(bQZ)S#P%*A* z-3PDF56_Q}kAw1njDgE$xy63->B;Y(DXscejb|b@(9~0+yPKs~+r5{)hu?UZ7M^ zE>aR+^tPIOe762`nXzIP9BXA~WmTCBH<^x-Fv=Ckk=G^wRfb7_v@sA`l#R)A{@qRL z2D9YOOHEF?o~KhN1Z*dVEB8ke0+m3BQL6G{59avN+bBt)>7S{m6NI9^1l^*rQ9=39m!^)j-|@ zmQYFU@pergFj<;;8Zhj+e|^0LR2@~ugJ+uwx>>=W!Nvx#XTeo4F85Vu5{CwFhc@Le zwlG#6VrVXDPG_G6msT9YfEg~SSDPqnepJ;wt2XMl-}w%aZkCal`|ALB zo&6*g`pAHA0nT>Z3O)O!@0i*F+g?cMV^gs-H1^0LF%N@IW!W#UHhu58|3^2HLmHJ- zQ%l^1qfl`>`I)IhAw`6Oz6SsaU|k~st}5hVa5#FmTUeULlk|X~40Qt^#CTPYD3Wj}%1w3X)M`gvKtTz# zxN6s!MR_q1xz1G?E9Oe(qAh>>maSH4U;%_lt&4kdHZozvymdl8uoiKQ>Q%E=4nMy7 zT!T`9Kb??mnQx<2BnmOy0(1ad{{F_)XC4s16Uu74FMir`1_Mzz#)~3iTFOS zzDs8B10CddbJeDqp8$KNS|BGptX*fNPD2bDb^}<;blc0)l5c-zN0#ZdHoae{1F8-K zWP05wUYCREQqA>We;uj|8r75p%e5MP&AZT)3xQ5+b^WLH54q-4Hsg?CdH^7vJGluc zl;zVcW7)>96nwo(CC0lb=jY1{1S__NQ`T3M4mlZAN;LxRCtbz6*T3$mb{qk$o|nTh zOE28f{}#CV)nqiSRDjd`I~?g3R|%*HfyaL1ClCVMmSl>4GN3H7vGq*)hW%hUfBZV@ zr4T(#0)op6b3wa;tSsM4?xt$Qp;sVG^?5Qk!D3$$Ap@5|NM zesQ}}I1cEY4jD!xV50&kqVYuKxxWpdj?3NRUo$mWFExQ9@YmWp;YN>&R7yi^^YY~L zdt~aZ=CTAR;!l5$e;W^RgJk4|c@MO;X9LfHih>O2_snF+%(44!Bhbe0BDuAhf|<1$blRMUs(=hYQxDK zqCe+A7||#OF?vi0G$JH0svi$9l>fBcRAT^oKoGZm(3Nl+Kr=7^GtEi^-~mh(8^AS` zXQ#?iim&W}-?H8UO;`uaOOqknnykA-JH9?rUgwhG`-sP{+nSs=wzp>*>_18gWd%$CSsdzfPQ5D-lAh=n>GFtiU1R{&hQ*66$}YxMET93i{W(T{y)InR z^*Vz{dNh?sQQVcCo6Te>Xu4;Rr9JEhxNGK>l>?1XJD8&2LI1t;q^|*_gJ;6`b|-JJ zE_}hRe=i7Hdq6dzXWIco{&-wnn>v}-g_+v^JxaZ&km3! z5G?D|wbOZ?A83p11Gah+Xt5ZIaK8c%oD4VxsleI?Rv2upHV+;uDlyF^Tk%`wI;%Nf zk86@&03i4`?l0COsgFPt{eI$W%YGKZt}(EG|Bf4-kYA8boC99T3+OzKWcBrbsyC0- z0Bxq#Q{g#2i{h+lxa{q3=z@+eE|Eb&tXj}w35*G@C>w!q0&SvcTUH(N zCeCejT?ptVx324KUBtwg{J>6A49pW`8bHjxO{0p@jGx=`~r14BE^Pjx|*kb-S*&}UOyEIWUC4s@+X!^u;iIR*su-85U!7=wFp(0n+gI(Z0-{lxo1 zwwB+(Nhy7bvhD|njnmABWL1_&w;+k5O$8`Co1n?360D3wdt)(Glq>T^gP$nixa&UD zp4aQ9A^US_4>s`&1OIn-Kxk@Pve(YP8#*rU*>s};Vp3OC*xeiJUsU1 zs%i2tX$`gI+e1OrBCDVXf=V)H%Ctq15vFzl(qucA53Rj?(lDNd;L%`e|FsY zF1{ovM=D=CndYZ72~?`!1{PO9ep-}PMiMbOsvvps`x|<#P%5vB^@hPCAH8B0Nby>g3Pc)!wRJEiMgZ;-^W8Zo4!t{NsZ^N73h~NQx;m6OaQZ=JJm;dtN)& z%uXJUJ+{iM=Qz^=Y2NiAJIK^HujiYbiaGI6xo;Vt5&uo~?lN?O^@x8NeC`4P7d73# zavM}AMVm(;D-P-%Q&Yyzpkm@^tG6x*i z*+fME!5IK3rFSm7lW(8A0O`P~-R;$BWcXFy!AvLC zbLmvtlE62iVPPIb{3;GWd=Dzo$p#@%AgPMRJg7Xtc`vA*_oho1LU0%~YRoc8gW+M2 zJ6wPm79h~XYkRIC6g0HgKhi*79ngMaFi&NeJn@EsfmvQ&RBx*@LBD1uOwz81}yBB3t~rs@wyf5D^JZr^Us_dR*&A7u!5}?d8 zQTsl(9>WYoUSP-sv0RXE6KMKI#8!bCUTgdaU~dm$!1c+FUtlN5>L!h|K+;--`SW|e zEXY!C#02o6r6+l2tSvb0R@#L|8jCv#QqLS!ShXg{!uEDSHjv5h&?;(M^q!RDo8cH; zFAN4gcl5vQWZ5`llk+pHGoPS#KidPzzAw)2N>IIAg#JwL`^N)gv7jj^{xcGih@C&% zqlOwwNMk~%t)TJ-BI7IW+|d@>RS{6Ufo~hoBb9`G{J0j^?-E$Ax?&gso9U?`^PL3a zbb&nODUnhHSKw*!2%uwW)vDgp2!(5&aO z(S8#uuGgo=MSJ>GWb!pPuiJ^4lG3xz^!W<_3l$xp%3W!3ofvPLEx&C>j=-I^kqc1L z)m?f2s)jsv9WMZUHNtxv8=HNEW|ESCZjcma3lvp7MM4_4xx6Gnf%2liVGkfR)=Tr0 zgkq~8ho+%z+ZbGz7pI&UC?{k zME{$#Ef9}^dUUP)-Cd;MT>(G$hMBe~5mU*^k3aCe=z`GX@Rkso4A^QzFvLMgTAObU zIuW5oW^SY74i>V{L+S$N{=n$|8*<|F{T(ud>HvKG#ZkQ|z}bJ>m>lhU;f*RN-=FwYm8L=`EJ>UNY%OicT diff --git a/docs/plantuml/lidoContracts.puml b/docs/plantuml/lidoContracts.puml index 1bf50a5..012a21e 100644 --- a/docs/plantuml/lidoContracts.puml +++ b/docs/plantuml/lidoContracts.puml @@ -31,8 +31,8 @@ object "WithdrawalQueueERC721" as lidoQ <><> #$thirdPartyColor { assets: stETH, WETH } -zap ..> arm -arm .> capMan +zap <..> arm +arm <.> capMan arm ..> lidoQ From 11eb8e3d258c39bc5bc786090d906c5ec4bec24e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 10 Oct 2024 21:03:58 +1100 Subject: [PATCH 177/196] Removed te extra logic that transfers and extra 2 wei for stETH --- docs/LidoARMSquashed.svg | 2 +- src/contracts/LidoARM.sol | 13 ------------- .../SwapTokensForExactTokens.t.sol | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/docs/LidoARMSquashed.svg b/docs/LidoARMSquashed.svg index 881bbad..22628ef 100644 --- a/docs/LidoARMSquashed.svg +++ b/docs/LidoARMSquashed.svg @@ -58,7 +58,7 @@    _setOperator(newOperator: address) <<OwnableOperable>>    _initARM(_operator: address, _name: string, _symbol: string, _fee: uint256, _feeCollector: address, _capManager: address) <<AbstractARM>>    _inDeadline(deadline: uint256) <<AbstractARM>> -    _transferAsset(asset: address, to: address, amount: uint256) <<LidoARM>> +    _transferAsset(asset: address, to: address, amount: uint256) <<AbstractARM>>    _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>> diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index dabbe78..3037d45 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -62,19 +62,6 @@ contract LidoARM is Initializable, AbstractARM { steth.approve(address(lidoWithdrawalQueue), type(uint256).max); } - /** - * @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 not used in swaps from stETH to WETH. - */ - function _transferAsset(address asset, address to, uint256 amount) internal override { - // Add 2 wei if transferring stETH - if (asset == address(steth)) amount += 2; - - super._transferAsset(asset, to, amount); - } - /** * @notice Request a stETH for ETH withdrawal. * Reference: https://docs.lido.fi/contracts/withdrawal-queue-erc721/ diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol index 5029dd4..230b676 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -309,7 +309,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(steth)}); - emit IERC20.Transfer(address(lidoARM), address(this), amountOut + 2); + emit IERC20.Transfer(address(lidoARM), address(this), amountOut); // Main call lidoARM.swapTokensForExactTokens( weth, // inToken From be9ad00efb1f5746ab2902194ef2a08a9629ba59 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 11 Oct 2024 19:32:31 +1100 Subject: [PATCH 178/196] snapLido now compares against 1Inch, Curve and Uniswap --- src/abis/CurveStEthPool.json | 484 ++++++++++++++++++++++++++++++++++ src/abis/UniswapV3Quoter.json | 97 +++++++ src/abis/wstETH.json | 252 ++++++++++++++++++ src/js/tasks/lido.js | 114 ++++---- src/js/tasks/markets.js | 193 ++++++++++++++ src/js/tasks/tasks.js | 4 + src/js/utils/1Inch.js | 122 +++++++++ src/js/utils/addresses.js | 12 +- src/js/utils/curve.js | 57 ++++ src/js/utils/uniswap.js | 96 +++++++ 10 files changed, 1374 insertions(+), 57 deletions(-) create mode 100644 src/abis/CurveStEthPool.json create mode 100644 src/abis/UniswapV3Quoter.json create mode 100644 src/abis/wstETH.json create mode 100644 src/js/tasks/markets.js create mode 100644 src/js/utils/1Inch.js create mode 100644 src/js/utils/curve.js create mode 100644 src/js/utils/uniswap.js diff --git a/src/abis/CurveStEthPool.json b/src/abis/CurveStEthPool.json new file mode 100644 index 0000000..a6b5566 --- /dev/null +++ b/src/abis/CurveStEthPool.json @@ -0,0 +1,484 @@ +[ + { + "name": "TokenExchange", + "inputs": [ + { "type": "address", "name": "buyer", "indexed": true }, + { "type": "int128", "name": "sold_id", "indexed": false }, + { "type": "uint256", "name": "tokens_sold", "indexed": false }, + { "type": "int128", "name": "bought_id", "indexed": false }, + { "type": "uint256", "name": "tokens_bought", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "TokenExchangeUnderlying", + "inputs": [ + { "type": "address", "name": "buyer", "indexed": true }, + { "type": "int128", "name": "sold_id", "indexed": false }, + { "type": "uint256", "name": "tokens_sold", "indexed": false }, + { "type": "int128", "name": "bought_id", "indexed": false }, + { "type": "uint256", "name": "tokens_bought", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "AddLiquidity", + "inputs": [ + { "type": "address", "name": "provider", "indexed": true }, + { "type": "uint256[2]", "name": "token_amounts", "indexed": false }, + { "type": "uint256[2]", "name": "fees", "indexed": false }, + { "type": "uint256", "name": "invariant", "indexed": false }, + { "type": "uint256", "name": "token_supply", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RemoveLiquidity", + "inputs": [ + { "type": "address", "name": "provider", "indexed": true }, + { "type": "uint256[2]", "name": "token_amounts", "indexed": false }, + { "type": "uint256[2]", "name": "fees", "indexed": false }, + { "type": "uint256", "name": "token_supply", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RemoveLiquidityOne", + "inputs": [ + { "type": "address", "name": "provider", "indexed": true }, + { "type": "uint256", "name": "token_amount", "indexed": false }, + { "type": "uint256", "name": "coin_amount", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RemoveLiquidityImbalance", + "inputs": [ + { "type": "address", "name": "provider", "indexed": true }, + { "type": "uint256[2]", "name": "token_amounts", "indexed": false }, + { "type": "uint256[2]", "name": "fees", "indexed": false }, + { "type": "uint256", "name": "invariant", "indexed": false }, + { "type": "uint256", "name": "token_supply", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "CommitNewAdmin", + "inputs": [ + { "type": "uint256", "name": "deadline", "indexed": true }, + { "type": "address", "name": "admin", "indexed": true } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "NewAdmin", + "inputs": [{ "type": "address", "name": "admin", "indexed": true }], + "anonymous": false, + "type": "event" + }, + { + "name": "CommitNewFee", + "inputs": [ + { "type": "uint256", "name": "deadline", "indexed": true }, + { "type": "uint256", "name": "fee", "indexed": false }, + { "type": "uint256", "name": "admin_fee", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "NewFee", + "inputs": [ + { "type": "uint256", "name": "fee", "indexed": false }, + { "type": "uint256", "name": "admin_fee", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RampA", + "inputs": [ + { "type": "uint256", "name": "old_A", "indexed": false }, + { "type": "uint256", "name": "new_A", "indexed": false }, + { "type": "uint256", "name": "initial_time", "indexed": false }, + { "type": "uint256", "name": "future_time", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "StopRampA", + "inputs": [ + { "type": "uint256", "name": "A", "indexed": false }, + { "type": "uint256", "name": "t", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + { "type": "address", "name": "_owner" }, + { "type": "address[2]", "name": "_coins" }, + { "type": "address", "name": "_pool_token" }, + { "type": "uint256", "name": "_A" }, + { "type": "uint256", "name": "_fee" }, + { "type": "uint256", "name": "_admin_fee" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "A", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 5289 + }, + { + "name": "A_precise", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 5251 + }, + { + "name": "balances", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [{ "type": "uint256", "name": "i" }], + "stateMutability": "view", + "type": "function", + "gas": 5076 + }, + { + "name": "get_virtual_price", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1114301 + }, + { + "name": "calc_token_amount", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [ + { "type": "uint256[2]", "name": "amounts" }, + { "type": "bool", "name": "is_deposit" } + ], + "stateMutability": "view", + "type": "function", + "gas": 2218181 + }, + { + "name": "add_liquidity", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [ + { "type": "uint256[2]", "name": "amounts" }, + { "type": "uint256", "name": "min_mint_amount" } + ], + "stateMutability": "payable", + "type": "function", + "gas": 3484118 + }, + { + "name": "get_dy", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [ + { "type": "int128", "name": "i" }, + { "type": "int128", "name": "j" }, + { "type": "uint256", "name": "dx" } + ], + "stateMutability": "view", + "type": "function", + "gas": 2654541 + }, + { + "name": "exchange", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [ + { "type": "int128", "name": "i" }, + { "type": "int128", "name": "j" }, + { "type": "uint256", "name": "dx" }, + { "type": "uint256", "name": "min_dy" } + ], + "stateMutability": "payable", + "type": "function", + "gas": 2810134 + }, + { + "name": "remove_liquidity", + "outputs": [{ "type": "uint256[2]", "name": "" }], + "inputs": [ + { "type": "uint256", "name": "_amount" }, + { "type": "uint256[2]", "name": "_min_amounts" } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 160545 + }, + { + "name": "remove_liquidity_imbalance", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [ + { "type": "uint256[2]", "name": "_amounts" }, + { "type": "uint256", "name": "_max_burn_amount" } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 3519382 + }, + { + "name": "calc_withdraw_one_coin", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [ + { "type": "uint256", "name": "_token_amount" }, + { "type": "int128", "name": "i" } + ], + "stateMutability": "view", + "type": "function", + "gas": 1435 + }, + { + "name": "remove_liquidity_one_coin", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [ + { "type": "uint256", "name": "_token_amount" }, + { "type": "int128", "name": "i" }, + { "type": "uint256", "name": "_min_amount" } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 4113806 + }, + { + "name": "ramp_A", + "outputs": [], + "inputs": [ + { "type": "uint256", "name": "_future_A" }, + { "type": "uint256", "name": "_future_time" } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 151834 + }, + { + "name": "stop_ramp_A", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 148595 + }, + { + "name": "commit_new_fee", + "outputs": [], + "inputs": [ + { "type": "uint256", "name": "new_fee" }, + { "type": "uint256", "name": "new_admin_fee" } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 110431 + }, + { + "name": "apply_new_fee", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 153115 + }, + { + "name": "revert_new_parameters", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 21865 + }, + { + "name": "commit_transfer_ownership", + "outputs": [], + "inputs": [{ "type": "address", "name": "_owner" }], + "stateMutability": "nonpayable", + "type": "function", + "gas": 74603 + }, + { + "name": "apply_transfer_ownership", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 116583 + }, + { + "name": "revert_transfer_ownership", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 21955 + }, + { + "name": "withdraw_admin_fees", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 137597 + }, + { + "name": "donate_admin_fees", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 42144 + }, + { + "name": "kill_me", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 37938 + }, + { + "name": "unkill_me", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 22075 + }, + { + "name": "coins", + "outputs": [{ "type": "address", "name": "" }], + "inputs": [{ "type": "uint256", "name": "arg0" }], + "stateMutability": "view", + "type": "function", + "gas": 2160 + }, + { + "name": "admin_balances", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [{ "type": "uint256", "name": "arg0" }], + "stateMutability": "view", + "type": "function", + "gas": 2190 + }, + { + "name": "fee", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2111 + }, + { + "name": "admin_fee", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2141 + }, + { + "name": "owner", + "outputs": [{ "type": "address", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2171 + }, + { + "name": "lp_token", + "outputs": [{ "type": "address", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2201 + }, + { + "name": "initial_A", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2231 + }, + { + "name": "future_A", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2261 + }, + { + "name": "initial_A_time", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2291 + }, + { + "name": "future_A_time", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2321 + }, + { + "name": "admin_actions_deadline", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2351 + }, + { + "name": "transfer_ownership_deadline", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2381 + }, + { + "name": "future_fee", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2411 + }, + { + "name": "future_admin_fee", + "outputs": [{ "type": "uint256", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2441 + }, + { + "name": "future_owner", + "outputs": [{ "type": "address", "name": "" }], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2471 + } +] diff --git a/src/abis/UniswapV3Quoter.json b/src/abis/UniswapV3Quoter.json new file mode 100644 index 0000000..e9adacb --- /dev/null +++ b/src/abis/UniswapV3Quoter.json @@ -0,0 +1,97 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_factory", "type": "address" }, + { "internalType": "address", "name": "_WETH9", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "WETH9", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "path", "type": "bytes" }, + { "internalType": "uint256", "name": "amountIn", "type": "uint256" } + ], + "name": "quoteExactInput", + "outputs": [ + { "internalType": "uint256", "name": "amountOut", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "tokenIn", "type": "address" }, + { "internalType": "address", "name": "tokenOut", "type": "address" }, + { "internalType": "uint24", "name": "fee", "type": "uint24" }, + { "internalType": "uint256", "name": "amountIn", "type": "uint256" }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "name": "quoteExactInputSingle", + "outputs": [ + { "internalType": "uint256", "name": "amountOut", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "path", "type": "bytes" }, + { "internalType": "uint256", "name": "amountOut", "type": "uint256" } + ], + "name": "quoteExactOutput", + "outputs": [ + { "internalType": "uint256", "name": "amountIn", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "tokenIn", "type": "address" }, + { "internalType": "address", "name": "tokenOut", "type": "address" }, + { "internalType": "uint24", "name": "fee", "type": "uint24" }, + { "internalType": "uint256", "name": "amountOut", "type": "uint256" }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "name": "quoteExactOutputSingle", + "outputs": [ + { "internalType": "uint256", "name": "amountIn", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "int256", "name": "amount0Delta", "type": "int256" }, + { "internalType": "int256", "name": "amount1Delta", "type": "int256" }, + { "internalType": "bytes", "name": "path", "type": "bytes" } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/abis/wstETH.json b/src/abis/wstETH.json new file mode 100644 index 0000000..fede488 --- /dev/null +++ b/src/abis/wstETH.json @@ -0,0 +1,252 @@ +[ + { + "inputs": [ + { "internalType": "contract IStETH", "name": "_stETH", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_wstETHAmount", "type": "uint256" } + ], + "name": "getStETHByWstETH", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_stETHAmount", "type": "uint256" } + ], + "name": "getWstETHByStETH", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "nonces", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stETH", + "outputs": [ + { "internalType": "contract IStETH", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "stEthPerToken", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokensPerStEth", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_wstETHAmount", "type": "uint256" } + ], + "name": "unwrap", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_stETHAmount", "type": "uint256" } + ], + "name": "wrap", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { "stateMutability": "payable", "type": "receive" } +] diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index 5ab05c8..5755656 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -1,5 +1,12 @@ const { formatUnits, parseUnits, MaxInt256 } = require("ethers"); +const addresses = require("../utils/addresses"); +const { + logArmPrices, + log1InchPrices, + logCurvePrices, + logUniswapSpotPrices, +} = require("./markets"); const { getBlock } = require("../utils/block"); const { getSigner } = require("../utils/signers"); const { logTxDetails } = require("../utils/txLogger"); @@ -90,9 +97,9 @@ const submitLido = async ({ amount }) => { await logTxDetails(tx, "submit"); }; -const snapLido = async ({ block }) => { +const snapLido = async ({ amount, block, curve, oneInch, uniswap }) => { const blockTag = await getBlock(block); - console.log(`\nLiquidity`); + const pair = "stETH/ETH"; const armAddress = await parseAddress("LIDO_ARM"); const lidoARM = await ethers.getContractAt("LidoARM", armAddress); @@ -102,7 +109,40 @@ const snapLido = async ({ block }) => { capManagerAddress ); - await logRates(lidoARM, blockTag); + const ammPrices = await logArmPrices(lidoARM, blockTag); + + if (oneInch) { + await log1InchPrices(amount, ammPrices); + } + + if (curve) { + await logCurvePrices( + { + blockTag, + amount, + pair, + poolName: "Old", + poolAddress: addresses.mainnet.CurveStEthPool, + }, + ammPrices + ); + + await logCurvePrices( + { + blockTag, + amount, + pair, + poolName: "NextGen", + poolAddress: addresses.mainnet.CurveNgStEthPool, + }, + ammPrices + ); + } + + if (uniswap) { + await logUniswapSpotPrices({ blockTag, pair, amount }, ammPrices); + } + const { totalAssets, totalSupply, liquidityWeth } = await logAssets( lidoARM, blockTag @@ -178,68 +218,30 @@ const logAssets = async (arm, blockTag) => { console.log(`\nAssets`); console.log( - `${formatUnits(liquidityWeth, 18)} WETH ${formatUnits(wethPercent, 2)}%` + `${formatUnits(liquidityWeth, 18).padEnd(23)} WETH ${formatUnits( + wethPercent, + 2 + )}%` ); console.log( - `${formatUnits(liquiditySteth, 18)} stETH ${formatUnits(oethPercent, 2)}%` + `${formatUnits(liquiditySteth, 18).padEnd(23)} stETH ${formatUnits( + oethPercent, + 2 + )}%` ); console.log( - `${formatUnits( - liquidityLidoWithdraws, - 18 - )} Lido withdrawal requests ${formatUnits(stethWithdrawsPercent, 2)}%` + `${formatUnits(liquidityLidoWithdraws, 18).padEnd( + 23 + )} Lido withdraw ${formatUnits(stethWithdrawsPercent, 2)}%` ); - console.log(`${formatUnits(total, 18)} total WETH and stETH`); - console.log(`${formatUnits(totalAssets, 18)} total assets`); - console.log(`${formatUnits(totalSupply, 18)} total supply`); - console.log(`${formatUnits(assetPerShare, 18)} asset per share`); + console.log(`${formatUnits(total, 18).padEnd(23)} total WETH and stETH`); + console.log(`${formatUnits(totalAssets, 18).padEnd(23)} total assets`); + console.log(`${formatUnits(totalSupply, 18).padEnd(23)} total supply`); + console.log(`${formatUnits(assetPerShare, 18).padEnd(23)} asset per share`); return { totalAssets, totalSupply, liquidityWeth }; }; -const logRates = async (arm, blockTag) => { - console.log(`\nPrices`); - // The rate of 1 WETH for stETH to 36 decimals from the perspective of the AMM. ie WETH/stETH - // from the trader's perspective, this is the stETH/WETH buy price - const OWethStEthRate = await arm.traderate0({ blockTag }); - console.log(`traderate0: ${formatUnits(OWethStEthRate, 36)} WETH/stETH`); - - // convert from WETH/stETH rate with 36 decimals to stETH/WETH rate with 18 decimals - const sellPrice = BigInt(1e54) / BigInt(OWethStEthRate); - - // The rate of 1 stETH for WETH to 36 decimals. ie stETH/WETH - const OStEthWethRate = await arm.traderate1({ blockTag }); - console.log(`traderate1: ${formatUnits(OStEthWethRate, 36)} stETH/WETH`); - // Convert back to 18 decimals - const buyPrice = BigInt(OStEthWethRate) / BigInt(1e18); - - const midPrice = (sellPrice + buyPrice) / 2n; - - const crossPrice = await arm.crossPrice({ blockTag }); - - console.log(`sell : ${formatUnits(sellPrice, 18).padEnd(20)} stETH/WETH`); - if (crossPrice > sellPrice) { - console.log(`cross : ${formatUnits(crossPrice, 36).padEnd(20)} stETH/WETH`); - console.log(`mid : ${formatUnits(midPrice, 18).padEnd(20)} stETH/WETH`); - } else { - console.log(`mid : ${formatUnits(midPrice, 18).padEnd(20)} stETH/WETH`); - console.log(`cross : ${formatUnits(crossPrice, 18).padEnd(20)} stETH/WETH`); - } - console.log(`buy : ${formatUnits(buyPrice, 18).padEnd(20)} stETH/WETH`); - - const spread = BigInt(sellPrice) - BigInt(buyPrice); - // Origin rates are to 36 decimals - console.log(`spread: ${formatUnits(spread, 14)} bps`); - - return { - buyPrice: sellPrice, - sellPrice: buyPrice, - midPrice, - crossPrice, - spread, - }; -}; - const swapLido = async ({ from, to, amount }) => { if (from && to) { throw new Error( diff --git a/src/js/tasks/markets.js b/src/js/tasks/markets.js new file mode 100644 index 0000000..a8cb059 --- /dev/null +++ b/src/js/tasks/markets.js @@ -0,0 +1,193 @@ +const { formatUnits } = require("ethers"); + +const { get1InchPrices } = require("../utils/1Inch"); +const { getCurvePrices } = require("../utils/curve"); +const { getUniswapV3SpotPrices } = require("../utils/uniswap"); + +const log = require("../utils/logger")("task:markets"); + +const logArmPrices = async (arm, blockTag) => { + console.log(`\nARM Prices`); + // The rate of 1 WETH for stETH to 36 decimals from the perspective of the AMM. ie WETH/stETH + // from the trader's perspective, this is the stETH/WETH buy price + const OWethStEthRate = await arm.traderate0({ blockTag }); + console.log(`traderate0: ${formatUnits(OWethStEthRate, 36)} WETH/stETH`); + + // convert from WETH/stETH rate with 36 decimals to stETH/WETH rate with 18 decimals + const sellPrice = BigInt(1e54) / BigInt(OWethStEthRate); + + // The rate of 1 stETH for WETH to 36 decimals. ie stETH/WETH + const OStEthWethRate = await arm.traderate1({ blockTag }); + console.log(`traderate1: ${formatUnits(OStEthWethRate, 36)} stETH/WETH`); + // Convert back to 18 decimals + const buyPrice = BigInt(OStEthWethRate) / BigInt(1e18); + + const midPrice = (sellPrice + buyPrice) / 2n; + + const crossPrice = await arm.crossPrice({ blockTag }); + + console.log(`sell : ${formatUnits(sellPrice, 18).padEnd(20)} stETH/WETH`); + if (crossPrice > sellPrice) { + console.log( + `cross : ${formatUnits(crossPrice, 36).padEnd(20)} stETH/WETH` + ); + console.log(`mid : ${formatUnits(midPrice, 18).padEnd(20)} stETH/WETH`); + } else { + console.log(`mid : ${formatUnits(midPrice, 18).padEnd(20)} stETH/WETH`); + console.log( + `cross : ${formatUnits(crossPrice, 18).padEnd(20)} stETH/WETH` + ); + } + console.log(`buy : ${formatUnits(buyPrice, 18).padEnd(20)} stETH/WETH`); + + const spread = BigInt(sellPrice) - BigInt(buyPrice); + // Origin rates are to 36 decimals + console.log(`spread : ${formatUnits(spread, 14)} bps`); + + return { + buyPrice: sellPrice, + sellPrice: buyPrice, + midPrice, + crossPrice, + spread, + }; +}; + +const log1InchPrices = async (amount, ammPrices) => { + const oneInch = await get1InchPrices(amount); + + log(`buy ${formatUnits(oneInch.buyToAmount)} stETH for ${amount} WETH`); + log(`sell ${amount} stETH for ${formatUnits(oneInch.sellToAmount)} WETH`); + + console.log(`\n1Inch prices for swap size ${amount}`); + const buyRateDiff = oneInch.buyPrice - ammPrices.buyPrice; + console.log( + `buy : ${formatUnits(oneInch.buyPrice, 18).padEnd( + 20 + )} stETH/WETH, diff ${formatUnits(buyRateDiff, 14).padEnd( + 17 + )} bps to ARM, ${oneInch.buyGas.toLocaleString()} gas` + ); + + const midRateDiff = oneInch.midPrice - ammPrices.midPrice; + console.log( + `mid : ${formatUnits(oneInch.midPrice, 18).padEnd( + 20 + )} stETH/WETH, diff ${formatUnits(midRateDiff, 14).padEnd(17)} bps to ARM` + ); + + const sellRateDiff = oneInch.sellPrice - ammPrices.sellPrice; + console.log( + `sell : ${formatUnits(oneInch.sellPrice, 18).padEnd( + 20 + )} stETH/WETH, diff ${formatUnits(sellRateDiff, 14).padEnd( + 17 + )} bps to ARM, ${oneInch.sellGas.toLocaleString()} gas` + ); + console.log(`spread : ${formatUnits(oneInch.spread, 14)} bps`); + + console.log(`buy path for stETH/WETH`); + log1InchProtocols(oneInch.buyQuote); + + console.log(`sell path for stETH/WETH`); + log1InchProtocols(oneInch.sellQuote); + + return oneInch; +}; + +const log1InchProtocols = (sellQuote) => { + // TODO need to better handle + sellQuote.protocols.forEach((p1) => { + p1.forEach((p2) => { + p2.forEach((p3) => { + console.log( + `${p3.part.toString().padEnd(3)}% ${p3.name.padEnd(12)} ${ + p3.fromTokenAddress + } -> ${p3.toTokenAddress}` + ); + }); + }); + }); +}; + +const logCurvePrices = async (options, ammPrices) => { + const { amount, pair, poolName } = options; + + const curve = await getCurvePrices(options); + const buyRateDiff = curve.buyPrice - ammPrices.buyPrice; + const midRateDiff = curve.midPrice - ammPrices.midPrice; + const sellRateDiff = curve.sellPrice - ammPrices.sellPrice; + + log(`buy ${formatUnits(curve.buyToAmount)} stETH for ${amount} WETH`); + log(`sell ${amount} stETH for ${formatUnits(curve.sellToAmount)} WETH`); + + console.log(`\n${poolName} Curve prices for swap size ${amount}`); + console.log( + `buy : ${formatUnits(curve.buyPrice, 18).padEnd( + 20 + )} ${pair}, diff ${formatUnits(buyRateDiff, 14).padEnd( + 17 + )} bps to ARM, ${curve.buyGas.toLocaleString()} gas` + ); + console.log( + `mid : ${formatUnits(curve.midPrice, 18).padEnd( + 20 + )} ${pair}, diff ${formatUnits(midRateDiff, 14).padEnd(17)} bps to ARM` + ); + console.log( + `sell : ${formatUnits(curve.sellPrice, 18).padEnd( + 20 + )} ${pair}, diff ${formatUnits(sellRateDiff, 1).padEnd( + 17 + )} bps to ARM, ${curve.sellGas.toLocaleString()} gas` + ); + console.log(`spread : ${formatUnits(curve.spread, 14)} bps`); + + return curve; +}; + +const logUniswapSpotPrices = async (options, ammPrices) => { + const { amount, pair } = options; + const uniswap = await getUniswapV3SpotPrices(options); + const buyRateDiff = uniswap.buyPrice - ammPrices.buyPrice; + const midRateDiff = uniswap.midPrice - ammPrices.midPrice; + const sellRateDiff = uniswap.sellPrice - ammPrices.sellPrice; + + log(`buy ${formatUnits(uniswap.buyToAmount)} stETH for ${amount} WETH`); + log(`sell ${amount} stETH for ${formatUnits(uniswap.sellToAmount)} WETH`); + + console.log( + `\nwstETH/ETH 0.01% Uniswap V3 spot prices for swap size ${amount}` + ); + console.log( + `buy : ${formatUnits(uniswap.buyPrice, 18).padEnd( + 20 + )} ${pair}, diff ${formatUnits( + buyRateDiff, + 14 + )} bps to ARM, ${uniswap.buyGas.toLocaleString()} gas` + ); + console.log( + `mid : ${formatUnits(uniswap.midPrice, 18).padEnd( + 20 + )} ${pair}, diff ${formatUnits(midRateDiff, 14)} bps to ARM` + ); + console.log( + `sell : ${formatUnits(uniswap.sellPrice, 18).padEnd( + 20 + )} ${pair}, diff ${formatUnits( + sellRateDiff, + 14 + )} bps to ARM, ${uniswap.sellGas.toLocaleString()} gas` + ); + console.log(`spread : ${formatUnits(uniswap.spread, 14)} bps`); + + return uniswap; +}; + +module.exports = { + log1InchPrices, + logArmPrices, + logCurvePrices, + logUniswapSpotPrices, +}; diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 79c6e66..0187deb 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -608,6 +608,10 @@ subtask("snapLido", "Take a snapshot of the Lido ARM") undefined, types.int ) + .addOptionalParam("amount", "Swap quantity", 100, types.int) + .addOptionalParam("oneInch", "Include 1Inch prices", true, types.boolean) + .addOptionalParam("curve", "Include Curve prices", true, types.boolean) + .addOptionalParam("uniswap", "Include Uniswap V3 prices", true, types.boolean) .setAction(snapLido); task("snapLido").setAction(async (_, __, runSuper) => { return runSuper(); diff --git a/src/js/utils/1Inch.js b/src/js/utils/1Inch.js new file mode 100644 index 0000000..77ad4d4 --- /dev/null +++ b/src/js/utils/1Inch.js @@ -0,0 +1,122 @@ +const axios = require("axios"); +const { parseUnits } = require("ethers"); + +const addresses = require("../utils/addresses"); +const { sleep } = require("../utils/time"); + +const log = require("./logger")("utils:1inch"); + +const ONEINCH_API_ENDPOINT = "https://api.1inch.dev/swap/v5.2/1/quote"; + +/** + * Gets a swap quote from 1Inch's V5.2 swap API + * @param fromAsset The address of the asset to swap from. + * @param toAsset The address of the asset to swap to. + * @param fromAmount The unit amount of fromAsset to swap. eg 1.1 WETH = 1.1e18 + * See https://docs.1inch.io/docs/aggregation-protocol/api/swagger + */ +const get1InchSwapQuote = async ({ fromAsset, toAsset, fromAmount }) => { + const apiKey = process.env.ONEINCH_API_KEY; + if (!apiKey) { + throw Error( + "ONEINCH_API_KEY environment variable not set. Visit the 1Inch Dev Portal https://portal.1inch.dev/" + ); + } + + const params = { + src: fromAsset, + dst: toAsset, + amount: fromAmount.toString(), + allowPartialFill: true, + disableEstimate: true, + includeProtocols: true, + includeGas: true, + includeTokensInfo: false, + }; + log("swap API params: ", params); + + let retries = 3; + + while (retries > 0) { + try { + const response = await axios.get(ONEINCH_API_ENDPOINT, { + params, + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + + if (!response.data.toAmount) { + console.error(response.data); + throw Error("response is missing toAmount"); + } + + log("swap API response data: %j", response.data); + + return response.data; + } catch (err) { + if (err.response) { + console.error("Response data : ", err.response.data); + console.error("Response status: ", err.response.status); + console.error("Response status: ", err.response.statusText); + } + if (err.response?.status == 429) { + retries = retries - 1; + console.error( + `Failed to get a 1Inch quote. Will try again in 2 seconds with ${retries} retries left` + ); + // Wait for 2s before next try + await new Promise((r) => setTimeout(r, 2000)); + continue; + } + throw Error(`Call to 1Inch swap quote API failed: ${err.message}`); + } + } + + throw Error(`Call to 1Inch swap quote API failed: Rate-limited`); +}; + +const get1InchPrices = async (amount) => { + + const amountBI = parseUnits(amount.toString(), 18); + + const buyQuote = await get1InchSwapQuote({ + fromAsset: addresses.mainnet.WETH, + toAsset: addresses.mainnet.stETH, + fromAmount: amountBI, // WETH amount + }); + // stETH buy amount + const buyToAmount = BigInt(buyQuote.toAmount); + // stETH/ETH rate = ETH amount / stETH amount + const buyPrice = (amountBI * BigInt(1e18)) / buyToAmount; + + await sleep(800); + + const sellQuote = await get1InchSwapQuote({ + fromAsset: addresses.mainnet.stETH, + toAsset: addresses.mainnet.WETH, + fromAmount: amountBI, // stETH amount + }); + // WETH sell amount + const sellToAmount = BigInt(sellQuote.toAmount); + // stETH/WETH rate = WETH amount / stETH amount + const sellPrice = (sellToAmount * BigInt(1e18)) / amountBI; + + const midPrice = (buyPrice + sellPrice) / 2n; + const spread = buyPrice - sellPrice; + + return { + buyQuote, + buyToAmount, + buyPrice, + buyGas: buyQuote.gas, + sellQuote, + sellToAmount, + sellPrice, + sellGas: sellQuote.gas, + midPrice, + spread, + }; +}; + +module.exports = { get1InchSwapQuote, get1InchPrices }; diff --git a/src/js/utils/addresses.js b/src/js/utils/addresses.js index 5bcae17..0695483 100644 --- a/src/js/utils/addresses.js +++ b/src/js/utils/addresses.js @@ -17,8 +17,18 @@ addresses.mainnet.OETHVaultProxy = "0x39254033945aa2e4809cc2977e7087bee48bd7ab"; // Tokens addresses.mainnet.WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; - +addresses.mainnet.stETH = "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"; +addresses.mainnet.wstETH = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"; addresses.mainnet.OethARM = "0x6bac785889A4127dB0e0CeFEE88E0a9F1Aaf3cC7"; +// AMMs +addresses.mainnet.CurveStEthPool = "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"; +addresses.mainnet.CurveNgStEthPool = + "0x21e27a5e5513d6e65c4f830167390997aa84843a"; +addresses.mainnet.UniswapV3Quoter = + "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6"; +addresses.mainnet.UniswapV3stETHWETHPool = + "0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa"; + module.exports = addresses; diff --git a/src/js/utils/curve.js b/src/js/utils/curve.js new file mode 100644 index 0000000..184f18a --- /dev/null +++ b/src/js/utils/curve.js @@ -0,0 +1,57 @@ +const { parseUnits } = require("ethers"); + +const curvePoolAbi = require("../../abis/CurveStEthPool.json"); + +const getCurvePrices = async ({ amount, poolAddress, blockTag }) => { + const pool = await ethers.getContractAt(curvePoolAbi, poolAddress); + + const amountBI = parseUnits(amount.toString(), 18); + + // Swap ETH for stETH + const buyToAmount = await pool["get_dy(int128,int128,uint256)"]( + 0, + 1, + amountBI, + { blockTag } + ); + const buyGas = await pool["get_dy(int128,int128,uint256)"].estimateGas( + 0, + 1, + amountBI, + { blockTag } + ); + // stETH/ETH rate = ETH amount / stETH amount + const buyPrice = (amountBI * BigInt(1e18)) / buyToAmount; + + // Swap stETH for ETH + const sellToAmount = await pool["get_dy(int128,int128,uint256)"]( + 1, + 0, + amountBI, + { blockTag } + ); + const sellGas = await pool["get_dy(int128,int128,uint256)"].estimateGas( + 1, + 0, + amountBI, + { blockTag } + ); + // stETH/WETH rate = WETH amount / stETH amount + const sellPrice = (sellToAmount * BigInt(1e18)) / amountBI; + + const midPrice = (buyPrice + sellPrice) / 2n; + const spread = buyPrice - sellPrice; + + return { + buyToAmount, + buyPrice, + buyGas, + sellToAmount, + sellPrice, + sellGas, + midPrice, + spread, + }; +}; + +module.exports = { getCurvePrices }; diff --git a/src/js/utils/uniswap.js b/src/js/utils/uniswap.js new file mode 100644 index 0000000..8655dec --- /dev/null +++ b/src/js/utils/uniswap.js @@ -0,0 +1,96 @@ +const { parseUnits } = require("ethers"); +const { ethers } = require("ethers"); + +const quoterAbi = require("../../abis/UniswapV3Quoter.json"); +const wstEthAbi = require("../../abis/wstETH.json"); +const addresses = require("./addresses"); +const { getSigner } = require("./signers"); + +const log = require("../utils/logger")("task:uniswap"); + +const getUniswapV3SpotPrices = async ({ amount, blockTag }) => { + const signer = await getSigner(); + const quoter = new ethers.Contract( + addresses.mainnet.UniswapV3Quoter, + quoterAbi, + signer + ); + + const wstEth = new ethers.Contract( + addresses.mainnet.wstETH, + wstEthAbi, + signer + ); + + const amountBI = parseUnits(amount.toString(), 18); + + // Swap WETH for stETH + const wstEthAmount = await quoter + .connect(signer) + .quoteExactInputSingle.staticCall( + addresses.mainnet.WETH, + addresses.mainnet.wstETH, + 100, + amountBI, + 0, + { blockTag } + ); + const buyToAmount = await wstEth.getStETHByWstETH(wstEthAmount); + log(`buyToAmount: ${buyToAmount}`); + const buyGas = await quoter + .connect(signer) + .quoteExactInputSingle.estimateGas( + addresses.mainnet.WETH, + addresses.mainnet.wstETH, + 100, + amountBI, + 0, + { blockTag } + ); + // stETH/ETH rate = ETH amount / stETH amount + const buyPrice = (amountBI * BigInt(1e18)) / buyToAmount; + + // Swap stETH for WETH + // Convert stETH to wstETH + const wstETHAmount = await wstEth.getWstETHByStETH(amountBI); + log(`wstETHAmount: ${wstETHAmount} ${typeof wstETHAmount}`); + // Convert wstETH to WETH + const sellToAmount = await quoter + .connect(signer) + .quoteExactInputSingle.staticCall( + addresses.mainnet.wstETH, + addresses.mainnet.WETH, + 100, + wstETHAmount, + 0, + { blockTag } + ); + const sellGas = await quoter + .connect(signer) + .quoteExactInputSingle.estimateGas( + addresses.mainnet.wstETH, + addresses.mainnet.WETH, + 100, + amountBI, + 0, + { blockTag } + ); + // stETH/WETH rate = WETH amount / stETH amount + const sellPrice = (sellToAmount * BigInt(1e18)) / amountBI; + + const midPrice = (buyPrice + sellPrice) / 2n; + const spread = buyPrice - sellPrice; + + return { + buyToAmount, + buyPrice, + buyGas, + sellToAmount, + sellPrice, + sellGas, + midPrice, + spread, + }; +}; + +module.exports = { getUniswapV3SpotPrices }; From 36a3132f9d12c37949c77b95777ab54eaadbdd85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Sat, 12 Oct 2024 15:33:08 +0200 Subject: [PATCH 179/196] Change Zap integration in ARM. (#33) * fix: remove zap restricted method.. * style: change liquidity provider param name. * fix: change state then write call. * fix: state change then transfer token. * fix: use `fees` instead of `fee` when collecting fees. * fix: move up `nextWithdrawalIndex` increase. * fix: avoid code duplication. * fix: pack variable action (read & write). --- .../mainnet/003_UpgradeLidoARMScript.sol | 4 -- src/contracts/AbstractARM.sol | 57 +++++++------------ test/fork/shared/Shared.sol | 3 - 3 files changed, 22 insertions(+), 42 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 2991caa..3d73922 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -137,10 +137,6 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("About to set prices on the ARM contract"); LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9994e36, 0.9998e36); - // Set the Zapper contract on the Lido ARM - console.log("About to set the Zapper on the ARM contract"); - LidoARM(payable(Mainnet.LIDO_ARM)).setZap(address(zapper)); - // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig console.log("About to set ARM owner to", Mainnet.GOV_MULTISIG); lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 4baaeec..ccfd963 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -105,10 +105,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { address public feeCollector; /// @notice The address of the CapManager contract used to manage the ARM's liquidity provider and total assets caps. address public capManager; - /// @notice The address of the Zapper contract that converts ETH to WETH before ARM deposits - address public zap; - uint256[41] private _gap; + uint256[42] private _gap; //////////////////////////////////////////////////// /// Events @@ -125,7 +123,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { event FeeUpdated(uint256 fee); event FeeCollectorUpdated(address indexed newFeeCollector); event CapManagerUpdated(address indexed capManager); - event ZapUpdated(address indexed zap); constructor(address _token0, address _token1, address _liquidityAsset, uint256 _claimDelay) { require(IERC20(_token0).decimals() == 18); @@ -433,37 +430,35 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } /// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares. - /// This function is restricted to the Zap contract. + /// Funds will be transfered from msg.sender. /// @param assets The amount of liquidity assets to deposit - /// @param liquidityProvider The address of the liquidity provider + /// @param receiver The address that will receive shares. /// @return shares The amount of shares that were minted - function deposit(uint256 assets, address liquidityProvider) external returns (uint256 shares) { - require(msg.sender == zap, "Only Zap"); - - shares = _deposit(assets, liquidityProvider); + function deposit(uint256 assets, address receiver) external returns (uint256 shares) { + shares = _deposit(assets, receiver); } /// @dev Internal logic for depositing liquidity assets in exchange for liquidity provider (LP) shares. - function _deposit(uint256 assets, address liquidityProvider) internal returns (uint256 shares) { + function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) { // Calculate the amount of shares to mint after the performance fees have been accrued // which reduces the available assets, and before new assets are deposited. shares = convertToShares(assets); + // Add the deposited assets to the last available assets + lastAvailableAssets += SafeCast.toInt128(SafeCast.toInt256(assets)); + // Transfer the liquidity asset from the sender to this contract IERC20(liquidityAsset).transferFrom(msg.sender, address(this), assets); // mint shares - _mint(liquidityProvider, shares); - - // Add the deposited assets to the last available assets - lastAvailableAssets += SafeCast.toInt128(SafeCast.toInt256(assets)); + _mint(receiver, shares); // Check the liquidity provider caps after the new assets have been deposited if (capManager != address(0)) { - ICapManager(capManager).postDepositHook(liquidityProvider, assets); + ICapManager(capManager).postDepositHook(receiver, assets); } - emit Deposit(liquidityProvider, assets, shares); + emit Deposit(receiver, assets, shares); } /// @notice Preview the amount of assets that would be received for burning a given amount of shares @@ -482,13 +477,15 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { assets = convertToAssets(shares); requestId = nextWithdrawalIndex; - uint120 queued = SafeCast.toUint120(withdrawsQueued + assets); - uint40 claimTimestamp = uint40(block.timestamp + claimDelay); - // Store the next withdrawal request nextWithdrawalIndex = SafeCast.toUint16(requestId + 1); + + uint120 queued = SafeCast.toUint120(withdrawsQueued + assets); // Store the updated queued amount which reserves liquidity assets (WETH) in the withdrawal queue withdrawsQueued = queued; + + uint40 claimTimestamp = uint40(block.timestamp + claimDelay); + // Store requests withdrawalRequests[requestId] = WithdrawalRequest({ withdrawer: msg.sender, @@ -516,10 +513,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { require(request.claimTimestamp <= block.timestamp, "Claim delay not met"); // Is there enough liquidity to claim this request? - require( - request.queued <= withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)), - "Queue pending liquidity" - ); + require(request.queued <= claimable(), "Queue pending liquidity"); require(request.withdrawer == msg.sender, "Not requester"); require(request.claimed == false, "Already claimed"); @@ -538,7 +532,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice Used to work out if an ARM's withdrawal request can be claimed. /// If the withdrawal request's `queued` amount is less than the returned `claimable` amount, then it can be claimed. - function claimable() external view returns (uint256) { + function claimable() public view returns (uint256) { return withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this)); } @@ -624,13 +618,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { emit CapManagerUpdated(_capManager); } - /// @notice Set the Zap contract address. - function setZap(address _zap) external onlyOwner { - zap = _zap; - - emit ZapUpdated(_zap); - } - //////////////////////////////////////////////////// /// Performance Fee Functions //////////////////////////////////////////////////// @@ -676,7 +663,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Accrue any performance fees up to this point (fees, newAvailableAssets) = _feesAccrued(); - if (fee == 0) return 0; + if (fees == 0) return 0; // Check there is enough liquidity assets (WETH) that are not reserved for the withdrawal queue // to cover the fee being collected. @@ -687,11 +674,11 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // a failed WETH transfer so we spend the extra gas to check and give a meaningful error message. require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); - IERC20(liquidityAsset).transfer(feeCollector, fees); - // Save the new available assets back to storage less the collected fees. lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(newAvailableAssets) - SafeCast.toInt256(fees)); + IERC20(liquidityAsset).transfer(feeCollector, fees); + emit FeeCollected(feeCollector, fees); } diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 915c9bd..bb6073d 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -169,9 +169,6 @@ abstract contract Fork_Shared_Test_ is Modifiers { // --- Deploy ZapperLidoARM --- zapperLidoARM = new ZapperLidoARM(address(weth), address(lidoProxy)); - - // Set zap address in LidoARM. - lidoARM.setZap(address(zapperLidoARM)); } function _label() internal { From e58a3222db28f43e37a94ae5af87cfbbf3401fd6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 14 Oct 2024 15:16:29 +1100 Subject: [PATCH 180/196] Added gas costs to lidoSnap --- src/js/tasks/lido.js | 18 +++---- src/js/tasks/markets.js | 102 ++++++++++++++++++++++++++++++---------- src/js/tasks/tasks.js | 1 + src/js/utils/curve.js | 36 +++++++++----- src/js/utils/uniswap.js | 44 ++++++++++------- 5 files changed, 137 insertions(+), 64 deletions(-) diff --git a/src/js/tasks/lido.js b/src/js/tasks/lido.js index 5755656..76b2deb 100644 --- a/src/js/tasks/lido.js +++ b/src/js/tasks/lido.js @@ -97,9 +97,9 @@ const submitLido = async ({ amount }) => { await logTxDetails(tx, "submit"); }; -const snapLido = async ({ amount, block, curve, oneInch, uniswap }) => { +const snapLido = async ({ amount, block, curve, oneInch, uniswap, gas }) => { const blockTag = await getBlock(block); - const pair = "stETH/ETH"; + const commonOptions = { amount, blockTag, pair: "stETH/ETH", gas }; const armAddress = await parseAddress("LIDO_ARM"); const lidoARM = await ethers.getContractAt("LidoARM", armAddress); @@ -109,18 +109,16 @@ const snapLido = async ({ amount, block, curve, oneInch, uniswap }) => { capManagerAddress ); - const ammPrices = await logArmPrices(lidoARM, blockTag); + const ammPrices = await logArmPrices(commonOptions, lidoARM); if (oneInch) { - await log1InchPrices(amount, ammPrices); + await log1InchPrices(commonOptions, ammPrices); } if (curve) { await logCurvePrices( { - blockTag, - amount, - pair, + ...commonOptions, poolName: "Old", poolAddress: addresses.mainnet.CurveStEthPool, }, @@ -129,9 +127,7 @@ const snapLido = async ({ amount, block, curve, oneInch, uniswap }) => { await logCurvePrices( { - blockTag, - amount, - pair, + ...commonOptions, poolName: "NextGen", poolAddress: addresses.mainnet.CurveNgStEthPool, }, @@ -140,7 +136,7 @@ const snapLido = async ({ amount, block, curve, oneInch, uniswap }) => { } if (uniswap) { - await logUniswapSpotPrices({ blockTag, pair, amount }, ammPrices); + await logUniswapSpotPrices(commonOptions, ammPrices); } const { totalAssets, totalSupply, liquidityWeth } = await logAssets( diff --git a/src/js/tasks/markets.js b/src/js/tasks/markets.js index a8cb059..a305b16 100644 --- a/src/js/tasks/markets.js +++ b/src/js/tasks/markets.js @@ -1,12 +1,14 @@ -const { formatUnits } = require("ethers"); +const { formatUnits, parseUnits } = require("ethers"); +const addresses = require("../utils/addresses"); const { get1InchPrices } = require("../utils/1Inch"); const { getCurvePrices } = require("../utils/curve"); const { getUniswapV3SpotPrices } = require("../utils/uniswap"); +const { getSigner } = require("../utils/signers"); const log = require("../utils/logger")("task:markets"); -const logArmPrices = async (arm, blockTag) => { +const logArmPrices = async ({ blockTag, gas }, arm) => { console.log(`\nARM Prices`); // The rate of 1 WETH for stETH to 36 decimals from the perspective of the AMM. ie WETH/stETH // from the trader's perspective, this is the stETH/WETH buy price @@ -26,7 +28,56 @@ const logArmPrices = async (arm, blockTag) => { const crossPrice = await arm.crossPrice({ blockTag }); - console.log(`sell : ${formatUnits(sellPrice, 18).padEnd(20)} stETH/WETH`); + let buyGasCosts = ""; + let sellGasCosts = ""; + if (gas) { + const signer = await getSigner(); + const amountBI = parseUnits("0.01", 18); + try { + const buyGas = await arm + .connect(signer) + [ + "swapExactTokensForTokens(address,address,uint256,uint256,address)" + ].estimateGas( + addresses.mainnet.WETH, + addresses.mainnet.stETH, + amountBI, + 0, + addresses.dead, + { + blockTag, + } + ); + buyGasCosts = `, ${buyGas.toLocaleString()} gas`; + } catch (e) { + log(`Failed to estimate buy gas for swap: ${e.message}`); + } + try { + const sellGas = await arm + .connect(signer) + [ + "swapExactTokensForTokens(address,address,uint256,uint256,address)" + ].estimateGas( + addresses.mainnet.stETH, + addresses.mainnet.WETH, + amountBI, + 0, + addresses.dead, + { + blockTag, + } + ); + sellGasCosts = `, ${sellGas.toLocaleString()} gas`; + } catch (e) { + log(`Failed to estimate sell gas for swap: ${e.message}`); + } + } + + console.log( + `sell : ${formatUnits(sellPrice, 18).padEnd( + 20 + )} stETH/WETH${sellGasCosts}` + ); if (crossPrice > sellPrice) { console.log( `cross : ${formatUnits(crossPrice, 36).padEnd(20)} stETH/WETH` @@ -38,22 +89,23 @@ const logArmPrices = async (arm, blockTag) => { `cross : ${formatUnits(crossPrice, 18).padEnd(20)} stETH/WETH` ); } - console.log(`buy : ${formatUnits(buyPrice, 18).padEnd(20)} stETH/WETH`); + console.log( + `buy : ${formatUnits(buyPrice, 18).padEnd(20)} stETH/WETH${buyGasCosts}` + ); const spread = BigInt(sellPrice) - BigInt(buyPrice); // Origin rates are to 36 decimals console.log(`spread : ${formatUnits(spread, 14)} bps`); return { - buyPrice: sellPrice, - sellPrice: buyPrice, + buyPrice, + sellPrice, midPrice, - crossPrice, spread, }; }; -const log1InchPrices = async (amount, ammPrices) => { +const log1InchPrices = async ({ amount, gas }, ammPrices) => { const oneInch = await get1InchPrices(amount); log(`buy ${formatUnits(oneInch.buyToAmount)} stETH for ${amount} WETH`); @@ -61,12 +113,13 @@ const log1InchPrices = async (amount, ammPrices) => { console.log(`\n1Inch prices for swap size ${amount}`); const buyRateDiff = oneInch.buyPrice - ammPrices.buyPrice; + const buyGasCosts = gas ? `, ${oneInch.buyGas.toLocaleString()} gas` : ""; console.log( `buy : ${formatUnits(oneInch.buyPrice, 18).padEnd( 20 )} stETH/WETH, diff ${formatUnits(buyRateDiff, 14).padEnd( 17 - )} bps to ARM, ${oneInch.buyGas.toLocaleString()} gas` + )} bps to ARM${buyGasCosts}` ); const midRateDiff = oneInch.midPrice - ammPrices.midPrice; @@ -77,12 +130,13 @@ const log1InchPrices = async (amount, ammPrices) => { ); const sellRateDiff = oneInch.sellPrice - ammPrices.sellPrice; + const sellGasCosts = gas ? `, ${oneInch.sellGas.toLocaleString()} gas` : ""; console.log( `sell : ${formatUnits(oneInch.sellPrice, 18).padEnd( 20 )} stETH/WETH, diff ${formatUnits(sellRateDiff, 14).padEnd( 17 - )} bps to ARM, ${oneInch.sellGas.toLocaleString()} gas` + )} bps to ARM${sellGasCosts}` ); console.log(`spread : ${formatUnits(oneInch.spread, 14)} bps`); @@ -111,7 +165,7 @@ const log1InchProtocols = (sellQuote) => { }; const logCurvePrices = async (options, ammPrices) => { - const { amount, pair, poolName } = options; + const { amount, pair, poolName, gas } = options; const curve = await getCurvePrices(options); const buyRateDiff = curve.buyPrice - ammPrices.buyPrice; @@ -122,12 +176,14 @@ const logCurvePrices = async (options, ammPrices) => { log(`sell ${amount} stETH for ${formatUnits(curve.sellToAmount)} WETH`); console.log(`\n${poolName} Curve prices for swap size ${amount}`); + const buyGasCosts = gas ? `, ${curve.buyGas.toLocaleString()} gas` : ""; + const sellGasCosts = gas ? `, ${curve.sellGas.toLocaleString()} gas` : ""; console.log( `buy : ${formatUnits(curve.buyPrice, 18).padEnd( 20 )} ${pair}, diff ${formatUnits(buyRateDiff, 14).padEnd( 17 - )} bps to ARM, ${curve.buyGas.toLocaleString()} gas` + )} bps to ARM${buyGasCosts}` ); console.log( `mid : ${formatUnits(curve.midPrice, 18).padEnd( @@ -139,7 +195,7 @@ const logCurvePrices = async (options, ammPrices) => { 20 )} ${pair}, diff ${formatUnits(sellRateDiff, 1).padEnd( 17 - )} bps to ARM, ${curve.sellGas.toLocaleString()} gas` + )} bps to ARM${sellGasCosts}` ); console.log(`spread : ${formatUnits(curve.spread, 14)} bps`); @@ -147,7 +203,7 @@ const logCurvePrices = async (options, ammPrices) => { }; const logUniswapSpotPrices = async (options, ammPrices) => { - const { amount, pair } = options; + const { amount, pair, gas } = options; const uniswap = await getUniswapV3SpotPrices(options); const buyRateDiff = uniswap.buyPrice - ammPrices.buyPrice; const midRateDiff = uniswap.midPrice - ammPrices.midPrice; @@ -159,26 +215,22 @@ const logUniswapSpotPrices = async (options, ammPrices) => { console.log( `\nwstETH/ETH 0.01% Uniswap V3 spot prices for swap size ${amount}` ); + const buyGasCosts = gas ? `, ${uniswap.buyGas.toLocaleString()} gas` : ""; + const sellGasCosts = gas ? `, ${uniswap.sellGas.toLocaleString()} gas` : ""; console.log( - `buy : ${formatUnits(uniswap.buyPrice, 18).padEnd( + `buy : ${formatUnits(uniswap.buyPrice, 18).padEnd( 20 - )} ${pair}, diff ${formatUnits( - buyRateDiff, - 14 - )} bps to ARM, ${uniswap.buyGas.toLocaleString()} gas` + )} ${pair}, diff ${formatUnits(buyRateDiff, 14)} bps to ARM${buyGasCosts}` ); console.log( - `mid : ${formatUnits(uniswap.midPrice, 18).padEnd( + `mid : ${formatUnits(uniswap.midPrice, 18).padEnd( 20 )} ${pair}, diff ${formatUnits(midRateDiff, 14)} bps to ARM` ); console.log( - `sell : ${formatUnits(uniswap.sellPrice, 18).padEnd( + `sell : ${formatUnits(uniswap.sellPrice, 18).padEnd( 20 - )} ${pair}, diff ${formatUnits( - sellRateDiff, - 14 - )} bps to ARM, ${uniswap.sellGas.toLocaleString()} gas` + )} ${pair}, diff ${formatUnits(sellRateDiff, 14)} bps to ARM${sellGasCosts}` ); console.log(`spread : ${formatUnits(uniswap.spread, 14)} bps`); diff --git a/src/js/tasks/tasks.js b/src/js/tasks/tasks.js index 0187deb..15b302d 100644 --- a/src/js/tasks/tasks.js +++ b/src/js/tasks/tasks.js @@ -612,6 +612,7 @@ subtask("snapLido", "Take a snapshot of the Lido ARM") .addOptionalParam("oneInch", "Include 1Inch prices", true, types.boolean) .addOptionalParam("curve", "Include Curve prices", true, types.boolean) .addOptionalParam("uniswap", "Include Uniswap V3 prices", true, types.boolean) + .addOptionalParam("gas", "Include gas costs", true, types.boolean) .setAction(snapLido); task("snapLido").setAction(async (_, __, runSuper) => { return runSuper(); diff --git a/src/js/utils/curve.js b/src/js/utils/curve.js index 184f18a..e8f6515 100644 --- a/src/js/utils/curve.js +++ b/src/js/utils/curve.js @@ -2,7 +2,7 @@ const { parseUnits } = require("ethers"); const curvePoolAbi = require("../../abis/CurveStEthPool.json"); -const getCurvePrices = async ({ amount, poolAddress, blockTag }) => { +const getCurvePrices = async ({ amount, poolAddress, blockTag, gas }) => { const pool = await ethers.getContractAt(curvePoolAbi, poolAddress); const amountBI = parseUnits(amount.toString(), 18); @@ -14,12 +14,6 @@ const getCurvePrices = async ({ amount, poolAddress, blockTag }) => { amountBI, { blockTag } ); - const buyGas = await pool["get_dy(int128,int128,uint256)"].estimateGas( - 0, - 1, - amountBI, - { blockTag } - ); // stETH/ETH rate = ETH amount / stETH amount const buyPrice = (amountBI * BigInt(1e18)) / buyToAmount; @@ -30,17 +24,35 @@ const getCurvePrices = async ({ amount, poolAddress, blockTag }) => { amountBI, { blockTag } ); + // stETH/WETH rate = WETH amount / stETH amount + const sellPrice = (sellToAmount * BigInt(1e18)) / amountBI; + + const midPrice = (buyPrice + sellPrice) / 2n; + const spread = buyPrice - sellPrice; + + if (!gas) { + return { + buyToAmount, + buyPrice, + sellToAmount, + sellPrice, + midPrice, + spread, + }; + } + + const buyGas = await pool["get_dy(int128,int128,uint256)"].estimateGas( + 0, + 1, + amountBI, + { blockTag } + ); const sellGas = await pool["get_dy(int128,int128,uint256)"].estimateGas( 1, 0, amountBI, { blockTag } ); - // stETH/WETH rate = WETH amount / stETH amount - const sellPrice = (sellToAmount * BigInt(1e18)) / amountBI; - - const midPrice = (buyPrice + sellPrice) / 2n; - const spread = buyPrice - sellPrice; return { buyToAmount, diff --git a/src/js/utils/uniswap.js b/src/js/utils/uniswap.js index 8655dec..28c0f51 100644 --- a/src/js/utils/uniswap.js +++ b/src/js/utils/uniswap.js @@ -8,7 +8,7 @@ const { getSigner } = require("./signers"); const log = require("../utils/logger")("task:uniswap"); -const getUniswapV3SpotPrices = async ({ amount, blockTag }) => { +const getUniswapV3SpotPrices = async ({ amount, blockTag, gas }) => { const signer = await getSigner(); const quoter = new ethers.Contract( addresses.mainnet.UniswapV3Quoter, @@ -37,16 +37,6 @@ const getUniswapV3SpotPrices = async ({ amount, blockTag }) => { ); const buyToAmount = await wstEth.getStETHByWstETH(wstEthAmount); log(`buyToAmount: ${buyToAmount}`); - const buyGas = await quoter - .connect(signer) - .quoteExactInputSingle.estimateGas( - addresses.mainnet.WETH, - addresses.mainnet.wstETH, - 100, - amountBI, - 0, - { blockTag } - ); // stETH/ETH rate = ETH amount / stETH amount const buyPrice = (amountBI * BigInt(1e18)) / buyToAmount; @@ -65,6 +55,33 @@ const getUniswapV3SpotPrices = async ({ amount, blockTag }) => { 0, { blockTag } ); + // stETH/WETH rate = WETH amount / stETH amount + const sellPrice = (sellToAmount * BigInt(1e18)) / amountBI; + + const midPrice = (buyPrice + sellPrice) / 2n; + const spread = buyPrice - sellPrice; + + if (!gas) { + return { + buyToAmount, + buyPrice, + sellToAmount, + sellPrice, + midPrice, + spread, + }; + } + + const buyGas = await quoter + .connect(signer) + .quoteExactInputSingle.estimateGas( + addresses.mainnet.WETH, + addresses.mainnet.wstETH, + 100, + amountBI, + 0, + { blockTag } + ); const sellGas = await quoter .connect(signer) .quoteExactInputSingle.estimateGas( @@ -75,11 +92,6 @@ const getUniswapV3SpotPrices = async ({ amount, blockTag }) => { 0, { blockTag } ); - // stETH/WETH rate = WETH amount / stETH amount - const sellPrice = (sellToAmount * BigInt(1e18)) / amountBI; - - const midPrice = (buyPrice + sellPrice) / 2n; - const spread = buyPrice - sellPrice; return { buyToAmount, From 6bdca993e05d8d2dbdee7f79e0f83a3f20330884 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 14 Oct 2024 15:58:04 +1100 Subject: [PATCH 181/196] Added RequestLidoWithdrawals and ClaimLidoWithdrawals events --- src/contracts/LidoARM.sol | 7 ++++ .../ClaimStETHWithdrawalForWETH.t.sol | 20 ++++++++- .../RequestStETHWithdrawalForETH.t.sol | 41 +++++++++++++------ 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/contracts/LidoARM.sol b/src/contracts/LidoARM.sol index 3037d45..4fe485e 100644 --- a/src/contracts/LidoARM.sol +++ b/src/contracts/LidoARM.sol @@ -26,6 +26,9 @@ contract LidoARM is Initializable, AbstractARM { /// @notice The amount of stETH in the Lido Withdrawal Queue uint256 public lidoWithdrawalQueueAmount; + event RequestLidoWithdrawals(uint256[] amounts, uint256[] requestIds); + event ClaimLidoWithdrawals(uint256[] requestIds); + /// @param _steth The address of the stETH token /// @param _weth The address of the WETH token /// @param _lidoWithdrawalQueue The address of the Lido's withdrawal queue contract @@ -82,6 +85,8 @@ contract LidoARM is Initializable, AbstractARM { // Increase the Ether outstanding from the Lido Withdrawal Queue lidoWithdrawalQueueAmount += totalAmountRequested; + + emit RequestLidoWithdrawals(amounts, requestIds); } /** @@ -103,6 +108,8 @@ contract LidoARM is Initializable, AbstractARM { // Wrap all the received ETH to WETH. weth.deposit{value: etherAfter}(); + + emit ClaimLidoWithdrawals(requestIds); } /** diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol index d73d32c..9384768 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimStETHWithdrawalForWETH.t.sol @@ -5,8 +5,8 @@ pragma solidity 0.8.23; import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts -import {IERC20} from "contracts/Interfaces.sol"; -import {IStETHWithdrawal} from "contracts/Interfaces.sol"; +import {IERC20, IStETHWithdrawal} from "contracts/Interfaces.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ { @@ -45,6 +45,12 @@ contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ assertEq(address(lidoARM).balance, 0); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); + uint256[] memory emptyList = new uint256[](0); + + // Expected events + vm.expectEmit({emitter: address(lidoARM)}); + emit LidoARM.ClaimLidoWithdrawals(emptyList); + // Main call lidoARM.claimLidoWithdrawals(new uint256[](0)); @@ -65,6 +71,11 @@ contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ stETHWithdrawal.getLastRequestId(); uint256[] memory requests = new uint256[](1); requests[0] = stETHWithdrawal.getLastRequestId(); + + // Expected events + vm.expectEmit({emitter: address(lidoARM)}); + emit LidoARM.ClaimLidoWithdrawals(requests); + // Main call lidoARM.claimLidoWithdrawals(requests); @@ -88,6 +99,11 @@ contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ uint256[] memory requests = new uint256[](2); requests[0] = stETHWithdrawal.getLastRequestId() - 1; requests[1] = stETHWithdrawal.getLastRequestId(); + + // Expected events + vm.expectEmit({emitter: address(lidoARM)}); + emit LidoARM.ClaimLidoWithdrawals(requests); + // Main call lidoARM.claimLidoWithdrawals(requests); diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol index a5646c7..9145a67 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestStETHWithdrawalForETH.t.sol @@ -5,7 +5,9 @@ pragma solidity 0.8.23; import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Contracts -import {IERC20} from "contracts/Interfaces.sol"; +import {IERC20, IStETHWithdrawal} from "contracts/Interfaces.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; +import {Mainnet} from "contracts/utils/Addresses.sol"; contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// @@ -40,54 +42,69 @@ contract Fork_Concrete_LidoARM_RequestLidoWithdrawals_Test_ is Fork_Shared_Test_ /// --- PASSING TESTS ////////////////////////////////////////////////////// function test_RequestLidoWithdrawals_EmptyList() public asOperator { - uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(new uint256[](0)); - assertEq(requestIds.length, 0); + uint256[] memory emptyList = new uint256[](0); + + // Expected events + vm.expectEmit({emitter: address(lidoARM)}); + emit LidoARM.RequestLidoWithdrawals(emptyList, emptyList); + + uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(emptyList); + + assertEq(requestIds, emptyList); } function test_RequestLidoWithdrawals_SingleAmount_1ether() public asOperator { uint256[] memory amounts = new uint256[](1); amounts[0] = DEFAULT_AMOUNT; + uint256[] memory expectedLidoRequestIds = new uint256[](1); + expectedLidoRequestIds[0] = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL).getLastRequestId() + 1; // Expected events vm.expectEmit({emitter: address(steth)}); emit IERC20.Transfer(address(lidoARM), address(lidoARM.lidoWithdrawalQueue()), amounts[0]); + vm.expectEmit({emitter: address(lidoARM)}); + emit LidoARM.RequestLidoWithdrawals(amounts, expectedLidoRequestIds); // Main call uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(amounts); - assertEq(requestIds.length, 1); - assertGt(requestIds[0], 0); + assertEq(requestIds, expectedLidoRequestIds); } function test_RequestLidoWithdrawals_SingleAmount_1000ethers() public asOperator { uint256[] memory amounts = new uint256[](1); amounts[0] = 1_000 ether; + uint256[] memory expectedLidoRequestIds = new uint256[](1); + expectedLidoRequestIds[0] = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL).getLastRequestId() + 1; // Expected events vm.expectEmit({emitter: address(steth)}); emit IERC20.Transfer(address(lidoARM), address(lidoARM.lidoWithdrawalQueue()), amounts[0]); + vm.expectEmit({emitter: address(lidoARM)}); + emit LidoARM.RequestLidoWithdrawals(amounts, expectedLidoRequestIds); // Main call uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(amounts); - assertEq(requestIds.length, 1); - assertGt(requestIds[0], 0); + assertEq(requestIds, expectedLidoRequestIds); } function test_RequestLidoWithdrawals_MultipleAmount() public asOperator { uint256 length = _bound(vm.randomUint(), 2, 10); uint256[] memory amounts = new uint256[](length); + uint256[] memory expectedLidoRequestIds = new uint256[](length); + uint256 startingLidoRequestId = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL).getLastRequestId() + 1; for (uint256 i = 0; i < amounts.length; i++) { amounts[i] = _bound(vm.randomUint(), 0, 1_000 ether); + expectedLidoRequestIds[i] = startingLidoRequestId + i; } + vm.expectEmit({emitter: address(lidoARM)}); + emit LidoARM.RequestLidoWithdrawals(amounts, expectedLidoRequestIds); + // Main call uint256[] memory requestIds = lidoARM.requestLidoWithdrawals(amounts); - uint256 initialRequestId = requestIds[0]; - assertGt(initialRequestId, 0); - for (uint256 i = 1; i < amounts.length; i++) { - assertEq(requestIds[i], initialRequestId + i); - } + assertEq(requestIds, expectedLidoRequestIds); } } From 5c30a4cf63c8b4b99e2ae3b2dc6defdcefa7a1b4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 14 Oct 2024 17:02:20 +1100 Subject: [PATCH 182/196] 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 183/196] 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 184/196] 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 185/196] 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 186/196] 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 ////////////////////////////////////////////////////// From 1ad9655026168f6c6113f8c8cc396e0203cb97c7 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 15 Oct 2024 22:41:31 +1100 Subject: [PATCH 187/196] Updated Lido ARM's token symbol initial performance fee set to 20% initial total assets cap set to 400 ether --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 11 ++++++----- test/smoke/LidoARMSmokeTest.t.sol | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 3d73922..63fda7a 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -50,7 +50,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { capManager = CapManager(address(capManProxy)); // 5. Set the liquidity Provider caps - capManager.setTotalAssetsCap(100 ether); + capManager.setTotalAssetsCap(400 ether); address[] memory liquidityProviders = new address[](1); liquidityProviders[0] = Mainnet.TREASURY; capManager.setLiquidityProviderCaps(liquidityProviders, 100 ether); @@ -107,9 +107,9 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { bytes memory data = abi.encodeWithSignature( "initialize(string,string,address,uint256,address,address)", "Lido ARM", - "ARM-ST", + "ARM-WETH-stETH", Mainnet.ARM_RELAYER, - 1500, // 15% performance fee + 2000, // 20% performance fee Mainnet.ARM_BUYBACK, address(capManProxy) ); @@ -133,9 +133,10 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("About to set the cross price on the ARM contract"); LidoARM(payable(Mainnet.LIDO_ARM)).setCrossPrice(0.9998e36); - // Set the buy price with a 8 basis point discount. The sell price is 1.0 + // Set the buy price with a 4 basis point discount. + // The sell price has a 1 basis point discount. console.log("About to set prices on the ARM contract"); - LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9994e36, 0.9998e36); + LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9996e36, 0.9999e36); // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig console.log("About to set ARM owner to", Mainnet.GOV_MULTISIG); diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 83b25e0..96f32b4 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -41,11 +41,11 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { function test_initialConfig() external view { assertEq(lidoARM.name(), "Lido ARM", "Name"); - assertEq(lidoARM.symbol(), "ARM-ST", "Symbol"); + assertEq(lidoARM.symbol(), "ARM-WETH-stETH", "Symbol"); assertEq(lidoARM.owner(), Mainnet.GOV_MULTISIG, "Owner"); assertEq(lidoARM.operator(), Mainnet.ARM_RELAYER, "Operator"); assertEq(lidoARM.feeCollector(), Mainnet.ARM_BUYBACK, "Fee collector"); - assertEq((100 * uint256(lidoARM.fee())) / lidoARM.FEE_SCALE(), 15, "Performance fee as a percentage"); + assertEq((100 * uint256(lidoARM.fee())) / lidoARM.FEE_SCALE(), 20, "Performance fee as a percentage"); // LidoLiquidityManager assertEq(address(lidoARM.lidoWithdrawalQueue()), Mainnet.LIDO_WITHDRAWAL, "Lido withdrawal queue"); assertEq(address(lidoARM.steth()), Mainnet.STETH, "stETH"); From 6427782beadb1bf9491459663bf01a43df2905f9 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 15 Oct 2024 23:42:05 +1100 Subject: [PATCH 188/196] Swap rounding (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Increased rounding in swapTokensForExactTokens to cover 1-2 wei lost with stETH transfers Added test_SwapTokensForExactTokens_Steth_Transfer_Truncated test Added test modifiers disableCaps, setPrices and setArmBalances * Updated fuzz tests for SwapTokensForExactTokens * Added to SwapExactTokensForTokens fuzz tests * Added workaround to swapExactTokensForTokens fuzz test * fix: adjust test structure. * fix: initialize `accountCapEnabled` to false. * fix: adjust lastTotalAsset check after swap. --------- Co-authored-by: Clément --- src/contracts/AbstractARM.sol | 5 +- src/contracts/CapManager.sol | 2 +- .../ClaimRedeem.t.sol | 13 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 118 ++++++---- .../RequestRedeem.t.sol | 16 +- .../LidoFixedPriceMultiLpARM/Setters.t.sol | 4 +- .../SwapExactTokensForTokens.t.sol | 184 +++++++++++----- .../SwapTokensForExactTokens.t.sol | 202 ++++++++++++------ test/fork/Zapper/Deposit.t.sol | 4 +- test/fork/utils/Modifiers.sol | 32 +++ test/smoke/LidoARMSmokeTest.t.sol | 4 +- 11 files changed, 406 insertions(+), 178 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index a64441c..1398410 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -366,7 +366,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } else { revert("ARM: Invalid in token"); } - amountIn = ((amountOut * PRICE_SCALE) / price) + 1; // +1 to always round in our favor + // always round in our favor + // +1 for truncation when dividing integers + // +2 to cover stETH transfers being up to 2 wei short of the requested transfer amount + amountIn = ((amountOut * PRICE_SCALE) / price) + 3; // Transfer the input tokens from the caller to this ARM contract _transferAssetFrom(address(inToken), msg.sender, address(this), amountIn); diff --git a/src/contracts/CapManager.sol b/src/contracts/CapManager.sol index 4e27895..756c74c 100644 --- a/src/contracts/CapManager.sol +++ b/src/contracts/CapManager.sol @@ -35,7 +35,7 @@ contract CapManager is Initializable, OwnableOperable { function initialize(address _operator) external initializer { _initOwnableOperable(_operator); - accountCapEnabled = true; + accountCapEnabled = false; } function postDepositHook(address liquidityProvider, uint256 assets) external { diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index 8e866e4..6b9c3d0 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -9,6 +9,7 @@ import {IERC20} from "contracts/Interfaces.sol"; import {AbstractARM} from "contracts/AbstractARM.sol"; contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { + bool private ac; uint256 private delay; ////////////////////////////////////////////////////// /// --- SETUP @@ -20,6 +21,8 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { delay = lidoARM.claimDelay(); deal(address(weth), address(this), 1_000 ether); + + ac = capManager.accountCapEnabled(); } ////////////////////////////////////////////////////// @@ -124,7 +127,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); @@ -146,7 +149,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); @@ -191,7 +194,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); @@ -215,7 +218,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, 2); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); @@ -239,7 +242,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); assertEqUserRequest(0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 7429549..320e71c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -11,8 +11,9 @@ import {IStETHWithdrawal} from "contracts/Interfaces.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { - uint256[] amounts1 = new uint256[](1); - IStETHWithdrawal public stETHWithdrawal = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL); + bool private ac; + uint256[] private amounts1 = new uint256[](1); + IStETHWithdrawal private stETHWithdrawal = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL); ////////////////////////////////////////////////////// /// --- SETUP @@ -30,6 +31,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Amounts arrays amounts1[0] = DEFAULT_AMOUNT; + + ac = capManager.accountCapEnabled(); } ////////////////////////////////////////////////////// @@ -37,6 +40,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapNull() public + enableCaps setLiquidityProviderCap(address(this), 0) { vm.expectRevert("LPC: LP cap exceeded"); @@ -45,6 +49,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapNotNull() public + enableCaps setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { vm.expectRevert("LPC: LP cap exceeded"); @@ -53,6 +58,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapReached() public + enableCaps setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // Initial deposit @@ -108,7 +114,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); + if (ac) assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); @@ -120,8 +126,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(address(this), 0); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); + } // Main call uint256 shares = lidoARM.deposit(amount); @@ -135,7 +143,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); - assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether } @@ -149,6 +157,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { depositInLidoARM(address(this), DEFAULT_AMOUNT) { uint256 amount = DEFAULT_AMOUNT; + // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); @@ -158,7 +167,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), amount); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(capManager.liquidityProviderCaps(address(this)), amount); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0); // Expected events @@ -166,8 +175,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(address(this), 0); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); + } // Main call uint256 shares = lidoARM.deposit(amount); @@ -181,7 +192,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), shares * 2); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -196,6 +207,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { depositInLidoARM(address(this), DEFAULT_AMOUNT) { uint256 amount = DEFAULT_AMOUNT; + // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); @@ -205,7 +217,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(alice), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(capManager.liquidityProviderCaps(alice), amount); + if (ac) assertEq(capManager.liquidityProviderCaps(alice), amount); assertEqQueueMetadata(0, 0, 0); // Expected events @@ -213,8 +225,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(alice, address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), alice, amount); // shares == amount here - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(alice, 0); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(alice, 0); + } vm.prank(alice); // Main call @@ -229,7 +243,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(alice), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(capManager.liquidityProviderCaps(alice), 0); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(alice), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -260,7 +274,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy // 80% of the asset gain goes to the total assets assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit, "Total assets before"); - assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); assertEqQueueMetadata(0, 0, 0); uint256 depositedAssets = DEFAULT_AMOUNT * 20; @@ -270,8 +284,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), depositedAssets); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), expectedShares); - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(address(this), 0); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); + } // deposit assets uint256 shares = lidoARM.deposit(depositedAssets); @@ -288,7 +304,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), expectedShares, "user shares after"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit + depositedAssets, "Total assets after"); - assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0); } @@ -305,9 +321,19 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { lidoARM.setCrossPrice(1e36); lidoARM.setPrices(1e36 - 1, 1e36); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT, "total assets before swap"); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), "last available before swap"); + // User Swap stETH for 3/4 of WETH in the ARM deal(address(steth), address(this), DEFAULT_AMOUNT); lidoARM.swapTokensForExactTokens(steth, weth, 3 * DEFAULT_AMOUNT / 4, DEFAULT_AMOUNT, address(this)); + assertApproxEqAbs( + lidoARM.totalAssets(), + MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + 2, + STETH_ERROR_ROUNDING, + "total assets after swap" + ); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), "last available after swap"); // First user requests a full withdrawal uint256 firstUserShares = lidoARM.balanceOf(address(this)); @@ -316,34 +342,42 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions Before uint256 stethBalanceBefore = 3 * DEFAULT_AMOUNT / 4; assertApproxEqAbs( - steth.balanceOf(address(lidoARM)), stethBalanceBefore, STETH_ERROR_ROUNDING, "stETH ARM balance before" + steth.balanceOf(address(lidoARM)), + stethBalanceBefore, + STETH_ERROR_ROUNDING, + "stETH ARM balance before deposit" ); uint256 wethBalanceBefore = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - 3 * DEFAULT_AMOUNT / 4; - assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); + assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before deposit"); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether before"); - assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before"); + assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before deposit"); 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"); - assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before"); + assertEq(lidoARM.balanceOf(alice), 0, "alice shares before deposit"); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before deposit"); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + 1, "total assets before deposit"); + if (ac) assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before deposit"); assertEqQueueMetadata(assetsRedeem, 0, 1); - assertApproxEqAbs(assetsRedeem, DEFAULT_AMOUNT, STETH_ERROR_ROUNDING, "assets redeem before"); + assertApproxEqAbs(assetsRedeem, DEFAULT_AMOUNT, STETH_ERROR_ROUNDING, "assets redeem before deposit"); uint256 amount = DEFAULT_AMOUNT * 2; + // Expected values + uint256 expectedShares = amount * MIN_TOTAL_SUPPLY / (MIN_TOTAL_SUPPLY + 1); + // Expected events vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(alice, address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); - emit IERC20.Transfer(address(0), alice, amount); // shares == amount here - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); + emit IERC20.Transfer(address(0), alice, expectedShares); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); + } vm.prank(alice); // Main call @@ -353,22 +387,22 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertApproxEqAbs( steth.balanceOf(address(lidoARM)), stethBalanceBefore, STETH_ERROR_ROUNDING, "stETH ARM balance after" ); - 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(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after deposit"); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether after deposit"); + assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after deposit"); // No perfs so no fees assertApproxEqAbs( lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount), STETH_ERROR_ROUNDING, - "last available assets after" + "last available assets after deposit" ); - 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"); - assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used + assertEq(lidoARM.balanceOf(alice), shares, "alice shares after deposit"); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after deposit"); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount + 1, "total assets after deposit"); + if (ac) assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after deposit"); // All the caps are used // withdrawal request is now claimable assertEqQueueMetadata(assetsRedeem, 0, 1); - assertApproxEqAbs(shares, amount, STETH_ERROR_ROUNDING, "shares after"); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether + assertApproxEqAbs(shares, expectedShares, STETH_ERROR_ROUNDING, "shares after deposit"); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } /// @notice Test the following scenario: @@ -396,7 +430,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued before deposit"); assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before deposit"); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); assertEqQueueMetadata(0, 0, 0); // Expected values = 1249998437501 @@ -452,7 +486,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), 4e6, "last available assets after redeem" ); assertEq(lidoARM.balanceOf(address(this)), 0, "User shares after redeem"); - assertEq(capManager.liquidityProviderCaps(address(this)), 0, "all user cap used"); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "all user cap used"); assertEqQueueMetadata(receivedAssets, 0, 1); // 6. collect fees @@ -510,7 +544,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 1); assertEq(receivedAssets, DEFAULT_AMOUNT, "received assets"); } @@ -579,7 +613,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ); assertEq(lidoARM.balanceOf(address(this)), 0, "user shares after"); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply after"); // Minted to dead on deploy - assertEq(capManager.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 1); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index bef9214..740b87f 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -9,6 +9,8 @@ import {IERC20} from "contracts/Interfaces.sol"; import {AbstractARM} from "contracts/AbstractARM.sol"; contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { + bool private ac; + ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -16,6 +18,8 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { super.setUp(); deal(address(weth), address(this), 1_000 ether); + + ac = capManager.accountCapEnabled(); } ////////////////////////////////////////////////////// @@ -36,7 +40,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(0, 0, 0); uint256 delay = lidoARM.claimDelay(); @@ -60,7 +64,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); } /// @notice Test the `requestRedeem` function when there are no profits and the first deposit is made. @@ -79,7 +83,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 1); uint256 delay = lidoARM.claimDelay(); @@ -107,7 +111,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only } /// @notice Test the `requestRedeem` function when there are profits and the first deposit is already made. @@ -152,7 +156,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { ); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( 0, @@ -204,7 +208,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0, "user LP balance"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply"); assertEq(lidoARM.totalAssets(), assetsAfterLoss - actualAssetsFromRedeem, "total assets"); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( 0, address(this), false, block.timestamp + delay, expectedAssetsFromRedeem, expectedAssetsFromRedeem diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 1a71cfc..9de35b6 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -212,7 +212,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { capManager.setAccountCapEnabled(false); } - function test_RevertWhen_CapManager_SetAccountCapEnabled_Because_AlreadySet() public asLidoARMOwner { + function test_RevertWhen_CapManager_SetAccountCapEnabled_Because_AlreadySet() public enableCaps asLidoARMOwner { vm.expectRevert("LPC: Account cap already set"); capManager.setAccountCapEnabled(true); } @@ -220,7 +220,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- AccountCapEnabled - PASSING TESTS ////////////////////////////////////////////////////// - function test_CapManager_SetAccountCapEnabled() public asLidoARMOwner { + function test_CapManager_SetAccountCapEnabled() public enableCaps asLidoARMOwner { vm.expectEmit({emitter: address(capManager)}); emit CapManager.AccountCapEnabled(false); capManager.setAccountCapEnabled(false); diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index 93c9798..d1db159 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -10,12 +10,11 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test ////////////////////////////////////////////////////// /// --- CONSTANTS ////////////////////////////////////////////////////// - uint256 private constant MIN_PRICE0 = 980e33; // 0.98 + uint256 private constant MIN_PRICE0 = 998e33; // 0.998 uint256 private constant MAX_PRICE0 = 1_000e33 - 1; // just under 1.00 uint256 private constant MIN_PRICE1 = 1_000e33; // 1.00 uint256 private constant MAX_PRICE1 = 1_020e33; // 1.02 - uint256 private constant MAX_WETH_RESERVE = 1_000_000 ether; // 1M WETH, no limit, but need to be consistent. - uint256 private constant MAX_STETH_RESERVE = 2_000_000 ether; // 2M stETH, limited by wsteth balance of steth. + uint256 private constant INITIAL_BALANCE = 1_000 ether; ////////////////////////////////////////////////////// /// --- SETUP @@ -23,11 +22,14 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test function setUp() public override { super.setUp(); - deal(address(weth), address(this), 1_000 ether); - deal(address(steth), address(this), 1_000 ether); + deal(address(weth), address(this), INITIAL_BALANCE); + deal(address(steth), address(this), INITIAL_BALANCE); - deal(address(weth), address(lidoARM), 1_000 ether); - deal(address(steth), address(lidoARM), 1_000 ether); + deal(address(weth), address(lidoARM), INITIAL_BALANCE); + deal(address(steth), address(lidoARM), INITIAL_BALANCE); + + // We are artificially adding assets so collect the performance fees to reset the fees collected + lidoARM.collectFees(); } ////////////////////////////////////////////////////// @@ -293,32 +295,78 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test assertEq(outputs[1], minAmount); } + /// @notice If the buy and sell prices are very close together and the stETH transferred into + /// the ARM is truncated, then there should be enough rounding protection against losing total assets. + function test_SwapExactTokensForTokens_Steth_Transfer_Truncated() + public + disableCaps + setArmBalances(MIN_TOTAL_SUPPLY, 0) + setPrices(1e36 - 1, 1e36, 1e36) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + { + // The exact amount of stETH to send to the ARM + uint256 amountIn = 3 * DEFAULT_AMOUNT / 4; + // Get minimum amount of WETH to receive + uint256 amountOutMin = amountIn * (1e36 - 1) / 1e36; + + deal(address(steth), address(this), amountIn); + + // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoARM), address(this), amountOutMin); + + // Main call + lidoARM.swapExactTokensForTokens( + steth, // inToken + weth, // outToken + amountIn, + amountOutMin, + address(this) // to + ); + + // Assertions + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets after"); + } + ////////////////////////////////////////////////////// /// --- FUZZING TESTS ////////////////////////////////////////////////////// /// @notice Fuzz test for swapExactTokensForTokens(IERC20,IERC20,uint256,uint256,address), with WETH to stETH. - /// @param amountIn Amount of WETH to swap. Fuzzed between 0 and steth in the ARM. - /// @param stethReserve Amount of stETH in the ARM. Fuzzed between 0 and MAX_STETH_RESERVE. + /// @param amountIn Amount of WETH to swap into the ARM. Fuzzed between 0 and steth in the ARM. + /// @param stethReserveGrowth Amount of stETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. /// @param price Price of the stETH in WETH. Fuzzed between 0.98 and 1. - function test_SwapExactTokensForTokens_Weth_To_Steth(uint256 amountIn, uint256 stethReserve, uint256 price) - public - { - // Use random stETH/WETH sell price between 0.98 and 1, + /// @param collectFees Whether to collect the accrued performance fees before the swap. + function test_SwapExactTokensForTokens_Weth_To_Steth( + uint256 amountIn, + uint256 stethReserveGrowth, + uint256 price, + bool collectFees + ) public { + // Use random stETH/WETH sell price between 1 and 1.02, // the buy price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); lidoARM.setCrossPrice(1e36); lidoARM.setPrices(MIN_PRICE0, price); // Set random amount of stETH in the ARM - stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoARM), stethReserve + (2 * STETH_ERROR_ROUNDING)); + stethReserveGrowth = _bound(stethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(steth), address(lidoARM), INITIAL_BALANCE + stethReserveGrowth); + + if (collectFees) { + // Collect and accrued performance fees before the swap + lidoARM.collectFees(); + } - // Calculate maximum amount of WETH to swap + // Random amount of WETH to swap into the ARM // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. - amountIn = _bound(amountIn, 0, stethReserve); + amountIn = _bound(amountIn, 0, steth.balanceOf(address(lidoARM))); deal(address(weth), address(this), amountIn); // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); @@ -345,81 +393,107 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test address(this) // to ); - // State after - uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); - uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); - // Assertions - assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn, "user WETH balance"); + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets"); + assertEq(weth.balanceOf(address(this)), balanceWETHBeforeThis - amountIn, "user WETH balance"); assertApproxEqAbs( - balanceSTETHBeforeThis + amountOutMin, balanceSTETHAfterThis, STETH_ERROR_ROUNDING * 2, "user stETH balance" + steth.balanceOf(address(this)), + balanceSTETHBeforeThis + amountOutMin, + STETH_ERROR_ROUNDING * 2, + "user stETH balance" ); - assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM, "ARM WETH balance"); + assertEq(weth.balanceOf(address(lidoARM)), balanceWETHBeforeARM + amountIn, "ARM WETH balance"); assertApproxEqAbs( - balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOutMin, STETH_ERROR_ROUNDING * 2, "ARM stETH balance" + steth.balanceOf(address(lidoARM)), + balanceSTETHBeforeARM - amountOutMin, + STETH_ERROR_ROUNDING * 2, + "ARM stETH balance" ); } /// @notice Fuzz test for swapExactTokensForTokens(IERC20,IERC20,uint256,uint256,address), with stETH to WETH. - /// @param amountIn Amount of stETH to swap. Fuzzed between 0 and weth in the ARM. - /// @param wethReserve Amount of WETH in the ARM. Fuzzed between 0 and MAX_WETH_RESERVE. + /// @param amountIn Amount of stETH to swap into the ARM. Fuzzed between 0 and WETH in the ARM. + /// @param wethReserveGrowth The amount WETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param stethReserveGrowth Amount of stETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. /// @param price Price of the stETH in WETH. Fuzzed between 1 and 1.02. - function test_SwapExactTokensForTokens_Steth_To_Weth(uint256 amountIn, uint256 wethReserve, uint256 price) public { + /// @param userStethBalance The amount of stETH the user has before the swap. + /// @param collectFees Whether to collect the accrued performance fees before the swap. + function test_SwapExactTokensForTokens_Steth_To_Weth( + uint256 amountIn, + uint256 wethReserveGrowth, + uint256 stethReserveGrowth, + uint256 price, + uint256 userStethBalance, + bool collectFees + ) public { // Use random stETH/WETH buy price between MIN_PRICE0 and MAX_PRICE0, // the sell price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); - lidoARM.setCrossPrice(1e36); lidoARM.setPrices(price, MAX_PRICE1); - // Set random amount of WETH in the ARM - wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); - deal(address(weth), address(lidoARM), wethReserve); + // Set random amount of WETH growth in the ARM + wethReserveGrowth = _bound(wethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(weth), address(lidoARM), INITIAL_BALANCE + wethReserveGrowth); + + // Set random amount of stETH growth in the ARM + stethReserveGrowth = _bound(stethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(steth), address(lidoARM), INITIAL_BALANCE + stethReserveGrowth); - // Calculate maximum amount of stETH to swap + if (collectFees) { + // Collect and accrued performance fees before the swap + lidoARM.collectFees(); + } + + // Random amount of stETH to swap into the ARM // As the price is below 1, we can take 100% of the balance of WETH of the ARM. - amountIn = _bound(amountIn, 0, wethReserve); + amountIn = _bound(amountIn, 0, weth.balanceOf(address(lidoARM)) * 1e36 / price); deal(address(steth), address(this), amountIn); + // Fuzz the user's stETH balance + userStethBalance = _bound(userStethBalance, amountIn, amountIn + 1 ether); + deal(address(steth), address(this), userStethBalance); + // State before - uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); - uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); + uint256 totalAssetsBefore = lidoARM.totalAssets(); + uint256 userBalanceWETHBefore = weth.balanceOf(address(this)); + uint256 userBalanceSTETHBefore = steth.balanceOf(address(this)); + uint256 armBalanceWETHBefore = weth.balanceOf(address(lidoARM)); + uint256 armBalanceSTETHBefore = steth.balanceOf(address(lidoARM)); - // Get minimum amount of WETH to receive - uint256 minAmount = amountIn * price / 1e36; + // Get minimum amount of WETH swapped out of the ARM + uint256 amountOutMin = amountIn * price / 1e36; // Expected events vm.expectEmit({emitter: address(steth)}); emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoARM), address(this), minAmount); + emit IERC20.Transfer(address(lidoARM), address(this), amountOutMin); // Main call lidoARM.swapExactTokensForTokens( steth, // inToken weth, // outToken - amountIn, // amountIn - minAmount, // amountOutMin + amountIn, + amountOutMin, address(this) // to ); - // State after - uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); - uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); - // Assertions - assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis, "user WETH balance"); + // TODO change the ARM so it doesn't lose 1 wei of assets on any swaps + assertGe(lidoARM.totalAssets() + 1, totalAssetsBefore, "total assets"); + assertEq(weth.balanceOf(address(this)), userBalanceWETHBefore + amountOutMin, "user WETH balance"); assertApproxEqAbs( - balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING, "user stETH balance" + steth.balanceOf(address(this)), + userBalanceSTETHBefore - amountIn, + STETH_ERROR_ROUNDING * 2, + "user stETH balance" ); - assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + minAmount, "ARM WETH balance"); + assertEq(weth.balanceOf(address(lidoARM)), armBalanceWETHBefore - amountOutMin, "ARM WETH balance"); assertApproxEqAbs( - balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING, "ARM stETH balance" + steth.balanceOf(address(lidoARM)), + armBalanceSTETHBefore + amountIn, + STETH_ERROR_ROUNDING * 2, + "ARM stETH balance" ); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol index 230b676..8a86f17 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -14,8 +14,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 private constant MAX_PRICE0 = 1_000e33 - 1; // just under 1.00 uint256 private constant MIN_PRICE1 = 1_000e33; // 1.00 uint256 private constant MAX_PRICE1 = 1_020e33; // 1.02 - uint256 private constant MAX_WETH_RESERVE = 1_000_000 ether; // 1M WETH, no limit, but need to be consistent. - uint256 private constant MAX_STETH_RESERVE = 2_000_000 ether; // 2M stETH, limited by wsteth balance of steth. + uint256 private constant INITIAL_BALANCE = 1_000 ether; ////////////////////////////////////////////////////// /// --- SETUP @@ -23,11 +22,14 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test function setUp() public override { super.setUp(); - deal(address(weth), address(this), 1_000 ether); - deal(address(steth), address(this), 1_000 ether); + deal(address(weth), address(this), INITIAL_BALANCE); + deal(address(steth), address(this), INITIAL_BALANCE); - deal(address(weth), address(lidoARM), 1_000 ether); - deal(address(steth), address(lidoARM), 1_000 ether); + deal(address(weth), address(lidoARM), INITIAL_BALANCE); + deal(address(steth), address(lidoARM), INITIAL_BALANCE); + + // We are artificially adding assets so collect the performance fees to reset the fees collected + lidoARM.collectFees(); } ////////////////////////////////////////////////////// @@ -191,7 +193,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test // Get maximum amount of WETH to send to the ARM uint256 traderates0 = lidoARM.traderate0(); - uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; + uint256 amountIn = (amountOut * 1e36 / traderates0) + 3; // Expected events: Already checked in fuzz tests @@ -238,7 +240,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test // Get maximum amount of stETH to send to the ARM uint256 traderates1 = lidoARM.traderate1(); - uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; + uint256 amountIn = (amountOut * 1e36 / traderates1) + 3; // Expected events: Already checked in fuzz tests @@ -271,45 +273,97 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test assertEq(outputs[1], amountOut, "Amount out"); } + /// @notice If the buy and sell prices are very close together and the stETH transferred into + /// the ARM is truncated, then there should be enough rounding protection against losing total assets. + function test_SwapTokensForExactTokens_Steth_Transfer_Truncated() + public + disableCaps + setArmBalances(MIN_TOTAL_SUPPLY, 0) + setPrices(1e36 - 1, 1e36, 1e36) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + { + // The exact amount of WETH to receive + uint256 amountOut = DEFAULT_AMOUNT; + // The max amount of stETH to send + uint256 amountInMax = amountOut + 3; + deal(address(steth), address(this), amountInMax); // Deal more as AmountIn is greater than AmountOut + + // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoARM), address(this), amountOut); + + // Main call + lidoARM.swapTokensForExactTokens( + steth, // inToken + weth, // outToken + amountOut, // amountOut + amountInMax, // amountInMax + address(this) // to + ); + + // Assertions + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets after"); + } + ////////////////////////////////////////////////////// /// --- FUZZING TESTS ////////////////////////////////////////////////////// /// @notice Fuzz test for swapTokensForExactTokens(IERC20,IERC20,uint256,uint256,address), with WETH to stETH. - /// @param amountOut Amount of WETH to swap. Fuzzed between 0 and steth in the ARM. - /// @param stethReserve Amount of stETH in the ARM. Fuzzed between 0 and MAX_STETH_RESERVE. - /// @param price Price of the stETH in WETH. Fuzzed between 0.98 and 1. - function test_SwapTokensForExactTokens_Weth_To_Steth(uint256 amountOut, uint256 stethReserve, uint256 price) - public - { - // Use random sell price between 0.98 and 1 for the stETH/WETH price, + /// @param amountOut Exact amount of stETH to swap out of the ARM. Fuzzed between 0 and stETH in the ARM. + /// @param wethReserveGrowth The amount WETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param stethReserveGrowth Amount of stETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param price Sell price of the stETH in WETH (stETH/WETH). Fuzzed between 1 and 1.02. + /// @param collectFees Whether to collect the accrued performance fees before the swap. + function test_SwapTokensForExactTokens_Weth_To_Steth( + uint256 amountOut, + uint256 wethReserveGrowth, + uint256 stethReserveGrowth, + uint256 price, + bool collectFees + ) public { + // Use random sell price between 1 and 1.02 for the stETH/WETH price, // The buy price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); lidoARM.setPrices(MIN_PRICE0, price); + // Set random amount of WETH in the ARM + wethReserveGrowth = _bound(wethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(weth), address(lidoARM), INITIAL_BALANCE + wethReserveGrowth); + // Set random amount of stETH in the ARM - stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoARM), stethReserve); + stethReserveGrowth = _bound(stethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(steth), address(lidoARM), INITIAL_BALANCE + stethReserveGrowth); + + if (collectFees) { + // Collect and accrued performance fees before the swap + lidoARM.collectFees(); + } - // Calculate maximum amount of WETH to swap - // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. - amountOut = _bound(amountOut, 0, stethReserve); - deal(address(weth), address(this), amountOut * 2 + 1); // // Deal more as AmountIn is greater than AmountOut + // Calculate the amount of stETH to swap out of the ARM + amountOut = _bound(amountOut, 0, steth.balanceOf(address(lidoARM))); + + // Get the maximum amount of WETH to swap into the ARM + // weth = steth * stETH/WETH price + uint256 amountIn = (amountOut * price / 1e36) + 3; + + deal(address(weth), address(this), amountIn); // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); - // Get minimum amount of STETH to receive - // weth = steth * stETH/WETH price - uint256 amountIn = (amountOut * price / 1e36) + 1; - // Expected events vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(steth)}); emit IERC20.Transfer(address(lidoARM), address(this), amountOut); + // Main call lidoARM.swapTokensForExactTokens( weth, // inToken @@ -319,54 +373,77 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test address(this) // to ); - // State after - uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); - uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); - // Assertions - assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn, "WETH user balance"); + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets after"); + assertEq(weth.balanceOf(address(this)), balanceWETHBeforeThis - amountIn, "WETH user balance"); assertApproxEqAbs( - balanceSTETHBeforeThis + amountOut, balanceSTETHAfterThis, STETH_ERROR_ROUNDING, "STETH user balance" + steth.balanceOf(address(this)), + balanceSTETHBeforeThis + amountOut, + STETH_ERROR_ROUNDING, + "STETH user balance" ); - assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM, "WETH ARM balance"); + assertEq(weth.balanceOf(address(lidoARM)), balanceWETHBeforeARM + amountIn, "WETH ARM balance"); assertApproxEqAbs( - balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOut, STETH_ERROR_ROUNDING, "STETH ARM balance" + steth.balanceOf(address(lidoARM)), + balanceSTETHBeforeARM - amountOut, + STETH_ERROR_ROUNDING, + "STETH ARM balance" ); } /// @notice Fuzz test for swapTokensForExactTokens(IERC20,IERC20,uint256,uint256,address), with stETH to WETH. - /// @param amountOut Amount of stETH to swap. Fuzzed between 0 and weth in the ARM. - /// @param wethReserve Amount of WETH in the ARM. Fuzzed between 0 and MAX_WETH_RESERVE. - /// @param price Price of the stETH in WETH. Fuzzed between 1 and 1.02. - function test_SwapTokensForExactTokens_Steth_To_Weth(uint256 amountOut, uint256 wethReserve, uint256 price) - public - { + /// @param amountOut Exact amount of WETH to swap out of the ARM. Fuzzed between 0 and WETH in the ARM. + /// @param wethReserveGrowth The amount WETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param stethReserveGrowth Amount of stETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param price Buy price of the stETH in WETH (stETH/WETH). Fuzzed between 0.998 and 1.02. + /// @param userStethBalance The amount of stETH the user has before the swap. + /// @param collectFees Whether to collect the accrued performance fees before the swap. + function test_SwapTokensForExactTokens_Steth_To_Weth( + uint256 amountOut, + uint256 wethReserveGrowth, + uint256 stethReserveGrowth, + uint256 price, + uint256 userStethBalance, + bool collectFees + ) public { + lidoARM.collectFees(); + // Use random stETH/WETH buy price between 0.98 and 1, // sell price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); lidoARM.setPrices(price, MAX_PRICE1); - // Set random amount of WETH in the ARM - wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); - deal(address(weth), address(lidoARM), wethReserve); + // Set random amount of WETH growth in the ARM + wethReserveGrowth = _bound(wethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(weth), address(lidoARM), INITIAL_BALANCE + wethReserveGrowth); + + // Set random amount of stETH growth in the ARM + stethReserveGrowth = _bound(stethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(steth), address(lidoARM), INITIAL_BALANCE + stethReserveGrowth); - // Calculate maximum amount of stETH to swap - // As the price is below 1, we can take 100% of the balance of WETH of the ARM. - amountOut = _bound(amountOut, 0, wethReserve); - deal(address(steth), address(this), amountOut * 2 + 1); // Deal more as AmountIn is greater than AmountOut + if (collectFees) { + // Collect and accrued performance fees before the swap + lidoARM.collectFees(); + } + + // Calculate the amount of WETH to swap out of the ARM + // Can take up to 100% of the WETH in the ARM even if there is some for the performance fee. + amountOut = _bound(amountOut, 0, weth.balanceOf(address(lidoARM))); + // Get the maximum amount of stETH to swap into of the ARM + // stETH = WETH / stETH/WETH price + uint256 amountIn = (amountOut * 1e36 / price) + 3; + + // Fuzz the user's stETH balance + userStethBalance = _bound(userStethBalance, amountIn, amountIn + 1 ether); + deal(address(steth), address(this), userStethBalance); // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); - // Get minimum amount of WETH to receive - // stETH = WETH / stETH/WETH price - uint256 amountIn = (amountOut * 1e36 / price); - // Expected events // TODO hard to check the exact amount of stETH due to rounding // vm.expectEmit({emitter: address(steth)}); @@ -383,20 +460,21 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test address(this) // to ); - // State after - uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); - uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); - // Assertions - assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis, "WETH user balance"); + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets after"); + assertEq(weth.balanceOf(address(this)), balanceWETHBeforeThis + amountOut, "WETH user balance"); assertApproxEqAbs( - balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING, "STETH user balance" + steth.balanceOf(address(this)), + balanceSTETHBeforeThis - amountIn, + STETH_ERROR_ROUNDING, + "STETH user balance" ); - assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + amountOut, "WETH ARM balance"); + assertEq(weth.balanceOf(address(lidoARM)), balanceWETHBeforeARM - amountOut, "WETH ARM balance"); assertApproxEqAbs( - balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING, "STETH ARM balance" + steth.balanceOf(address(lidoARM)), + balanceSTETHBeforeARM + amountIn, + STETH_ERROR_ROUNDING, + "STETH ARM balance" ); } } diff --git a/test/fork/Zapper/Deposit.t.sol b/test/fork/Zapper/Deposit.t.sol index eb54f1c..f2f1522 100644 --- a/test/fork/Zapper/Deposit.t.sol +++ b/test/fork/Zapper/Deposit.t.sol @@ -15,7 +15,7 @@ contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { vm.deal(address(this), DEFAULT_AMOUNT); } - function test_Deposit_ViaFunction() public { + function test_Deposit_ViaFunction() public enableCaps { assertEq(lidoARM.balanceOf(address(this)), 0); uint256 expectedShares = lidoARM.previewDeposit(DEFAULT_AMOUNT); uint256 capBefore = capManager.liquidityProviderCaps(address(this)); @@ -32,7 +32,7 @@ contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(capManager.liquidityProviderCaps(address(this)), capBefore - DEFAULT_AMOUNT); } - function test_Deposit_ViaCall() public { + function test_Deposit_ViaCall() public enableCaps { assertEq(lidoARM.balanceOf(address(this)), 0); uint256 expectedShares = lidoARM.previewDeposit(DEFAULT_AMOUNT); uint256 capBefore = capManager.liquidityProviderCaps(address(this)); diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index 5081900..f55933d 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -71,6 +71,38 @@ abstract contract Modifiers is Helpers { _; } + /// @notice disable both the total assets and liquidity provider caps + modifier disableCaps() { + lidoARM.setCapManager(address(0)); + _; + } + + /// @notice Enable the total assets cap on the CapManager contract. + modifier enableCaps() { + require(address(capManager) != address(0), "CapManager not set"); + vm.prank(lidoARM.owner()); + lidoARM.setCapManager(address(capManager)); + + if (!capManager.accountCapEnabled()) { + vm.prank(capManager.owner()); + capManager.setAccountCapEnabled(true); + } + _; + } + + /// @notice Set the stETH/WETH swap prices on the LidoARM contract. + modifier setPrices(uint256 buyPrice, uint256 crossPrice, uint256 sellPrice) { + lidoARM.setCrossPrice(crossPrice); + lidoARM.setPrices(buyPrice, sellPrice); + _; + } + + modifier setArmBalances(uint256 wethBalance, uint256 stethBalance) { + deal(address(weth), address(lidoARM), wethBalance); + deal(address(steth), address(lidoARM), stethBalance); + _; + } + /// @notice Set the total assets cap on the CapManager contract. modifier setTotalAssetsCap(uint256 cap) { capManager.setTotalAssetsCap(uint248(cap)); diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 83b25e0..97f8841 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -54,7 +54,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(lidoARM.claimDelay(), 10 minutes, "claim delay"); assertEq(lidoARM.crossPrice(), 0.9998e36, "cross price"); - assertEq(capManager.accountCapEnabled(), true, "account cap enabled"); + assertEq(capManager.accountCapEnabled(), false, "account cap enabled"); assertEq(capManager.operator(), Mainnet.ARM_RELAYER, "Operator"); assertEq(capManager.arm(), address(lidoARM), "arm"); } @@ -142,7 +142,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { deal(address(weth), address(lidoARM), 1000 ether); // _dealWETH(address(lidoARM), 1000 ether); - expectedIn = amountOut * 1e36 / price; + expectedIn = amountOut * 1e36 / price + 3; vm.prank(Mainnet.ARM_RELAYER); uint256 sellPrice = price < 9996e32 ? 9998e32 : price + 2e32; From 6b923cb3c49dc31ce062d672ff1cb8bbe3584ab1 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 16 Oct 2024 17:20:38 +1100 Subject: [PATCH 189/196] Added new Treasury address --- src/contracts/utils/Addresses.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/utils/Addresses.sol b/src/contracts/utils/Addresses.sol index c610ab0..2ab88ed 100644 --- a/src/contracts/utils/Addresses.sol +++ b/src/contracts/utils/Addresses.sol @@ -13,7 +13,7 @@ library Mainnet { address public constant GOVERNOR_FIVE = 0x3cdD07c16614059e66344a7b579DAB4f9516C0b6; address public constant GOVERNOR_SIX = 0x1D3Fbd4d129Ddd2372EA85c5Fa00b2682081c9EC; address public constant STRATEGIST = 0xF14BBdf064E3F67f51cd9BD646aE3716aD938FDC; - address public constant TREASURY = 0x6E3fddab68Bf1EBaf9daCF9F7907c7Bc0951D1dc; + address public constant TREASURY = 0x70fCE97d671E81080CA3ab4cc7A59aAc2E117137; // Multisig and EOAs address public constant INITIAL_DEPLOYER = address(0x1001); From 77321c0b3693a82740198a8e29367c86140a8ba9 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 16 Oct 2024 17:26:49 +1100 Subject: [PATCH 190/196] Removed individual caps from deploy script --- .../mainnet/003_UpgradeLidoARMScript.sol | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index 63fda7a..aaaa493 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -25,6 +25,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { Proxy lidoARMProxy; Proxy capManProxy; LidoARM lidoARMImpl; + LidoARM lidoARM; CapManager capManager; ZapperLidoARM zapper; @@ -49,11 +50,8 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { capManProxy.initialize(address(capManagerImpl), deployer, data); capManager = CapManager(address(capManProxy)); - // 5. Set the liquidity Provider caps - capManager.setTotalAssetsCap(400 ether); - address[] memory liquidityProviders = new address[](1); - liquidityProviders[0] = Mainnet.TREASURY; - capManager.setLiquidityProviderCaps(liquidityProviders, 100 ether); + // 5. Set total assets cap + capManager.setTotalAssetsCap(740 ether); // 6. Deploy Lido implementation uint256 claimDelay = tenderlyTestnet ? 1 minutes : 10 minutes; @@ -101,7 +99,7 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("About to withdraw stETH from legacy Lido ARM"); LegacyAMM(Mainnet.LIDO_ARM).transferToken(Mainnet.STETH, Mainnet.ARM_MULTISIG, stethLegacyBalance); } - // TODO need to also remove anything in the Lido withdrawal queue + // need to also remove anything in the Lido withdrawal queue // Initialize Lido ARM proxy and implementation contract bytes memory data = abi.encodeWithSignature( @@ -123,11 +121,12 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // IWETH(Mainnet.WETH).deposit{value: tinyMintAmount}(); // Approve the Lido ARM proxy to spend WETH - IERC20(Mainnet.WETH).approve(address(lidoARMProxy), tinyMintAmount); + IERC20(Mainnet.WETH).approve(address(lidoARMProxy), type(uint256).max); // upgrade and initialize the Lido ARM console.log("About to upgrade the ARM contract"); lidoARMProxy.upgradeToAndCall(address(lidoARMImpl), data); + lidoARM = LidoARM(payable(Mainnet.LIDO_ARM)); // Set the price that buy and sell prices can not cross console.log("About to set the cross price on the ARM contract"); @@ -142,25 +141,11 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("About to set ARM owner to", Mainnet.GOV_MULTISIG); lidoARMProxy.setOwner(Mainnet.GOV_MULTISIG); - console.log("Finished running initializing Lido ARM as ARM_MULTISIG"); - - if (tenderlyTestnet) { - vm.stopBroadcast(); - } else { - vm.stopPrank(); - } - - if (tenderlyTestnet) { - console.log("Broadcasting fork script to Tenderly as: %s", Mainnet.ARM_RELAYER); - vm.startBroadcast(Mainnet.ARM_RELAYER); - } else { - vm.startPrank(Mainnet.ARM_RELAYER); - } + // Deposit 10 WETH to the Lido ARM + console.log("About to deposit 10 WETH into the ARM contract", Mainnet.GOV_MULTISIG); + lidoARM.deposit(10 ether); - // Add some test liquidity providers - address[] memory testProviders = new address[](1); - testProviders[0] = 0x3bB354a1E0621F454c5D5CE98f6ea21a53bf2d7d; - capManager.setLiquidityProviderCaps(testProviders, 100 ether); + console.log("Finished running initializing Lido ARM as ARM_MULTISIG"); if (tenderlyTestnet) { vm.stopBroadcast(); From 5883df2e63f41b4eceb40bbf0285a97984565493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:07:54 +0200 Subject: [PATCH 191/196] Invariant testing campaign (#29) * chore: add solmate. * feat: add mock for stETH. * feat: add mock for Lido Withdraw. * fix: cleanup variables * feat: create shared contract for invariant. * [WIP] feat: add handler logic. * feat: add more users. * [WIP] feat: add distribution handler. * chore: add invariant config * fix: address null user bug. * feat: add LP Handler. * [WIP]: feat: swapHandler. * feat: add swapTokensForExactTokens to handler. * fix: adjust handler weight. * fix: set default verbosity to 3. * fix: adjust console log * feat: add Owner handler. * fix: account withdraw queue in available liquidity. * fix: adjust failing tx. * fix: use vm to send ETH instead of .call. * feat: add Lido Liquidity Manager Handler. * feat: check preview return corerct amount. * fix: use correct price * fix: adjust new availableTotalAssets * chore: use --fail-fast for test * [WIP] feat: add first invariants. * [WIP] feat: add more invariants. * [WIP] feat: adjust handler and add more invariants. * [WIP] feat: add invariant for lido liquidity manager. * feat: add donation handler. * test: add new concrete scenario test. * test: fix CI. * try to fix CI. * test[invariant]: ensure enough liquiidty before claiming fees. * chore: add --show-progress for `make test` * test[invariant]: adjust `withdrawsClaimable` removal. * fix: use aproxEq instead of Eq due to rounding error * chore: remove --show-progress * test[invariant]: only request when there is enough liquidity * test[invariant]: add more invariant. * fix: remove wrong invariant. * fix: adjust with latest update. * test: adjust with lastest update. * test[invariant]: use owner to set price. * chore: add --show-progress for tests. * fix: use new claimDelay. * docs: adjust comments. * test[invariant]: add setCrossPrice to handlers. * test[invariant]: adjust test. * forge fmt * feat: add stats for invariants. * chore: ignore warning in Proxy.sol. * fix: adjust with new variable name. * test[invariant]: adjust swap handler with latest update. * fix: import console. * fix: adjust claimResquest for invariant. * test[invariant]: WIP. * test[invariant]: add LLM to invariant. * test[invariant]: adjust owner handler. * perf: optimize calls for logs. * perf: adjust % for call distribution. * feat: add skip %. * test[invariant]: adjust last invariant. * fix: log stat bool. * test[invariant]: up only shares values invariant. * fix: adjust type uint120 to uint128 * fix: prevetn setCrossPrice to revert. * feat: remove all user funds after invariants. * test[invariant]: add approx up-only invariant [WIP] * test[invariant]: add invariant for approx up-only. * docs: adjust natspec and description. * forge fmt * fix: increase error tolerance. --- Makefile | 2 +- foundry.toml | 9 + test/Base.sol | 49 +- test/fork/shared/Shared.sol | 1 - test/invariants/BaseInvariants.sol | 454 ++++++++++++++++++ test/invariants/BasicInvariants.sol | 144 ++++++ test/invariants/handlers/BaseHandler.sol | 98 ++++ .../handlers/DistributionHandler.sol | 67 +++ test/invariants/handlers/DonationHandler.sol | 74 +++ test/invariants/handlers/LLMHandler.sol | 151 ++++++ test/invariants/handlers/LpHandler.sol | 262 ++++++++++ test/invariants/handlers/OwnerHandler.sol | 169 +++++++ test/invariants/handlers/SwapHandler.sol | 273 +++++++++++ test/invariants/mocks/MockLidoWithdraw.sol | 87 ++++ test/invariants/mocks/MockSTETH.sol | 48 ++ test/invariants/shared/Shared.sol | 153 ++++++ 16 files changed, 2038 insertions(+), 3 deletions(-) create mode 100644 test/invariants/BaseInvariants.sol create mode 100644 test/invariants/BasicInvariants.sol create mode 100644 test/invariants/handlers/BaseHandler.sol create mode 100644 test/invariants/handlers/DistributionHandler.sol create mode 100644 test/invariants/handlers/DonationHandler.sol create mode 100644 test/invariants/handlers/LLMHandler.sol create mode 100644 test/invariants/handlers/LpHandler.sol create mode 100644 test/invariants/handlers/OwnerHandler.sol create mode 100644 test/invariants/handlers/SwapHandler.sol create mode 100644 test/invariants/mocks/MockLidoWithdraw.sol create mode 100644 test/invariants/mocks/MockSTETH.sol create mode 100644 test/invariants/shared/Shared.sol diff --git a/Makefile b/Makefile index cdcaa82..d125703 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ snapshot: # Tests test: - @forge test --summary + @forge test --summary --fail-fast --show-progress test-f-%: @FOUNDRY_MATCH_TEST=$* make test diff --git a/foundry.toml b/foundry.toml index e755671..04bad5f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,6 +9,7 @@ auto_detect_remappings = false gas_reports = ["OEthARM", "Proxy"] fs_permissions = [{ access = "read-write", path = "./build" }] extra_output_files = ["metadata"] +ignored_warnings_from = ["src/contracts/Proxy.sol"] remappings = [ "contracts/=./src/contracts", "script/=./script", @@ -17,15 +18,23 @@ remappings = [ "forge-std/=dependencies/forge-std-1.9.2/src/", "@openzeppelin/contracts/=dependencies/@openzeppelin-contracts-5.0.2/", "@openzeppelin/contracts-upgradeable/=dependencies/@openzeppelin-contracts-upgradeable-5.0.2/", + "@solmate/=dependencies/solmate-6.7.0/src/", ] [fuzz] runs = 1_000 +[invariant] +runs = 256 +depth = 500 +fail_on_revert = true +shrink_run_limit = 5_000 + [dependencies] "@openzeppelin-contracts" = "5.0.2" "@openzeppelin-contracts-upgradeable" = "5.0.2" forge-std = { version = "1.9.2", git = "https://github.com/foundry-rs/forge-std.git", rev = "5a802d7c10abb4bbfb3e7214c75052ef9e6a06f8" } +solmate = "6.7.0" [soldeer] recursive_deps = false diff --git a/test/Base.sol b/test/Base.sol index 32ac957..e120471 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -34,7 +34,6 @@ abstract contract Base_Test_ is Test { Proxy public proxy; Proxy public lpcProxy; Proxy public lidoProxy; - Proxy public lidoOwnerProxy; OethARM public oethARM; LidoARM public lidoARM; CapManager public capManager; @@ -51,11 +50,20 @@ abstract contract Base_Test_ is Test { /// --- Governance, multisigs and EOAs ////////////////////////////////////////////////////// address public alice; + address public bob; + address public charlie; + address public dave; + address public eve; + address public frank; + address public george; + address public harry; + address public deployer; address public governor; address public operator; address public oethWhale; address public feeCollector; + address public lidoWithdraw; ////////////////////////////////////////////////////// /// --- DEFAULT VALUES @@ -70,4 +78,43 @@ abstract contract Base_Test_ is Test { function setUp() public virtual { resolver = new AddressResolver(); } + + /// @notice Better if called once all contract have been depoyed. + function labelAll() public virtual { + // Contracts + _labelNotNull(address(proxy), "DEFAULT PROXY"); + _labelNotNull(address(lpcProxy), "LPC PROXY"); + _labelNotNull(address(lidoProxy), "LIDO ARM PROXY"); + _labelNotNull(address(oethARM), "OETH ARM"); + _labelNotNull(address(lidoARM), "LIDO ARM"); + _labelNotNull(address(capManager), "CAP MANAGER"); + + _labelNotNull(address(oeth), "OETH"); + _labelNotNull(address(weth), "WETH"); + _labelNotNull(address(steth), "STETH"); + _labelNotNull(address(wsteth), " WRAPPED STETH"); + _labelNotNull(address(badToken), "BAD TOKEN"); + _labelNotNull(address(vault), "OETH VAULT"); + + // Governance, multisig and EOAs + _labelNotNull(alice, "Alice"); + _labelNotNull(bob, "Bob"); + _labelNotNull(charlie, "Charlie"); + _labelNotNull(dave, "Dave"); + _labelNotNull(eve, "Eve"); + _labelNotNull(frank, "Frank"); + _labelNotNull(george, "George"); + _labelNotNull(harry, "Harry"); + + _labelNotNull(deployer, "Deployer"); + _labelNotNull(governor, "Governor"); + _labelNotNull(operator, "Operator"); + _labelNotNull(oethWhale, "OETH Whale"); + _labelNotNull(feeCollector, "Fee Collector"); + _labelNotNull(lidoWithdraw, "Lido Withdraw"); + } + + function _labelNotNull(address _address, string memory _name) internal { + if (_address != address(0)) vm.label(_address, _name); + } } diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index bb6073d..93a259d 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -109,7 +109,6 @@ abstract contract Fork_Shared_Test_ is Modifiers { proxy = new Proxy(); lpcProxy = new Proxy(); lidoProxy = new Proxy(); - lidoOwnerProxy = new Proxy(); // --- Deploy OethARM implementation --- // Deploy OethARM implementation. diff --git a/test/invariants/BaseInvariants.sol b/test/invariants/BaseInvariants.sol new file mode 100644 index 0000000..ddc377c --- /dev/null +++ b/test/invariants/BaseInvariants.sol @@ -0,0 +1,454 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {console} from "forge-std/console.sol"; + +// Test imports +import {Invariant_Shared_Test_} from "./shared/Shared.sol"; + +// Handlers +import {LpHandler} from "./handlers/LpHandler.sol"; +import {LLMHandler} from "./handlers/LLMHandler.sol"; +import {SwapHandler} from "./handlers/SwapHandler.sol"; +import {OwnerHandler} from "./handlers/OwnerHandler.sol"; +import {DonationHandler} from "./handlers/DonationHandler.sol"; + +// Mocks +import {MockSTETH} from "./mocks/MockSTETH.sol"; + +/// @notice Base invariant test contract +/// @dev This contract should be used as a base contract that hold all +/// invariants properties independently from deployment context. +abstract contract Invariant_Base_Test_ is Invariant_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- VARIABLES + ////////////////////////////////////////////////////// + address[] public lps; // Users that provide liquidity + address[] public swaps; // Users that perform swap + + LpHandler public lpHandler; + LLMHandler public llmHandler; + SwapHandler public swapHandler; + OwnerHandler public ownerHandler; + DonationHandler public donationHandler; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public virtual override { + super.setUp(); + } + + ////////////////////////////////////////////////////// + /// --- INVARIANTS + ////////////////////////////////////////////////////// + /* + * Swap functionnalities (swap) + * Invariant A: weth balance == ∑deposit + ∑wethIn + ∑wethRedeem + ∑wethDonated - ∑withdraw - ∑wethOut - ∑feesCollected + * Invariant B: steth balance >= ∑stethIn + ∑stethDonated - ∑stethOut - ∑stethRedeem + + * Liquidity provider functionnalities (lp) + * Shares: + * Invariant A: ∑shares > 0 due to initial deposit + * Invariant B: totalShares == ∑userShares + deadShares + * Invariant C: previewRedeem(∑shares) == totalAssets + * Invariant D: previewRedeem(shares) == (, uint256 assets) = previewRedeem(shares) Not really invariant, but tested on handler + * Invariant E: previewDeposit(amount) == uint256 shares = previewDeposit(amount) Not really invariant, but tested on handler + * Invariant L: ∀ user, user.weth + previewRedeem(user.shares) >=~ initialBalance , approxGe, to handle rounding error on deposit. + + * Withdraw Queue: + * Invariant F: nextWithdrawalIndex == requestRedeem call count + * Invariant G: withdrawsQueued == ∑requestRedeem.amount + * Invariant H: withdrawsQueued > withdrawsClaimed + * Invariant I: withdrawsQueued == ∑request.assets + * Invariant J: withdrawsClaimed == ∑claimRedeem.amount + * Invariant K: ∀ requestId, request.queued >= request.assets + + * Fees: + * Invariant M: ∑feesCollected == feeCollector.balance + + * Lido Liquidity Manager functionnalities + * Invariant A: lidoWithdrawalQueueAmount == ∑lidoRequestRedeem.assets + * Invariant B: address(arm).balance == 0 + * Invariant C: All slot allow for gap are empty + + * After invariants: + * All user can withdraw their funds + * Log stats + + + */ + + ////////////////////////////////////////////////////// + /// --- SWAP ASSERTIONS + ////////////////////////////////////////////////////// + function assert_swap_invariant_A() public view { + uint256 inflows = lpHandler.sum_of_deposits() + swapHandler.sum_of_weth_in() + + llmHandler.sum_of_redeemed_ether() + donationHandler.sum_of_weth_donated() + MIN_TOTAL_SUPPLY; + uint256 outflows = lpHandler.sum_of_withdraws() + swapHandler.sum_of_weth_out() + ownerHandler.sum_of_fees(); + assertEq(weth.balanceOf(address(lidoARM)), inflows - outflows, "swapHandler.invariant_A"); + } + + function assert_swap_invariant_B() public view { + uint256 inflows = swapHandler.sum_of_steth_in() + donationHandler.sum_of_steth_donated(); + uint256 outflows = swapHandler.sum_of_steth_out() + llmHandler.sum_of_requested_ether(); + uint256 sum_of_errors = MockSTETH(address(steth)).sum_of_errors(); + assertApproxEqAbs( + steth.balanceOf(address(lidoARM)), absDiff(inflows, outflows), sum_of_errors, "swapHandler.invariant_B" + ); + } + + ////////////////////////////////////////////////////// + /// --- LIQUIDITY PROVIDER ASSERTIONS + ////////////////////////////////////////////////////// + function assert_lp_invariant_A() public view { + assertGt(lidoARM.totalSupply(), 0, "lpHandler.invariant_A"); + } + + function assert_lp_invariant_B() public view { + uint256 sumOfUserShares; + for (uint256 i; i < lps.length; i++) { + address user = lps[i]; + sumOfUserShares += lidoARM.balanceOf(user); + } + assertEq(lidoARM.totalSupply(), _sumOfUserShares(), "lpHandler.invariant_B"); + } + + function assert_lp_invariant_C() public view { + assertEq(lidoARM.previewRedeem(_sumOfUserShares()), lidoARM.totalAssets(), "lpHandler.invariant_C"); + } + + function assert_lp_invariant_D() public view { + // Not really an invariant, but tested on handler + } + + function assert_lp_invariant_E() public view { + // Not really an invariant, but tested on handler + } + + function assert_lp_invariant_F() public view { + assertEq( + lidoARM.nextWithdrawalIndex(), lpHandler.numberOfCalls("lpHandler.requestRedeem"), "lpHandler.invariant_F" + ); + } + + function assert_lp_invariant_G() public view { + assertEq(lidoARM.withdrawsQueued(), lpHandler.sum_of_requests(), "lpHandler.invariant_G"); + } + + function assert_lp_invariant_H() public view { + assertGe(lidoARM.withdrawsQueued(), lidoARM.withdrawsClaimed(), "lpHandler.invariant_H"); + } + + function assert_lp_invariant_I() public view { + uint256 sum; + uint256 nextWithdrawalIndex = lidoARM.nextWithdrawalIndex(); + for (uint256 i; i < nextWithdrawalIndex; i++) { + (,,, uint128 assets,) = lidoARM.withdrawalRequests(i); + sum += assets; + } + + assertEq(lidoARM.withdrawsQueued(), sum, "lpHandler.invariant_I"); + } + + function assert_lp_invariant_J() public view { + assertEq(lidoARM.withdrawsClaimed(), lpHandler.sum_of_withdraws(), "lpHandler.invariant_J"); + } + + function assert_lp_invariant_K() public view { + uint256 nextWithdrawalIndex = lidoARM.nextWithdrawalIndex(); + for (uint256 i; i < nextWithdrawalIndex; i++) { + (,,, uint128 assets, uint128 queued) = lidoARM.withdrawalRequests(i); + assertGe(queued, assets, "lpHandler.invariant_L"); + } + } + + function assert_lp_invariant_L(uint256 initialBalance, uint256 maxError) public { + // As we will manipulate state here, we will snapshot the state and revert it after + uint256 snapshotId = vm.snapshot(); + + // 1. Finalize all claims on Lido + llmHandler.finalizeAllClaims(); + + // 2. Swap all stETH to WETH + _sweepAllStETH(); + + // 3. Finalize all claim redeem on ARM. + lpHandler.finalizeAllClaims(); + + for (uint256 i; i < lps.length; i++) { + address user = lps[i]; + uint256 userShares = lidoARM.balanceOf(user); + uint256 assets = lidoARM.previewRedeem(userShares); + uint256 sum = assets + weth.balanceOf(user); + + if (sum < initialBalance) { + // In this situation user have lost a bit of asset, ensure this is not too much + assertApproxEqRel(sum, initialBalance, maxError, "lpHandler.invariant_L_a"); + } else { + // In this case user have gained asset. + assertGe(sum, initialBalance, "lpHandler.invariant_L_b"); + } + } + + vm.revertToAndDelete(snapshotId); + } + + function assert_lp_invariant_M() public view { + address feeCollector = lidoARM.feeCollector(); + assertEq(weth.balanceOf(feeCollector), ownerHandler.sum_of_fees(), "lpHandler.invariant_M"); + } + + ////////////////////////////////////////////////////// + /// --- LIDO LIQUIDITY MANAGER ASSERTIONS + ////////////////////////////////////////////////////// + function assert_llm_invariant_A() public view { + assertEq( + lidoARM.lidoWithdrawalQueueAmount(), + llmHandler.sum_of_requested_ether() - llmHandler.sum_of_redeemed_ether(), + "llmHandler.invariant_A" + ); + } + + function assert_llm_invariant_B() public view { + assertEq(address(lidoARM).balance, 0, "llmHandler.invariant_B"); + } + + function assert_llm_invariant_C() public view { + uint256 slotGap1 = 1; + uint256 slotGap2 = 59; + uint256 gap1Length = 49; + uint256 gap2Length = 41; + + for (uint256 i = slotGap1; i < slotGap1 + gap1Length; i++) { + assertEq(readStorageSlotOnARM(i), 0, "lpHandler.invariant_C.gap1"); + } + + for (uint256 i = slotGap2; i < slotGap2 + gap2Length; i++) { + assertEq(readStorageSlotOnARM(i), 0, "lpHandler.invariant_C.gap2"); + } + } + + ////////////////////////////////////////////////////// + /// --- HELPERS + ////////////////////////////////////////////////////// + /// @notice Sum of users shares, including dead shares + function _sumOfUserShares() internal view returns (uint256) { + uint256 sumOfUserShares; + for (uint256 i; i < lps.length; i++) { + address user = lps[i]; + sumOfUserShares += lidoARM.balanceOf(user); + } + return sumOfUserShares + lidoARM.balanceOf(address(0xdEaD)); + } + + /// @notice Swap all stETH to WETH at the current price + function _sweepAllStETH() internal { + uint256 stETHBalance = steth.balanceOf(address(lidoARM)); + deal(address(weth), address(this), 1_000_000_000 ether); + weth.approve(address(lidoARM), type(uint256).max); + lidoARM.swapTokensForExactTokens(weth, steth, stETHBalance, type(uint256).max, address(this)); + assertApproxEqAbs(steth.balanceOf(address(lidoARM)), 0, 1, "SwepAllStETH"); + } + + /// @notice Empties the ARM + /// @dev Finalize all claims on lido, swap all stETH to WETH, finalize all + /// claim redeem on ARM and withdraw all user funds. + function emptiesARM() internal { + // 1. Finalize all claims on Lido + llmHandler.finalizeAllClaims(); + + // 2. Swap all stETH to WETH + _sweepAllStETH(); + + // 3. Finalize all claim redeem on ARM. + lpHandler.finalizeAllClaims(); + + // 4. Withdraw all user funds + lpHandler.withdrawAllUserFunds(); + } + + /// @notice Absolute difference between two numbers + function absDiff(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a - b : b - a; + } + + function readStorageSlotOnARM(uint256 slotNumber) internal view returns (uint256 value) { + value = uint256(vm.load(address(lidoARM), bytes32(slotNumber))); + } + + function logStats() public view { + // Don't trace this function as it's only for logging data. + vm.pauseTracing(); + // Get data + _LPHandler memory lpHandlerStats = _LPHandler({ + deposit: lpHandler.numberOfCalls("lpHandler.deposit"), + deposit_skip: lpHandler.numberOfCalls("lpHandler.deposit.skip"), + requestRedeem: lpHandler.numberOfCalls("lpHandler.requestRedeem"), + requestRedeem_skip: lpHandler.numberOfCalls("lpHandler.requestRedeem.skip"), + claimRedeem: lpHandler.numberOfCalls("lpHandler.claimRedeem"), + claimRedeem_skip: lpHandler.numberOfCalls("lpHandler.claimRedeem.skip") + }); + + _SwapHandler memory swapHandlerStats = _SwapHandler({ + swapExact: swapHandler.numberOfCalls("swapHandler.swapExact"), + swapExact_skip: swapHandler.numberOfCalls("swapHandler.swapExact.skip"), + swapTokens: swapHandler.numberOfCalls("swapHandler.swapTokens"), + swapTokens_skip: swapHandler.numberOfCalls("swapHandler.swapTokens.skip") + }); + + _OwnerHandler memory ownerHandlerStats = _OwnerHandler({ + setPrices: ownerHandler.numberOfCalls("ownerHandler.setPrices"), + setPrices_skip: ownerHandler.numberOfCalls("ownerHandler.setPrices.skip"), + setCrossPrice: ownerHandler.numberOfCalls("ownerHandler.setCrossPrice"), + setCrossPrice_skip: ownerHandler.numberOfCalls("ownerHandler.setCrossPrice.skip"), + collectFees: ownerHandler.numberOfCalls("ownerHandler.collectFees"), + collectFees_skip: ownerHandler.numberOfCalls("ownerHandler.collectFees.skip"), + setFees: ownerHandler.numberOfCalls("ownerHandler.setFees"), + setFees_skip: ownerHandler.numberOfCalls("ownerHandler.setFees.skip") + }); + + _LLMHandler memory llmHandlerStats = _LLMHandler({ + requestStETHWithdraw: llmHandler.numberOfCalls("llmHandler.requestStETHWithdraw"), + claimStETHWithdraw: llmHandler.numberOfCalls("llmHandler.claimStETHWithdraw") + }); + + _DonationHandler memory donationHandlerStats = _DonationHandler({ + donateStETH: donationHandler.numberOfCalls("donationHandler.donateStETH"), + donateWETH: donationHandler.numberOfCalls("donationHandler.donateWETH") + }); + + // Log data + console.log(""); + console.log(""); + console.log(""); + console.log("--- Stats ---"); + + // --- LP Handler --- + console.log(""); + console.log("# LP Handler # "); + console.log("Number of Call: Deposit %d (skipped: %d)", lpHandlerStats.deposit, lpHandlerStats.deposit_skip); + console.log( + "Number of Call: RequestRedeem %d (skipped: %d)", + lpHandlerStats.requestRedeem, + lpHandlerStats.requestRedeem_skip + ); + console.log( + "Number of Call: ClaimRedeem %d (skipped: %d)", lpHandlerStats.claimRedeem, lpHandlerStats.claimRedeem_skip + ); + + // --- Swap Handler --- + console.log(""); + console.log("# Swap Handler #"); + console.log( + "Number of Call: SwapExactTokensForTokens %d (skipped: %d)", + swapHandlerStats.swapExact, + swapHandlerStats.swapExact_skip + ); + console.log( + "Number of Call: SwapTokensForExactTokens %d (skipped: %d)", + swapHandlerStats.swapTokens, + swapHandlerStats.swapTokens_skip + ); + + // --- Owner Handler --- + console.log(""); + console.log("# Owner Handler #"); + console.log( + "Number of Call: SetPrices %d (skipped: %d)", ownerHandlerStats.setPrices, ownerHandlerStats.setPrices_skip + ); + console.log( + "Number of Call: SetCrossPrice %d (skipped: %d)", + ownerHandlerStats.setCrossPrice, + ownerHandlerStats.setCrossPrice_skip + ); + console.log( + "Number of Call: CollectFees %d (skipped: %d)", + ownerHandlerStats.collectFees, + ownerHandlerStats.collectFees_skip + ); + console.log( + "Number of Call: SetFees %d (skipped: %d)", ownerHandlerStats.setFees, ownerHandlerStats.setFees_skip + ); + + // --- LLM Handler --- + console.log(""); + console.log("# LLM Handler #"); + console.log( + "Number of Call: RequestStETHWithdrawalForETH %d (skipped: %d)", llmHandlerStats.requestStETHWithdraw, 0 + ); + console.log( + "Number of Call: ClaimStETHWithdrawalForWETH %d (skipped: %d)", llmHandlerStats.claimStETHWithdraw, 0 + ); + + // --- Donation Handler --- + console.log(""); + console.log("# Donation Handler #"); + console.log("Number of Call: DonateStETH %d (skipped: %d)", donationHandlerStats.donateStETH, 0); + console.log("Number of Call: DonateWETH %d (skipped: %d)", donationHandlerStats.donateWETH, 0); + + // --- Global --- + console.log(""); + console.log("# Global Data #"); + uint256 sumOfCall = donationHandlerStats.donateStETH + donationHandlerStats.donateWETH + + llmHandlerStats.requestStETHWithdraw + llmHandlerStats.claimStETHWithdraw + ownerHandlerStats.setPrices + + ownerHandlerStats.setCrossPrice + ownerHandlerStats.collectFees + ownerHandlerStats.setFees + + swapHandlerStats.swapExact + swapHandlerStats.swapTokens + lpHandlerStats.deposit + + lpHandlerStats.requestRedeem + lpHandlerStats.claimRedeem; + uint256 sumOfCall_skip = ownerHandlerStats.setPrices_skip + ownerHandlerStats.setCrossPrice_skip + + ownerHandlerStats.collectFees_skip + ownerHandlerStats.setFees_skip + swapHandlerStats.swapExact_skip + + swapHandlerStats.swapTokens_skip + lpHandlerStats.deposit_skip + lpHandlerStats.requestRedeem_skip + + lpHandlerStats.claimRedeem_skip; + + uint256 skipPct = (sumOfCall_skip * 10_000) / max(sumOfCall, 1); + console.log("Total call: %d (skipped: %d) -> %2e%", sumOfCall, sumOfCall_skip, skipPct); + console.log(""); + console.log("-------------"); + console.log(""); + console.log(""); + console.log(""); + vm.resumeTracing(); + } + + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + struct _LPHandler { + uint256 deposit; + uint256 deposit_skip; + uint256 requestRedeem; + uint256 requestRedeem_skip; + uint256 claimRedeem; + uint256 claimRedeem_skip; + } + + struct _SwapHandler { + uint256 swapExact; + uint256 swapExact_skip; + uint256 swapTokens; + uint256 swapTokens_skip; + } + + struct _OwnerHandler { + uint256 setPrices; + uint256 setPrices_skip; + uint256 setCrossPrice; + uint256 setCrossPrice_skip; + uint256 collectFees; + uint256 collectFees_skip; + uint256 setFees; + uint256 setFees_skip; + } + + struct _LLMHandler { + uint256 requestStETHWithdraw; + uint256 claimStETHWithdraw; + } + + struct _DonationHandler { + uint256 donateStETH; + uint256 donateWETH; + } +} diff --git a/test/invariants/BasicInvariants.sol b/test/invariants/BasicInvariants.sol new file mode 100644 index 0000000..551d5f2 --- /dev/null +++ b/test/invariants/BasicInvariants.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Invariant_Base_Test_} from "./BaseInvariants.sol"; + +// Handlers +import {LpHandler} from "./handlers/LpHandler.sol"; +import {LLMHandler} from "./handlers/LLMHandler.sol"; +import {SwapHandler} from "./handlers/SwapHandler.sol"; +import {OwnerHandler} from "./handlers/OwnerHandler.sol"; +import {DonationHandler} from "./handlers/DonationHandler.sol"; +import {DistributionHandler} from "./handlers/DistributionHandler.sol"; + +/// @notice Basic invariant test contract +/// @dev This contract holds all the configuration needed for the basic invariant tests, +/// like call distribution %, user configuration, max values etc. +/// @dev This is where all the invariant are checked. +contract Invariant_Basic_Test_ is Invariant_Base_Test_ { + ////////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + ////////////////////////////////////////////////////// + uint256 private constant NUM_LPS = 4; + uint256 private constant NUM_SWAPS = 3; + uint256 private constant MAX_FEES = 5_000; // 50% + uint256 private constant MIN_BUY_T1 = 0.98 * 1e36; // We could have use 0, but this is non-sense + uint256 private constant MAX_SELL_T1 = 1.02 * 1e36; // We could have use type(uint256).max, but this is non-sense + uint256 private constant MAX_WETH_PER_USERS = 10_000 ether; // 10M + uint256 private constant MAX_STETH_PER_USERS = 10_000 ether; // 10M, actual total supply + uint256 private constant MAX_LOSS_IN_PCT = 1e13; // 0.001% + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public virtual override { + super.setUp(); + + // --- Create Users --- + // In this configuration, an user is either a LP or a Swap, but not both. + require(NUM_LPS + NUM_SWAPS <= users.length, "IBT: NOT_ENOUGH_USERS"); + for (uint256 i; i < NUM_LPS; i++) { + address user = users[i]; + require(user != address(0), "IBT: INVALID_USER"); + lps.push(user); + + // Give them a lot of wETH + deal(address(weth), user, MAX_WETH_PER_USERS); + } + for (uint256 i = NUM_LPS; i < NUM_LPS + NUM_SWAPS; i++) { + address user = users[i]; + require(user != address(0), "IBT: INVALID_USER"); + swaps.push(user); + + // Give them a lot of wETH and stETH + deal(address(weth), user, MAX_WETH_PER_USERS); + deal(address(steth), user, MAX_STETH_PER_USERS); + } + + // --- Setup ARM --- + // Max caps on the total asset that can be deposited + vm.prank(capManager.owner()); + capManager.setTotalAssetsCap(type(uint248).max); + + // Set prices, start with almost 1:1 + vm.prank(lidoARM.owner()); + lidoARM.setPrices(1e36 - 1, 1e36); + + // --- Handlers --- + lpHandler = new LpHandler(address(lidoARM), address(weth), lps); + swapHandler = new SwapHandler(address(lidoARM), address(weth), address(steth), swaps); + ownerHandler = + new OwnerHandler(address(lidoARM), address(weth), address(steth), MIN_BUY_T1, MAX_SELL_T1, MAX_FEES); + llmHandler = new LLMHandler(address(lidoARM), address(steth)); + donationHandler = new DonationHandler(address(lidoARM), address(weth), address(steth)); + + lpHandler.setSelectorWeight(lpHandler.deposit.selector, 5_000); // 50% + lpHandler.setSelectorWeight(lpHandler.requestRedeem.selector, 2_500); // 25% + lpHandler.setSelectorWeight(lpHandler.claimRedeem.selector, 2_500); // 25% + swapHandler.setSelectorWeight(swapHandler.swapExactTokensForTokens.selector, 5_000); // 50% + swapHandler.setSelectorWeight(swapHandler.swapTokensForExactTokens.selector, 5_000); // 50% + ownerHandler.setSelectorWeight(ownerHandler.setPrices.selector, 5_000); // 50% + ownerHandler.setSelectorWeight(ownerHandler.setCrossPrice.selector, 2_000); // 20% + ownerHandler.setSelectorWeight(ownerHandler.collectFees.selector, 2_000); // 20% + ownerHandler.setSelectorWeight(ownerHandler.setFees.selector, 1_000); // 10% + llmHandler.setSelectorWeight(llmHandler.requestLidoWithdrawals.selector, 5_000); // 50% + llmHandler.setSelectorWeight(llmHandler.claimLidoWithdrawals.selector, 5_000); // 50% + donationHandler.setSelectorWeight(donationHandler.donateStETH.selector, 5_000); // 50% + donationHandler.setSelectorWeight(donationHandler.donateWETH.selector, 5_000); // 50% + + address[] memory targetContracts = new address[](5); + targetContracts[0] = address(lpHandler); + targetContracts[1] = address(swapHandler); + targetContracts[2] = address(ownerHandler); + targetContracts[3] = address(llmHandler); + targetContracts[4] = address(donationHandler); + + uint256[] memory weightsDistributorHandler = new uint256[](5); + weightsDistributorHandler[0] = 4_000; // 40% + weightsDistributorHandler[1] = 4_000; // 40% + weightsDistributorHandler[2] = 1_000; // 10% + weightsDistributorHandler[3] = 700; // 7% + weightsDistributorHandler[4] = 300; // 3% + + address distributionHandler = address(new DistributionHandler(targetContracts, weightsDistributorHandler)); + + // All call will be done through the distributor, so we set it as the target contract + targetContract(distributionHandler); + } + + ////////////////////////////////////////////////////// + /// --- INVARIANTS + ////////////////////////////////////////////////////// + function invariant_lp() external { + assert_lp_invariant_A(); + assert_lp_invariant_B(); + assert_lp_invariant_C(); + assert_lp_invariant_D(); + assert_lp_invariant_E(); + assert_lp_invariant_F(); + assert_lp_invariant_G(); + assert_lp_invariant_H(); + assert_lp_invariant_I(); + assert_lp_invariant_J(); + assert_lp_invariant_K(); + assert_lp_invariant_L(MAX_WETH_PER_USERS, MAX_LOSS_IN_PCT); + assert_lp_invariant_M(); + } + + function invariant_swap() external view { + assert_swap_invariant_A(); + assert_swap_invariant_B(); + } + + function invariant_llm() external view { + assert_llm_invariant_A(); + assert_llm_invariant_B(); + assert_llm_invariant_C(); + } + + function afterInvariant() external { + logStats(); + emptiesARM(); + } +} diff --git a/test/invariants/handlers/BaseHandler.sol b/test/invariants/handlers/BaseHandler.sol new file mode 100644 index 0000000..8714990 --- /dev/null +++ b/test/invariants/handlers/BaseHandler.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {Vm} from "forge-std/Vm.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; + +/// @notice Base handler contract +/// @dev This contract should be used as a base contract for all handlers +/// as this it holds the sole and exclusive callable function `entryPoint`. +/// @dev Highly inspired from Maple-Core-V2 repo: https://github.com/maple-labs/maple-core-v2 +abstract contract BaseHandler is StdUtils, StdCheats { + ////////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + ////////////////////////////////////////////////////// + Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + uint256 internal constant WEIGHTS_RANGE = 10_000; + + ////////////////////////////////////////////////////// + /// --- VARIABLES + ////////////////////////////////////////////////////// + uint256 public numCalls; + uint256 public totalWeight; + + bytes4[] public selectors; + + mapping(address => string) public names; + mapping(bytes4 => uint256) public weights; + mapping(bytes32 => uint256) public numberOfCalls; + + constructor() { + // Default names + names[makeAddr("Alice")] = "Alice"; + names[makeAddr("Bob")] = "Bob"; + names[makeAddr("Charlie")] = "Charlie"; + names[makeAddr("Dave")] = "Dave"; + names[makeAddr("Eve")] = "Eve"; + names[makeAddr("Frank")] = "Frank"; + names[makeAddr("George")] = "George"; + names[makeAddr("Harry")] = "Harry"; + } + + ////////////////////////////////////////////////////// + /// --- FUNCTIONS + ////////////////////////////////////////////////////// + function setSelectorWeight(bytes4 funcSelector, uint256 weight_) external { + // Set Selector weight + weights[funcSelector] = weight_; + + // Add selector to the selector list + selectors.push(funcSelector); + + // Increase totalWeight + totalWeight += weight_; + } + + function entryPoint(uint256 seed_) external { + require(totalWeight == WEIGHTS_RANGE, "HB:INVALID_WEIGHTS"); + + numCalls++; + + uint256 range_; + + uint256 value_ = uint256(keccak256(abi.encodePacked(seed_, numCalls))) % WEIGHTS_RANGE + 1; // 1 - 100 + + for (uint256 i = 0; i < selectors.length; i++) { + uint256 weight_ = weights[selectors[i]]; + + range_ += weight_; + if (value_ <= range_ && weight_ != 0) { + (bool success,) = address(this).call(abi.encodeWithSelector(selectors[i], seed_)); + + // TODO: Parse error from low-level call and revert with it + require(success, "HB:CALL_FAILED"); + break; + } + } + } + + ////////////////////////////////////////////////////// + /// --- HELPERS + ////////////////////////////////////////////////////// + function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(seed, salt))); + } + + /// @notice Return the minimum between two uint256 + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /// @notice Return the maximum between two uint256 + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } +} diff --git a/test/invariants/handlers/DistributionHandler.sol b/test/invariants/handlers/DistributionHandler.sol new file mode 100644 index 0000000..ded559c --- /dev/null +++ b/test/invariants/handlers/DistributionHandler.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Contract +import {BaseHandler} from "./BaseHandler.sol"; + +/// @title Distribution Handler contract +/// @dev This contract should be the only callable contract from test and will distribute calls to other contracts +/// @dev Highly inspired from Maple-Core-V2 repo: https://github.com/maple-labs/maple-core-v2 +contract DistributionHandler { + ////////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + ////////////////////////////////////////////////////// + uint256 internal constant WEIGHTS_RANGE = 10_000; + + ////////////////////////////////////////////////////// + /// --- VARIABLES + ////////////////////////////////////////////////////// + uint256 public numOfCallsTotal; + + address[] public targetContracts; + + uint256[] public weights; + + mapping(address => uint256) public numOfCalls; + + ////////////////////////////////////////////////////// + /// --- CONSTRUCTOR + ////////////////////////////////////////////////////// + constructor(address[] memory targetContracts_, uint256[] memory weights_) { + // NOTE: Order of arrays must match + require(targetContracts_.length == weights_.length, "DH:INVALID_LENGTHS"); + + uint256 weightsTotal; + + for (uint256 i; i < weights_.length; ++i) { + weightsTotal += weights_[i]; + } + + require(weightsTotal == WEIGHTS_RANGE, "DH:INVALID_WEIGHTS"); + + targetContracts = targetContracts_; + weights = weights_; + } + + ////////////////////////////////////////////////////// + /// --- FUNCTIONS + ////////////////////////////////////////////////////// + function distributorEntryPoint(uint256 seed_) external { + numOfCallsTotal++; + + uint256 range_; + + uint256 value_ = uint256(keccak256(abi.encodePacked(seed_, numOfCallsTotal))) % WEIGHTS_RANGE + 1; // 1 - 100 + + for (uint256 i = 0; i < targetContracts.length; i++) { + uint256 weight_ = weights[i]; + + range_ += weight_; + if (value_ <= range_ && weight_ != 0) { + numOfCalls[targetContracts[i]]++; + BaseHandler(targetContracts[i]).entryPoint(seed_); + break; + } + } + } +} diff --git a/test/invariants/handlers/DonationHandler.sol b/test/invariants/handlers/DonationHandler.sol new file mode 100644 index 0000000..f689412 --- /dev/null +++ b/test/invariants/handlers/DonationHandler.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {console} from "forge-std/console.sol"; + +// Handlers +import {BaseHandler} from "./BaseHandler.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; + +/// @notice DonaitonHandler contract +/// @dev This contract is used to simulate donation of stETH or wETH to the ARM. +contract DonationHandler is BaseHandler { + //////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + //////////////////////////////////////////////////// + IERC20 public immutable weth; + IERC20 public immutable steth; + LidoARM public immutable arm; + + //////////////////////////////////////////////////// + /// --- VARIABLES + //////////////////////////////////////////////////// + + //////////////////////////////////////////////////// + /// --- VARIABLES FOR INVARIANT ASSERTIONS + //////////////////////////////////////////////////// + uint256 public sum_of_weth_donated; + uint256 public sum_of_steth_donated; + + //////////////////////////////////////////////////// + /// --- CONSTRUCTOR + //////////////////////////////////////////////////// + constructor(address _arm, address _weth, address _steth) { + arm = LidoARM(payable(_arm)); + weth = IERC20(_weth); + steth = IERC20(_steth); + + names[address(weth)] = "WETH"; + names[address(steth)] = "STETH"; + } + + //////////////////////////////////////////////////// + /// --- ACTIONS + //////////////////////////////////////////////////// + function donateStETH(uint256 _seed) external { + numberOfCalls["donationHandler.donateStETH"]++; + + uint256 amount = _bound(_seed, 1, 1 ether); + console.log("DonationHandler.donateStETH(%18e)", amount); + + deal(address(steth), address(this), amount); + + steth.transfer(address(arm), amount); + + sum_of_steth_donated += amount; + } + + function donateWETH(uint256 _seed) external { + numberOfCalls["donationHandler.donateWETH"]++; + + uint256 amount = _bound(_seed, 1, 1 ether); + console.log("DonationHandler.donateWETH(%18e)", amount); + + deal(address(weth), address(this), amount); + + weth.transfer(address(arm), amount); + + sum_of_weth_donated += amount; + } +} diff --git a/test/invariants/handlers/LLMHandler.sol b/test/invariants/handlers/LLMHandler.sol new file mode 100644 index 0000000..731a332 --- /dev/null +++ b/test/invariants/handlers/LLMHandler.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {console} from "forge-std/console.sol"; + +// Handlers +import {BaseHandler} from "./BaseHandler.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; + +/// @notice LidoLiquidityManager Handler contract +/// @dev This contract is used to handle all functionnalities that are related to the Lido Liquidity Manager. +contract LLMHandler is BaseHandler { + //////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + //////////////////////////////////////////////////// + IERC20 public immutable steth; + LidoARM public immutable arm; + address public immutable owner; + uint256 public constant MAX_AMOUNT = 1_000 ether; + + //////////////////////////////////////////////////// + /// --- VARIABLES + //////////////////////////////////////////////////// + uint256[] public requestIds; + + //////////////////////////////////////////////////// + /// --- VARIABLES FOR INVARIANT ASSERTIONS + //////////////////////////////////////////////////// + uint256 public sum_of_requested_ether; + uint256 public sum_of_redeemed_ether; + + //////////////////////////////////////////////////// + /// --- CONSTRUCTOR + //////////////////////////////////////////////////// + constructor(address _arm, address _steth) { + arm = LidoARM(payable(_arm)); + owner = arm.owner(); + steth = IERC20(_steth); + } + + //////////////////////////////////////////////////// + /// --- ACTIONS + //////////////////////////////////////////////////// + function requestLidoWithdrawals(uint256 _seed) external { + numberOfCalls["llmHandler.requestStETHWithdraw"]++; + + // Select a random amount + uint256 totalAmount = _bound(_seed, 0, min(MAX_AMOUNT * 3, steth.balanceOf(address(arm)))); + + // We can only request only 1k amount at a time + uint256 batch = (totalAmount / MAX_AMOUNT) + 1; + uint256[] memory amounts = new uint256[](batch); + uint256 totalAmount_ = totalAmount; + for (uint256 i = 0; i < batch; i++) { + if (totalAmount_ >= MAX_AMOUNT) { + amounts[i] = MAX_AMOUNT; + totalAmount_ -= MAX_AMOUNT; + } else { + amounts[i] = totalAmount_; + totalAmount_ = 0; + } + } + require(totalAmount_ == 0, "LLMHandler: Invalid total amount"); + + console.log("LLMHandler.requestLidoWithdrawals(%18e)", totalAmount); + + // Prank Owner + vm.startPrank(owner); + + // Request stETH withdrawal for ETH + uint256[] memory requestId = arm.requestLidoWithdrawals(amounts); + + // Stop Prank + vm.stopPrank(); + + // Update state + for (uint256 i = 0; i < requestId.length; i++) { + requestIds.push(requestId[i]); + } + + // Update sum of requested ether + sum_of_requested_ether += totalAmount; + } + + function claimLidoWithdrawals(uint256 _seed) external { + numberOfCalls["llmHandler.claimStETHWithdraw"]++; + + // Select multiple requestIds + uint256 len = requestIds.length; + uint256 requestCount = _bound(_seed, 0, len); + uint256[] memory requestIds_ = new uint256[](requestCount); + for (uint256 i = 0; i < requestCount; i++) { + requestIds_[i] = requestIds[i]; + } + + // Remove requestIds from list + uint256[] memory newRequestIds = new uint256[](len - requestCount); + for (uint256 i = requestCount; i < len; i++) { + newRequestIds[i - requestCount] = requestIds[i]; + } + requestIds = newRequestIds; + + // As `claimLidoWithdrawals` doesn't send back the amount, we need to calculate it + uint256 outstandingBefore = arm.lidoWithdrawalQueueAmount(); + + // Prank Owner + vm.startPrank(owner); + + // Claim stETH withdrawal for WETH + arm.claimLidoWithdrawals(requestIds_); + + // Stop Prank + vm.stopPrank(); + + uint256 outstandingAfter = arm.lidoWithdrawalQueueAmount(); + uint256 diff = outstandingBefore - outstandingAfter; + + console.log("LLMHandler.claimLidoWithdrawals(%18e -- count: %d)", diff, requestCount); + + // Update sum of redeemed ether + sum_of_redeemed_ether += diff; + } + + //////////////////////////////////////////////////// + /// --- HELPERS + //////////////////////////////////////////////////// + /// @notice Claim all the remaining requested withdrawals + function finalizeAllClaims() external { + // As `claimLidoWithdrawals` doesn't send back the amount, we need to calculate it + uint256 outstandingBefore = arm.lidoWithdrawalQueueAmount(); + + // Prank Owner + vm.startPrank(owner); + + // Claim stETH withdrawal for WETH + arm.claimLidoWithdrawals(requestIds); + + // Stop Prank + vm.stopPrank(); + + uint256 outstandingAfter = arm.lidoWithdrawalQueueAmount(); + uint256 diff = outstandingBefore - outstandingAfter; + + // Update sum of redeemed ether + sum_of_redeemed_ether += diff; + } +} diff --git a/test/invariants/handlers/LpHandler.sol b/test/invariants/handlers/LpHandler.sol new file mode 100644 index 0000000..095cfb4 --- /dev/null +++ b/test/invariants/handlers/LpHandler.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {console} from "forge-std/console.sol"; + +// Handlers +import {BaseHandler} from "./BaseHandler.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; + +/// @notice LpHandler contract +/// @dev This contract is used to handle all functionnalities related to providing liquidity in the ARM. +contract LpHandler is BaseHandler { + //////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + //////////////////////////////////////////////////// + IERC20 public immutable weth; + LidoARM public immutable arm; + + //////////////////////////////////////////////////// + /// --- VARIABLES + //////////////////////////////////////////////////// + address[] public lps; // Users that provide liquidity + mapping(address user => uint256[] ids) public requests; + + //////////////////////////////////////////////////// + /// --- VARIABLES FOR INVARIANT ASSERTIONS + //////////////////////////////////////////////////// + uint256 public sum_of_deposits; + uint256 public sum_of_requests; + uint256 public sum_of_withdraws; + + //////////////////////////////////////////////////// + /// --- CONSTRUCTOR + //////////////////////////////////////////////////// + constructor(address _arm, address _weth, address[] memory _lps) { + arm = LidoARM(payable(_arm)); + weth = IERC20(_weth); + + require(_lps.length > 0, "LH: EMPTY_LPS"); + lps = _lps; + } + + //////////////////////////////////////////////////// + /// --- ACTIONS + //////////////////////////////////////////////////// + /// @notice Provide liquidity to the ARM with a given amount of WETH + /// @dev This assumes that lps have unlimited capacity to provide liquidity on LPC contracts. + function deposit(uint256 _seed) external { + numberOfCalls["lpHandler.deposit"]++; + + // Get a user + address user = lps[_seed % lps.length]; + + // Amount of WETH to deposit should be between 0 and total WETH balance + uint256 amount = _bound(_seed, 0, weth.balanceOf(user)); + console.log("LpHandler.deposit(%18e), %s", amount, names[user]); + + // Prank user + vm.startPrank(user); + + // Approve WETH to ARM + weth.approve(address(arm), amount); + + // Deposit WETH + uint256 expectedShares = arm.previewDeposit(amount); + uint256 shares = arm.deposit(amount); + + // This is an invariant check. The shares should be equal to the expected shares + require(shares == expectedShares, "LH: DEPOSIT - INVALID_SHARES"); + + // End prank + vm.stopPrank(); + + // Update sum of deposits + sum_of_deposits += amount; + } + + /// @notice Request to redeem a given amount of shares from the ARM + /// @dev This is allowed to redeem 0 shares. + function requestRedeem(uint256 _seed) external { + numberOfCalls["lpHandler.requestRedeem"]++; + + // Try to get a user that have shares, i.e. that have deposited and not redeemed all + // If there is not such user, get a random user and 0redeem + address user; + uint256 len = lps.length; + uint256 __seed = _bound(_seed, 0, type(uint256).max - len); + for (uint256 i; i < len; i++) { + user = lps[(__seed + i) % len]; + if (arm.balanceOf(user) > 0) break; + } + require(user != address(0), "LH: REDEEM_REQUEST - NO_USER"); // Should not happen, but just in case + + // Amount of shares to redeem should be between 0 and user total shares balance + uint256 shares = _bound(_seed, 0, arm.balanceOf(user)); + console.log("LpHandler.requestRedeem(%18e -- id: %d), %s", shares, arm.nextWithdrawalIndex(), names[user]); + + // Prank user + vm.startPrank(user); + + // Redeem shares + uint256 expectedAmount = arm.previewRedeem(shares); + (uint256 id, uint256 amount) = arm.requestRedeem(shares); + + // This is an invariant check. The amount should be equal to the expected amount + require(amount == expectedAmount, "LH: REDEEM_REQUEST - INVALID_AMOUNT"); + + // End prank + vm.stopPrank(); + + // Add request to user + requests[user].push(id); + + // Update sum of requests + sum_of_requests += amount; + } + + event UserFound(address user, uint256 requestId, uint256 requestIndex); + + /// @notice Claim redeem request for a user on the ARM + /// @dev This call will be skipped if there is no request to claim at all. However, claiming zero is allowed. + /// @dev A jump in time is done to the request deadline, but the time is rewinded back to the current time. + function claimRedeem(uint256 _seed) external { + numberOfCalls["lpHandler.claimRedeem"]++; + + // Get a user that have a request to claim + // If no user have a request, skip this call + address user; + uint256 requestId; // on the ARM + uint256 requestIndex; // local + uint256 requestAmount; + uint256 len = lps.length; + uint256 __seed = _bound(_seed, 0, type(uint256).max - len); + uint256 withdrawsClaimed = arm.withdrawsClaimed(); + + // 1. Loop to find a user with a request + for (uint256 i; i < len; i++) { + // Take a random user + address user_ = lps[(__seed + i) % len]; + // Check if user have a request + if (requests[user_].length > 0) { + // Cache user requests length + uint256 requestLen = requests[user_].length; + + // 2. Loop to find a request that can be claimed + for (uint256 j; j < requestLen; j++) { + uint256 ___seed = _bound(_seed, 0, type(uint256).max - requestLen); + // Take a random request among user requests + uint256 requestIndex_ = (___seed + j) % requestLen; + + // Get data about the request (in ARM contract) + (,,, uint128 amount_, uint128 queued) = arm.withdrawalRequests(requests[user_][requestIndex_]); + + // 3. Check if the request can be claimed + if (queued < withdrawsClaimed + weth.balanceOf(address(arm))) { + user = user_; + requestId = requests[user_][requestIndex_]; + requestIndex = requestIndex_; + requestAmount = amount_; + emit UserFound(user, requestId, requestIndex); + break; + } + } + } + + // If we found a user with a request, break the loop + if (user != address(0)) break; + } + + // If no user have a request, skip this call + if (user == address(0)) { + console.log("LpHandler.claimRedeem - No user have a request"); + numberOfCalls["lpHandler.claimRedeem.skip"]++; + return; + } + + console.log("LpHandler.claimRedeem(%18e -- id: %d), %s", requestAmount, requestId, names[user]); + + // Timejump to request deadline + skip(arm.claimDelay()); + + // Prank user + vm.startPrank(user); + + // Claim redeem + (uint256 amount) = arm.claimRedeem(requestId); + require(amount == requestAmount, "LH: CLAIM_REDEEM - INVALID_AMOUNT"); + + // End prank + vm.stopPrank(); + + // Jump back to current time, to avoid issues with other tests + rewind(arm.claimDelay()); + + // Remove request + uint256[] storage userRequests = requests[user]; + userRequests[requestIndex] = userRequests[userRequests.length - 1]; + userRequests.pop(); + + // Update sum of withdraws + sum_of_withdraws += amount; + } + + //////////////////////////////////////////////////// + /// --- HELPERS + //////////////////////////////////////////////////// + /// @notice Finalize all user claim request for all users + function finalizeAllClaims() external { + // Timejump to request deadline + skip(arm.claimDelay()); + + for (uint256 i; i < lps.length; i++) { + address user = lps[i]; + + vm.startPrank(user); + uint256[] memory userRequests = requests[user]; + for (uint256 j; j < userRequests.length; j++) { + uint256 amount = arm.claimRedeem(userRequests[j]); + sum_of_withdraws += amount; + } + // Delete all requests + delete requests[user]; + + vm.stopPrank(); + } + + // Jump back to current time, to avoid issues with other tests + rewind(arm.claimDelay()); + } + + /// @notice Withdraw all user funds + /// @dev This function assumes that all pending request on lido have been finalized, + /// all stETH have been swapped to WETH and all claim redeem requests have been finalized. + function withdrawAllUserFunds() external { + for (uint256 i; i < lps.length; i++) { + address user = lps[i]; + vm.startPrank(user); + + // Request Claim + (uint256 requestId,) = arm.requestRedeem(arm.balanceOf(user)); + + // Timejump to request deadline + skip(arm.claimDelay()); + + // Claim request + arm.claimRedeem(requestId); + + // Jump back to current time, to avoid issues with other tests + rewind(arm.claimDelay()); + vm.stopPrank(); + } + } + + /// @notice Get all requests for a user + function getRequests(address user) external view returns (uint256[] memory) { + return requests[user]; + } +} diff --git a/test/invariants/handlers/OwnerHandler.sol b/test/invariants/handlers/OwnerHandler.sol new file mode 100644 index 0000000..d392dcd --- /dev/null +++ b/test/invariants/handlers/OwnerHandler.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {console} from "forge-std/console.sol"; + +// Handlers +import {BaseHandler} from "./BaseHandler.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; + +/// @notice OwnerHandler contract +/// @dev This contract is used to handle all functionnalities restricted to the owner of the ARM. +contract OwnerHandler is BaseHandler { + //////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + //////////////////////////////////////////////////// + IERC20 public immutable weth; + IERC20 public immutable steth; + LidoARM public immutable arm; + address public immutable owner; + uint256 public immutable maxFees; + address public immutable operator; + uint256 public immutable minBuyT1; + uint256 public immutable maxSellT1; + uint256 public immutable priceScale; + uint256 public constant MIN_TOTAL_SUPPLY = 1e12; + + //////////////////////////////////////////////////// + /// --- VARIABLES FOR INVARIANT ASSERTIONS + //////////////////////////////////////////////////// + uint256 public sum_of_fees; + + //////////////////////////////////////////////////// + /// --- CONSTRUCTOR + //////////////////////////////////////////////////// + constructor(address _arm, address _weth, address _steth, uint256 _minBuyT1, uint256 _maxSellT1, uint256 _maxFees) { + arm = LidoARM(payable(_arm)); + weth = IERC20(_weth); + steth = IERC20(_steth); + maxFees = _maxFees; + minBuyT1 = _minBuyT1; + maxSellT1 = _maxSellT1; + owner = arm.owner(); + operator = arm.operator(); + priceScale = arm.PRICE_SCALE(); + } + + //////////////////////////////////////////////////// + /// --- ACTIONS + //////////////////////////////////////////////////// + /// @notice Set prices for the ARM + function setPrices(uint256 _seed) external { + numberOfCalls["ownerHandler.setPrices"]++; + + // Bound prices + uint256 crossPrice = arm.crossPrice(); + uint256 buyT1 = _bound(_randomize(_seed, "buy"), minBuyT1, crossPrice - 1); + uint256 sellT1 = _bound(_randomize(_seed, "sell"), crossPrice, maxSellT1); + + console.log("OwnerHandler.setPrices(%36e,%36e)", buyT1, sellT1); + + // Prank owner instead of operator to bypass price check + vm.startPrank(owner); + + // Set prices + arm.setPrices(buyT1, sellT1); + + // Stop prank + vm.stopPrank(); + } + + /// @notice Set cross price for the ARM + function setCrossPrice(uint256 _seed) external { + numberOfCalls["ownerHandler.setCrossPrice"]++; + + // Bound prices + uint256 currentPrice = arm.crossPrice(); + // Condition 1: 1e36 - 20e32 <= newCrossPrice <= 1e36 + // Condition 2: buyPrice < newCrossPrice <= sellPrice + // <=> + // max(buyPrice, 1e36 - 20e32) < newCrossPrice <= min(sellPrice, 1e36) + uint256 sellPrice = priceScale * priceScale / arm.traderate0(); + uint256 buyPrice = arm.traderate1(); + uint256 newCrossPrice = + _bound(_seed, max(priceScale - arm.MAX_CROSS_PRICE_DEVIATION(), buyPrice) + 1, min(priceScale, sellPrice)); + + if (newCrossPrice < currentPrice && steth.balanceOf(address(arm)) >= MIN_TOTAL_SUPPLY) { + console.log("OwnerHandler.setCrossPrice() - Skipping price decrease"); + numberOfCalls["ownerHandler.setCrossPrice.skip"]++; + return; + } + + console.log("OwnerHandler.setCrossPrice(%36e)", newCrossPrice); + + // Prank owner instead of operator to bypass price check + vm.startPrank(owner); + + // Set prices + arm.setCrossPrice(newCrossPrice); + + // Stop prank + vm.stopPrank(); + } + + /// @notice Set fees for the ARM + function setFees(uint256 _seed) external { + numberOfCalls["ownerHandler.setFees"]++; + + uint256 feeAccrued = arm.feesAccrued(); + if (!enoughLiquidityAvailable(feeAccrued) || feeAccrued > weth.balanceOf(address(arm))) { + console.log("OwnerHandler.setFees() - Not enough liquidity to collect fees"); + numberOfCalls["ownerHandler.setFees.skip"]++; + return; + } + + uint256 fee = _bound(_seed, 0, maxFees); + console.log("OwnerHandler.setFees(%2e)", fee); + + // Prank owner + vm.startPrank(owner); + + // Set fees + arm.setFee(fee); + + // Stop prank + vm.stopPrank(); + + // Update sum of fees + sum_of_fees += feeAccrued; + } + + /// @notice Collect fees from the ARM + /// @dev skipped if there is not enough liquidity to collect fees + function collectFees(uint256) external { + numberOfCalls["ownerHandler.collectFees"]++; + + uint256 feeAccrued = arm.feesAccrued(); + if (!enoughLiquidityAvailable(feeAccrued) || feeAccrued > weth.balanceOf(address(arm))) { + console.log("OwnerHandler.collectFees() - Not enough liquidity to collect fees"); + numberOfCalls["ownerHandler.collectFees.skip"]++; + return; + } + + console.log("OwnerHandler.collectFees(%18e)", feeAccrued); + + // Collect fees + uint256 fees = arm.collectFees(); + require(feeAccrued == fees, "OwnerHandler.collectFees() - Fees collected do not match fees accrued"); + + // Update sum of fees + sum_of_fees += fees; + } + + //////////////////////////////////////////////////// + /// --- ACTIONS + //////////////////////////////////////////////////// + function enoughLiquidityAvailable(uint256 amount) public view returns (bool) { + // The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue + uint256 outstandingWithdrawals = arm.withdrawsQueued() - arm.withdrawsClaimed(); + + // Save gas on an external balanceOf call if there are no outstanding withdrawals + if (outstandingWithdrawals == 0) return true; + + return amount + outstandingWithdrawals <= weth.balanceOf(address(arm)); + } +} diff --git a/test/invariants/handlers/SwapHandler.sol b/test/invariants/handlers/SwapHandler.sol new file mode 100644 index 0000000..5987b99 --- /dev/null +++ b/test/invariants/handlers/SwapHandler.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {console} from "forge-std/console.sol"; + +// Handlers +import {BaseHandler} from "./BaseHandler.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; + +/// @notice SwapHandler contract +/// @dev This contract is used to handle all functionnalities related to the swap in the ARM. +contract SwapHandler is BaseHandler { + //////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + //////////////////////////////////////////////////// + IERC20 public immutable weth; + IERC20 public immutable steth; + LidoARM public immutable arm; + + //////////////////////////////////////////////////// + /// --- VARIABLES + //////////////////////////////////////////////////// + address[] public swaps; // Users that perform swap + + //////////////////////////////////////////////////// + /// --- VARIABLES FOR INVARIANT ASSERTIONS + //////////////////////////////////////////////////// + uint256 public sum_of_weth_in; + uint256 public sum_of_weth_out; + uint256 public sum_of_steth_in; + uint256 public sum_of_steth_out; + + //////////////////////////////////////////////////// + /// --- EVENTS + //////////////////////////////////////////////////// + event GetAmountInMax(uint256 amount); + event GetAmountOutMax(uint256 amount); + event EstimateAmountIn(uint256 amount); + event EstimateAmountOut(uint256 amount); + + //////////////////////////////////////////////////// + /// --- CONSTRUCTOR + //////////////////////////////////////////////////// + constructor(address _arm, address _weth, address _steth, address[] memory _swaps) { + arm = LidoARM(payable(_arm)); + weth = IERC20(_weth); + steth = IERC20(_steth); + + require(_swaps.length > 0, "SH: EMPTY_SWAPS"); + swaps = _swaps; + + names[address(weth)] = "WETH"; + names[address(steth)] = "STETH"; + } + + //////////////////////////////////////////////////// + /// --- ACTIONS + //////////////////////////////////////////////////// + function swapExactTokensForTokens(uint256 _seed) external { + numberOfCalls["swapHandler.swapExact"]++; + + // Select an input token and build path + IERC20 inputToken = _seed % 2 == 0 ? weth : steth; + IERC20 outputToken = inputToken == weth ? steth : weth; + address[] memory path = new address[](2); + path[0] = address(inputToken); + path[1] = address(outputToken); + + // Select a random user thah have the input token. If no one, it will be skipped after. + address user; + uint256 len = swaps.length; + uint256 __seed = _bound(_seed, 0, type(uint256).max - len); + for (uint256 i; i < len; i++) { + user = swaps[(__seed + i) % len]; + if (inputToken.balanceOf(user) > 0) break; + } + + // Select a random amount, maximum is the minimum between the balance of the user and the liquidity available + uint256 amountIn = _bound(_seed, 0, min(inputToken.balanceOf(user), getAmountInMax(inputToken))); + uint256 estimatedAmountOut = estimateAmountOut(inputToken, amountIn); + + // Even this is possible in some case, there is not interest to swap 0 amount, so we skip it. + if (amountIn == 0) { + numberOfCalls["swapHandler.swapExact.skip"]++; + console.log("SwapHandler.swapExactTokensForTokens - Swapping 0 amount"); + return; + } + + console.log( + "SwapHandler.swapExactTokensForTokens(%18e), %s, %s", amountIn, names[user], names[address(inputToken)] + ); + + // Prank user + vm.startPrank(user); + + // Approve the ARM to spend the input token + inputToken.approve(address(arm), amountIn); + + // Swap + // Note: this implementation is prefered as it returns the amountIn of output tokens + uint256[] memory amounts = arm.swapExactTokensForTokens({ + amountIn: amountIn, + amountOutMin: estimatedAmountOut, + path: path, + to: address(user), + deadline: block.timestamp + 1 + }); + + // End prank + vm.stopPrank(); + + // Update sum of swaps + if (inputToken == weth) { + sum_of_weth_in += amounts[0]; + sum_of_steth_out += amounts[1]; + } else { + sum_of_steth_in += amounts[0]; + sum_of_weth_out += amounts[1]; + } + + require(amountIn == amounts[0], "SH: SWAP - INVALID_AMOUNT_IN"); + require(estimatedAmountOut == amounts[1], "SH: SWAP - INVALID_AMOUNT_OUT"); + } + + function swapTokensForExactTokens(uint256 _seed) external { + numberOfCalls["swapHandler.swapTokens"]++; + + // Select an input token and build path + IERC20 inputToken = _seed % 2 == 0 ? weth : steth; + IERC20 outputToken = inputToken == weth ? steth : weth; + address[] memory path = new address[](2); + path[0] = address(inputToken); + path[1] = address(outputToken); + + // Select a random user thah have the input token. If no one, it will be skipped after. + address user; + uint256 len = swaps.length; + uint256 __seed = _bound(_seed, 0, type(uint256).max - len); + for (uint256 i; i < len; i++) { + user = swaps[(__seed + i) % len]; + if (inputToken.balanceOf(user) > 0) break; + } + + // Select a random amount, maximum is the minimum between the balance of the user and the liquidity available + uint256 amountOut = _bound(_seed, 0, min(liquidityAvailable(outputToken), getAmountOutMax(outputToken, user))); + + // Even this is possible in some case, there is not interest to swap 0 amount, so we skip it. + // It could have been interesting to check it, to see what's happen if someone swap 0 and thus send 1 wei to the contract, + // but this will be tested with Donation Handler. So we skip it. + if (amountOut == 0) { + numberOfCalls["swapHandler.swapTokens.skip"]++; + console.log("SwapHandler.swapTokensForExactTokens - Swapping 0 amount"); + return; + } + + uint256 estimatedAmountIn = estimateAmountIn(outputToken, amountOut); + console.log( + "SwapHandler.swapTokensForExactTokens(%18e), %s, %s", + estimatedAmountIn, + names[user], + names[address(inputToken)] + ); + + // Prank user + vm.startPrank(user); + + // Approve the ARM to spend the input token + // Approve max, to avoid calculating the exact amount + inputToken.approve(address(arm), type(uint256).max); + + // Swap + // Note: this implementation is prefered as it returns the amountIn of output tokens + uint256[] memory amounts = arm.swapTokensForExactTokens({ + amountOut: amountOut, + amountInMax: type(uint256).max, + path: path, + to: address(user), + deadline: block.timestamp + 1 + }); + + // End prank + vm.stopPrank(); + + // Update sum of swaps + if (inputToken == weth) { + sum_of_weth_in += amounts[0]; + sum_of_steth_out += amounts[1]; + } else { + sum_of_steth_in += amounts[0]; + sum_of_weth_out += amounts[1]; + } + + require(estimatedAmountIn == amounts[0], "SH: SWAP - INVALID_AMOUNT_IN"); + require(amountOut == amounts[1], "SH: SWAP - INVALID_AMOUNT_OUT"); + } + + //////////////////////////////////////////////////// + /// --- HELPERS + //////////////////////////////////////////////////// + /// @notice Helpers to calcul the maximum amountIn of token that we can use as input in swapExactTokensForTokens. + /// @dev Depends on the reserve of the output token in ARM and the price of the input token. + function getAmountInMax(IERC20 tokenIn) public returns (uint256) { + IERC20 tokenOut = tokenIn == weth ? steth : weth; + + uint256 reserveOut = liquidityAvailable(tokenOut); + + uint256 amount = (reserveOut * arm.PRICE_SCALE()) / price(tokenIn); + + // Emit event to see it directly in logs + emit GetAmountInMax(amount); + + return amount; + } + + /// @notice Helpers to calcul the maximum amountOut of token that we can use as input in swapTokensForExactTokens. + /// @dev Depends on the reserve of the input token of user and the price of the output token. + function getAmountOutMax(IERC20 tokenOut, address user) public returns (uint256) { + IERC20 tokenIn = tokenOut == weth ? steth : weth; + + uint256 reserveUser = tokenIn.balanceOf(user); + if (reserveUser < 3) return 0; + + uint256 amount = ((reserveUser - 3) * price(tokenIn)) / arm.PRICE_SCALE(); + + // Emit event to see it directly in logs + emit GetAmountOutMax(amount); + + return amount; + } + + /// @notice Helpers to calcul the expected amountIn of tokenIn used in swapTokensForExactTokens. + function estimateAmountIn(IERC20 tokenOut, uint256 amountOut) public returns (uint256) { + IERC20 tokenIn = tokenOut == weth ? steth : weth; + + uint256 amountIn = (amountOut * arm.PRICE_SCALE()) / price(tokenIn) + 3; + + // Emit event to see it directly in logs + emit EstimateAmountIn(amountIn); + + return amountIn; + } + + /// @notice Helpers to calcul the expected amountOut of tokenOut used in swapExactTokensForTokens. + function estimateAmountOut(IERC20 tokenIn, uint256 amountIn) public returns (uint256) { + uint256 amountOut = (amountIn * price(tokenIn)) / arm.PRICE_SCALE(); + + // Emit event to see it directly in logs + emit EstimateAmountOut(amountOut); + + return amountOut; + } + + /// @notice Helpers to calcul the liquidity available for a token, especially for WETH and withdraw queue. + function liquidityAvailable(IERC20 token) public view returns (uint256 liquidity) { + if (token == weth) { + uint256 outstandingWithdrawals = arm.withdrawsQueued() - arm.withdrawsClaimed(); + uint256 reserve = weth.balanceOf(address(arm)); + if (outstandingWithdrawals > reserve) return 0; + return reserve - outstandingWithdrawals; + } else if (token == steth) { + return steth.balanceOf(address(arm)); + } + } + + /// @notice Helpers to get the price of a token in the ARM. + function price(IERC20 token) public view returns (uint256) { + return token == arm.token0() ? arm.traderate0() : arm.traderate1(); + } +} diff --git a/test/invariants/mocks/MockLidoWithdraw.sol b/test/invariants/mocks/MockLidoWithdraw.sol new file mode 100644 index 0000000..b69cfe2 --- /dev/null +++ b/test/invariants/mocks/MockLidoWithdraw.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {Vm} from "forge-std/Vm.sol"; + +// Solmate +import {ERC20} from "@solmate/tokens/ERC20.sol"; + +contract MockLidoWithdraw { + ////////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + ////////////////////////////////////////////////////// + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + ////////////////////////////////////////////////////// + /// --- STRUCTS & ENUMS + ////////////////////////////////////////////////////// + struct Request { + bool claimed; + address owner; + uint256 amount; + } + + ////////////////////////////////////////////////////// + /// --- VARIABLES + ////////////////////////////////////////////////////// + ERC20 public steth; + + uint256 public counter; + + // Request Id -> Request struct + mapping(uint256 => Request) public requests; + + ////////////////////////////////////////////////////// + /// --- CONSTRUCTOR + ////////////////////////////////////////////////////// + constructor(address _steth) { + steth = ERC20(_steth); + } + + ////////////////////////////////////////////////////// + /// --- FUNCTIONS + ////////////////////////////////////////////////////// + function requestWithdrawals(uint256[] memory amounts, address owner) external returns (uint256[] memory) { + uint256 len = amounts.length; + uint256[] memory userRequests = new uint256[](len); + + for (uint256 i; i < len; i++) { + require(amounts[i] <= 1_000 ether, "Mock LW: Withdraw amount too big"); + + // Due to rounding error issue, we need to check balance before and after. + uint256 balBefore = steth.balanceOf(address(this)); + steth.transferFrom(msg.sender, address(this), amounts[i]); + uint256 amount = steth.balanceOf(address(this)) - balBefore; + + // Update request mapping + requests[counter] = Request({claimed: false, owner: owner, amount: amount}); + userRequests[i] = counter; + // Increase request count + counter++; + } + + return userRequests; + } + + function claimWithdrawals(uint256[] memory requestId, uint256[] memory) external { + uint256 sum; + uint256 len = requestId.length; + for (uint256 i; i < len; i++) { + // Cache id + uint256 id = requestId[i]; + + // Ensure msg.sender is the owner + require(requests[id].owner == msg.sender, "Mock LW: Not owner"); + requests[id].claimed = true; + sum += requests[id].amount; + } + + // Send sum of eth + vm.deal(address(msg.sender), address(msg.sender).balance + sum); + } + + function getLastCheckpointIndex() external returns (uint256) {} + + function findCheckpointHints(uint256[] memory, uint256, uint256) external returns (uint256[] memory) {} +} diff --git a/test/invariants/mocks/MockSTETH.sol b/test/invariants/mocks/MockSTETH.sol new file mode 100644 index 0000000..ca5f3e1 --- /dev/null +++ b/test/invariants/mocks/MockSTETH.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Vm} from "forge-std/Vm.sol"; + +import {ERC20} from "@solmate/tokens/ERC20.sol"; + +contract MockSTETH is ERC20 { + ////////////////////////////////////////////////////// + /// --- CONSTANTS & IMMUTABLES + ////////////////////////////////////////////////////// + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + ////////////////////////////////////////////////////// + /// --- VARIABLES + ////////////////////////////////////////////////////// + uint256 public sum_of_errors; + + ////////////////////////////////////////////////////// + /// --- CONSTRUCTOR + ////////////////////////////////////////////////////// + constructor() ERC20("Liquid staked Ether 2.0", "stETH", 18) {} + + ////////////////////////////////////////////////////// + /// --- FUNCTIONS + ////////////////////////////////////////////////////// + function transfer(address to, uint256 amount) public override returns (bool) { + return super.transfer(to, brutalizeAmount(amount)); + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + return super.transferFrom(from, to, brutalizeAmount(amount)); + } + + function brutalizeAmount(uint256 amount) public returns (uint256) { + // Only brutalize the sender doesn't sent all of their balance + if (balanceOf[msg.sender] != amount && amount > 0) { + // Get a random number between 0 and 1 + uint256 randomUint = vm.randomUint(0, 1); + // If the amount is greater than the random number, subtract the random number from the amount + if (amount > randomUint) { + amount -= randomUint; + sum_of_errors += randomUint; + } + } + return amount; + } +} diff --git a/test/invariants/shared/Shared.sol b/test/invariants/shared/Shared.sol new file mode 100644 index 0000000..53a4463 --- /dev/null +++ b/test/invariants/shared/Shared.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Base_Test_} from "test/Base.sol"; + +// Contracts +import {Proxy} from "contracts/Proxy.sol"; +import {LidoARM} from "contracts/LidoARM.sol"; +import {CapManager} from "contracts/CapManager.sol"; +import {WETH} from "@solmate/tokens/WETH.sol"; + +// Mocks +import {MockSTETH} from "../mocks/MockSTETH.sol"; +import {MockLidoWithdraw} from "../mocks/MockLidoWithdraw.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +/// @notice Shared invariant test contract +/// @dev This contract should be used for deploying all contracts and mocks needed for the test. +abstract contract Invariant_Shared_Test_ is Base_Test_ { + address[] public users; + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + + function setUp() public virtual override { + super.setUp(); + + // 1. Setup a realistic test environnement, not needed as not time related. + // _setUpRealisticEnvironnement() + + // 2. Create user + _createUsers(); + + // To increase performance, we will not use fork., mocking contract instead. + // 3. Deploy mocks. + _deployMocks(); + + // 4. Deploy contracts. + _deployContracts(); + + // 5. Label addresses + labelAll(); + } + + function _setUpRealisticEnvironnement() private { + vm.warp(1000); + vm.roll(1000); + } + + function _createUsers() private { + // Users with role + deployer = makeAddr("Deployer"); + governor = makeAddr("Governor"); + operator = makeAddr("Operator"); + feeCollector = makeAddr("Fee Collector"); + + // Random users + alice = makeAddr("Alice"); + bob = makeAddr("Bob"); + charlie = makeAddr("Charlie"); + dave = makeAddr("Dave"); + eve = makeAddr("Eve"); + frank = makeAddr("Frank"); + george = makeAddr("George"); + harry = makeAddr("Harry"); + + // Add users to the list + users.push(alice); + users.push(bob); + users.push(charlie); + users.push(dave); + users.push(eve); + users.push(frank); + users.push(george); + users.push(harry); + } + + ////////////////////////////////////////////////////// + /// --- MOCKS + ////////////////////////////////////////////////////// + function _deployMocks() private { + // WETH + weth = IERC20(address(new WETH())); + + // STETH + steth = IERC20(address(new MockSTETH())); + + // Lido Withdraw + lidoWithdraw = address(new MockLidoWithdraw(address(steth))); + } + + ////////////////////////////////////////////////////// + /// --- CONTRACTS + ////////////////////////////////////////////////////// + function _deployContracts() private { + vm.startPrank(deployer); + + // 1. Deploy all proxies. + _deployProxies(); + + // 2. Deploy Liquidity Provider Controller. + _deployLPC(); + + // 3. Deploy Lido ARM. + _deployLidoARM(); + + vm.stopPrank(); + } + + function _deployProxies() private { + lpcProxy = new Proxy(); + lidoProxy = new Proxy(); + } + + function _deployLPC() private { + // Deploy CapManager implementation. + CapManager lpcImpl = new CapManager(address(lidoProxy)); + + // Initialize Proxy with CapManager implementation. + bytes memory data = abi.encodeWithSignature("initialize(address)", operator); + lpcProxy.initialize(address(lpcImpl), address(this), data); + + // Set the Proxy as the CapManager. + capManager = CapManager(payable(address(lpcProxy))); + } + + function _deployLidoARM() private { + // Deploy LidoARM implementation. + LidoARM lidoImpl = new LidoARM(address(steth), address(weth), lidoWithdraw, 10 minutes); + + // Deployer will need WETH to initialize the ARM. + deal(address(weth), address(deployer), MIN_TOTAL_SUPPLY); + weth.approve(address(lidoProxy), MIN_TOTAL_SUPPLY); + + // Initialize Proxy with LidoARM implementation. + bytes memory data = abi.encodeWithSignature( + "initialize(string,string,address,uint256,address,address)", + "Lido ARM", + "ARM-ST", + operator, + 2000, // 20% performance fee + feeCollector, + address(lpcProxy) + ); + lidoProxy.initialize(address(lidoImpl), address(this), data); + + // Set the Proxy as the LidoARM. + lidoARM = LidoARM(payable(address(lidoProxy))); + } +} From 4839ca6f4a201194dbe1de42e890f03d5896ee7e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 16 Oct 2024 18:38:59 +1100 Subject: [PATCH 192/196] minor change to remove Solidity warning --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index aaaa493..df02dcc 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -114,9 +114,8 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("LidoARM initialize data:"); console.logBytes(data); - uint256 tinyMintAmount = 1e12; - // Get some WETH which has already been done on mainnet + // uint256 tinyMintAmount = 1e12; // vm.deal(Mainnet.ARM_MULTISIG, tinyMintAmount); // IWETH(Mainnet.WETH).deposit{value: tinyMintAmount}(); From d0bd97a7d60ddec124e78223029c8e1ea5ac8e0f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 16 Oct 2024 19:33:59 +1100 Subject: [PATCH 193/196] Updated prices in deploy script --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index df02dcc..cb2298d 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -67,11 +67,6 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { _recordDeploy("LIDO_ARM_ZAPPER", address(zapper)); console.log("Finished deploying", DEPLOY_NAME); - - // Post deploy - // 1. The Lido ARM multisig needs to set the owner to the mainnet 5/8 multisig - // 1. The mainnet 5/8 multisig needs to upgrade and call initialize on the Lido ARM - // 2. the Relayer needs to set the swap prices } function _buildGovernanceProposal() internal override {} @@ -131,10 +126,10 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { console.log("About to set the cross price on the ARM contract"); LidoARM(payable(Mainnet.LIDO_ARM)).setCrossPrice(0.9998e36); - // Set the buy price with a 4 basis point discount. + // Set the buy price with a 2.5 basis point discount. // The sell price has a 1 basis point discount. console.log("About to set prices on the ARM contract"); - LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.9996e36, 0.9999e36); + LidoARM(payable(Mainnet.LIDO_ARM)).setPrices(0.99975e36, 0.9999e36); // transfer ownership of the Lido ARM proxy to the mainnet 5/8 multisig console.log("About to set ARM owner to", Mainnet.GOV_MULTISIG); From eaabf905a1206dc29474f011f3f604ab838abf45 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 16 Oct 2024 21:08:17 +1100 Subject: [PATCH 194/196] Change tx order in deploy script --- script/deploy/mainnet/003_UpgradeLidoARMScript.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol index cb2298d..3e390e7 100644 --- a/script/deploy/mainnet/003_UpgradeLidoARMScript.sol +++ b/script/deploy/mainnet/003_UpgradeLidoARMScript.sol @@ -53,14 +53,14 @@ contract UpgradeLidoARMMainnetScript is AbstractDeployScript { // 5. Set total assets cap capManager.setTotalAssetsCap(740 ether); - // 6. Deploy Lido implementation + // 6. Transfer ownership of CapManager to the mainnet 5/8 multisig + capManProxy.setOwner(Mainnet.GOV_MULTISIG); + + // 7. Deploy Lido implementation uint256 claimDelay = tenderlyTestnet ? 1 minutes : 10 minutes; lidoARMImpl = new LidoARM(Mainnet.STETH, Mainnet.WETH, Mainnet.LIDO_WITHDRAWAL, claimDelay); _recordDeploy("LIDO_ARM_IMPL", address(lidoARMImpl)); - // 7. Transfer ownership of CapManager to the mainnet 5/8 multisig - capManProxy.setOwner(Mainnet.GOV_MULTISIG); - // 8. Deploy the Zapper zapper = new ZapperLidoARM(Mainnet.WETH, Mainnet.LIDO_ARM); zapper.setOwner(Mainnet.STRATEGIST); From fa3be77646fc64f318440d4fdc821af5a7540664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Wed, 16 Oct 2024 19:50:36 +0200 Subject: [PATCH 195/196] Fix contract verification & update Makefile (#37) * fix: remove etherscan setting from config. * chore: update forge version. * chore: exclude invariant from classic test. --- Makefile | 12 +++++++++--- foundry.toml | 7 ++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index d125703..886fa46 100644 --- a/Makefile +++ b/Makefile @@ -26,14 +26,20 @@ snapshot: @forge snapshot # Tests +test-std: + forge test --summary --fail-fast --show-progress + test: - @forge test --summary --fail-fast --show-progress + @FOUNDRY_NO_MATCH_CONTRACT=Invariant make test-std test-f-%: - @FOUNDRY_MATCH_TEST=$* make test + @FOUNDRY_MATCH_TEST=$* make test-std test-c-%: - @FOUNDRY_MATCH_CONTRACT=$* make test + @FOUNDRY_MATCH_CONTRACT=$* make test-std + +test-all: + @make test-std # Coverage coverage: diff --git a/foundry.toml b/foundry.toml index 04bad5f..a576e61 100644 --- a/foundry.toml +++ b/foundry.toml @@ -15,7 +15,7 @@ remappings = [ "script/=./script", "test/=./test", "utils/=./src/contracts/utils", - "forge-std/=dependencies/forge-std-1.9.2/src/", + "forge-std/=dependencies/forge-std-1.9.3/src/", "@openzeppelin/contracts/=dependencies/@openzeppelin-contracts-5.0.2/", "@openzeppelin/contracts-upgradeable/=dependencies/@openzeppelin-contracts-upgradeable-5.0.2/", "@solmate/=dependencies/solmate-6.7.0/src/", @@ -33,8 +33,8 @@ shrink_run_limit = 5_000 [dependencies] "@openzeppelin-contracts" = "5.0.2" "@openzeppelin-contracts-upgradeable" = "5.0.2" -forge-std = { version = "1.9.2", git = "https://github.com/foundry-rs/forge-std.git", rev = "5a802d7c10abb4bbfb3e7214c75052ef9e6a06f8" } solmate = "6.7.0" +forge-std = "1.9.3" [soldeer] recursive_deps = false @@ -47,7 +47,4 @@ remappings_location = "config" [rpc_endpoints] mainnet = "${PROVIDER_URL}" -[etherscan] -mainnet = { key = "${ETHERSCAN_API_KEY}", url = "https://etherscan.io/", chain = "mainnet" } - # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options From f330e03feb6a4ed913e183bfdf5a02b6fdc57ce1 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Fri, 18 Oct 2024 06:08:09 +1100 Subject: [PATCH 196/196] Deployed Lido ARM contracts (#36) --- build/deployments-1.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build/deployments-1.json b/build/deployments-1.json index 09445e3..0d4b24f 100644 --- a/build/deployments-1.json +++ b/build/deployments-1.json @@ -1,9 +1,15 @@ { "executions": { "001_CoreMainnet": 1723685111, - "002_UpgradeMainnet": 1726812322 + "002_UpgradeMainnet": 1726812322, + "003_UpgradeLidoARMScript": 1729073099 }, "contracts": { + "LIDO_ARM": "0x85B78AcA6Deae198fBF201c82DAF6Ca21942acc6", + "LIDO_ARM_CAP_IMPL": "0x8506486813d025C5935dF481E450e27D2e483dc9", + "LIDO_ARM_CAP_MAN": "0xf54ebff575f699d281645c6F14Fe427dFFE629CF", + "LIDO_ARM_IMPL": "0x3d724176c8f1F965eF4020CB5DA5ad1a891BEEf1", + "LIDO_ARM_ZAPPER": "0x01F30B7358Ba51f637d1aa05D9b4A60f76DAD680", "OETH_ARM": "0x6bac785889A4127dB0e0CeFEE88E0a9F1Aaf3cC7", "OETH_ARM_IMPL": "0x187FfF686a5f42ACaaF56469FcCF8e6Feca18248" }