diff --git a/test/fork/root/RootERC20BridgeFlowRate.t.sol b/test/fork/root/RootERC20BridgeFlowRate.t.sol index cc8e4a9d..934383a2 100644 --- a/test/fork/root/RootERC20BridgeFlowRate.t.sol +++ b/test/fork/root/RootERC20BridgeFlowRate.t.sol @@ -29,36 +29,44 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER * * NOTE: Foundry's deal() function does not currently support contracts that use the proxy pattern, such as USDC. * Hence this test is limited to ETH and ERC20 tokens that do not use the proxy pattern. + * Details: https://github.com/foundry-rs/forge-std/issues/318#issuecomment-1452463876 */ contract RootERC20BridgeFlowRateForkTest is Test, Utils { address private constant ETH = address(0xeee); string[] private deployments = vm.envString("DEPLOYMENTS", ","); + + // rpc endpoints to the root chain for each environment mapping(string => string) private rpcURLForEnv; - mapping(string => uint256) private forkOfEnv; - mapping(string => RootERC20BridgeFlowRate) private bridgeForEnv; + // the fork id for each environment + mapping(string => uint256) private forkIdForEnv; + // the list of tokens for which flow rate parameters have been configured, in each environment mapping(string => address[]) private tokensForEnv; + // the root bridge address in each environment + mapping(string => RootERC20BridgeFlowRate) private bridgeInEnv; + // the deployment environment currently being tested string private deployment; /** - * @dev runs a given test function for each deployment in the DEPLOYMENTS environment variable (e.g. MAINNET, TESTNET) + * @dev Runs a test function against each deployment listed in the DEPLOYMENTS environment variable (e.g. MAINNET, TESTNET) */ modifier forEachDeployment() { for (uint256 i; i < deployments.length; i++) { deployment = deployments[i]; - vm.selectFork(forkOfEnv[deployment]); + vm.selectFork(forkIdForEnv[deployment]); _; } } function setUp() public { + // extract the rpc endpoint, bridge address, and tokens for each deployment for (uint256 i; i < deployments.length; i++) { string memory dep = deployments[i]; rpcURLForEnv[dep] = vm.envString(string.concat(dep, "_RPC_URL")); - bridgeForEnv[dep] = RootERC20BridgeFlowRate(payable(vm.envAddress(string.concat(dep, "_BRIDGE_ADDRESS")))); + bridgeInEnv[dep] = RootERC20BridgeFlowRate(payable(vm.envAddress(string.concat(dep, "_BRIDGE_ADDRESS")))); tokensForEnv[dep] = vm.envAddress(string.concat(dep, "_FLOW_RATED_TOKENS"), ","); - forkOfEnv[dep] = vm.createFork(rpcURLForEnv[dep]); + forkIdForEnv[dep] = vm.createFork(rpcURLForEnv[dep]); } } @@ -68,25 +76,25 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { * The test also checks that flow rate parameters configured are valid. */ function test_withdrawalQueueEnforcedWhenFlowRateExceeded() public forEachDeployment { - RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; + RootERC20BridgeFlowRate bridge = bridgeInEnv[deployment]; address[] memory tokens = tokensForEnv[deployment]; - // preconditions - assertFalse(bridge.withdrawalQueueActivated()); - assertGt(bridge.withdrawalDelay(), 0); + // precondition: sanity check on the the current state and parameters of the bridge + assertFalse(bridge.withdrawalQueueActivated(), "Precondition: Withdrawal queue should not already be active"); + assertGt(bridge.withdrawalDelay(), 0, "Precondition: Withdrawal delay should be greater than 0"); - address receiver1 = createAddress(1); - address receiver2 = createAddress(2); + address withdrawer1 = createAddress(1); + address withdrawer2 = createAddress(2); uint256 snapshotId = vm.snapshot(); for (uint256 i; i < tokens.length; i++) { address token = tokens[i]; - // exceed flow rate for any token - _exceedFlowRateParameters(bridge, token, receiver1); + // exceed flow rate for token + _exceedFlowRateParameters(bridge, token, withdrawer1); // Verify that any subsequent withdrawal by other users for other tokens gets queued address otherToken = tokens[(i + 1) % tokens.length]; - _sendWithdrawalMessage(bridge, otherToken, receiver2, 1 ether); - _verifyWithdrawalWasQueued(bridge, otherToken, receiver2, 1 ether); - _verifyBalance(otherToken, receiver2, 0); + _sendWithdrawalMessage(bridge, otherToken, withdrawer2, 1 ether); + _verifyWithdrawalWasQueued(bridge, otherToken, withdrawer2, 1 ether); + _verifyBalance(otherToken, withdrawer2, 0); // roll back state for subsequent test vm.revertTo(snapshotId); @@ -94,68 +102,74 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } /** - * @dev Tests that withdrawal of non-flow rated tokens are queued. - * This test is run for each deployment environment. + * @dev Tests that withdrawal of non-flow rated tokens get queued. + * This test is run for each deployment environment. */ function test_nonFlowRatedTokenWithdrawalsAreQueued() public forEachDeployment { - RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; - address receiver = createAddress(1); + RootERC20BridgeFlowRate bridge = bridgeInEnv[deployment]; + address withdrawer = createAddress(1); // preconditions assertFalse(bridge.withdrawalQueueActivated(), "Precondition: Withdrawal queue should not activate"); // deploy and map a token - ERC20 erc20 = new ERC20("Test Token", "TEST"); - bridge.mapToken{value: 100 gwei}(erc20); - _giveBridgeFunds(address(erc20), address(erc20), 1 ether); + ERC20 nonFlowRatedToken = new ERC20("Test Token", "TEST"); + bridge.mapToken{value: 100 gwei}(nonFlowRatedToken); + _giveBridgeFunds(address(bridge), address(nonFlowRatedToken), 1 ether); // ensure withdrawals for the token, which does not have flow rate configured, is queued - _sendWithdrawalMessage(bridge, address(erc20), receiver, 1 ether); - _verifyWithdrawalWasQueued(bridge, address(erc20), receiver, 1 ether); + _sendWithdrawalMessage(bridge, address(nonFlowRatedToken), withdrawer, 1 ether); + _verifyWithdrawalWasQueued(bridge, address(nonFlowRatedToken), withdrawer, 1 ether); // The queue should only affect the specific token assertFalse(bridge.withdrawalQueueActivated()); } /** - * @dev Tests that for queued withdrawal the mandatory delay is enforced. Also ensures that the withdrawal delay parameters for a deployment are valid. + * @dev Tests that for queued withdrawal the mandatory delay is enforced. + * Also ensures that the withdrawal delay parameters for a deployment are valid. */ function test_withdrawalQueueDelayEnforced() public forEachDeployment { - uint256 snapshotId = vm.snapshot(); + RootERC20BridgeFlowRate bridge = bridgeInEnv[deployment]; + // preconditions: sanity check on the current state and parameters of the bridge + assertGt(bridge.withdrawalDelay(), 0 days, "Precondition: Withdrawal delay should be greater than 0"); + address[] memory tokens = tokensForEnv[deployment]; + uint256 snapshotId = vm.snapshot(); for (uint256 i; i < tokens.length; i++) { address token = tokens[i]; - RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; - - assertTrue( - bridge.withdrawalDelay() > 0 days && bridge.withdrawalDelay() <= 3 days, - "Precondition: Withdrawal delay appears either too low or too high" - ); - - address receiver = address(12234); uint256 amount = bridge.largeTransferThresholds(token) + 1; + _giveBridgeFunds(address(bridge), token, amount); + + address withdrawer = createAddress(1); + _sendWithdrawalMessage(bridge, token, withdrawer, amount); + _verifyWithdrawalWasQueued(bridge, token, withdrawer, amount); - _sendWithdrawalMessage(bridge, token, receiver, amount); - _verifyWithdrawalWasQueued(bridge, token, receiver, amount); + console.log("Bridge balance: ", address(bridge).balance); + console.log("Test contract balance: ", address(this).balance); // check that early withdrawal attempt fails vm.expectRevert(); - bridge.finaliseQueuedWithdrawal(receiver, 0); + bridge.finaliseQueuedWithdrawal(withdrawer, 0); // check that timely withdrawal succeeds vm.warp(block.timestamp + bridge.withdrawalDelay()); - bridge.finaliseQueuedWithdrawal(receiver, 0); - _verifyBalance(token, receiver, amount); + bridge.finaliseQueuedWithdrawal(withdrawer, 0); + _verifyBalance(token, withdrawer, amount); // roll back state for subsequent test vm.revertTo(snapshotId); } } + /** + * @dev Tests that withdrawals that exceed the size threshold for a given token get queued. + * Also ensures that the threshold parameters for a token are valid. + */ function test_withdrawalIsQueuedIfSizeThresholdForTokenExceeded() public forEachDeployment { address[] memory tokens = tokensForEnv[deployment]; - RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment]; - address receiver = createAddress(1); + RootERC20BridgeFlowRate bridge = bridgeInEnv[deployment]; + address withdrawer = createAddress(1); uint256 snapshotId = vm.snapshot(); for (uint256 i; i < tokens.length; i++) { @@ -165,8 +179,8 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { // preconditions _checkIsValidLargeThreshold(token, largeAmount); - _sendWithdrawalMessage(bridge, token, receiver, largeAmount); - _verifyWithdrawalWasQueued(bridge, token, receiver, largeAmount); + _sendWithdrawalMessage(bridge, token, withdrawer, largeAmount); + _verifyWithdrawalWasQueued(bridge, token, withdrawer, largeAmount); // The queue should only affect the specific token assertFalse(bridge.withdrawalQueueActivated()); @@ -176,7 +190,7 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } } - // check that the large transfer threshold for the token is at least greater than 1 whole unit of the token + /// @dev check that the large transfer threshold for the token is at least greater than 1 whole unit of the token function _checkIsValidLargeThreshold(address token, uint256 amount) private { if (token == ETH) { assertGe(amount, 1 ether); @@ -185,7 +199,8 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } } - function _exceedFlowRateParameters(RootERC20BridgeFlowRate bridge, address token, address receiver) private { + /// @dev sends a number of withdrawal messages to the bridge that exceeds the flow rate parameters for a given token + function _exceedFlowRateParameters(RootERC20BridgeFlowRate bridge, address token, address withdrawer) private { (uint256 capacity, uint256 depth,, uint256 refillRate) = bridge.flowRateBuckets(token); uint256 oneUnit = token == ETH ? 1 ether : 1 ^ IERC20Metadata(token).decimals(); @@ -202,7 +217,7 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { ); assertGt(capacity, oneUnit); assertGt(refillRate, 0); - assertEq(bridge.getPendingWithdrawalsLength(receiver), 0); + assertEq(bridge.getPendingWithdrawalsLength(withdrawer), 0); uint256 txValue = bridge.largeTransferThresholds(token) - 1; uint256 numTxs = depth > txValue ? (depth / txValue) + 2 : 1; @@ -212,14 +227,15 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { // withdraw until flow rate is exceeded for (uint256 i = 0; i < numTxs; i++) { (, depth,,) = bridge.flowRateBuckets(token); - _sendWithdrawalMessage(bridge, token, receiver, txValue); + _sendWithdrawalMessage(bridge, token, withdrawer, txValue); } assertTrue(bridge.withdrawalQueueActivated()); - _verifyWithdrawalWasQueued(bridge, token, receiver, txValue); - _verifyBalance(token, receiver, (numTxs - 1) * txValue); + _verifyWithdrawalWasQueued(bridge, token, withdrawer, txValue); + _verifyBalance(token, withdrawer, (numTxs - 1) * txValue); } + /// @dev sends an amount of a given token to the bridge. function _giveBridgeFunds(address bridge, address token, uint256 amount) private { if (token == ETH) { deal(bridge, amount); @@ -228,26 +244,27 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils { } } + /// @dev checks that a withdrawal was queued for a given token and user function _verifyWithdrawalWasQueued( RootERC20BridgeFlowRate bridge, address token, - address receiver, + address withdrawer, uint256 txValue ) private { uint256[] memory indices = new uint256[](1); indices[0] = 0; - assertEq(bridge.getPendingWithdrawalsLength(receiver), 1, "Expected 1 pending withdrawal"); - RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = bridge.getPendingWithdrawals(receiver, indices); - assertEq(pending[0].withdrawer, receiver, "Unexpected withdrawer"); + assertEq(bridge.getPendingWithdrawalsLength(withdrawer), 1, "Expected 1 pending withdrawal"); + RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending = bridge.getPendingWithdrawals(withdrawer, indices); + assertEq(pending[0].withdrawer, withdrawer, "Unexpected withdrawer"); assertEq(pending[0].token, token, "Unexpected token"); assertEq(pending[0].amount, txValue, "Unexpected amount"); } - function _verifyBalance(address token, address receiver, uint256 expectedAmount) private { + function _verifyBalance(address token, address withdrawer, uint256 expectedAmount) private { if (token == ETH) { - assertEq(receiver.balance, expectedAmount); + assertEq(withdrawer.balance, expectedAmount); } else { - assertEq(ERC20(token).balanceOf(receiver), expectedAmount); + assertEq(ERC20(token).balanceOf(withdrawer), expectedAmount); } }