diff --git a/README.md b/README.md index 3dfd5517..8dd875e5 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ This is a linear rate limit that increases over time with a maximum limit. This Below are all stated trust assumptions for using this contract in production: - The `DEFAULT_ADMIN_ROLE` is fully trusted, to be run by governance. - The `RELAYER` role is assumed to be able to be fully compromised by a malicious actor. **This should be a major consideration during auditing engagements.** - - The logic in the smart contracts must prevent the movement of value anywhere outside of the ALM system of contracts. + - The logic in the smart contracts must prevent the movement of value anywhere outside of the ALM system of contracts. The exception for this is in asynchronous style integrations such as BUIDL, where `transferAsset` can be used to send funds to a whitelisted address. LP tokens are then asynchronously minted into the ALMProxy in a separate transaction. - Any action must be limited to "reasonable" slippage/losses/opportunity cost by rate limits. - The `FREEZER` must be able to stop the compromised `RELAYER` from performing more harmful actions within the max rate limits by using the `removeRelayer` function. - A compromised `RELAYER` can perform DOS attacks. These attacks along with their respective recovery procedures are outlined in the `Attacks.t.sol` test files. diff --git a/audits/20250225-chainsecurity-audit.pdf b/audits/20250225-chainsecurity-audit.pdf new file mode 100644 index 00000000..1ccf6b88 Binary files /dev/null and b/audits/20250225-chainsecurity-audit.pdf differ diff --git a/audits/20250227-cantina-audit.pdf b/audits/20250227-cantina-audit.pdf new file mode 100644 index 00000000..42288dba Binary files /dev/null and b/audits/20250227-cantina-audit.pdf differ diff --git a/foundry.toml b/foundry.toml index 3cbbdf2c..25ddb6ff 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ out = "out" libs = ["lib"] solc_version = '0.8.25' optimizer = true -optimizer_runs = 200 +optimizer_runs = 1 fs_permissions = [ { access = "read", path = "./script/input/"}, { access = "read-write", path = "./script/output/"} @@ -14,6 +14,10 @@ evm_version = 'cancun' [fuzz] runs = 1000 +[invariant] +runs = 1 +depth = 100 + [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY}" } optimism = { key = "${OPTIMISMSCAN_API_KEY}" } diff --git a/src/MainnetController.sol b/src/MainnetController.sol index 38e4cd62..b8900c2e 100644 --- a/src/MainnetController.sol +++ b/src/MainnetController.sol @@ -29,8 +29,33 @@ interface IBuidlRedeemLike { function redeem(uint256 usdcAmount) external; } +interface ICurvePoolLike is IERC20 { + function add_liquidity( + uint256[] memory amounts, + uint256 minMintAmount, + address receiver + ) external; + function balances(uint256 index) external view returns (uint256); + function coins(uint256 index) external returns (address); + function exchange( + int128 inputIndex, + int128 outputIndex, + uint256 amountIn, + uint256 minAmountOut, + address receiver + ) external returns (uint256 tokensOut); + function get_virtual_price() external view returns (uint256); + function N_COINS() external view returns (uint256); + function remove_liquidity( + uint256 burnAmount, + uint256[] memory minAmounts, + address receiver + ) external; + function stored_rates() external view returns (uint256[] memory); +} + interface IDaiUsdsLike { - function dai() external view returns(address); + function dai() external view returns (address); function daiToUsds(address usr, uint256 wad) external; function usdsToDai(address usr, uint256 wad) external; } @@ -57,7 +82,7 @@ interface IMapleTokenLike is IERC4626 { interface IPSMLike { function buyGemNoFee(address usr, uint256 usdcAmount) external returns (uint256 usdsAmount); function fill() external returns (uint256 wad); - function gem() external view returns(address); + function gem() external view returns (address); function sellGemNoFee(address usr, uint256 usdcAmount) external returns (uint256 usdsAmount); function to18ConversionFactor() external view returns (uint256); } @@ -79,7 +104,7 @@ interface IUSTBLike is IERC20 { } interface IVaultLike { - function buffer() external view returns(address); + function buffer() external view returns (address); function draw(uint256 usdsAmount) external; function wipe(uint256 usdsAmount) external; } @@ -98,8 +123,8 @@ contract MainnetController is AccessControl { uint256 usdcAmount ); + event MaxSlippageSet(address indexed pool, uint256 maxSlippage); event MintRecipientSet(uint32 indexed destinationDomain, bytes32 mintRecipient); - event RelayerRemoved(address indexed relayer); /**********************************************************************************************/ @@ -117,6 +142,9 @@ contract MainnetController is AccessControl { bytes32 public constant LIMIT_AAVE_WITHDRAW = keccak256("LIMIT_AAVE_WITHDRAW"); bytes32 public constant LIMIT_ASSET_TRANSFER = keccak256("LIMIT_ASSET_TRANSFER"); bytes32 public constant LIMIT_BUIDL_REDEEM_CIRCLE = keccak256("LIMIT_BUIDL_REDEEM_CIRCLE"); + bytes32 public constant LIMIT_CURVE_DEPOSIT = keccak256("LIMIT_CURVE_DEPOSIT"); + bytes32 public constant LIMIT_CURVE_SWAP = keccak256("LIMIT_CURVE_SWAP"); + bytes32 public constant LIMIT_CURVE_WITHDRAW = keccak256("LIMIT_CURVE_WITHDRAW"); bytes32 public constant LIMIT_MAPLE_REDEEM = keccak256("LIMIT_MAPLE_REDEEM"); bytes32 public constant LIMIT_SUPERSTATE_REDEEM = keccak256("LIMIT_SUPERSTATE_REDEEM"); bytes32 public constant LIMIT_SUPERSTATE_SUBSCRIBE = keccak256("LIMIT_SUPERSTATE_SUBSCRIBE"); @@ -149,6 +177,8 @@ contract MainnetController is AccessControl { uint256 public immutable psmTo18ConversionFactor; + mapping(address pool => uint256 maxSlippage) public maxSlippages; // 1e18 precision + mapping(uint32 destinationDomain => bytes32 mintRecipient) public mintRecipients; /**********************************************************************************************/ @@ -188,50 +218,28 @@ contract MainnetController is AccessControl { psmTo18ConversionFactor = psm.to18ConversionFactor(); } - /**********************************************************************************************/ - /*** Modifiers ***/ - /**********************************************************************************************/ - - modifier rateLimited(bytes32 key, uint256 amount) { - rateLimits.triggerRateLimitDecrease(key, amount); - _; - } - - modifier rateLimitedAsset(bytes32 key, address asset, uint256 amount) { - rateLimits.triggerRateLimitDecrease(RateLimitHelpers.makeAssetKey(key, asset), amount); - _; - } - - modifier cancelRateLimit(bytes32 key, uint256 amount) { - rateLimits.triggerRateLimitIncrease(key, amount); - _; - } - - modifier rateLimitExists(bytes32 key) { - require( - rateLimits.getRateLimitData(key).maxAmount > 0, - "MainnetController/invalid-action" - ); - _; - } - /**********************************************************************************************/ /*** Admin functions ***/ /**********************************************************************************************/ - function setMintRecipient(uint32 destinationDomain, bytes32 mintRecipient) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { + function setMintRecipient(uint32 destinationDomain, bytes32 mintRecipient) external { + _checkRole(DEFAULT_ADMIN_ROLE); mintRecipients[destinationDomain] = mintRecipient; emit MintRecipientSet(destinationDomain, mintRecipient); } + function setMaxSlippage(address pool, uint256 maxSlippage) external { + _checkRole(DEFAULT_ADMIN_ROLE); + maxSlippages[pool] = maxSlippage; + emit MaxSlippageSet(pool, maxSlippage); + } + /**********************************************************************************************/ /*** Freezer functions ***/ /**********************************************************************************************/ - function removeRelayer(address relayer) external onlyRole(FREEZER) { + function removeRelayer(address relayer) external { + _checkRole(FREEZER); _revokeRole(RELAYER, relayer); emit RelayerRemoved(relayer); } @@ -240,11 +248,10 @@ contract MainnetController is AccessControl { /*** Relayer vault functions ***/ /**********************************************************************************************/ - function mintUSDS(uint256 usdsAmount) - external - onlyRole(RELAYER) - rateLimited(LIMIT_USDS_MINT, usdsAmount) - { + function mintUSDS(uint256 usdsAmount) external { + _checkRole(RELAYER); + _rateLimited(LIMIT_USDS_MINT, usdsAmount); + // Mint USDS into the buffer proxy.doCall( address(vault), @@ -258,11 +265,10 @@ contract MainnetController is AccessControl { ); } - function burnUSDS(uint256 usdsAmount) - external - onlyRole(RELAYER) - cancelRateLimit(LIMIT_USDS_MINT, usdsAmount) - { + function burnUSDS(uint256 usdsAmount) external { + _checkRole(RELAYER); + _cancelRateLimit(LIMIT_USDS_MINT, usdsAmount); + // Transfer USDS from the proxy to the buffer proxy.doCall( address(usds), @@ -280,14 +286,13 @@ contract MainnetController is AccessControl { /*** Relayer ERC20 functions ***/ /**********************************************************************************************/ - function transferAsset(address asset, address destination, uint256 amount) - external - onlyRole(RELAYER) - rateLimited( + function transferAsset(address asset, address destination, uint256 amount) external { + _checkRole(RELAYER); + _rateLimited( RateLimitHelpers.makeAssetDestinationKey(LIMIT_ASSET_TRANSFER, asset, destination), amount - ) - { + ); + proxy.doCall( asset, abi.encodeCall(IERC20(asset).transfer, (destination, amount)) @@ -298,12 +303,10 @@ contract MainnetController is AccessControl { /*** Relayer ERC4626 functions ***/ /**********************************************************************************************/ - function depositERC4626(address token, uint256 amount) - external - onlyRole(RELAYER) - rateLimitedAsset(LIMIT_4626_DEPOSIT, token, amount) - returns (uint256 shares) - { + function depositERC4626(address token, uint256 amount) external returns (uint256 shares) { + _checkRole(RELAYER); + _rateLimitedAsset(LIMIT_4626_DEPOSIT, token, amount); + // Note that whitelist is done by rate limits IERC20 asset = IERC20(IERC4626(token).asset()); @@ -320,12 +323,10 @@ contract MainnetController is AccessControl { ); } - function withdrawERC4626(address token, uint256 amount) - external - onlyRole(RELAYER) - rateLimitedAsset(LIMIT_4626_WITHDRAW, token, amount) - returns (uint256 shares) - { + function withdrawERC4626(address token, uint256 amount) external returns (uint256 shares) { + _checkRole(RELAYER); + _rateLimitedAsset(LIMIT_4626_WITHDRAW, token, amount); + // Withdraw asset from a token, decode resulting shares. // Assumes proxy has adequate token shares. shares = abi.decode( @@ -338,11 +339,9 @@ contract MainnetController is AccessControl { } // NOTE: !!! Rate limited at end of function !!! - function redeemERC4626(address token, uint256 shares) - external - onlyRole(RELAYER) - returns (uint256 assets) - { + function redeemERC4626(address token, uint256 shares) external returns (uint256 assets) { + _checkRole(RELAYER); + // Redeem shares for assets from the token, decode the resulting assets. // Assumes proxy has adequate token shares. assets = abi.decode( @@ -363,11 +362,10 @@ contract MainnetController is AccessControl { /*** Relayer ERC7540 functions ***/ /**********************************************************************************************/ - function requestDepositERC7540(address token, uint256 amount) - external - onlyRole(RELAYER) - rateLimitedAsset(LIMIT_7540_DEPOSIT, token, amount) - { + function requestDepositERC7540(address token, uint256 amount) external { + _checkRole(RELAYER); + _rateLimitedAsset(LIMIT_7540_DEPOSIT, token, amount); + // Note that whitelist is done by rate limits IERC20 asset = IERC20(IERC7540(token).asset()); @@ -381,11 +379,10 @@ contract MainnetController is AccessControl { ); } - function claimDepositERC7540(address token) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token)) - { + function claimDepositERC7540(address token) external { + _checkRole(RELAYER); + _rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token)); + uint256 shares = IERC7540(token).maxMint(address(proxy)); // Claim shares from the vault to the proxy @@ -395,15 +392,14 @@ contract MainnetController is AccessControl { ); } - function requestRedeemERC7540(address token, uint256 shares) - external - onlyRole(RELAYER) - rateLimitedAsset( + function requestRedeemERC7540(address token, uint256 shares) external { + _checkRole(RELAYER); + _rateLimitedAsset( LIMIT_7540_REDEEM, token, IERC7540(token).convertToAssets(shares) - ) - { + ); + // Submit redeem request by transferring shares proxy.doCall( token, @@ -411,11 +407,10 @@ contract MainnetController is AccessControl { ); } - function claimRedeemERC7540(address token) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token)) - { + function claimRedeemERC7540(address token) external { + _checkRole(RELAYER); + _rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token)); + uint256 assets = IERC7540(token).maxWithdraw(address(proxy)); // Claim assets from the vault to the proxy @@ -433,11 +428,10 @@ contract MainnetController is AccessControl { uint256 CENTRIFUGE_REQUEST_ID = 0; - function cancelCentrifugeDepositRequest(address token) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token)) - { + function cancelCentrifugeDepositRequest(address token) external { + _checkRole(RELAYER); + _rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token)); + // NOTE: While the cancelation is pending, no new deposit request can be submitted proxy.doCall( token, @@ -448,11 +442,10 @@ contract MainnetController is AccessControl { ); } - function claimCentrifugeCancelDepositRequest(address token) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token)) - { + function claimCentrifugeCancelDepositRequest(address token) external { + _checkRole(RELAYER); + _rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token)); + proxy.doCall( token, abi.encodeCall( @@ -462,11 +455,10 @@ contract MainnetController is AccessControl { ); } - function cancelCentrifugeRedeemRequest(address token) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token)) - { + function cancelCentrifugeRedeemRequest(address token) external { + _checkRole(RELAYER); + _rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token)); + // NOTE: While the cancelation is pending, no new redeem request can be submitted proxy.doCall( token, @@ -477,11 +469,10 @@ contract MainnetController is AccessControl { ); } - function claimCentrifugeCancelRedeemRequest(address token) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token)) - { + function claimCentrifugeCancelRedeemRequest(address token) external { + _checkRole(RELAYER); + _rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token)); + proxy.doCall( token, abi.encodeCall( @@ -495,11 +486,10 @@ contract MainnetController is AccessControl { /*** Relayer Aave functions ***/ /**********************************************************************************************/ - function depositAave(address aToken, uint256 amount) - external - onlyRole(RELAYER) - rateLimitedAsset(LIMIT_AAVE_DEPOSIT, aToken, amount) - { + function depositAave(address aToken, uint256 amount) external { + _checkRole(RELAYER); + _rateLimitedAsset(LIMIT_AAVE_DEPOSIT, aToken, amount); + IERC20 underlying = IERC20(IATokenWithPool(aToken).UNDERLYING_ASSET_ADDRESS()); IAavePool pool = IAavePool(IATokenWithPool(aToken).POOL()); @@ -516,9 +506,10 @@ contract MainnetController is AccessControl { // NOTE: !!! Rate limited at end of function !!! function withdrawAave(address aToken, uint256 amount) external - onlyRole(RELAYER) returns (uint256 amountWithdrawn) { + _checkRole(RELAYER); + IAavePool pool = IAavePool(IATokenWithPool(aToken).POOL()); // Withdraw underlying from Aave pool, decode resulting amount withdrawn. @@ -544,11 +535,10 @@ contract MainnetController is AccessControl { /*** Relayer BlackRock BUIDL functions ***/ /**********************************************************************************************/ - function redeemBUIDLCircleFacility(uint256 usdcAmount) - external - onlyRole(RELAYER) - rateLimited(LIMIT_BUIDL_REDEEM_CIRCLE, usdcAmount) - { + function redeemBUIDLCircleFacility(uint256 usdcAmount) external { + _checkRole(RELAYER); + _rateLimited(LIMIT_BUIDL_REDEEM_CIRCLE, usdcAmount); + _approve(address(buidlRedeem.asset()), address(buidlRedeem), usdcAmount); proxy.doCall( @@ -557,18 +547,225 @@ contract MainnetController is AccessControl { ); } + /**********************************************************************************************/ + /*** Relayer Curve StableSwap functions ***/ + /**********************************************************************************************/ + + function swapCurve( + address pool, + uint256 inputIndex, + uint256 outputIndex, + uint256 amountIn, + uint256 minAmountOut + ) + external returns (uint256 amountOut) + { + _checkRole(RELAYER); + + require(inputIndex != outputIndex, "MainnetController/invalid-indices"); + + uint256 maxSlippage = maxSlippages[pool]; + require(maxSlippage != 0, "MainnetController/max-slippage-not-set"); + + ICurvePoolLike curvePool = ICurvePoolLike(pool); + + uint256 numCoins = curvePool.N_COINS(); + require( + inputIndex < numCoins && outputIndex < numCoins, + "MainnetController/index-too-high" + ); + + // Normalized to provide 36 decimal precision when multiplied by asset amount + uint256[] memory rates = curvePool.stored_rates(); + + // Below code is simplified from the following logic. + // `maxSlippage` was multipled first to avoid precision loss. + // valueIn = amountIn * rates[inputIndex] / 1e18 // 18 decimal precision, USD + // tokensOut = valueIn * 1e18 / rates[outputIndex] // Token precision, token amount + // result = tokensOut * maxSlippage / 1e18 + uint256 minimumMinAmountOut = amountIn + * rates[inputIndex] + * maxSlippage + / rates[outputIndex] + / 1e18; + + require( + minAmountOut >= minimumMinAmountOut, + "MainnetController/min-amount-not-met" + ); + + rateLimits.triggerRateLimitDecrease( + RateLimitHelpers.makeAssetKey(LIMIT_CURVE_SWAP, pool), + amountIn * rates[inputIndex] / 1e18 + ); + + _approve(curvePool.coins(inputIndex), pool, amountIn); + + amountOut = abi.decode( + proxy.doCall( + pool, + abi.encodeCall( + curvePool.exchange, + ( + int128(int256(inputIndex)), // safe cast because of 8 token max + int128(int256(outputIndex)), // safe cast because of 8 token max + amountIn, + minAmountOut, + address(proxy) + ) + ) + ), + (uint256) + ); + } + + function addLiquidityCurve( + address pool, + uint256[] memory depositAmounts, + uint256 minLpAmount + ) + external returns (uint256 shares) + { + _checkRole(RELAYER); + + uint256 maxSlippage = maxSlippages[pool]; + require(maxSlippage != 0, "MainnetController/max-slippage-not-set"); + + ICurvePoolLike curvePool = ICurvePoolLike(pool); + + require( + depositAmounts.length == curvePool.N_COINS(), + "MainnetController/invalid-deposit-amounts" + ); + + // Normalized to provide 36 decimal precision when multiplied by asset amount + uint256[] memory rates = curvePool.stored_rates(); + + // Aggregate the value of the deposited assets (e.g. USD) + uint256 valueDeposited; + for (uint256 i = 0; i < depositAmounts.length; i++) { + _approve(curvePool.coins(i), pool, depositAmounts[i]); + valueDeposited += depositAmounts[i] * rates[i]; + } + valueDeposited /= 1e18; + + // Ensure minimum LP amount expected is greater than max slippage amount. + require( + minLpAmount >= valueDeposited * maxSlippage / curvePool.get_virtual_price(), + "MainnetController/min-amount-not-met" + ); + + // Reduce the rate limit by the aggregated underlying asset value of the deposit (e.g. USD) + rateLimits.triggerRateLimitDecrease( + RateLimitHelpers.makeAssetKey(LIMIT_CURVE_DEPOSIT, pool), + valueDeposited + ); + + shares = abi.decode( + proxy.doCall( + pool, + abi.encodeCall( + curvePool.add_liquidity, + (depositAmounts, minLpAmount, address(proxy)) + ) + ), + (uint256) + ); + + // Compute the swap value by taking the difference of the current underlying + // asset values from minted shares vs the deposited funds, converting this into an + // aggregated swap "amount in" by dividing the total value moved by two and decrease the + // swap rate limit by this amount. + uint256 totalSwapped; + for (uint256 i; i < depositAmounts.length; i++) { + totalSwapped += _absSubtraction( + curvePool.balances(i) * rates[i] * shares / curvePool.totalSupply(), + depositAmounts[i] * rates[i] + ); + } + uint256 averageSwap = totalSwapped / 2 / 1e18; + + rateLimits.triggerRateLimitDecrease( + RateLimitHelpers.makeAssetKey(LIMIT_CURVE_SWAP, pool), + averageSwap + ); + } + + function removeLiquidityCurve( + address pool, + uint256 lpBurnAmount, + uint256[] memory minWithdrawAmounts + ) + external returns (uint256[] memory withdrawnTokens) + { + _checkRole(RELAYER); + + uint256 maxSlippage = maxSlippages[pool]; + require(maxSlippage != 0, "MainnetController/max-slippage-not-set"); + + ICurvePoolLike curvePool = ICurvePoolLike(pool); + + require( + minWithdrawAmounts.length == curvePool.N_COINS(), + "MainnetController/invalid-min-withdraw-amounts" + ); + + // Normalized to provide 36 decimal precision when multiplied by asset amount + uint256[] memory rates = curvePool.stored_rates(); + + // Aggregate the minimum values of the withdrawn assets (e.g. USD) + uint256 valueMinWithdrawn; + for (uint256 i = 0; i < minWithdrawAmounts.length; i++) { + valueMinWithdrawn += minWithdrawAmounts[i] * rates[i]; + } + valueMinWithdrawn /= 1e18; + + // Check that the aggregated minimums are greater than the max slippage amount + require( + valueMinWithdrawn >= lpBurnAmount * curvePool.get_virtual_price() * maxSlippage / 1e36, + "MainnetController/min-amount-not-met" + ); + + withdrawnTokens = abi.decode( + proxy.doCall( + pool, + abi.encodeCall( + curvePool.remove_liquidity, + (lpBurnAmount, minWithdrawAmounts, address(proxy)) + ) + ), + (uint256[]) + ); + + // Aggregate value withdrawn to reduce the rate limit + uint256 valueWithdrawn; + for (uint256 i = 0; i < withdrawnTokens.length; i++) { + valueWithdrawn += withdrawnTokens[i] * rates[i]; + } + valueWithdrawn /= 1e18; + + rateLimits.triggerRateLimitDecrease( + RateLimitHelpers.makeAssetKey(LIMIT_CURVE_WITHDRAW, pool), + valueWithdrawn + ); + } + /**********************************************************************************************/ /*** Relayer Ethena functions ***/ /**********************************************************************************************/ - function setDelegatedSigner(address delegatedSigner) external onlyRole(RELAYER) { + function setDelegatedSigner(address delegatedSigner) external { + _checkRole(RELAYER); + proxy.doCall( address(ethenaMinter), abi.encodeCall(ethenaMinter.setDelegatedSigner, (address(delegatedSigner))) ); } - function removeDelegatedSigner(address delegatedSigner) external onlyRole(RELAYER) { + function removeDelegatedSigner(address delegatedSigner) external { + _checkRole(RELAYER); + proxy.doCall( address(ethenaMinter), abi.encodeCall(ethenaMinter.removeDelegatedSigner, (address(delegatedSigner))) @@ -576,27 +773,22 @@ contract MainnetController is AccessControl { } // Note that Ethena's mint/redeem per-block limits include other users - function prepareUSDeMint(uint256 usdcAmount) - external - onlyRole(RELAYER) - rateLimited(LIMIT_USDE_MINT, usdcAmount) - { + function prepareUSDeMint(uint256 usdcAmount) external { + _checkRole(RELAYER); + _rateLimited(LIMIT_USDE_MINT, usdcAmount); _approve(address(usdc), address(ethenaMinter), usdcAmount); } - function prepareUSDeBurn(uint256 usdeAmount) - external - onlyRole(RELAYER) - rateLimited(LIMIT_USDE_BURN, usdeAmount) - { + function prepareUSDeBurn(uint256 usdeAmount) external { + _checkRole(RELAYER); + _rateLimited(LIMIT_USDE_BURN, usdeAmount); _approve(address(usde), address(ethenaMinter), usdeAmount); } - function cooldownAssetsSUSDe(uint256 usdeAmount) - external - onlyRole(RELAYER) - rateLimited(LIMIT_SUSDE_COOLDOWN, usdeAmount) - { + function cooldownAssetsSUSDe(uint256 usdeAmount) external { + _checkRole(RELAYER); + _rateLimited(LIMIT_SUSDE_COOLDOWN, usdeAmount); + proxy.doCall( address(susde), abi.encodeCall(susde.cooldownAssets, (usdeAmount)) @@ -606,9 +798,10 @@ contract MainnetController is AccessControl { // NOTE: !!! Rate limited at end of function !!! function cooldownSharesSUSDe(uint256 susdeAmount) external - onlyRole(RELAYER) returns (uint256 cooldownAmount) { + _checkRole(RELAYER); + cooldownAmount = abi.decode( proxy.doCall( address(susde), @@ -620,7 +813,9 @@ contract MainnetController is AccessControl { rateLimits.triggerRateLimitDecrease(LIMIT_SUSDE_COOLDOWN, cooldownAmount); } - function unstakeSUSDe() external onlyRole(RELAYER) { + function unstakeSUSDe() external { + _checkRole(RELAYER); + proxy.doCall( address(susde), abi.encodeCall(susde.unstake, (address(proxy))) @@ -631,26 +826,24 @@ contract MainnetController is AccessControl { /*** Relayer Maple functions ***/ /**********************************************************************************************/ - function requestMapleRedemption(address mapleToken, uint256 shares) - external - onlyRole(RELAYER) - rateLimitedAsset( + function requestMapleRedemption(address mapleToken, uint256 shares) external { + _checkRole(RELAYER); + _rateLimitedAsset( LIMIT_MAPLE_REDEEM, mapleToken, IMapleTokenLike(mapleToken).convertToAssets(shares) - ) - { + ); + proxy.doCall( mapleToken, abi.encodeCall(IMapleTokenLike(mapleToken).requestRedeem, (shares, address(proxy))) ); } - function cancelMapleRedemption(address mapleToken, uint256 shares) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_MAPLE_REDEEM, mapleToken)) - { + function cancelMapleRedemption(address mapleToken, uint256 shares) external { + _checkRole(RELAYER); + _rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_MAPLE_REDEEM, mapleToken)); + proxy.doCall( mapleToken, abi.encodeCall(IMapleTokenLike(mapleToken).removeShares, (shares, address(proxy))) @@ -658,70 +851,66 @@ contract MainnetController is AccessControl { } /**********************************************************************************************/ - /*** Relayer Morpho functions ***/ + /*** Relayer Superstate functions ***/ /**********************************************************************************************/ - function setSupplyQueueMorpho(address morphoVault, Id[] memory newSupplyQueue) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_4626_DEPOSIT, morphoVault)) - { - proxy.doCall( - morphoVault, - abi.encodeCall(IMetaMorpho(morphoVault).setSupplyQueue, (newSupplyQueue)) - ); - } + function subscribeSuperstate(uint256 usdcAmount) external { + _checkRole(RELAYER); + _rateLimited(LIMIT_SUPERSTATE_SUBSCRIBE, usdcAmount); + + _approve(address(usdc), address(ustb), usdcAmount); - function updateWithdrawQueueMorpho(address morphoVault, uint256[] calldata indexes) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_4626_DEPOSIT, morphoVault)) - { proxy.doCall( - morphoVault, - abi.encodeCall(IMetaMorpho(morphoVault).updateWithdrawQueue, (indexes)) + address(ustb), + abi.encodeCall(ustb.subscribe, (usdcAmount, address(usdc))) ); } - function reallocateMorpho(address morphoVault, MarketAllocation[] calldata allocations) - external - onlyRole(RELAYER) - rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_4626_DEPOSIT, morphoVault)) - { + // NOTE: Rate limited outside of modifier because of tuple return + function redeemSuperstate(uint256 ustbAmount) external { + _checkRole(RELAYER); + + ( uint256 usdcAmount, ) = superstateRedemption.calculateUsdcOut(ustbAmount); + + rateLimits.triggerRateLimitDecrease(LIMIT_SUPERSTATE_REDEEM, usdcAmount); + + _approve(address(ustb), address(superstateRedemption), ustbAmount); + proxy.doCall( - morphoVault, - abi.encodeCall(IMetaMorpho(morphoVault).reallocate, (allocations)) + address(superstateRedemption), + abi.encodeCall(superstateRedemption.redeem, (ustbAmount)) ); } /**********************************************************************************************/ - /*** Relayer Superstate functions ***/ + /*** Relayer DaiUsds functions ***/ /**********************************************************************************************/ - function subscribeSuperstate(uint256 usdcAmount) + function swapUSDSToDAI(uint256 usdsAmount) external onlyRole(RELAYER) - rateLimited(LIMIT_SUPERSTATE_SUBSCRIBE, usdcAmount) { - _approve(address(usdc), address(ustb), usdcAmount); + // Approve USDS to DaiUsds migrator from the proxy (assumes the proxy has enough USDS) + _approve(address(usds), address(daiUsds), usdsAmount); + // Swap USDS to DAI 1:1 proxy.doCall( - address(ustb), - abi.encodeCall(ustb.subscribe, (usdcAmount, address(usdc))) + address(daiUsds), + abi.encodeCall(daiUsds.usdsToDai, (address(proxy), usdsAmount)) ); } - // NOTE: Rate limited outside of modifier because of tuple return - function redeemSuperstate(uint256 ustbAmount) external onlyRole(RELAYER) { - ( uint256 usdcAmount, ) = superstateRedemption.calculateUsdcOut(ustbAmount); - - rateLimits.triggerRateLimitDecrease(LIMIT_SUPERSTATE_REDEEM, usdcAmount); - - _approve(address(ustb), address(superstateRedemption), ustbAmount); + function swapDAIToUSDS(uint256 daiAmount) + external + onlyRole(RELAYER) + { + // Approve DAI to DaiUsds migrator from the proxy (assumes the proxy has enough DAI) + _approve(address(dai), address(daiUsds), daiAmount); + // Swap DAI to USDS 1:1 proxy.doCall( - address(superstateRedemption), - abi.encodeCall(superstateRedemption.redeem, (ustbAmount)) + address(daiUsds), + abi.encodeCall(daiUsds.daiToUsds, (address(proxy), daiAmount)) ); } @@ -731,11 +920,10 @@ contract MainnetController is AccessControl { // NOTE: The param `usdcAmount` is denominated in 1e6 precision to match how PSM uses // USDC precision for both `buyGemNoFee` and `sellGemNoFee` - function swapUSDSToUSDC(uint256 usdcAmount) - external - onlyRole(RELAYER) - rateLimited(LIMIT_USDS_TO_USDC, usdcAmount) - { + function swapUSDSToUSDC(uint256 usdcAmount) external { + _checkRole(RELAYER); + _rateLimited(LIMIT_USDS_TO_USDC, usdcAmount); + uint256 usdsAmount = usdcAmount * psmTo18ConversionFactor; // Approve USDS to DaiUsds migrator from the proxy (assumes the proxy has enough USDS) @@ -757,11 +945,10 @@ contract MainnetController is AccessControl { ); } - function swapUSDCToUSDS(uint256 usdcAmount) - external - onlyRole(RELAYER) - cancelRateLimit(LIMIT_USDS_TO_USDC, usdcAmount) - { + function swapUSDCToUSDS(uint256 usdcAmount) external { + _checkRole(RELAYER); + _cancelRateLimit(LIMIT_USDS_TO_USDC, usdcAmount); + // Approve USDC to PSM from the proxy (assumes the proxy has enough USDC) _approve(address(usdc), address(psm), usdcAmount); @@ -807,15 +994,14 @@ contract MainnetController is AccessControl { /*** Relayer bridging functions ***/ /**********************************************************************************************/ - function transferUSDCToCCTP(uint256 usdcAmount, uint32 destinationDomain) - external - onlyRole(RELAYER) - rateLimited(LIMIT_USDC_TO_CCTP, usdcAmount) - rateLimited( + function transferUSDCToCCTP(uint256 usdcAmount, uint32 destinationDomain) external { + _checkRole(RELAYER); + _rateLimited(LIMIT_USDC_TO_CCTP, usdcAmount); + _rateLimited( RateLimitHelpers.makeDomainKey(LIMIT_USDC_TO_DOMAIN, destinationDomain), usdcAmount - ) - { + ); + bytes32 mintRecipient = mintRecipients[destinationDomain]; require(mintRecipient != 0, "MainnetController/domain-not-configured"); @@ -838,9 +1024,13 @@ contract MainnetController is AccessControl { } /**********************************************************************************************/ - /*** Internal helper functions ***/ + /*** Relayer helper functions ***/ /**********************************************************************************************/ + function _absSubtraction(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a - b : b - a; + } + function _approve(address token, address spender, uint256 amount) internal { proxy.doCall(token, abi.encodeCall(IERC20.approve, (spender, amount))); } @@ -879,5 +1069,28 @@ contract MainnetController is AccessControl { ); } + /**********************************************************************************************/ + /*** Rate Limit helper functions ***/ + /**********************************************************************************************/ + + function _rateLimited(bytes32 key, uint256 amount) internal { + rateLimits.triggerRateLimitDecrease(key, amount); + } + + function _rateLimitedAsset(bytes32 key, address asset, uint256 amount) internal { + rateLimits.triggerRateLimitDecrease(RateLimitHelpers.makeAssetKey(key, asset), amount); + } + + function _cancelRateLimit(bytes32 key, uint256 amount) internal { + rateLimits.triggerRateLimitIncrease(key, amount); + } + + function _rateLimitExists(bytes32 key) internal view { + require( + rateLimits.getRateLimitData(key).maxAmount > 0, + "MainnetController/invalid-action" + ); + } + } diff --git a/test/mainnet-fork/Attacks.t.sol b/test/mainnet-fork/Attacks.t.sol index bcbf7c65..8e661211 100644 --- a/test/mainnet-fork/Attacks.t.sol +++ b/test/mainnet-fork/Attacks.t.sol @@ -7,8 +7,6 @@ import { MainnetControllerBUIDLTestBase } from "./Buidl.t.sol"; import { MainnetControllerEthenaE2ETests } from "./Ethena.t.sol"; import { MapleTestBase } from "./Maple.t.sol"; -import { Id, MarketParamsLib, MorphoTestBase, MarketAllocation } from "./MorphoAllocations.t.sol"; - import { IMapleTokenLike } from "../../src/MainnetController.sol"; interface IBuidlLike is IERC20 { @@ -201,132 +199,3 @@ contract BUIDLAttackTests is MainnetControllerBUIDLTestBase { vm.stopPrank(); } } - -contract MorphoAttackTests is MorphoTestBase { - - function test_attack_compromisedRelayer_setSupplyQueue() external { - Id[] memory supplyQueueUSDC = new Id[](2); - supplyQueueUSDC[0] = MarketParamsLib.id(market1); - supplyQueueUSDC[1] = MarketParamsLib.id(market2); - - // No supply queue to start, but caps are above zero - assertEq(morphoVault.supplyQueueLength(), 0); - - vm.prank(relayer); - mainnetController.setSupplyQueueMorpho(address(morphoVault), supplyQueueUSDC); - - assertEq(morphoVault.supplyQueueLength(), 2); - - assertEq(Id.unwrap(morphoVault.supplyQueue(0)), Id.unwrap(MarketParamsLib.id(market1))); - assertEq(Id.unwrap(morphoVault.supplyQueue(1)), Id.unwrap(MarketParamsLib.id(market2))); - - vm.startPrank(Ethereum.SPARK_PROXY); - rateLimits.setRateLimitData( - RateLimitHelpers.makeAssetKey( - mainnetController.LIMIT_4626_DEPOSIT(), - address(morphoVault) - ), - 25_000_000e18, - uint256(5_000_000e18) / 1 days - ); - vm.stopPrank(); - - deal(address(dai), address(almProxy), 1_000_000e18); - - // Able to deposit - vm.prank(relayer); - mainnetController.depositERC4626(address(morphoVault), 500_000e18); - - Id[] memory emptySupplyQueue = new Id[](0); - - // Malicious relayer empties the supply queue - vm.prank(relayer); - mainnetController.setSupplyQueueMorpho(address(morphoVault), emptySupplyQueue); - - // DOS deposits into morpho vault - vm.prank(relayer); - vm.expectRevert(abi.encodeWithSignature("AllCapsReached()")); - mainnetController.depositERC4626(address(morphoVault), 500_000e18); - - // Frezer can remove the compromised relayer and fallback to the governance relayer - vm.prank(freezer); - mainnetController.removeRelayer(relayer); - - // Compromised relayer can no longer perform the attack - vm.prank(relayer); - vm.expectRevert(abi.encodeWithSignature( - "AccessControlUnauthorizedAccount(address,bytes32)", - relayer, - RELAYER - )); - mainnetController.setSupplyQueueMorpho(address(morphoVault), emptySupplyQueue); - - // Backstop relayer can restore original supply queue - vm.prank(backstopRelayer); - mainnetController.setSupplyQueueMorpho(address(morphoVault), supplyQueueUSDC); - - // Deposit works again - vm.prank(backstopRelayer); - mainnetController.depositERC4626(address(morphoVault), 500_000e18); - } - - function test_attack_compromisedRelayer_reallocateMorpho() public { - vm.startPrank(Ethereum.SPARK_PROXY); - rateLimits.setRateLimitData( - RateLimitHelpers.makeAssetKey( - mainnetController.LIMIT_4626_DEPOSIT(), - address(morphoVault) - ), - 25_000_000e6, - uint256(5_000_000e6) / 1 days - ); - vm.stopPrank(); - - uint256 market1Position = positionAssets(market1); - uint256 market2Position = positionAssets(market2); - - // Move 1m from market1 to market2 - MarketAllocation[] memory reallocations = new MarketAllocation[](2); - reallocations[0] = MarketAllocation({ - marketParams : market1, - assets : market1Position - 1_000_000e18 - }); - reallocations[1] = MarketAllocation({ - marketParams : market2, - assets : type(uint256).max - }); - - // Malicious relayer reallocates freely - vm.prank(relayer); - mainnetController.reallocateMorpho(address(morphoVault), reallocations); - - // Frezer can remove the compromised relayer and fallback to the governance relayer - vm.prank(freezer); - mainnetController.removeRelayer(relayer); - - // Compromised relayer can no longer perform the attack - vm.prank(relayer); - vm.expectRevert(abi.encodeWithSignature( - "AccessControlUnauthorizedAccount(address,bytes32)", - relayer, - RELAYER - )); - mainnetController.reallocateMorpho(address(morphoVault), reallocations); - - market1Position = positionAssets(market1); - market2Position = positionAssets(market2); - - // Backstop relayer can restore original allocations - reallocations[0] = MarketAllocation({ - marketParams : market2, - assets : market2Position - 1_000_000e18 - }); - reallocations[1] = MarketAllocation({ - marketParams : market1, - assets : type(uint256).max - }); - vm.prank(backstopRelayer); - mainnetController.reallocateMorpho(address(morphoVault), reallocations); - } - -} diff --git a/test/mainnet-fork/Centrifuge.t.sol b/test/mainnet-fork/Centrifuge.t.sol index 37d117c0..075fc6e9 100644 --- a/test/mainnet-fork/Centrifuge.t.sol +++ b/test/mainnet-fork/Centrifuge.t.sol @@ -61,12 +61,12 @@ interface ICentrifugeToken is IERC7540 { contract CentrifugeTestBase is ForkTestBase { - address constant ESCROW = 0x0000000005F458Fd6ba9EEb5f365D83b7dA913dD; - address constant INVESTMENT_MANAGER = 0xE79f06573d6aF1B66166A926483ba00924285d20; - address constant JTREASURY_RESTRICTION_MANAGER = 0x4737C3f62Cc265e786b280153fC666cEA2fBc0c0; - address constant JTREASURY_TOKEN = 0x8c213ee79581Ff4984583C6a801e5263418C4b86; - address constant JTREASURY_VAULT_USDC = 0x1d01Ef1997d44206d839b78bA6813f60F1B3A970; - address constant ROOT = 0x0C1fDfd6a1331a875EA013F3897fc8a76ada5DfC; + address constant ESCROW = 0x0000000005F458Fd6ba9EEb5f365D83b7dA913dD; + address constant INVESTMENT_MANAGER = 0x427A1ce127b1775e4Cbd4F58ad468B9F832eA7e9; + address constant JTREASURY_RESTRICTION_MANAGER = 0x4737C3f62Cc265e786b280153fC666cEA2fBc0c0; + address constant JTREASURY_TOKEN = 0x8c213ee79581Ff4984583C6a801e5263418C4b86; + address constant JTREASURY_VAULT_USDC = 0x36036fFd9B1C6966ab23209E073c68Eb9A992f50; + address constant ROOT = 0x0C1fDfd6a1331a875EA013F3897fc8a76ada5DfC; bytes16 constant JTREASURY_TRANCHE_ID = 0x97aa65f23e7be09fcd62d0554d2e9273; uint128 constant USDC_ASSET_ID = 242333941209166991950178742833476896417; @@ -82,7 +82,7 @@ contract CentrifugeTestBase is ForkTestBase { IERC20Mintable jTreasuryToken = IERC20Mintable(JTREASURY_TOKEN); function _getBlock() internal pure override returns (uint256) { - return 21570000; // Jan 7, 2024 + return 21988625; // Mar 6, 2025 } } @@ -155,8 +155,10 @@ contract MainnetControllerRequestDepositERC7540SuccessTests is CentrifugeTestBas assertEq(usdc.allowance(address(almProxy), address(jTreasuryVault)), 0); + uint256 initialEscrowBal = usdc.balanceOf(ESCROW); + assertEq(usdc.balanceOf(address(almProxy)), 1_000_000e6); - assertEq(usdc.balanceOf(ESCROW), 0); + assertEq(usdc.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 0); @@ -168,7 +170,7 @@ contract MainnetControllerRequestDepositERC7540SuccessTests is CentrifugeTestBas assertEq(usdc.allowance(address(almProxy), address(jTreasuryVault)), 0); assertEq(usdc.balanceOf(address(almProxy)), 0); - assertEq(usdc.balanceOf(ESCROW), 1_000_000e6); + assertEq(usdc.balanceOf(ESCROW), initialEscrowBal + 1_000_000e6); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 1_000_000e6); } @@ -225,7 +227,9 @@ contract MainnetControllerClaimDepositERC7540SuccessTests is CentrifugeTestBase uint256 totalSupply = jTreasuryToken.totalSupply(); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + uint256 initialEscrowBal = jTreasuryToken.balanceOf(ESCROW); + + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 1_000_000e6); @@ -243,7 +247,7 @@ contract MainnetControllerClaimDepositERC7540SuccessTests is CentrifugeTestBase ); assertEq(jTreasuryToken.totalSupply(), totalSupply + 500_000e6); - assertEq(jTreasuryToken.balanceOf(ESCROW), 500_000e6); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal + 500_000e6); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 0); @@ -253,7 +257,7 @@ contract MainnetControllerClaimDepositERC7540SuccessTests is CentrifugeTestBase vm.prank(relayer); mainnetController.claimDepositERC7540(address(jTreasuryVault)); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 500_000e6); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 0); @@ -273,7 +277,9 @@ contract MainnetControllerClaimDepositERC7540SuccessTests is CentrifugeTestBase uint256 totalSupply = jTreasuryToken.totalSupply(); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + uint256 initialEscrowBal = jTreasuryToken.balanceOf(ESCROW); + + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 1_000_000e6); @@ -283,7 +289,7 @@ contract MainnetControllerClaimDepositERC7540SuccessTests is CentrifugeTestBase vm.prank(relayer); mainnetController.requestDepositERC7540(address(jTreasuryVault), 500_000e6); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 1_500_000e6); @@ -301,7 +307,7 @@ contract MainnetControllerClaimDepositERC7540SuccessTests is CentrifugeTestBase ); assertEq(jTreasuryToken.totalSupply(), totalSupply + 750_000e6); - assertEq(jTreasuryToken.balanceOf(ESCROW), 750_000e6); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal + 750_000e6); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 0); @@ -311,7 +317,7 @@ contract MainnetControllerClaimDepositERC7540SuccessTests is CentrifugeTestBase vm.prank(relayer); mainnetController.claimDepositERC7540(address(jTreasuryVault)); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 750_000e6); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 0); @@ -417,8 +423,10 @@ contract MainnetControllerClaimCentrifugeCancelDepositSuccessTests is Centrifuge function test_claimCentrifugeCancelDepositRequest() external { deal(address(usdc), address(almProxy), 1_000_000e6); + uint256 initialEscrowBal = usdc.balanceOf(ESCROW); + assertEq(usdc.balanceOf(address(almProxy)), 1_000_000e6); - assertEq(usdc.balanceOf(ESCROW), 0); + assertEq(usdc.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 0); assertEq(jTreasuryVault.pendingCancelDepositRequest(REQUEST_ID, address(almProxy)), false); @@ -430,7 +438,7 @@ contract MainnetControllerClaimCentrifugeCancelDepositSuccessTests is Centrifuge vm.stopPrank(); assertEq(usdc.balanceOf(address(almProxy)), 0); - assertEq(usdc.balanceOf(ESCROW), 1_000_000e6); + assertEq(usdc.balanceOf(ESCROW), initialEscrowBal + 1_000_000e6); assertEq(jTreasuryVault.pendingDepositRequest(REQUEST_ID, address(almProxy)), 1_000_000e6); assertEq(jTreasuryVault.pendingCancelDepositRequest(REQUEST_ID, address(almProxy)), true); @@ -459,7 +467,7 @@ contract MainnetControllerClaimCentrifugeCancelDepositSuccessTests is Centrifuge assertEq(jTreasuryVault.claimableCancelDepositRequest(REQUEST_ID, address(almProxy)), 0); assertEq(usdc.balanceOf(address(almProxy)), 1_000_000e6); - assertEq(usdc.balanceOf(ESCROW), 0); + assertEq(usdc.balanceOf(ESCROW), initialEscrowBal); } } @@ -498,10 +506,10 @@ contract MainnetControllerRequestRedeemERC7540FailureTests is CentrifugeTestBase jTreasuryToken.mint(address(almProxy), 1_000_000e6); vm.stopPrank(); - uint256 overBoundaryShares = jTreasuryVault.convertToShares(1_000_000e6 + 2); + uint256 overBoundaryShares = jTreasuryVault.convertToShares(1_000_000e6 + 3); uint256 atBoundaryShares = jTreasuryVault.convertToShares(1_000_000e6 + 1); - assertEq(jTreasuryVault.convertToAssets(overBoundaryShares), 1_000_000e6 + 1); + assertEq(jTreasuryVault.convertToAssets(overBoundaryShares), 1_000_000e6 + 2); assertEq(jTreasuryVault.convertToAssets(atBoundaryShares), 1_000_000e6); vm.startPrank(relayer); @@ -535,15 +543,17 @@ contract MainnetControllerRequestRedeemERC7540SuccessTests is CentrifugeTestBase function test_requestRedeemERC7540() external { uint256 shares = jTreasuryVault.convertToShares(1_000_000e6); - assertEq(shares, 951_771.227025e6); + assertEq(shares, 948_558.832635e6); vm.prank(ROOT); jTreasuryToken.mint(address(almProxy), shares); assertEq(rateLimits.getCurrentRateLimit(key), 1_000_000e6); + uint256 initialEscrowBal = jTreasuryToken.balanceOf(ESCROW); + assertEq(jTreasuryToken.balanceOf(address(almProxy)), shares); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), 0); @@ -553,7 +563,7 @@ contract MainnetControllerRequestRedeemERC7540SuccessTests is CentrifugeTestBase assertEq(rateLimits.getCurrentRateLimit(key), 1); // Rounding assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); - assertEq(jTreasuryToken.balanceOf(ESCROW), shares); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal + shares); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), shares); } @@ -603,8 +613,10 @@ contract MainnetControllerClaimRedeemERC7540SuccessTests is CentrifugeTestBase { vm.prank(ROOT); jTreasuryToken.mint(address(almProxy), 1_000_000e6); + uint256 initialEscrowBal = jTreasuryToken.balanceOf(ESCROW); + assertEq(jTreasuryToken.balanceOf(address(almProxy)), 1_000_000e6); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), 0); assertEq(jTreasuryVault.claimableRedeemRequest(REQUEST_ID, address(almProxy)), 0); @@ -616,7 +628,7 @@ contract MainnetControllerClaimRedeemERC7540SuccessTests is CentrifugeTestBase { uint256 totalSupply = jTreasuryToken.totalSupply(); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); - assertEq(jTreasuryToken.balanceOf(ESCROW), 1_000_000e6); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal + 1_000_000e6); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), 1_000_000e6); assertEq(jTreasuryVault.claimableRedeemRequest(REQUEST_ID, address(almProxy)), 0); @@ -635,7 +647,7 @@ contract MainnetControllerClaimRedeemERC7540SuccessTests is CentrifugeTestBase { assertEq(jTreasuryToken.totalSupply(), totalSupply - 1_000_000e6); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(usdc.balanceOf(ESCROW), 2_000_000e6); assertEq(usdc.balanceOf(address(almProxy)), 0); @@ -658,8 +670,10 @@ contract MainnetControllerClaimRedeemERC7540SuccessTests is CentrifugeTestBase { vm.prank(ROOT); jTreasuryToken.mint(address(almProxy), 1_500_000e6); + uint256 initialEscrowBal = jTreasuryToken.balanceOf(ESCROW); + assertEq(jTreasuryToken.balanceOf(address(almProxy)), 1_500_000e6); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), 0); assertEq(jTreasuryVault.claimableRedeemRequest(REQUEST_ID, address(almProxy)), 0); @@ -671,7 +685,7 @@ contract MainnetControllerClaimRedeemERC7540SuccessTests is CentrifugeTestBase { uint256 totalSupply = jTreasuryToken.totalSupply(); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 500_000e6); - assertEq(jTreasuryToken.balanceOf(ESCROW), 1_000_000e6); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal + 1_000_000e6); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), 1_000_000e6); assertEq(jTreasuryVault.claimableRedeemRequest(REQUEST_ID, address(almProxy)), 0); @@ -681,7 +695,7 @@ contract MainnetControllerClaimRedeemERC7540SuccessTests is CentrifugeTestBase { mainnetController.requestRedeemERC7540(address(jTreasuryVault), 500_000e6); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); - assertEq(jTreasuryToken.balanceOf(ESCROW), 1_500_000e6); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal + 1_500_000e6); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), 1_500_000e6); assertEq(jTreasuryVault.claimableRedeemRequest(REQUEST_ID, address(almProxy)), 0); @@ -700,7 +714,7 @@ contract MainnetControllerClaimRedeemERC7540SuccessTests is CentrifugeTestBase { assertEq(jTreasuryToken.totalSupply(), totalSupply - 1_500_000e6); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(usdc.balanceOf(ESCROW), 3_000_000e6); assertEq(usdc.balanceOf(address(almProxy)), 0); @@ -826,8 +840,10 @@ contract MainnetControllerClaimCentrifugeCancelRedeemRequestSuccessTests is Cent vm.prank(ROOT); jTreasuryToken.mint(address(almProxy), shares); + uint256 initialEscrowBal = jTreasuryToken.balanceOf(ESCROW); + assertEq(jTreasuryToken.balanceOf(address(almProxy)), shares); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), 0); assertEq(jTreasuryVault.pendingCancelRedeemRequest(REQUEST_ID, address(almProxy)), false); @@ -839,7 +855,7 @@ contract MainnetControllerClaimCentrifugeCancelRedeemRequestSuccessTests is Cent vm.stopPrank(); assertEq(jTreasuryToken.balanceOf(address(almProxy)), 0); - assertEq(jTreasuryToken.balanceOf(ESCROW), shares); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal + shares); assertEq(jTreasuryVault.pendingRedeemRequest(REQUEST_ID, address(almProxy)), shares); assertEq(jTreasuryVault.pendingCancelRedeemRequest(REQUEST_ID, address(almProxy)), true); @@ -867,7 +883,7 @@ contract MainnetControllerClaimCentrifugeCancelRedeemRequestSuccessTests is Cent assertEq(jTreasuryVault.claimableCancelRedeemRequest(REQUEST_ID, address(almProxy)), 0); assertEq(jTreasuryToken.balanceOf(address(almProxy)), shares); - assertEq(jTreasuryToken.balanceOf(ESCROW), 0); + assertEq(jTreasuryToken.balanceOf(ESCROW), initialEscrowBal); } } diff --git a/test/mainnet-fork/Curve.t.sol b/test/mainnet-fork/Curve.t.sol new file mode 100644 index 00000000..a3cfb73c --- /dev/null +++ b/test/mainnet-fork/Curve.t.sol @@ -0,0 +1,1100 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import { IERC4626 } from "lib/forge-std/src/interfaces/IERC4626.sol"; + +import "./ForkTestBase.t.sol"; + +import { ICurvePoolLike } from "../../src/MainnetController.sol"; + +contract CurveTestBase is ForkTestBase { + + address constant RLUSD = 0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD; + address constant CURVE_POOL = 0xD001aE433f254283FeCE51d4ACcE8c53263aa186; + + IERC20 rlUsd = IERC20(RLUSD); + IERC20 curveLp = IERC20(CURVE_POOL); + + ICurvePoolLike curvePool = ICurvePoolLike(CURVE_POOL); + + bytes32 curveDepositKey; + bytes32 curveSwapKey; + bytes32 curveWithdrawKey; + + function setUp() public virtual override { + super.setUp(); + + curveDepositKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_DEPOSIT(), CURVE_POOL); + curveSwapKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_SWAP(), CURVE_POOL); + curveWithdrawKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_WITHDRAW(), CURVE_POOL); + + vm.startPrank(SPARK_PROXY); + rateLimits.setRateLimitData(curveDepositKey, 2_000_000e18, uint256(2_000_000e18) / 1 days); + rateLimits.setRateLimitData(curveSwapKey, 1_000_000e18, uint256(1_000_000e18) / 1 days); + rateLimits.setRateLimitData(curveWithdrawKey, 3_000_000e18, uint256(3_000_000e18) / 1 days); + vm.stopPrank(); + + // Set a higher slippage to allow for successes + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0.98e18); + } + + function _addLiquidity(uint256 usdcAmount, uint256 rlUsdAmount) + internal returns (uint256 lpTokensReceived) + { + deal(address(usdc), address(almProxy), usdcAmount); + deal(RLUSD, address(almProxy), rlUsdAmount); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = usdcAmount; + amounts[1] = rlUsdAmount; + + uint256 minLpAmount = (usdcAmount * 1e12 + rlUsdAmount) * 98/100; + + vm.prank(relayer); + return mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + } + + function _addLiquidity() internal returns (uint256 lpTokensReceived) { + return _addLiquidity(1_000_000e6, 1_000_000e18); + } + + function _getBlock() internal pure override returns (uint256) { + return 22000000; // March 8, 2025 + } + +} + +contract MainnetControllerAddLiquidityCurveFailureTests is CurveTestBase { + + function test_addLiquidityCurve_notRelayer() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1_000_000e6; + amounts[1] = 1_000_000e18; + + uint256 minLpAmount = 1_990_000e18; + + vm.expectRevert(abi.encodeWithSignature( + "AccessControlUnauthorizedAccount(address,bytes32)", + address(this), + RELAYER + )); + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + } + + function test_addLiquidityCurve_slippageNotSet() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1_000_000e6; + amounts[1] = 1_000_000e18; + + uint256 minLpAmount = 1_990_000e18; + + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0); + + vm.prank(relayer); + vm.expectRevert("MainnetController/max-slippage-not-set"); + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + } + + function test_addLiquidityCurve_invalidDepositAmountsLength() public { + uint256[] memory amounts = new uint256[](3); + amounts[0] = 1_000_000e6; + amounts[1] = 1_000_000e18; + amounts[2] = 1_000_000e18; + + uint256 minLpAmount = 0; + + vm.startPrank(relayer); + + vm.expectRevert("MainnetController/invalid-deposit-amounts"); + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + uint256[] memory amounts2 = new uint256[](1); + amounts[0] = 1_000_000e6; + + vm.expectRevert("MainnetController/invalid-deposit-amounts"); + mainnetController.addLiquidityCurve(CURVE_POOL, amounts2, minLpAmount); + } + + function test_addLiquidityCurve_underAllowableSlippageBoundary() public { + deal(address(usdc), address(almProxy), 1_000_000e6); + deal(RLUSD, address(almProxy), 1_000_000e18); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1_000_000e6; + amounts[1] = 1_000_000e18; + + uint256 boundaryAmount = 2_000_000e18 * 0.98e18 / curvePool.get_virtual_price(); + + assertApproxEqAbs(boundaryAmount, 1_950_000e18, 50_000e18); // Sanity check on precision + + uint256 minLpAmount = boundaryAmount - 1; + + vm.startPrank(relayer); + vm.expectRevert("MainnetController/min-amount-not-met"); + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + minLpAmount = boundaryAmount; + + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + } + + function test_addLiquidityCurve_zeroMaxAmount() public { + bytes32 curveDeposit = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_DEPOSIT(), CURVE_POOL); + + vm.prank(SPARK_PROXY); + rateLimits.setRateLimitData(curveDeposit, 0, 0); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1_000_000e6; + amounts[1] = 1_000_000e18; + + uint256 minLpAmount = 1_990_000e18; + + vm.prank(relayer); + vm.expectRevert("RateLimits/zero-maxAmount"); + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + } + + function test_addLiquidityCurve_rateLimitBoundaryAsset0() public { + deal(address(usdc), address(almProxy), 1_000_000e6); + deal(RLUSD, address(almProxy), 1_000_000e18); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1_000_000e6 + 1; + amounts[1] = 1_000_000e18; + + uint256 minLpAmount = 1_990_000e18; + + vm.startPrank(relayer); + vm.expectRevert("RateLimits/rate-limit-exceeded"); + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + amounts[0] = 1_000_000e6; + + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + } + + function test_addLiquidityCurve_rateLimitBoundaryAsset1() public { + deal(address(usdc), address(almProxy), 1_000_000e6); + deal(RLUSD, address(almProxy), 1_000_000e18); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1_000_000e6; + amounts[1] = 1_000_000e18 + 1; + + uint256 minLpAmount = 1_990_000e18; + + vm.startPrank(relayer); + vm.expectRevert("RateLimits/rate-limit-exceeded"); + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + amounts[1] = 1_000_000e18; + + mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + } + +} + +contract MainnetControllerAddLiquiditySuccessTests is CurveTestBase { + + function test_addLiquidityCurve() public { + deal(address(usdc), address(almProxy), 1_000_000e6); + deal(RLUSD, address(almProxy), 1_000_000e18); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1_000_000e6; + amounts[1] = 1_000_000e18; + + uint256 minLpAmount = 1_990_000e18; + + uint256 startingPyUsdBalance = rlUsd.balanceOf(CURVE_POOL); + uint256 startingUsdcBalance = usdc.balanceOf(CURVE_POOL); + uint256 startingTotalSupply = curveLp.totalSupply(); + + assertEq(usdc.balanceOf(address(almProxy)), 1_000_000e6); + assertEq(usdc.balanceOf(CURVE_POOL), startingUsdcBalance); + + assertEq(rlUsd.balanceOf(address(almProxy)), 1_000_000e18); + assertEq(rlUsd.balanceOf(CURVE_POOL), startingPyUsdBalance); + + assertEq(curveLp.balanceOf(address(almProxy)), 0); + assertEq(curveLp.totalSupply(), startingTotalSupply); + + assertEq(rateLimits.getCurrentRateLimit(curveDepositKey), 2_000_000e18); + assertEq(rateLimits.getCurrentRateLimit(curveSwapKey), 1_000_000e18); + + vm.prank(relayer); + uint256 lpTokensReceived = mainnetController.addLiquidityCurve( + CURVE_POOL, + amounts, + minLpAmount + ); + + assertEq(lpTokensReceived, 1_997_612.166757892422937582e18); + + assertEq(usdc.balanceOf(address(almProxy)), 0); + assertEq(usdc.balanceOf(CURVE_POOL), startingUsdcBalance + 1_000_000e6); + + assertEq(rlUsd.balanceOf(address(almProxy)), 0); + assertEq(rlUsd.balanceOf(CURVE_POOL), startingPyUsdBalance + 1_000_000e18); + + assertEq(curveLp.balanceOf(address(almProxy)), lpTokensReceived); + assertEq(curveLp.totalSupply(), startingTotalSupply + lpTokensReceived); + + assertEq(rateLimits.getCurrentRateLimit(curveDepositKey), 0); + assertEq(rateLimits.getCurrentRateLimit(curveSwapKey), 999_998.512329762328287296e18); // Small swap occurs on deposit + } + + function test_addLiquidityCurve_swapRateLimit() public { + // Set a higher slippage to allow for successes + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0.001e18); + + deal(address(usdc), address(almProxy), 2_000_000e6); + + // Step 1: Add liquidity, check how much the rate limit was reduced + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 2_000_000e6; + amounts[1] = 0; + + uint256 minLpAmount = 1_000_000e18; + + uint256 startingRateLimit = rateLimits.getCurrentRateLimit(curveSwapKey); + + vm.startPrank(relayer); + + uint256 lpTokens = mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + uint256 derivedSwapAmount = startingRateLimit - rateLimits.getCurrentRateLimit(curveSwapKey); + + // Step 2: Withdraw full balance of LP tokens, withdrawing proportional amounts from the pool + + uint256[] memory minWithdrawnAmounts = new uint256[](2); + minWithdrawnAmounts[0] = 500_000e6; + minWithdrawnAmounts[1] = 500_000e18; + + uint256[] memory withdrawnAmounts = mainnetController.removeLiquidityCurve(CURVE_POOL, lpTokens, minWithdrawnAmounts); + + // Step 3: Calculate the average difference between the assets deposited and withdrawn, into an average swap amount + // and compare against the derived swap amount + + uint256[] memory rates = ICurvePoolLike(CURVE_POOL).stored_rates(); + + uint256 totalSwapped; + for (uint256 i; i < withdrawnAmounts.length; i++) { + totalSwapped += _absSubtraction(withdrawnAmounts[i] * rates[i], amounts[i] * rates[i]) / 1e18; + } + totalSwapped /= 2; + + // Difference is accurate to within 1 unit of USDC + assertApproxEqAbs(derivedSwapAmount, totalSwapped, 0.000001e18); + + // Check real values, comparing amount of USDC deposited with amount withdrawn as a result of the "swap" + assertEq(withdrawnAmounts[0], 1_167_803.429987e6); + assertEq(withdrawnAmounts[1], 831_961.163091701652224522e18); + + // Some accuracy differences because of fees + assertEq(derivedSwapAmount, 832_078.866551978168996427e18); + assertEq(2_000_000e6 - withdrawnAmounts[0], 832_196.570013e6); + } + + function testFuzz_addLiquidityCurve_swapRateLimit(uint256 usdcAmount, uint256 rlUsdAmount) public { + // Set slippage to be zero and unlimited rate limits for purposes of this test + // Not using actual unlimited rate limit because need to get swap amount to be reduced. + vm.startPrank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 1); // 1e-16% + rateLimits.setUnlimitedRateLimitData(curveDepositKey); + rateLimits.setUnlimitedRateLimitData(curveWithdrawKey); + rateLimits.setRateLimitData(curveSwapKey, type(uint256).max - 1, type(uint256).max - 1); + vm.stopPrank(); + + usdcAmount = _bound(usdcAmount, 1_000_000e6, 10_000_000_000e6); + rlUsdAmount = _bound(rlUsdAmount, 1_000_000e18, 10_000_000_000e18); + + deal(address(usdc), address(almProxy), usdcAmount); + deal(RLUSD, address(almProxy), rlUsdAmount); + + // Step 1: Add liquidity with fuzzed inputs, check how much the rate limit was reduced + + uint256[] memory amounts = new uint256[](2); + amounts[0] = usdcAmount; + amounts[1] = rlUsdAmount; + + uint256 startingRateLimit = rateLimits.getCurrentRateLimit(curveSwapKey); + + vm.startPrank(relayer); + + uint256 lpTokens = mainnetController.addLiquidityCurve(CURVE_POOL, amounts, 1e18); + + uint256 derivedSwapAmount = startingRateLimit - rateLimits.getCurrentRateLimit(curveSwapKey); + + // Step 2: Withdraw full balance of LP tokens, withdrawing proportional amounts from the pool + + uint256[] memory minWithdrawnAmounts = new uint256[](2); + minWithdrawnAmounts[0] = 1e6; + minWithdrawnAmounts[1] = 1e18; + + uint256[] memory withdrawnAmounts = mainnetController.removeLiquidityCurve(CURVE_POOL, lpTokens, minWithdrawnAmounts); + + // Step 3: Calculate the average difference between the assets deposited and withdrawn, into an average swap amount + // and compare against the derived swap amount + + uint256[] memory rates = ICurvePoolLike(CURVE_POOL).stored_rates(); + + uint256 totalSwapped; + for (uint256 i; i < withdrawnAmounts.length; i++) { + totalSwapped += _absSubtraction(withdrawnAmounts[i] * rates[i], amounts[i] * rates[i]) / 1e18; + } + totalSwapped /= 2; + + // Difference is accurate to within 1 unit of USDC + assertApproxEqAbs(derivedSwapAmount, totalSwapped, 0.000001e18); + } + +} + +contract MainnetControllerRemoveLiquidityCurveFailureTests is CurveTestBase { + + function test_removeLiquidityCurve_notRelayer() public { + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = 1_000_000e6; + minWithdrawAmounts[1] = 1_000_000e18; + + uint256 lpReturn = 1_980_000e18; + + vm.expectRevert(abi.encodeWithSignature( + "AccessControlUnauthorizedAccount(address,bytes32)", + address(this), + RELAYER + )); + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + } + + function test_removeLiquidityCurve_slippageNotSet() public { + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = 1_000_000e6; + minWithdrawAmounts[1] = 1_000_000e18; + + uint256 lpReturn = 1_980_000e18; + + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0); + + vm.prank(relayer); + vm.expectRevert("MainnetController/max-slippage-not-set"); + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + } + + function test_removeLiquidityCurve_invalidDepositAmountsLength() public { + uint256[] memory minWithdrawAmounts = new uint256[](3); + minWithdrawAmounts[0] = 1_000_000e6; + minWithdrawAmounts[1] = 1_000_000e18; + minWithdrawAmounts[2] = 1_000_000e18; + + uint256 lpReturn = 1_980_000e18; + + vm.startPrank(relayer); + + vm.expectRevert("MainnetController/invalid-min-withdraw-amounts"); + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + + uint256[] memory minWithdrawAmounts2 = new uint256[](1); + minWithdrawAmounts[0] = 1_000_000e6; + + vm.expectRevert("MainnetController/invalid-min-withdraw-amounts"); + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts2); + } + + function test_removeLiquidityCurve_underAllowableSlippageBoundary() public { + bytes32 curveDeposit = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_DEPOSIT(), CURVE_POOL); + + vm.prank(SPARK_PROXY); + rateLimits.setRateLimitData(curveDeposit, 4_000_000e18, uint256(4_000_000e18) / 1 days); + + _addLiquidity(2_000_000e6, 2_000_000e18); // Get more than 2m LP tokens in return + + uint256 lpReturn = 2_000_000e18; // 2% on 2m + + uint256 minTotalReturned = lpReturn * curvePool.get_virtual_price() * 98/100 / 1e18; + + assertApproxEqAbs(minTotalReturned, 1_960_000e18, 50_000e18); // Sanity check on precision + + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = minTotalReturned / 2 / 1e12; // Rounding down causes boundary + minWithdrawAmounts[1] = minTotalReturned / 2; + + vm.startPrank(relayer); + vm.expectRevert("MainnetController/min-amount-not-met"); + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + + minWithdrawAmounts[0] = minTotalReturned / 2 / 1e12 + 1; + minWithdrawAmounts[1] = minTotalReturned / 2; + + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + } + + function test_removeLiquidityCurve_zeroMaxAmount() public { + bytes32 curveWithdraw = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_WITHDRAW(), CURVE_POOL); + + vm.prank(SPARK_PROXY); + rateLimits.setRateLimitData(curveWithdraw, 0, 0); + + _addLiquidity(); + + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = 499_000e6; + minWithdrawAmounts[1] = 499_000e18; + + uint256 lpReturn = 1_000_000e18; + + vm.prank(relayer); + vm.expectRevert("RateLimits/zero-maxAmount"); + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + } + + function test_removeLiquidityCurve_rateLimitBoundary() public { + _addLiquidity(); + + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = 499_000e6; + minWithdrawAmounts[1] = 499_000e18; + + uint256 lpReturn = 1_000_000e18; + + uint256 id = vm.snapshotState(); + + // Use a success call to see how many tokens are returned from burning 1e18 LP tokens + vm.prank(relayer); + uint256[] memory withdrawnAmounts = mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + + uint256 totalWithdrawn = withdrawnAmounts[0] * 1e12 + withdrawnAmounts[1]; + + vm.revertToState(id); + + bytes32 curveWithdraw = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_WITHDRAW(), CURVE_POOL); + + // Set to below boundary + vm.prank(SPARK_PROXY); + rateLimits.setRateLimitData(curveWithdraw, totalWithdrawn - 1, totalWithdrawn / 1 days); + + vm.prank(relayer); + vm.expectRevert("RateLimits/rate-limit-exceeded"); + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + + // Set to boundary + vm.prank(SPARK_PROXY); + rateLimits.setRateLimitData(curveWithdraw, totalWithdrawn, totalWithdrawn / 1 days); + + vm.prank(relayer); + mainnetController.removeLiquidityCurve(CURVE_POOL, lpReturn, minWithdrawAmounts); + } + +} + +contract MainnetControllerRemoveLiquiditySuccessTests is CurveTestBase { + + function test_removeLiquidityCurve() public { + uint256 lpTokensReceived = _addLiquidity(1_000_000e6, 1_000_000e18); + + uint256 startingPyUsdBalance = rlUsd.balanceOf(CURVE_POOL); + uint256 startingUsdcBalance = usdc.balanceOf(CURVE_POOL); + uint256 startingTotalSupply = curveLp.totalSupply(); + + assertEq(lpTokensReceived, 1_997_612.166757892422937582e18); + + assertEq(rlUsd.balanceOf(address(almProxy)), 0); + assertEq(rlUsd.balanceOf(CURVE_POOL), startingPyUsdBalance); + + assertEq(usdc.balanceOf(address(almProxy)), 0); + assertEq(usdc.balanceOf(CURVE_POOL), startingUsdcBalance); + + assertEq(curveLp.balanceOf(address(almProxy)), lpTokensReceived); + assertEq(curveLp.totalSupply(), startingTotalSupply); + + assertEq(rateLimits.getCurrentRateLimit(curveWithdrawKey), 3_000_000e18); + + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = 980_000e6; + minWithdrawAmounts[1] = 980_000e18; + + vm.prank(relayer); + uint256[] memory assetsReceived = mainnetController.removeLiquidityCurve( + CURVE_POOL, + lpTokensReceived, + minWithdrawAmounts + ); + + assertEq(assetsReceived[0], 1_000_001.487588e6); + assertEq(assetsReceived[1], 999_998.512248098338560810e18); + + uint256 sumAssetsReceived = assetsReceived[0] * 1e12 + assetsReceived[1]; + + assertApproxEqAbs(sumAssetsReceived, 2_000_000e18, 1e18); + + assertEq(usdc.balanceOf(address(almProxy)), assetsReceived[0]); + + assertApproxEqAbs(usdc.balanceOf(CURVE_POOL), startingUsdcBalance - assetsReceived[0], 10e6); // Fees from other deposits + + assertEq(rlUsd.balanceOf(address(almProxy)), assetsReceived[1]); + + assertApproxEqAbs(rlUsd.balanceOf(CURVE_POOL), startingPyUsdBalance - assetsReceived[1], 10e18); // Fees from other deposits + + assertEq(curveLp.balanceOf(address(almProxy)), 0); + assertEq(curveLp.totalSupply(), startingTotalSupply - lpTokensReceived); + + assertEq(rateLimits.getCurrentRateLimit(curveWithdrawKey), 3_000_000e18 - sumAssetsReceived); + } + +} + +contract MainnetControllerSwapCurveFailureTests is CurveTestBase { + + function test_swapCurve_notRelayer() public { + vm.expectRevert(abi.encodeWithSignature( + "AccessControlUnauthorizedAccount(address,bytes32)", + address(this), + RELAYER + )); + mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18, 980_000e6); + } + + function test_swapCurve_sameIndex() public { + vm.prank(relayer); + vm.expectRevert("MainnetController/invalid-indices"); + mainnetController.swapCurve(CURVE_POOL, 1, 1, 1_000_000e18, 980_000e6); + } + + function test_swapCurve_firstIndexTooHighBoundary() public { + _addLiquidity(); + skip(1 days); // Recharge swap rate limit from deposit + + deal(RLUSD, address(almProxy), 1_000_000e18); + + vm.prank(relayer); + vm.expectRevert("MainnetController/index-too-high"); + mainnetController.swapCurve(CURVE_POOL, 2, 0, 1_000_000e18, 980_000e6); + + vm.prank(relayer); + mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18, 980_000e6); + } + + function test_swapCurve_secondIndexTooHighBoundary() public { + _addLiquidity(); + skip(1 days); // Recharge swap rate limit from deposit + + deal(address(usdc), address(almProxy), 1_000_000e6); + + vm.prank(relayer); + vm.expectRevert("MainnetController/index-too-high"); + mainnetController.swapCurve(CURVE_POOL, 0, 2, 1_000_000e6, 980_000e18); + + vm.prank(relayer); + mainnetController.swapCurve(CURVE_POOL, 0, 1, 1_000_000e6, 980_000e18); + } + + function test_swapCurve_slippageNotSet() public { + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0); + + vm.prank(relayer); + vm.expectRevert("MainnetController/max-slippage-not-set"); + mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18, 980_000e6); + } + + function test_swapCurve_underAllowableSlippageBoundaryAsset0To1() public { + _addLiquidity(); + skip(1 days); // Recharge swap rate limit from deposit + + deal(address(usdc), address(almProxy), 1_000_000e6); + + vm.startPrank(relayer); + vm.expectRevert("MainnetController/min-amount-not-met"); + mainnetController.swapCurve(CURVE_POOL, 0, 1, 1_000_000e6, 980_000e18 - 1); + + mainnetController.swapCurve(CURVE_POOL, 0, 1, 1_000_000e6, 980_000e18); + } + + function test_swapCurve_underAllowableSlippageBoundaryAsset1To0() public { + _addLiquidity(); + skip(1 days); // Recharge swap rate limit from deposit + + deal(RLUSD, address(almProxy), 1_000_000e18); + + vm.startPrank(relayer); + vm.expectRevert("MainnetController/min-amount-not-met"); + mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18, 980_000e6 - 1); + + mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18, 980_000e6); + } + + function test_swapCurve_zeroMaxAmount() public { + bytes32 curveSwap = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_SWAP(), CURVE_POOL); + + vm.prank(SPARK_PROXY); + rateLimits.setRateLimitData(curveSwap, 0, 0); + + vm.prank(relayer); + vm.expectRevert("RateLimits/zero-maxAmount"); + mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18, 980_000e6); + } + + function test_swapCurve_rateLimitBoundary() public { + _addLiquidity(); + skip(1 days); // Recharge swap rate limit from deposit + + deal(RLUSD, address(almProxy), 1_000_000e18 + 1); + + vm.startPrank(relayer); + vm.expectRevert("RateLimits/rate-limit-exceeded"); + mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18 + 1, 998_000e6); + + mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18, 998_000e6); + } + +} + +contract MainnetControllerSwapCurveSuccessTests is CurveTestBase { + + function test_swapCurve() public { + _addLiquidity(1_000_000e6, 1_000_000e18); + skip(1 days); // Recharge swap rate limit from deposit + + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0.999e18); // 0.1% + + uint256 startingPyUsdBalance = rlUsd.balanceOf(CURVE_POOL); + uint256 startingUsdcBalance = usdc.balanceOf(CURVE_POOL); + + deal(RLUSD, address(almProxy), 1_000_000e18); + + assertEq(rlUsd.balanceOf(address(almProxy)), 1_000_000e18); + assertEq(rlUsd.balanceOf(CURVE_POOL), startingPyUsdBalance); + + assertEq(usdc.balanceOf(address(almProxy)), 0); + assertEq(usdc.balanceOf(CURVE_POOL), startingUsdcBalance); + + assertEq(rateLimits.getCurrentRateLimit(curveSwapKey), 1_000_000e18); + + vm.prank(relayer); + uint256 amountOut = mainnetController.swapCurve(CURVE_POOL, 1, 0, 1_000_000e18, 999_500e6); + + assertEq(amountOut, 999_726.854240e6); + + assertEq(rlUsd.balanceOf(address(almProxy)), 0); + assertEq(rlUsd.balanceOf(CURVE_POOL), startingPyUsdBalance + 1_000_000e18); + + assertEq(usdc.balanceOf(address(almProxy)), amountOut); + assertEq(usdc.balanceOf(CURVE_POOL), startingUsdcBalance - amountOut); + + assertEq(rateLimits.getCurrentRateLimit(curveSwapKey), 0); + } + +} + +contract MainnetControllerGetVirtualPriceStressTests is CurveTestBase { + + function test_getVirtualPrice_stressTest() public { + vm.startPrank(SPARK_PROXY); + rateLimits.setUnlimitedRateLimitData(curveDepositKey); + rateLimits.setUnlimitedRateLimitData(curveSwapKey); + rateLimits.setUnlimitedRateLimitData(curveWithdrawKey); + vm.stopPrank(); + + _addLiquidity(100_000_000e6, 100_000_000e18); + + uint256 virtualPrice1 = curvePool.get_virtual_price(); + + assertEq(virtualPrice1, 1.001195343715175271e18); + + deal(address(usdc), address(almProxy), 100_000_000e6); + + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 1); // 1e-16% + + // Perform a massive swap to stress the virtual price + vm.prank(relayer); + uint256 amountOut = mainnetController.swapCurve(CURVE_POOL, 0, 1, 100_000_000e6, 1000e18); + + assertEq(amountOut, 99_123_484.133360978396763017e18); + + // Assert price rises + uint256 virtualPrice2 = curvePool.get_virtual_price(); + + assertEq(virtualPrice2, 1.001228501012622650e18); + assertGt(virtualPrice2, virtualPrice1); + + // Add one sided liquidity to stress the virtual price + _addLiquidity(0, 100_000_000e18); + + // Assert price rises + uint256 virtualPrice3 = curvePool.get_virtual_price(); + + assertEq(virtualPrice3, 1.001245739473410937e18); + assertGt(virtualPrice3, virtualPrice2); + + // Remove liquidity + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = 1000e6; + minWithdrawAmounts[1] = 1000e18; + + vm.startPrank(relayer); + mainnetController.removeLiquidityCurve( + CURVE_POOL, + curveLp.balanceOf(address(almProxy)), + minWithdrawAmounts + ); + vm.stopPrank(); + + // Assert price rises + uint256 virtualPrice4 = curvePool.get_virtual_price(); + + assertEq(virtualPrice4, 1.001245739473435168e18); + assertGt(virtualPrice4, virtualPrice3); + } + +} + +contract MainnetController3PoolSwapRateLimitTest is ForkTestBase { + + // Working in BTC terms because only high TVL active NG three asset pool is BTC + address CURVE_POOL = 0xabaf76590478F2fE0b396996f55F0b61101e9502; + + IERC20 ebtc = IERC20(0x657e8C867D8B37dCC18fA4Caead9C45EB088C642); + IERC20 lbtc = IERC20(0x8236a87084f8B84306f72007F36F2618A5634494); + IERC20 wbtc = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + + bytes32 curveDepositKey; + bytes32 curveSwapKey; + bytes32 curveWithdrawKey; + + function setUp() public virtual override { + super.setUp(); + + curveDepositKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_DEPOSIT(), CURVE_POOL); + curveSwapKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_SWAP(), CURVE_POOL); + curveWithdrawKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_WITHDRAW(), CURVE_POOL); + + vm.startPrank(SPARK_PROXY); + rateLimits.setRateLimitData(curveDepositKey, 5_000_000e18, uint256(5_000_000e18) / 1 days); + rateLimits.setRateLimitData(curveSwapKey, 5_000_000e18, uint256(5_000_000e18) / 1 days); + rateLimits.setRateLimitData(curveWithdrawKey, 5_000_000e18, uint256(5_000_000e18) / 1 days); + vm.stopPrank(); + + // Set a higher slippage to allow for successes + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0.001e18); + } + + function _getBlock() internal pure override returns (uint256) { + return 22000000; // March 8, 2025 + } + + function test_addLiquidityCurve_swapRateLimit() public { + deal(address(ebtc), address(almProxy), 2_000e8); + + // Step 1: Add liquidity, check how much the rate limit was reduced + + uint256[] memory amounts = new uint256[](3); + amounts[0] = 1e8; + amounts[1] = 0; + amounts[2] = 0; + + uint256 minLpAmount = 0.1e18; + + uint256 startingRateLimit = rateLimits.getCurrentRateLimit(curveSwapKey); + + vm.startPrank(relayer); + + uint256 lpTokens = mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + uint256 derivedSwapAmount = startingRateLimit - rateLimits.getCurrentRateLimit(curveSwapKey); + + // Step 2: Withdraw full balance of LP tokens, withdrawing proportional amounts from the pool + + uint256[] memory minWithdrawnAmounts = new uint256[](3); + minWithdrawnAmounts[0] = 0.01e8; + minWithdrawnAmounts[1] = 0.01e8; + minWithdrawnAmounts[2] = 0.01e8; + + uint256[] memory withdrawnAmounts = mainnetController.removeLiquidityCurve(CURVE_POOL, lpTokens, minWithdrawnAmounts); + + // Step 3: Show "swapped" asset results, demonstrate that the swap rate limit was reduced by the amount + // of eBTC that was reduced, 1e8 deposited + ~0.35e8 withdrawn = ~0.65e8 swapped + + assertEq(withdrawnAmounts[0], 0.35689723e8); + assertEq(withdrawnAmounts[1], 0.22809783e8); + assertEq(withdrawnAmounts[2], 0.41478858e8); + + // Some accuracy differences because of fees + assertEq(derivedSwapAmount, 0.642994597417510402e18); + assertEq(1e8 - withdrawnAmounts[0], 0.64310277e8); + } + +} + +contract MainnetControllerSUsdeSUsdsSwapRateLimitTest is ForkTestBase { + + address constant CURVE_POOL = 0x3CEf1AFC0E8324b57293a6E7cE663781bbEFBB79; + + IERC20 curveLp = IERC20(CURVE_POOL); + + bytes32 curveDepositKey; + bytes32 curveSwapKey; + bytes32 curveWithdrawKey; + + function setUp() public virtual override { + super.setUp(); + + curveDepositKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_DEPOSIT(), CURVE_POOL); + curveSwapKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_SWAP(), CURVE_POOL); + curveWithdrawKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_WITHDRAW(), CURVE_POOL); + + vm.startPrank(SPARK_PROXY); + rateLimits.setRateLimitData(curveDepositKey, 5_000_000e18, uint256(5_000_000e18) / 1 days); + rateLimits.setRateLimitData(curveSwapKey, 5_000_000e18, uint256(5_000_000e18) / 1 days); + rateLimits.setRateLimitData(curveWithdrawKey, 5_000_000e18, uint256(5_000_000e18) / 1 days); + vm.stopPrank(); + + // Set a higher slippage to allow for successes + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0.01e18); + } + + function _getBlock() internal pure override returns (uint256) { + return 22000000; // March 8, 2025 + } + + function test_addLiquidityCurve_swapRateLimit() public { + uint256 susdeAmount = susde.convertToShares(1_000_000e18); + + deal(address(susde), address(almProxy), susdeAmount); + + // Step 1: Add liquidity, check how much the rate limit was reduced + + uint256[] memory amounts = new uint256[](2); + amounts[0] = susdeAmount; + amounts[1] = 0; + + uint256 minLpAmount = 100_000e18; + + uint256 startingRateLimit = rateLimits.getCurrentRateLimit(curveSwapKey); + + vm.startPrank(relayer); + + uint256 lpTokens = mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + uint256 derivedSwapAmount = startingRateLimit - rateLimits.getCurrentRateLimit(curveSwapKey); + + // Step 2: Withdraw full balance of LP tokens, withdrawing proportional amounts from the pool + + uint256[] memory minWithdrawnAmounts = new uint256[](2); + minWithdrawnAmounts[0] = 100_000e18; + minWithdrawnAmounts[1] = 100_000e18; + + uint256[] memory withdrawnAmounts = mainnetController.removeLiquidityCurve(CURVE_POOL, lpTokens, minWithdrawnAmounts); + + // Step 3: Show "swapped" asset results, demonstrate that the swap rate limit was reduced by the dollar amount + // of sUSDe that was reduced, 1m deposited + ~850k withdrawn = ~150k swapped + + assertEq(susde.convertToAssets(withdrawnAmounts[0]), 850_583.458247970197966075e18); + assertEq(susds.convertToAssets(withdrawnAmounts[1]), 148_671.435052597244493444e18); + + // Some accuracy differences because of fees + assertEq(derivedSwapAmount, 149_043.988402313523216974e18); + + assertEq(1_000_000e18 - susde.convertToAssets(withdrawnAmounts[0]), 149_416.541752029802033925e18); + } + +} + +contract MainnetControllerE2ECurveRLUsdUsdcPoolTest is CurveTestBase { + + function test_e2e_addAndRemoveLiquidityCurve() public { + deal(address(usdc), address(almProxy), 1_000_000e6); + deal(RLUSD, address(almProxy), 1_000_000e18); + + uint256 usdcBalance = usdc.balanceOf(CURVE_POOL); + uint256 rlUsdBalance = rlUsd.balanceOf(CURVE_POOL); + + // Step 1: Add liquidity + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1_000_000e6; + amounts[1] = 1_000_000e18; + + uint256 minLpAmount = 1_990_000e18; + + assertEq(curveLp.balanceOf(address(almProxy)), 0); + + assertEq(usdc.balanceOf(address(almProxy)), 1_000_000e6); + assertEq(rlUsd.balanceOf(address(almProxy)), 1_000_000e18); + + vm.prank(relayer); + uint256 lpTokensReceived = mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + assertEq(curveLp.balanceOf(address(almProxy)), lpTokensReceived); + + assertEq(usdc.balanceOf(address(almProxy)), 0); + assertEq(rlUsd.balanceOf(address(almProxy)), 0); + + assertEq(usdc.balanceOf(CURVE_POOL), usdcBalance + 1_000_000e6); + assertEq(rlUsd.balanceOf(CURVE_POOL), rlUsdBalance + 1_000_000e18); + + // Step 2: Swap USDC for RLUSD + + deal(address(usdc), address(almProxy), 100e6); + + assertEq(usdc.balanceOf(address(almProxy)), 100e6); + assertEq(rlUsd.balanceOf(address(almProxy)), 0); + + vm.prank(relayer); + uint256 rlUsdReturned = mainnetController.swapCurve(CURVE_POOL, 0, 1, 100e6, 99.9e18); + + assertEq(rlUsdReturned, 99.989998025251364331e18); + + assertEq(usdc.balanceOf(address(almProxy)), 0); + assertEq(rlUsd.balanceOf(address(almProxy)), rlUsdReturned); + + // Step 3: Remove liquidity + + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = 999_900e6; + minWithdrawAmounts[1] = 999_900e18; + + vm.prank(relayer); + uint256[] memory assetsReceived = mainnetController.removeLiquidityCurve( + CURVE_POOL, + lpTokensReceived, + minWithdrawAmounts + ); + + assertEq(assetsReceived[0], 1_000_018.281459e6); + assertEq(assetsReceived[1], 999_981.719217481940282590e18); + + uint256 sumAssetsReceived = assetsReceived[0] * 1e12 + assetsReceived[1]; + + assertEq(sumAssetsReceived, 2_000_000.000676481940282590e18); + + assertEq(usdc.balanceOf(address(almProxy)), assetsReceived[0]); + assertEq(rlUsd.balanceOf(address(almProxy)), assetsReceived[1] + rlUsdReturned); + + assertEq(curveLp.balanceOf(address(almProxy)), 0); + } + +} + +contract MainnetControllerE2ECurveSUsdeSUsdsPoolTest is ForkTestBase { + + address constant CURVE_POOL = 0x3CEf1AFC0E8324b57293a6E7cE663781bbEFBB79; + + IERC20 curveLp = IERC20(CURVE_POOL); + + bytes32 curveDepositKey; + bytes32 curveSwapKey; + bytes32 curveWithdrawKey; + + function setUp() public virtual override { + super.setUp(); + + curveDepositKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_DEPOSIT(), CURVE_POOL); + curveSwapKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_SWAP(), CURVE_POOL); + curveWithdrawKey = RateLimitHelpers.makeAssetKey(mainnetController.LIMIT_CURVE_WITHDRAW(), CURVE_POOL); + + vm.startPrank(SPARK_PROXY); + rateLimits.setRateLimitData(curveDepositKey, 2_000_000e18, uint256(2_000_000e18) / 1 days); + rateLimits.setRateLimitData(curveSwapKey, 1_000_000e18, uint256(1_000_000e18) / 1 days); + rateLimits.setRateLimitData(curveWithdrawKey, 3_000_000e18, uint256(3_000_000e18) / 1 days); + vm.stopPrank(); + + // Set a higher slippage to allow for successes + vm.prank(SPARK_PROXY); + mainnetController.setMaxSlippage(CURVE_POOL, 0.95e18); + } + + function _getBlock() internal pure override returns (uint256) { + return 22000000; // March 8, 2025 + } + + function test_e2e_addAndRemoveLiquidityCurve() public { + uint256 susdeAmount = susde.convertToShares(1_000_000e18); + uint256 susdsAmount = susds.convertToShares(1_000_000e18); + + deal(address(susde), address(almProxy), susdeAmount); + deal(address(susds), address(almProxy), susdsAmount); + + uint256 susdeBalance = susde.balanceOf(CURVE_POOL); + uint256 susdsBalance = susds.balanceOf(CURVE_POOL); + + // Step 1: Add liquidity + + uint256[] memory amounts = new uint256[](2); + amounts[0] = susdeAmount; + amounts[1] = susdsAmount; + + uint256 minLpAmount = 1_990_000e18; // 0.5% on 2m + + assertEq(curveLp.balanceOf(address(almProxy)), 0); + + assertEq(susde.balanceOf(address(almProxy)), susdeAmount); + assertEq(susds.balanceOf(address(almProxy)), susdsAmount); + + vm.prank(relayer); + uint256 lpTokensReceived = mainnetController.addLiquidityCurve(CURVE_POOL, amounts, minLpAmount); + + assertEq(curveLp.balanceOf(address(almProxy)), lpTokensReceived); + + assertEq(susde.balanceOf(address(almProxy)), 0); + assertEq(susds.balanceOf(address(almProxy)), 0); + + assertEq(susde.balanceOf(CURVE_POOL), susdeBalance + susdeAmount); + assertEq(susds.balanceOf(CURVE_POOL), susdsBalance + susdsAmount); + + // Step 2: Swap susde for susds + + uint256 susdeSwapAmount = susde.convertToShares(100e18); + uint256 minSUsdsAmount = susds.convertToShares(99.5e18); + + deal(address(susde), address(almProxy), susdeSwapAmount); + + assertEq(susde.balanceOf(address(almProxy)), susdeSwapAmount); + assertEq(susds.balanceOf(address(almProxy)), 0); + + vm.prank(relayer); + uint256 susdsReturned = mainnetController.swapCurve(CURVE_POOL, 0, 1, susdeSwapAmount, minSUsdsAmount); + + assertEq(susds.convertToAssets(susdsReturned), 99.881093521220159847e18); + + assertEq(susde.balanceOf(address(almProxy)), 0); + assertEq(susds.balanceOf(address(almProxy)), susdsReturned); + + // Step 3: Remove liquidity + + uint256[] memory minWithdrawAmounts = new uint256[](2); + minWithdrawAmounts[0] = susdeAmount * 130/100; + minWithdrawAmounts[1] = susdsAmount * 65/100; + + vm.prank(relayer); + uint256[] memory assetsReceived = mainnetController.removeLiquidityCurve( + CURVE_POOL, + lpTokensReceived, + minWithdrawAmounts + ); + + assertEq(susde.convertToAssets(assetsReceived[0]), 1_317_667.897665048206964107e18); + assertEq(susds.convertToAssets(assetsReceived[1]), 682_834.255232605539287062e18); + + assertEq( + susde.convertToAssets(assetsReceived[0]) + susds.convertToAssets(assetsReceived[1]), + 2_000_502.152897653746251169e18 + ); + + assertEq(susde.balanceOf(address(almProxy)), assetsReceived[0]); + assertEq(susds.balanceOf(address(almProxy)), assetsReceived[1] + susdsReturned); + + assertEq(curveLp.balanceOf(address(almProxy)), 0); + } + +} diff --git a/test/mainnet-fork/DaiUsds.t.sol b/test/mainnet-fork/DaiUsds.t.sol new file mode 100644 index 00000000..5a517ce7 --- /dev/null +++ b/test/mainnet-fork/DaiUsds.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import "./ForkTestBase.t.sol"; + +contract MainnetControllerSwapUSDSToDAIFailureTests is ForkTestBase { + + function test_swapUSDSToDAI_notRelayer() external { + vm.expectRevert(abi.encodeWithSignature( + "AccessControlUnauthorizedAccount(address,bytes32)", + address(this), + RELAYER + )); + mainnetController.swapUSDSToDAI(1_000_000e18); + } + +} + +contract MainnetControllerSwapUSDSToDAITests is ForkTestBase { + + function test_swapUSDSToDAI() external { + vm.prank(relayer); + mainnetController.mintUSDS(1_000_000e18); + + assertEq(usds.balanceOf(address(almProxy)), 1_000_000e18); + assertEq(usds.totalSupply(), USDS_SUPPLY + 1_000_000e18); + + assertEq(dai.balanceOf(address(almProxy)), 0); + assertEq(dai.totalSupply(), DAI_SUPPLY); + + assertEq(usds.allowance(address(almProxy), DAI_USDS), 0); + + vm.prank(relayer); + mainnetController.swapUSDSToDAI(1_000_000e18); + + assertEq(usds.balanceOf(address(almProxy)), 0); + assertEq(usds.totalSupply(), USDS_SUPPLY); + + assertEq(dai.balanceOf(address(almProxy)), 1_000_000e18); + assertEq(dai.totalSupply(), DAI_SUPPLY + 1_000_000e18); + + assertEq(usds.allowance(address(almProxy), DAI_USDS), 0); + } + +} + +contract MainnetControllerSwapDAIToUSDSFailureTests is ForkTestBase { + + function test_swapDAIToUSDS_notRelayer() external { + vm.expectRevert(abi.encodeWithSignature( + "AccessControlUnauthorizedAccount(address,bytes32)", + address(this), + RELAYER + )); + mainnetController.swapDAIToUSDS(1_000_000e18); + } + +} + +contract MainnetControllerSwapDAIToUSDSTests is ForkTestBase { + + function test_swapDAIToUSDS() external { + deal(address(dai), address(almProxy), 1_000_000e18); + + assertEq(usds.balanceOf(address(almProxy)), 0); + assertEq(usds.totalSupply(), USDS_SUPPLY); + + assertEq(dai.balanceOf(address(almProxy)), 1_000_000e18); + assertEq(dai.totalSupply(), DAI_SUPPLY); // Supply not updated on deal + + assertEq(dai.allowance(address(almProxy), DAI_USDS), 0); + + vm.prank(relayer); + mainnetController.swapDAIToUSDS(1_000_000e18); + + assertEq(usds.balanceOf(address(almProxy)), 1_000_000e18); + assertEq(usds.totalSupply(), USDS_SUPPLY + 1_000_000e18); + + assertEq(dai.balanceOf(address(almProxy)), 0); + assertEq(dai.totalSupply(), DAI_SUPPLY - 1_000_000e18); + + assertEq(dai.allowance(address(almProxy), DAI_USDS), 0); + } + +} + diff --git a/test/mainnet-fork/ForkTestBase.t.sol b/test/mainnet-fork/ForkTestBase.t.sol index bb2387cc..035bf9ec 100644 --- a/test/mainnet-fork/ForkTestBase.t.sol +++ b/test/mainnet-fork/ForkTestBase.t.sol @@ -315,4 +315,8 @@ contract ForkTestBase is DssTest { return 20917850; // October 7, 2024 } + function _absSubtraction(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a - b : b - a; + } + } diff --git a/test/mainnet-fork/MorphoAllocations.t.sol b/test/mainnet-fork/MorphoAllocations.t.sol deleted file mode 100644 index dc2bb992..00000000 --- a/test/mainnet-fork/MorphoAllocations.t.sol +++ /dev/null @@ -1,279 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { IERC4626 } from "forge-std/interfaces/IERC4626.sol"; - -import { IMetaMorpho, Id, MarketAllocation } from "metamorpho/interfaces/IMetaMorpho.sol"; - -import { MarketParamsLib } from "morpho-blue/src/libraries/MarketParamsLib.sol"; -import { IMorpho, MarketParams } from "morpho-blue/src/interfaces/IMorpho.sol"; - -import { RateLimitHelpers } from "../../src/RateLimitHelpers.sol"; - -import "./ForkTestBase.t.sol"; - -contract MorphoTestBase is ForkTestBase { - - address internal constant PT_SUSDE_27MAR2025_PRICE_FEED = 0x38d130cEe60CDa080A3b3aC94C79c34B6Fc919A7; - address internal constant PT_SUSDE_27MAR2025 = 0xE00bd3Df25fb187d6ABBB620b3dfd19839947b81; - address internal constant PT_SUSDE_29MAY2025_PRICE_FEED = 0xE84f7e0a890e5e57d0beEa2c8716dDf0c9846B4A; - address internal constant PT_SUSDE_29MAY2025 = 0xb7de5dFCb74d25c2f21841fbd6230355C50d9308; - - IMetaMorpho morphoVault = IMetaMorpho(Ethereum.MORPHO_VAULT_DAI_1); - IMorpho morpho = IMorpho(Ethereum.MORPHO); - - // Using March and May 2025 sUSDe PT markets for testing - MarketParams market1 = MarketParams({ - loanToken : Ethereum.DAI, - collateralToken : PT_SUSDE_27MAR2025, - oracle : PT_SUSDE_27MAR2025_PRICE_FEED, - irm : Ethereum.MORPHO_DEFAULT_IRM, - lltv : 0.915e18 - }); - MarketParams market2 = MarketParams({ - loanToken : Ethereum.DAI, - collateralToken : PT_SUSDE_29MAY2025, - oracle : PT_SUSDE_29MAY2025_PRICE_FEED, - irm : Ethereum.MORPHO_DEFAULT_IRM, - lltv : 0.915e18 - }); - - function setUp() public override { - super.setUp(); - - // Spell onboarding (Ability to deposit necessary to onboard a vault for allocations) - vm.startPrank(Ethereum.SPARK_PROXY); - morphoVault.setIsAllocator(address(almProxy), true); - rateLimits.setRateLimitData( - RateLimitHelpers.makeAssetKey( - mainnetController.LIMIT_4626_DEPOSIT(), - address(morphoVault) - ), - 1_000_000e6, - uint256(1_000_000e6) / 1 days - ); - vm.stopPrank(); - } - - function _getBlock() internal pure override returns (uint256) { - return 21680000; // Jan 22, 2024 - } - - function positionShares(MarketParams memory marketParams) internal view returns (uint256) { - return morpho.position(MarketParamsLib.id(marketParams), address(morphoVault)).supplyShares; - } - - function positionAssets(MarketParams memory marketParams) internal view returns (uint256) { - return positionShares(marketParams) - * marketAssets(marketParams) - / morpho.market(MarketParamsLib.id(marketParams)).totalSupplyShares; - } - - function marketAssets(MarketParams memory marketParams) internal view returns (uint256) { - return morpho.market(MarketParamsLib.id(marketParams)).totalSupplyAssets; - } - -} - -contract MorphoSetSupplyQueueMorphoFailureTests is MorphoTestBase { - - function test_setSupplyQueueMorpho_notRelayer() external { - vm.expectRevert(abi.encodeWithSignature( - "AccessControlUnauthorizedAccount(address,bytes32)", - address(this), - RELAYER - )); - mainnetController.setSupplyQueueMorpho(address(morphoVault), new Id[](0)); - } - - function test_setSupplyQueueMorpho_invalidVault() external { - vm.prank(relayer); - vm.expectRevert("MainnetController/invalid-action"); - mainnetController.setSupplyQueueMorpho(makeAddr("fake-vault"), new Id[](0)); - } - -} - -contract MorphoSetSupplyQueueMorphoSuccessTests is MorphoTestBase { - - function test_setSupplyQueueMorpho() external { - // Switch order of existing markets - Id[] memory supplyQueueUSDC = new Id[](2); - supplyQueueUSDC[0] = MarketParamsLib.id(market1); - supplyQueueUSDC[1] = MarketParamsLib.id(market2); - - // No supply queue to start, but caps are above zero - assertEq(morphoVault.supplyQueueLength(), 0); - - vm.prank(relayer); - mainnetController.setSupplyQueueMorpho(address(morphoVault), supplyQueueUSDC); - - assertEq(morphoVault.supplyQueueLength(), 2); - - assertEq(Id.unwrap(morphoVault.supplyQueue(0)), Id.unwrap(MarketParamsLib.id(market1))); - assertEq(Id.unwrap(morphoVault.supplyQueue(1)), Id.unwrap(MarketParamsLib.id(market2))); - } - -} - -contract MorphoUpdateWithdrawQueueMorphoFailureTests is MorphoTestBase { - - function test_updateWithdrawQueueMorpho_notRelayer() external { - vm.expectRevert(abi.encodeWithSignature( - "AccessControlUnauthorizedAccount(address,bytes32)", - address(this), - RELAYER - )); - mainnetController.updateWithdrawQueueMorpho(address(morphoVault), new uint256[](0)); - } - - function test_updateWithdrawQueueMorpho_invalidVault() external { - vm.prank(relayer); - vm.expectRevert("MainnetController/invalid-action"); - mainnetController.updateWithdrawQueueMorpho(makeAddr("fake-vault"), new uint256[](0)); - } - -} - -contract MorphoUpdateWithdrawQueueMorphoSuccessTests is MorphoTestBase { - - function test_updateWithdrawQueueMorpho() external { - // Switch order of existing markets - uint256[] memory newWithdrawQueueUsdc = new uint256[](14); - Id[] memory startingWithdrawQueue = new Id[](14); - - // Set all markets in same order then adjust - for (uint256 i = 0; i < 14; i++) { - newWithdrawQueueUsdc[i] = i; - startingWithdrawQueue[i] = morphoVault.withdrawQueue(i); - } - - assertEq(morphoVault.withdrawQueueLength(), 14); - - assertEq(Id.unwrap(morphoVault.withdrawQueue(11)), Id.unwrap(MarketParamsLib.id(market1))); - assertEq(Id.unwrap(morphoVault.withdrawQueue(13)), Id.unwrap(MarketParamsLib.id(market2))); - - // Switch order of market1 and market2 - newWithdrawQueueUsdc[11] = 13; - newWithdrawQueueUsdc[13] = 11; - - vm.prank(relayer); - mainnetController.updateWithdrawQueueMorpho(address(morphoVault), newWithdrawQueueUsdc); - - assertEq(morphoVault.withdrawQueueLength(), 14); - - assertEq(Id.unwrap(morphoVault.withdrawQueue(11)), Id.unwrap(MarketParamsLib.id(market2))); - assertEq(Id.unwrap(morphoVault.withdrawQueue(13)), Id.unwrap(MarketParamsLib.id(market1))); - - // Ensure the rest is kept in order - for (uint256 i = 0; i < 14; i++) { - if (i == 11 || i == 13) continue; - assertEq(Id.unwrap(morphoVault.withdrawQueue(i)), Id.unwrap(startingWithdrawQueue[i])); - } - } - -} - -contract MorphoReallocateMorphoFailureTests is MorphoTestBase { - - function test_reallocateMorpho_notRelayer() external { - vm.expectRevert(abi.encodeWithSignature( - "AccessControlUnauthorizedAccount(address,bytes32)", - address(this), - RELAYER - )); - mainnetController.reallocateMorpho(address(morphoVault), new MarketAllocation[](0)); - } - - function test_reallocateMorpho_invalidVault() external { - vm.prank(relayer); - vm.expectRevert("MainnetController/invalid-action"); - mainnetController.reallocateMorpho(makeAddr("fake-vault"), new MarketAllocation[](0)); - } - -} - -contract MorphoReallocateMorphoSuccessTests is MorphoTestBase { - - function test_reallocateMorpho() external { - vm.startPrank(Ethereum.SPARK_PROXY); - rateLimits.setRateLimitData( - RateLimitHelpers.makeAssetKey( - mainnetController.LIMIT_4626_DEPOSIT(), - address(morphoVault) - ), - 25_000_000e6, - uint256(5_000_000e6) / 1 days - ); - vm.stopPrank(); - - // Refresh markets so calculations don't include interest - vm.prank(relayer); - mainnetController.depositERC4626(address(morphoVault), 0); - - uint256 market1Position = positionAssets(market1); - uint256 market2Position = positionAssets(market2); - - uint256 market1Assets = marketAssets(market1); - uint256 market2Assets = marketAssets(market2); - - assertEq(market1Position, 356_456_521.341763767525558015e18); - assertEq(market2Position, 50_038_784.076802509703226888e18); - - assertEq(market1Assets, 390_003_166.284505547080982600e18); - assertEq(market2Assets, 50_038_786.142322219196324919e18); - - // Move 1m from market1 to market2 - MarketAllocation[] memory reallocations = new MarketAllocation[](2); - reallocations[0] = MarketAllocation({ - marketParams : market1, - assets : market1Position - 1_000_000e18 - }); - reallocations[1] = MarketAllocation({ - marketParams : market2, - assets : type(uint256).max - }); - - vm.prank(relayer); - mainnetController.reallocateMorpho(address(morphoVault), reallocations); - - uint256 positionInterest = 9_803.525491215426215841e18; - uint256 market1Interest = 223.610631168657703153e18; - uint256 market2Interest = 9_803.525797810824423503e18; // Slightly higher than position from external liquidity - - // Interest from position1 moves as well, resulting position is as specified - assertEq(positionAssets(market1), market1Position - 1_000_000e18); - assertEq(positionAssets(market2), market2Position + 1_000_000e18 + positionInterest); - - assertEq(marketAssets(market1), market1Assets - 1_000_000e18 + market1Interest); - assertEq(marketAssets(market2), market2Assets + 1_000_000e18 + market2Interest); - - // Overwrite values for simpler assertions - market1Position = positionAssets(market1); - market2Position = positionAssets(market2); - market1Assets = marketAssets(market1); - market2Assets = marketAssets(market2); - - // Move another 500k from market1 to market2 - reallocations = new MarketAllocation[](2); - reallocations[0] = MarketAllocation({ - marketParams : market1, - assets : market1Position - 500_000e18 - }); - reallocations[1] = MarketAllocation({ - marketParams : market2, - assets : market2Position + 500_000e18 - }); - - vm.prank(relayer); - mainnetController.reallocateMorpho(address(morphoVault), reallocations); - - // No new interest has been accounted for so values are exact - assertEq(positionAssets(market1), market1Position - 500_000e18); - assertEq(positionAssets(market2), market2Position + 500_000e18); - - assertEq(marketAssets(market1), market1Assets - 500_000e18); - assertEq(marketAssets(market2), market2Assets + 500_000e18); - } - -} diff --git a/test/unit/controllers/Admin.t.sol b/test/unit/controllers/Admin.t.sol index 90ee41be..7cd7b320 100644 --- a/test/unit/controllers/Admin.t.sol +++ b/test/unit/controllers/Admin.t.sol @@ -10,8 +10,9 @@ import { MockVault } from "../mocks/MockVault.sol"; import "../UnitTestBase.t.sol"; -contract MainnetControllerAdminTests is UnitTestBase { +contract MainnetControllerAdminTestBase is UnitTestBase { + event MaxSlippageSet(address indexed pool, uint256 maxSlippage); event MintRecipientSet(uint32 indexed destinationDomain, bytes32 mintRecipient); bytes32 mintRecipient1 = bytes32(uint256(uint160(makeAddr("mintRecipient1")))); @@ -35,6 +36,10 @@ contract MainnetControllerAdminTests is UnitTestBase { ); } +} + +contract MainnetControllerSetMintRecipientTests is MainnetControllerAdminTestBase { + function test_setMintRecipient_unauthorizedAccount() public { vm.expectRevert(abi.encodeWithSignature( "AccessControlUnauthorizedAccount(address,bytes32)", @@ -80,6 +85,47 @@ contract MainnetControllerAdminTests is UnitTestBase { } +contract MainnetControllerSetMaxSlippageTests is MainnetControllerAdminTestBase { + + function test_setMaxSlippage_unauthorizedAccount() public { + vm.expectRevert(abi.encodeWithSignature( + "AccessControlUnauthorizedAccount(address,bytes32)", + address(this), + DEFAULT_ADMIN_ROLE + )); + mainnetController.setMaxSlippage(makeAddr("pool"), 0.01e18); + + vm.prank(freezer); + vm.expectRevert(abi.encodeWithSignature( + "AccessControlUnauthorizedAccount(address,bytes32)", + freezer, + DEFAULT_ADMIN_ROLE + )); + mainnetController.setMaxSlippage(makeAddr("pool"), 0.01e18); + } + + function test_setMaxSlippage() public { + address pool = makeAddr("pool"); + + assertEq(mainnetController.maxSlippages(pool), 0); + + vm.prank(admin); + vm.expectEmit(address(mainnetController)); + emit MaxSlippageSet(pool, 0.01e18); + mainnetController.setMaxSlippage(pool, 0.01e18); + + assertEq(mainnetController.maxSlippages(pool), 0.01e18); + + vm.prank(admin); + vm.expectEmit(address(mainnetController)); + emit MaxSlippageSet(pool, 0.02e18); + mainnetController.setMaxSlippage(pool, 0.02e18); + + assertEq(mainnetController.maxSlippages(pool), 0.02e18); + } + +} + contract ForeignControllerAdminTests is UnitTestBase { event MintRecipientSet(uint32 indexed destinationDomain, bytes32 mintRecipient); diff --git a/test/unit/rate-limits/RateLimitHelpers.t.sol b/test/unit/rate-limits/RateLimitHelpers.t.sol index a2ff89d3..af2d79d1 100644 --- a/test/unit/rate-limits/RateLimitHelpers.t.sol +++ b/test/unit/rate-limits/RateLimitHelpers.t.sol @@ -74,28 +74,28 @@ contract RateLimitHelpersTestBase is UnitTestBase { contract RateLimitHelpersPureFunctionTests is RateLimitHelpersTestBase { - function test_makeAssetKey() public { + function test_makeAssetKey() public view { assertEq( wrapper.makeAssetKey(KEY, address(this)), keccak256(abi.encode(KEY, address(this))) ); } - function test_makeAssetDestinationKey() public { + function test_makeAssetDestinationKey() public view { assertEq( wrapper.makeAssetDestinationKey(KEY, address(this), address(0)), keccak256(abi.encode(KEY, address(this), address(0))) ); } - function test_makeDomainKey() public { + function test_makeDomainKey() public view { assertEq( wrapper.makeDomainKey(KEY, 123), keccak256(abi.encode(KEY, 123)) ); } - function test_unlimitedRateLimit() public { + function test_unlimitedRateLimit() public view { RateLimitData memory data = wrapper.unlimitedRateLimit(); assertEq(data.maxAmount, type(uint256).max);