Skip to content

Commit

Permalink
Refactor test
Browse files Browse the repository at this point in the history
  • Loading branch information
ermyas committed Jan 31, 2024
1 parent e7b9a6a commit f1c08fd
Showing 1 changed file with 150 additions and 101 deletions.
251 changes: 150 additions & 101 deletions test/fork/root/RootERC20BridgeFlowRate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,34 @@ pragma solidity 0.8.19;
import {Test, console2} from "forge-std/Test.sol";
import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol";
import {IFlowRateWithdrawalQueueErrors} from "../../../src/root/flowrate/FlowRateWithdrawalQueue.sol";
import {console} from "forge-std/console.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {console} from "forge-std/Console.sol";

import {Utils} from "../../utils.t.sol";

import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

/**
* @dev This test suite evaluates the flow rate functionality of already deployed RootERC20BridgeFlowRate contracts.
* The tests are executed against forked chains for each deployment (e.g., mainnet, testnet).
* This test suite's objective is not to exhaustively test the flow rate functionality, as this is adequately
* addressed in unit and integration tests. Instead, it aims to ensure that the functionality works as expected
* in each deployed environment. Conducting live E2E tests on the flow rate capability in a mainnet environment
* for each configured token would be complex, expensive, and potentially disruptive.
* Therefore, these tests provide an alternative way to verify that these capabilities function correctly in
* the deployed environment. They can help identify any issues related to deployment parameters or
* specific tokens or deployment conditions that may not have been captured in unit and integration tests.
*
* The test suite is parameterized by the following environment variables:
* - DEPLOYMENTS: comma-separated list of deployments to test (e.g., MAINNET, TESTNET)
* - <DEPLOYMENT>_RPC_URL: RPC URL for the forked chain for the deployment (e.g., MAINNET_RPC_URL)
* - <DEPLOYMENT>_BRIDGE_ADDRESS: address of the RootERC20BridgeFlowRate contract for the deployment (e.g., MAINNET_BRIDGE_ADDRESS)
* - <DEPLOYMENT>_FLOW_RATED_TOKENS: comma-separated list of tokens to test for the deployment (e.g., MAINNET_FLOW_RATED_TOKENS)
*
* 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.
*/
contract RootERC20BridgeFlowRateForkTest is Test, Utils {
address public constant NATIVE_ETH = address(0xeee);
address private constant ETH = address(0xeee);

string[] private deployments = vm.envString("DEPLOYMENTS", ",");
mapping(string => string) private rpcURLForEnv;
Expand All @@ -19,6 +40,10 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils {
mapping(string => address[]) private tokensForEnv;

string private deployment;

/**
* @dev runs a given test function for each deployment in the DEPLOYMENTS environment variable (e.g. MAINNET, TESTNET)
*/
modifier forEachDeployment() {
for (uint256 i; i < deployments.length; i++) {
deployment = deployments[i];
Expand All @@ -37,76 +62,152 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils {
}
}

/**
* @dev Tests that that exceeding the flow rate parameters for any of the configured tokens in a given deployment, triggers the withdrawal delays.
* This test is run for each deployment environment, and against each configured token.
* The test also checks that flow rate parameters configured are valid.
*/
function test_withdrawalQueueEnforcedWhenFlowRateExceeded() public forEachDeployment {
console.log("Testing deployment: ", deployment);
vm.selectFork(forkOfEnv[deployment]);
_verifyWithdrawalQueueEnforcedForAllTokens(bridgeForEnv[deployment], tokensForEnv[deployment]);
RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment];
address[] memory tokens = tokensForEnv[deployment];
// preconditions
assertFalse(bridge.withdrawalQueueActivated());
assertGt(bridge.withdrawalDelay(), 0);

address receiver1 = createAddress(1);
address receiver2 = 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);

// 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);

// roll back state for subsequent test
vm.revertTo(snapshotId);
}
}

function test_nonFlowRatedTokenIsQueued() public forEachDeployment {
vm.selectFork(forkOfEnv[deployment]);
/**
* @dev Tests that withdrawal of non-flow rated tokens are queued.
* This test is run for each deployment environment.
*/
function test_nonFlowRatedTokenWithdrawalsAreQueued() public forEachDeployment {
RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment];
address receiver = createAddress(1);

// preconditions
assertFalse(bridge.withdrawalQueueActivated());
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);

// ensure withdrawals for the token, which does not have flow rate configured, is queued
_sendWithdrawalMessage(bridge, address(erc20), address(1), 1 ether);
_verifyWithdrawalWasQueued(bridge, address(erc20), address(1), 1 ether);
_sendWithdrawalMessage(bridge, address(erc20), receiver, 1 ether);
_verifyWithdrawalWasQueued(bridge, address(erc20), receiver, 1 ether);

// The queue should only affect the specific token
assertFalse(bridge.withdrawalQueueActivated());
}

function test_withdrawalQueueDelayEnforced() public {}
/**
* @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();
address[] memory tokens = tokensForEnv[deployment];
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"
);

function test_withdrawalQueuedIfTransferIsTooLarge() public {
address receiver = address(12234);
uint256 amount = bridge.largeTransferThresholds(token) + 1;

}
_sendWithdrawalMessage(bridge, token, receiver, amount);
_verifyWithdrawalWasQueued(bridge, token, receiver, amount);

function test_queuedWithdrawalsCanBeFinalised() public {}
// check that early withdrawal attempt fails
vm.expectRevert();
bridge.finaliseQueuedWithdrawal(receiver, 0);

function _verifyWithdrawalQueueEnforcedForAllTokens(RootERC20BridgeFlowRate bridge, address[] memory tokens)
private
{
// preconditions
assertFalse(bridge.withdrawalQueueActivated());
assertGt(bridge.withdrawalDelay(), 0);
// check that timely withdrawal succeeds
vm.warp(block.timestamp + bridge.withdrawalDelay());
bridge.finaliseQueuedWithdrawal(receiver, 0);
_verifyBalance(token, receiver, amount);

// roll back state for subsequent test
vm.revertTo(snapshotId);
}
}

function test_withdrawalIsQueuedIfSizeThresholdForTokenExceeded() public forEachDeployment {
address[] memory tokens = tokensForEnv[deployment];
RootERC20BridgeFlowRate bridge = bridgeForEnv[deployment];
address receiver = createAddress(1);

uint256 snapshotId = vm.snapshot();
for (uint256 i; i < tokens.length; i++) {
address token = tokens[i];
console.log("Testing flow rate for token: ", token);
// exceed flow rate for any token
_exceedFlowRateParameters(bridge, token, address(11));
uint256 largeAmount = bridge.largeTransferThresholds(token) + 1;

// Verify that any subsequent withdrawal by other users for other tokens gets queued
address otherToken = tokens[(i + 1) % tokens.length];
_sendWithdrawalMessage(bridge, otherToken, address(12), 1 ether);
_verifyWithdrawalWasQueued(bridge, otherToken, address(12), 1 ether);
// preconditions
_checkIsValidLargeThreshold(token, largeAmount);

_sendWithdrawalMessage(bridge, token, receiver, largeAmount);
_verifyWithdrawalWasQueued(bridge, token, receiver, largeAmount);

// The queue should only affect the specific token
assertFalse(bridge.withdrawalQueueActivated());

// roll back state for subsequent test
vm.revertTo(snapshotId);
}
}

// 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);
} else {
assertGe(amount, 1 ^ IERC20Metadata(token).decimals());
}
}

function _exceedFlowRateParameters(RootERC20BridgeFlowRate bridge, address token, address receiver) private {
(uint256 capacity, uint256 depth,, uint256 refillRate) = bridge.flowRateBuckets(token);
// check preconditions
assertGt(bridge.largeTransferThresholds(token), 0);
assertGt(capacity, 0);

uint256 oneUnit = token == ETH ? 1 ether : 1 ^ IERC20Metadata(token).decimals();
// Check if the thresholds are within reasonable range
assertGt(
bridge.largeTransferThresholds(token),
oneUnit,
"Precondition: Large transfer threshold should be greater than 1 unit of token"
);
assertLt(
bridge.largeTransferThresholds(token),
capacity,
"Precondition: Large transfer threshold should be less than capacity"
);
assertGt(capacity, oneUnit);
assertGt(refillRate, 0);
assertEq(bridge.getPendingWithdrawalsLength(receiver), 0);

uint256 txValue = bridge.largeTransferThresholds(token) - 1;
uint256 numTxs = depth > txValue ? (depth / txValue) + 2 : 1;

_giveBridgeFunds(address(bridge), token, numTxs * txValue * 2);
_giveBridgeFunds(address(bridge), token, numTxs * txValue);

// withdraw until flow rate is exceeded
for (uint256 i = 0; i < numTxs; i++) {
Expand All @@ -116,10 +217,11 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils {

assertTrue(bridge.withdrawalQueueActivated());
_verifyWithdrawalWasQueued(bridge, token, receiver, txValue);
_verifyBalance(token, receiver, (numTxs - 1) * txValue);
}

function _giveBridgeFunds(address bridge, address token, uint256 amount) private {
if (token == NATIVE_ETH) {
if (token == ETH) {
deal(bridge, amount);
} else {
deal(token, bridge, amount);
Expand All @@ -141,80 +243,27 @@ contract RootERC20BridgeFlowRateForkTest is Test, Utils {
assertEq(pending[0].amount, txValue, "Unexpected amount");
}

// ensure subsequent withdrawals for any token, by any entity are queued
// _sendWithdrawMessage(NATIVE_ETH, receiver, receiver, txValue);

/*
Check that the withdrawal queue has been activated
Check that the user has a pending withdrawal for the specified token
*/
//
// assertEq(bridge.getPendingWithdrawalsLength(receiver1), 0);
//
// //send one more tx to receiver2 and make sure it gets queued
// _sendWithdrawMessage(token, receiver2, receiver2, txValue);
//
// //attempt to withdraw for receiver 1
// uint256 okTime1 = _attemptEarlyWithdraw(token, receiver1, txValue);
//
// //attempt to withdraw for receiver 2
// uint256 okTime2 = _attemptEarlyWithdraw(token, receiver2, txValue);
//
// //fast forward past withdrawal delay time and withdraw for receiver 1
// vm.warp(okTime1 + 1);
// bridge.finaliseQueuedWithdrawal(receiver1, 0);
//
// //fast forward past withdrawal delay time and withdraw for receiver 2
// vm.warp(okTime2 + 1);
// bridge.finaliseQueuedWithdrawal(receiver2, 0);

// function _attemptEarlyWithdraw(address token, address receiver, uint256 txValue) public returns (uint256 okTime) {
// uint withdrawDelay = bridge.withdrawalDelay();
// uint256 pendingLength = bridge.getPendingWithdrawalsLength(receiver);
//
// assertEq(pendingLength, 1);
//
// uint256[] memory indices = new uint256[](1);
// indices[0] = 0;
//
// RootERC20BridgeFlowRate.PendingWithdrawal[] memory pending =
// bridge.getPendingWithdrawals(receiver, indices);
//
// assertEq(pending.length, 1);
// assertEq(pending[0].withdrawer, receiver);
// assertEq(pending[0].token, token);
// assertEq(pending[0].amount, txValue);
// uint256 timestamp = pending[0].timestamp;
//
// okTime = timestamp + withdrawDelay;
//
// //deal some eth to pay withdraw gas
// vm.deal(address(this), 1 ether);
//
// //try to process the withdrawal
// vm.expectRevert(
// abi.encodeWithSelector(IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector, timestamp, okTime)
// );
// bridge.finaliseQueuedWithdrawal(receiver, 0);
// }
function _verifyBalance(address token, address receiver, uint256 expectedAmount) private {
if (token == ETH) {
assertEq(receiver.balance, expectedAmount);
} else {
assertEq(ERC20(token).balanceOf(receiver), expectedAmount);
}
}

function _sendWithdrawalMessage(RootERC20BridgeFlowRate bridge, address token, address sender, uint256 txValue)
private
private
{
_sendWithdrawMessage(bridge, token, sender, sender, txValue);
}

function _sendWithdrawMessage(
RootERC20BridgeFlowRate bridge,
address token,
address sender,
address receiver,
uint256 txValue
) private {
//prank as axelar sending a message to the adapter
vm.startPrank(address(bridge.rootBridgeAdaptor()));
bytes memory predictedPayload = abi.encode(bridge.WITHDRAW_SIG(), token, sender, receiver, txValue);
bytes memory predictedPayload = abi.encode(bridge.WITHDRAW_SIG(), token, sender, sender, txValue);
bridge.onMessageReceive(predictedPayload);
vm.stopPrank();
}

function createAddress(uint256 index) private view returns (address) {
return address(
uint160(uint256(keccak256(abi.encodePacked("root-bridge-fork-test", index, blockhash(block.number)))))
);
}
}

0 comments on commit f1c08fd

Please sign in to comment.