Skip to content

Commit

Permalink
Refactor and improve comments
Browse files Browse the repository at this point in the history
  • Loading branch information
ermyas committed Feb 1, 2024
1 parent f1c08fd commit e8998cd
Showing 1 changed file with 75 additions and 58 deletions.
133 changes: 75 additions & 58 deletions test/fork/root/RootERC20BridgeFlowRate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
}

Expand All @@ -68,94 +76,100 @@ 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);
}
}

/**
* @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++) {
Expand All @@ -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());
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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);
}
}

Expand Down

0 comments on commit e8998cd

Please sign in to comment.