Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/nicka/lp' into clement/invariant…
Browse files Browse the repository at this point in the history
…-testing
  • Loading branch information
clement-ux committed Oct 15, 2024
2 parents b67985a + 18b96c8 commit 23d45e6
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 210 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
139 changes: 73 additions & 66 deletions docs/LidoARMPublicSquashed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 64 additions & 65 deletions docs/LidoARMSquashed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 36 additions & 19 deletions src/contracts/AbstractARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -72,21 +74,21 @@ 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;
bool claimed;
// 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.
Expand All @@ -106,7 +108,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
Expand Down Expand Up @@ -163,13 +165,19 @@ 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
// 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()));
Expand Down Expand Up @@ -380,8 +388,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);
}
Expand All @@ -390,21 +398,30 @@ 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.
*/
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");

// If the new cross price is lower than the current cross price
// 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
// 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);
Expand All @@ -430,7 +447,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
Expand Down Expand Up @@ -478,9 +495,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;

Expand All @@ -491,7 +508,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
withdrawer: msg.sender,
claimed: false,
claimTimestamp: claimTimestamp,
assets: SafeCast.toUint120(assets),
assets: SafeCast.toUint128(assets),
queued: queued
});

Expand All @@ -508,7 +525,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");
Expand All @@ -522,7 +539,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);
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/LidoARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
95 changes: 95 additions & 0 deletions test/fork/LidoFixedPriceMultiLpARM/SetCrossPrice.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
60 changes: 2 additions & 58 deletions test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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
//////////////////////////////////////////////////////
Expand Down

0 comments on commit 23d45e6

Please sign in to comment.