diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eba27536..c46f5c72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,11 @@ jobs: with: submodules: recursive + - name: Set Node.js 18.18.x + uses: actions/setup-node@v3 + with: + node-version: 18.18.x + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: @@ -35,12 +40,38 @@ jobs: forge build --sizes id: build - - name: Run unit and integration tests + - name: Run install + uses: borales/actions-yarn@v4 + with: + cmd: install + + - name: Run build + uses: borales/actions-yarn@v4 + with: + cmd: build + + - name: Run Unit Tests run: | - forge test --no-match-path "test/fork/**" -vvv - id: unit_integration_test + forge test --match-path "test/unit/**" -vvv + id: unit_test + + - name: Run Integration Tests + run: | + forge test --match-path "test/integration/**" -vvv + id: integration_test + + - name: Run Fuzz Tests + run: | + forge test --match-path "test/fuzz/**" -vvv + id: fuzz_test - name: Run Fork Tests run: | forge test --match-path "test/fork/**" -vvvvv id: fork_test + + - name: Run Invariant Tests + run: | + yarn local:testchain + forge test --match-path "test/invariant/**" -vvvvv + id: invariant_test \ No newline at end of file diff --git a/package.json b/package.json index f3648521..10840708 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "local:ci": "cd scripts/localdev; rm -rf .child.bridge.contracts.json .root.bridge.contracts.json; ./ci.sh && ./deploy.sh && AXELAR_API_URL=skip npx mocha --require mocha-suppress-logs ../e2e/e2e.ts && ./stop.sh", "local:chainonly": "cd scripts/localdev; LOCAL_CHAIN_ONLY=true ./start.sh", "local:axelaronly": "cd scripts/localdev; npx ts-node axelar_setup.ts", + "local:testchain": "cd scripts/localdev; ./chains.sh", "stop": "cd scripts/localdev; ./stop.sh" }, "author": "", diff --git a/scripts/localdev/chains.sh b/scripts/localdev/chains.sh new file mode 100755 index 00000000..d515fdd0 --- /dev/null +++ b/scripts/localdev/chains.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -ex +set -o pipefail + +# Stop previous deployment. +./stop.sh + +# Start root & child chain. +npx hardhat node --config ./rootchain.config.ts --port 8500 > /dev/null 2>&1 & +sleep 10 diff --git a/scripts/localdev/resetchain.config.ts b/scripts/localdev/resetchain.config.ts new file mode 100644 index 00000000..116a3eba --- /dev/null +++ b/scripts/localdev/resetchain.config.ts @@ -0,0 +1,21 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; + +const config: HardhatUserConfig = { + networks: { + hardhat: { + hardfork: "shanghai", + mining: { + auto: false, + interval: 1200 + }, + chainId: 2502, + accounts: [], + }, + localhost: { + url: "http://127.0.0.1:8502/", + } + }, + solidity: "0.8.19", +}; +export default config; \ No newline at end of file diff --git a/test/invariant/InvariantBridge.t.sol b/test/invariant/InvariantBridge.t.sol new file mode 100644 index 00000000..12ec8695 --- /dev/null +++ b/test/invariant/InvariantBridge.t.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../src/child/ChildERC20.sol"; +import {WIMX} from "../../src/child/WIMX.sol"; +import {WETH} from "../../src/lib/WETH.sol"; +import {IChildERC20Bridge, ChildERC20Bridge} from "../../src/child/ChildERC20Bridge.sol"; +import {IRootERC20Bridge, IERC20Metadata} from "../../src/root/RootERC20Bridge.sol"; +import {RootERC20BridgeFlowRate} from "../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import {MockAdaptor} from "./MockAdaptor.sol"; +import {ChildHelper} from "./child/ChildHelper.sol"; +import {RootHelper} from "./root/RootHelper.sol"; +import {ChildERC20BridgeHandler} from "./child/ChildERC20BridgeHandler.sol"; +import {RootERC20BridgeFlowRateHandler} from "./root/RootERC20BridgeFlowRateHandler.sol"; +import "forge-std/console.sol"; + +contract InvariantBridge is Test { + string public constant CHAIN_URL = "http://127.0.0.1:8500"; + uint256 public constant IMX_DEPOSIT_LIMIT = 10000 ether; + uint256 public constant MAX_AMOUNT = 10000; + address public constant ADMIN = address(0x111); + uint256 public constant NO_OF_USERS = 20; + uint256 public constant NO_OF_TOKENS = 10; + + address[] users; + address[] rootTokens; + + uint256 childId; + uint256 rootId; + uint256 resetId; + ChildERC20Bridge childBridge; + RootERC20BridgeFlowRate rootBridge; + MockAdaptor childAdaptor; + MockAdaptor rootAdaptor; + ChildHelper childHelper; + RootHelper rootHelper; + ChildERC20BridgeHandler childBridgeHandler; + RootERC20BridgeFlowRateHandler rootBridgeHandler; + + uint256 mappingGas; + + function setUp() public { + childId = vm.createFork(CHAIN_URL); + rootId = vm.createFork(CHAIN_URL); + // Forge has an issue that fails to reset state at the end of each run. + // For example, we found out that if the context stays at child chain at the end of setUp(), + // the state on child chain will not be reset or if the context stays at root chain, the state + // on the root chain will not be reset, which causes subsequent runs to fail. + // We introduced a third chain called reset chain and we make the context to stay on the reset chain + // in order to reset state on both child chain and root chain. + resetId = vm.createFork(CHAIN_URL); + + // Deploy contracts on child chain. + vm.selectFork(childId); + vm.startPrank(ADMIN); + ChildERC20 childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + childAdaptor = new MockAdaptor(); + vm.stopPrank(); + + childBridge = new ChildERC20Bridge(address(this)); + WIMX wIMX = new WIMX(); + + // Deploy contracts on root chain. + vm.selectFork(rootId); + vm.startPrank(ADMIN); + ChildERC20 rootTokenTemplate = new ChildERC20(); + rootTokenTemplate.initialize(address(123), "Test", "TST", 18); + rootAdaptor = new MockAdaptor(); + vm.stopPrank(); + + rootBridge = new RootERC20BridgeFlowRate(address(this)); + ChildERC20 rootIMXToken = new ChildERC20(); + rootIMXToken.initialize(address(123), "Immutable X", "IMX", 18); + WETH wETH = new WETH(); + + // Configure contracts on child chain. + vm.selectFork(childId); + childAdaptor.initialize(rootId, address(childBridge)); + IChildERC20Bridge.InitializationRoles memory childRoles = IChildERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + adaptorManager: address(this), + initialDepositor: address(this), + treasuryManager: address(this) + }); + childBridge.initialize( + childRoles, address(childAdaptor), address(childTokenTemplate), address(rootIMXToken), address(wIMX) + ); + vm.deal(address(childBridge), IMX_DEPOSIT_LIMIT); + + // Configure contracts on root chain. + vm.selectFork(rootId); + rootAdaptor.initialize(childId, address(rootBridge)); + IRootERC20Bridge.InitializationRoles memory rootRoles = IRootERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + variableManager: address(this), + adaptorManager: address(this) + }); + rootBridge.initialize( + rootRoles, + address(rootAdaptor), + address(childBridge), + address(rootTokenTemplate), + address(rootIMXToken), + address(wETH), + IMX_DEPOSIT_LIMIT, + ADMIN + ); + + // Create users. + vm.selectFork(rootId); + for (uint256 i = 0; i < NO_OF_USERS; i++) { + address user = vm.addr(0x10000 + i); + // Mint ETH token + vm.deal(user, MAX_AMOUNT); + // Mint IMX token + rootIMXToken.mint(user, MAX_AMOUNT); + users.push(user); + } + // Create tokens. + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + vm.startPrank(address(0x234)); + ChildERC20 rootToken = new ChildERC20(); + rootToken.initialize(address(123), "Test", "TST", 18); + // Mint token to user + for (uint256 j = 0; j < NO_OF_USERS; j++) { + rootToken.mint(users[j], MAX_AMOUNT); + } + vm.stopPrank(); + // Configure rate for half tokens + if (i % 2 == 0) { + vm.prank(ADMIN); + rootBridge.setRateControlThreshold(address(rootToken), MAX_AMOUNT, MAX_AMOUNT / 3600, MAX_AMOUNT / 2); + } + rootTokens.push(address(rootToken)); + } + + // Deploy helpers and handlers on all chains. + vm.selectFork(childId); + vm.startPrank(ADMIN); + childHelper = new ChildHelper(payable(childBridge)); + address temp = address(new RootHelper(ADMIN, payable(rootBridge))); + childBridgeHandler = new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), temp); + new RootERC20BridgeFlowRateHandler(childId, rootId, users, rootTokens, address(childHelper), temp); + vm.stopPrank(); + + vm.selectFork(rootId); + vm.startPrank(ADMIN); + new ChildHelper(payable(childBridge)); + rootHelper = new RootHelper(ADMIN, payable(rootBridge)); + new ChildERC20BridgeHandler(childId, rootId, users, rootTokens, address(childHelper), address(rootHelper)); + rootBridgeHandler = new RootERC20BridgeFlowRateHandler( + childId, rootId, users, rootTokens, address(childHelper), address(rootHelper) + ); + vm.stopPrank(); + + // Map tokens + vm.selectFork(rootId); + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + address rootToken = rootTokens[i]; + rootBridge.mapToken{value: 1}(IERC20Metadata(rootToken)); + mappingGas += 1; + // Verify + address childTokenL1 = rootBridge.rootTokenToChildToken(address(rootToken)); + + vm.selectFork(childId); + address childTokenL2 = childBridge.rootTokenToChildToken(address(rootToken)); + vm.selectFork(rootId); + + assertEq(childTokenL1, childTokenL2, "Child token address mismatch between L1 and L2"); + } + + // Target contracts + bytes4[] memory childSelectors = new bytes4[](8); + childSelectors[0] = childBridgeHandler.withdraw.selector; + childSelectors[1] = childBridgeHandler.withdrawTo.selector; + childSelectors[2] = childBridgeHandler.withdrawIMX.selector; + childSelectors[3] = childBridgeHandler.withdrawIMXTo.selector; + childSelectors[4] = childBridgeHandler.withdrawWIMX.selector; + childSelectors[5] = childBridgeHandler.withdrawWIMXTo.selector; + childSelectors[6] = childBridgeHandler.withdrawETH.selector; + childSelectors[7] = childBridgeHandler.withdrawETHTo.selector; + targetSelector(FuzzSelector({addr: address(childBridgeHandler), selectors: childSelectors})); + + bytes4[] memory rootSelectors = new bytes4[](8); + rootSelectors[0] = rootBridgeHandler.deposit.selector; + rootSelectors[1] = rootBridgeHandler.depositTo.selector; + rootSelectors[2] = rootBridgeHandler.depositIMX.selector; + rootSelectors[3] = rootBridgeHandler.depositIMXTo.selector; + rootSelectors[4] = rootBridgeHandler.depositETH.selector; + rootSelectors[5] = rootBridgeHandler.depositETHTo.selector; + rootSelectors[6] = rootBridgeHandler.depositWETH.selector; + rootSelectors[7] = rootBridgeHandler.depositWETHTo.selector; + targetSelector(FuzzSelector({addr: address(rootBridgeHandler), selectors: rootSelectors})); + + targetContract(address(childBridgeHandler)); + targetContract(address(rootBridgeHandler)); + + vm.selectFork(resetId); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_ERC20TokenBalanced() external { + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + address rootToken = rootTokens[i]; + + vm.selectFork(rootId); + uint256 bridgeBalance = ChildERC20(rootToken).balanceOf(address(rootBridge)); + address childToken = rootBridge.rootTokenToChildToken(rootToken); + + vm.selectFork(childId); + uint256 totalSupply = ChildERC20(childToken).totalSupply(); + + uint256 userBalanceSum = 0; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + userBalanceSum += ChildERC20(childToken).balanceOf(user); + } + + assertEq(bridgeBalance, totalSupply); + assertEq(bridgeBalance, userBalanceSum); + } + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IndividualERC20TokenBalanced() external { + for (uint256 i = 0; i < NO_OF_TOKENS; i++) { + address rootToken = rootTokens[i]; + vm.selectFork(rootId); + address childToken = rootBridge.rootTokenToChildToken(rootToken); + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + + vm.selectFork(rootId); + uint256 balanceL1 = ChildERC20(rootToken).balanceOf(user); + + vm.selectFork(childId); + uint256 balanceL2 = ChildERC20(childToken).balanceOf(user); + + assertEq(balanceL1 + balanceL2, MAX_AMOUNT); + } + } + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IMXBalanced() external { + vm.selectFork(rootId); + uint256 bridgeBalance = ChildERC20(rootBridge.rootIMXToken()).balanceOf(address(rootBridge)); + + vm.selectFork(childId); + uint256 totalSupply = IMX_DEPOSIT_LIMIT - address(childBridge).balance; + + uint256 userBalanceSum = 0; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + userBalanceSum += user.balance; + } + + assertEq(bridgeBalance, totalSupply); + assertEq(bridgeBalance, userBalanceSum); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IndividualIMXBalanced() external { + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + + vm.selectFork(rootId); + uint256 balanceL1 = ChildERC20(rootBridge.rootIMXToken()).balanceOf(user); + + vm.selectFork(childId); + uint256 balanceL2 = user.balance; + + assertEq(balanceL1 + balanceL2, MAX_AMOUNT); + } + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_ETHBalanced() external { + vm.selectFork(rootId); + uint256 bridgeBalance = address(rootBridge).balance; + + vm.selectFork(childId); + uint256 totalSupply = ChildERC20(childBridge.childETHToken()).totalSupply(); + + uint256 userBalanceSum = 0; + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + userBalanceSum += ChildERC20(childBridge.childETHToken()).balanceOf(user); + } + + assertEq(bridgeBalance, totalSupply); + assertEq(bridgeBalance, userBalanceSum); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_IndividualETHBalanced() external { + for (uint256 j = 0; j < NO_OF_USERS; j++) { + address user = users[j]; + + vm.selectFork(rootId); + uint256 balanceL1 = user.balance; + + vm.selectFork(childId); + uint256 balanceL2 = ChildERC20(childBridge.childETHToken()).balanceOf(user); + + assertEq(balanceL1 + balanceL2, MAX_AMOUNT); + } + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_NoRemainingWETH() external { + vm.selectFork(rootId); + assertEq(rootBridge.rootWETHToken().balance, 0); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_NoRemainingWIMX() external { + vm.selectFork(childId); + assertEq(childBridge.wIMXToken().balance, 0); + } + + /// forge-config: default.invariant.runs = 256 + /// forge-config: default.invariant.depth = 15 + /// forge-config: default.invariant.fail-on-revert = true + function invariant_GasBalanced() external { + vm.selectFork(rootId); + assertEq(address(rootAdaptor).balance - mappingGas, rootHelper.totalGas()); + vm.selectFork(childId); + assertEq(address(childAdaptor).balance, childHelper.totalGas()); + } +} diff --git a/test/invariant/MockAdaptor.sol b/test/invariant/MockAdaptor.sol new file mode 100644 index 00000000..0a5bfe83 --- /dev/null +++ b/test/invariant/MockAdaptor.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {IChildBridgeAdaptor} from "../../src/interfaces/child/IChildBridgeAdaptor.sol"; +import {IRootBridgeAdaptor} from "../../src/interfaces/root/IRootBridgeAdaptor.sol"; + +interface MessageReceiver { + function onMessageReceive(bytes calldata data) external; +} + +contract MockAdaptor is Test, IChildBridgeAdaptor, IRootBridgeAdaptor { + uint256 otherChainId; + MessageReceiver messageReceiver; + + constructor() {} + + function initialize(uint256 _otherChainId, address _messageReceiver) public { + otherChainId = _otherChainId; + messageReceiver = MessageReceiver(_messageReceiver); + } + + function sendMessage(bytes calldata payload, address /*refundRecipient*/ ) + external + payable + override(IChildBridgeAdaptor, IRootBridgeAdaptor) + { + uint256 original = vm.activeFork(); + + // Switch to the other chain. + vm.selectFork(otherChainId); + onMessageReceive(payload); + + vm.selectFork(original); + } + + function onMessageReceive(bytes calldata data) public { + messageReceiver.onMessageReceive(data); + } +} diff --git a/test/invariant/child/ChildERC20BridgeHandler.sol b/test/invariant/child/ChildERC20BridgeHandler.sol new file mode 100644 index 00000000..65f9d485 --- /dev/null +++ b/test/invariant/child/ChildERC20BridgeHandler.sol @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {ChildHelper} from "./ChildHelper.sol"; +import {RootHelper} from "../root/RootHelper.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; + +contract ChildERC20BridgeHandler is Test { + uint256 public constant MAX_AMOUNT = 10000; + uint256 public constant MAX_GAS = 100; + + uint256 childId; + uint256 rootId; + address[] users; + address[] rootTokens; + ChildHelper childHelper; + RootHelper rootHelper; + + constructor( + uint256 _childId, + uint256 _rootId, + address[] memory _users, + address[] memory _rootTokens, + address _childHelper, + address _rootHelper + ) { + childId = _childId; + rootId = _rootId; + users = _users; + rootTokens = _rootTokens; + childHelper = ChildHelper(_childHelper); + rootHelper = RootHelper(_rootHelper); + } + + function initialize( + uint256 _childId, + uint256 _rootId, + address[] memory _users, + address[] memory _rootTokens, + address _childHelper, + address _rootHelper + ) public { + childId = _childId; + rootId = _rootId; + users = _users; + rootTokens = _rootTokens; + childHelper = ChildHelper(_childHelper); + rootHelper = RootHelper(_rootHelper); + } + + function withdraw(uint256 userIndexSeed, uint256 tokenIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address rootToken = rootTokens[bound(tokenIndexSeed, 0, rootTokens.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get child token + address childToken = childHelper.childBridge().rootTokenToChildToken(rootToken); + + // Get current balance + uint256 currentBalance = ChildERC20(childToken).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + + childHelper.withdraw(user, childToken, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + + vm.selectFork(original); + } + + function withdrawTo( + uint256 userIndexSeed, + uint256 recipientIndexSeed, + uint256 tokenIndexSeed, + uint256 amount, + uint256 gasAmt + ) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + address rootToken = rootTokens[bound(tokenIndexSeed, 0, rootTokens.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get child token + address childToken = childHelper.childBridge().rootTokenToChildToken(rootToken); + + // Get current balance + uint256 currentBalance = ChildERC20(childToken).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.deposit(user, rootToken, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(recipient); + vm.selectFork(childId); + + childHelper.withdrawTo(user, recipient, childToken, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(recipient, previousLen); + // If recipient is different, transfer back + if (user != recipient) { + vm.prank(recipient); + ChildERC20(rootToken).transfer(user, amount); + } + + vm.selectFork(original); + } + + function withdrawIMX(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + + childHelper.withdrawIMX(user, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + + vm.selectFork(original); + } + + function withdrawIMXTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(recipient); + vm.selectFork(childId); + + childHelper.withdrawIMXTo(user, recipient, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(recipient, previousLen); + // If recipient is different, transfer back + if (user != recipient) { + address imx = rootHelper.rootBridge().rootIMXToken(); + vm.prank(recipient); + ChildERC20(imx).transfer(user, amount); + } + + vm.selectFork(original); + } + + function withdrawWIMX(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + + // Wrap IMX + address payable wIMX = payable(childHelper.childBridge().wIMXToken()); + vm.prank(user); + WIMX(wIMX).deposit{value: amount}(); + + childHelper.withdrawWIMX(user, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + + vm.selectFork(original); + } + + function withdrawWIMXTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(recipient); + vm.selectFork(childId); + + // Wrap IMX + address payable wIMX = payable(childHelper.childBridge().wIMXToken()); + vm.prank(user); + WIMX(wIMX).deposit{value: amount}(); + + childHelper.withdrawWIMXTo(user, recipient, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(recipient, previousLen); + // If recipient is different, transfer back + if (user != recipient) { + address imx = rootHelper.rootBridge().rootIMXToken(); + vm.prank(recipient); + ChildERC20(imx).transfer(user, amount); + } + + vm.selectFork(original); + } + + function withdrawETH(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(childHelper.childBridge().childETHToken()).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositETH(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + + childHelper.withdrawETH(user, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + + vm.selectFork(original); + } + + function withdrawETHTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to child chain + vm.selectFork(childId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(childHelper.childBridge().childETHToken()).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + vm.selectFork(rootId); + rootHelper.depositETH(user, amount - currentBalance, gasAmt); + vm.selectFork(childId); + } + + vm.selectFork(rootId); + uint256 previousLen = rootHelper.getQueueSize(recipient); + vm.selectFork(childId); + + childHelper.withdrawETHTo(user, recipient, amount, gasAmt); + + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(recipient, previousLen); + // If recipient is different, transfer back + if (user != recipient) { + vm.prank(recipient); + user.call{value: amount}(""); + } + + vm.selectFork(original); + } +} diff --git a/test/invariant/child/ChildHelper.sol b/test/invariant/child/ChildHelper.sol new file mode 100644 index 00000000..ac1a8d3e --- /dev/null +++ b/test/invariant/child/ChildHelper.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; +import {ChildERC20Bridge} from "../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../src/interfaces/child/IChildERC20.sol"; + +contract ChildHelper is Test { + ChildERC20Bridge public childBridge; + + uint256 public totalGas; + + constructor(address payable _childBridge) { + childBridge = ChildERC20Bridge(_childBridge); + } + + function withdraw(address user, address childToken, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdraw{value: gasAmt}(IChildERC20(childToken), amount); + } + + function withdrawTo(address user, address recipient, address childToken, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawTo{value: gasAmt}(IChildERC20(childToken), recipient, amount); + } + + function withdrawIMX(address user, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawIMX{value: gasAmt + amount}(amount); + } + + function withdrawIMXTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawIMXTo{value: gasAmt + amount}(recipient, amount); + } + + function withdrawWIMX(address user, uint256 amount, uint256 gasAmt) public { + address payable wIMX = payable(childBridge.wIMXToken()); + + vm.prank(user); + WIMX(wIMX).approve(address(childBridge), amount); + + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawWIMX{value: gasAmt}(amount); + } + + function withdrawWIMXTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + address payable wIMX = payable(childBridge.wIMXToken()); + + vm.prank(user); + WIMX(wIMX).approve(address(childBridge), amount); + + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawWIMXTo{value: gasAmt}(recipient, amount); + } + + function withdrawETH(address user, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawETH{value: gasAmt}(amount); + } + + function withdrawETHTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + childBridge.withdrawETHTo{value: gasAmt}(recipient, amount); + } +} diff --git a/test/invariant/root/RootERC20BridgeFlowRateHandler.sol b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol new file mode 100644 index 00000000..a4334d6d --- /dev/null +++ b/test/invariant/root/RootERC20BridgeFlowRateHandler.sol @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {ChildHelper} from "../child/ChildHelper.sol"; +import {RootHelper} from "./RootHelper.sol"; +import {WIMX as WETH} from "../../../src/child/WIMX.sol"; + +contract RootERC20BridgeFlowRateHandler is Test { + uint256 public constant MAX_AMOUNT = 10000; + uint256 public constant MAX_GAS = 100; + + uint256 childId; + uint256 rootId; + address[] users; + address[] rootTokens; + ChildHelper childHelper; + RootHelper rootHelper; + + constructor( + uint256 _childId, + uint256 _rootId, + address[] memory _users, + address[] memory _rootTokens, + address _childHelper, + address _rootHelper + ) { + childId = _childId; + rootId = _rootId; + users = _users; + rootTokens = _rootTokens; + childHelper = ChildHelper(_childHelper); + rootHelper = RootHelper(_rootHelper); + } + + function deposit(uint256 userIndexSeed, uint256 tokenIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address rootToken = rootTokens[bound(tokenIndexSeed, 0, rootTokens.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get child token + address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); + + // Get current balance + uint256 currentBalance = ChildERC20(rootToken).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.deposit(user, rootToken, amount, gasAmt); + + vm.selectFork(original); + } + + function depositTo( + uint256 userIndexSeed, + uint256 recipientIndexSeed, + uint256 tokenIndexSeed, + uint256 amount, + uint256 gasAmt + ) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + address rootToken = rootTokens[bound(tokenIndexSeed, 0, rootTokens.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get child token + address childToken = rootHelper.rootBridge().rootTokenToChildToken(rootToken); + + // Get current balance + uint256 currentBalance = ChildERC20(rootToken).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdraw(user, childToken, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositTo(user, recipient, rootToken, amount, gasAmt); + + // If recipient is different, transfer back + if (user != recipient) { + vm.selectFork(childId); + vm.prank(recipient); + ChildERC20(childToken).transfer(user, amount); + vm.selectFork(rootId); + } + + vm.selectFork(original); + } + + function depositIMX(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(rootHelper.rootBridge().rootIMXToken()).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositIMX(user, amount, gasAmt); + + vm.selectFork(original); + } + + function depositIMXTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = ChildERC20(rootHelper.rootBridge().rootIMXToken()).balanceOf(user); + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawIMX(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositIMXTo(user, recipient, amount, gasAmt); + + // If recipient is different, transfer back + if (user != recipient) { + vm.selectFork(childId); + vm.prank(recipient); + user.call{value: amount}(""); + vm.selectFork(rootId); + } + + vm.selectFork(original); + } + + function depositETH(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawETH(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositETH(user, amount, gasAmt); + + vm.selectFork(original); + } + + function depositETHTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawETH(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + rootHelper.depositETHTo(user, recipient, amount, gasAmt); + + // If recipient is different, transfer back + if (user != recipient) { + vm.selectFork(childId); + address eth = childHelper.childBridge().childETHToken(); + vm.prank(recipient); + ChildERC20(eth).transfer(user, amount); + } + vm.selectFork(childId); + + vm.selectFork(original); + } + + function depositWETH(uint256 userIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawETH(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + // Wrap ETH + address payable wETH = payable(rootHelper.rootBridge().rootWETHToken()); + vm.prank(user); + WETH(wETH).deposit{value: amount}(); + + rootHelper.depositWETH(user, amount, gasAmt); + + vm.selectFork(original); + } + + function depositWETHTo(uint256 userIndexSeed, uint256 recipientIndexSeed, uint256 amount, uint256 gasAmt) public { + uint256 original = vm.activeFork(); + + // Switch to root chain + vm.selectFork(rootId); + + // Bound + address user = users[bound(userIndexSeed, 0, users.length - 1)]; + address recipient = users[bound(recipientIndexSeed, 0, users.length - 1)]; + amount = bound(amount, 1, MAX_AMOUNT); + gasAmt = bound(gasAmt, 1, MAX_GAS); + + // Get current balance + uint256 currentBalance = user.balance; + + if (currentBalance < amount) { + // Fund difference + uint256 previousLen = rootHelper.getQueueSize(user); + vm.selectFork(childId); + childHelper.withdrawETH(user, amount - currentBalance, gasAmt); + vm.selectFork(rootId); + rootHelper.finaliseWithdrawal(user, previousLen); + } + + // Wrap ETH + address payable wETH = payable(rootHelper.rootBridge().rootWETHToken()); + vm.prank(user); + WETH(wETH).deposit{value: amount}(); + + rootHelper.depositWETHTo(user, recipient, amount, gasAmt); + + // If recipient is different, transfer back + if (user != recipient) { + vm.selectFork(childId); + address eth = childHelper.childBridge().childETHToken(); + vm.prank(recipient); + ChildERC20(eth).transfer(user, amount); + vm.selectFork(rootId); + } + + vm.selectFork(original); + } +} diff --git a/test/invariant/root/RootHelper.sol b/test/invariant/root/RootHelper.sol new file mode 100644 index 00000000..6f0521c7 --- /dev/null +++ b/test/invariant/root/RootHelper.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {WIMX as WETH} from "../../../src/child/WIMX.sol"; +import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import {IERC20Metadata} from "../../../src/root/RootERC20Bridge.sol"; + +contract RootHelper is Test { + address admin; + RootERC20BridgeFlowRate public rootBridge; + + uint256 public totalGas; + + constructor(address _admin, address payable _rootBridge) { + admin = _admin; + rootBridge = RootERC20BridgeFlowRate(_rootBridge); + } + + function deposit(address user, address rootToken, uint256 amount, uint256 gasAmt) public { + vm.prank(user); + ChildERC20(rootToken).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.deposit{value: gasAmt}(IERC20Metadata(rootToken), amount); + } + + function depositTo(address user, address recipient, address rootToken, uint256 amount, uint256 gasAmt) public { + vm.prank(user); + ChildERC20(rootToken).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositTo{value: gasAmt}(IERC20Metadata(rootToken), recipient, amount); + } + + function depositIMX(address user, uint256 amount, uint256 gasAmt) public { + address IMX = rootBridge.rootIMXToken(); + + vm.prank(user); + ChildERC20(IMX).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.deposit{value: gasAmt}(IERC20Metadata(IMX), amount); + } + + function depositIMXTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + address IMX = rootBridge.rootIMXToken(); + + vm.prank(user); + ChildERC20(IMX).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositTo{value: gasAmt}(IERC20Metadata(IMX), recipient, amount); + } + + function depositETH(address user, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositETH{value: gasAmt + amount}(amount); + } + + function depositETHTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositToETH{value: gasAmt + amount}(recipient, amount); + } + + function depositWETH(address user, uint256 amount, uint256 gasAmt) public { + address payable wETH = payable(rootBridge.rootWETHToken()); + + vm.prank(user); + WETH(wETH).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.deposit{value: gasAmt}(IERC20Metadata(wETH), amount); + } + + function depositWETHTo(address user, address recipient, uint256 amount, uint256 gasAmt) public { + address payable wETH = payable(rootBridge.rootWETHToken()); + + vm.prank(user); + WETH(wETH).approve(address(rootBridge), amount); + + vm.deal(user, gasAmt + user.balance); + totalGas += gasAmt; + + vm.prank(user); + rootBridge.depositTo{value: gasAmt}(IERC20Metadata(wETH), recipient, amount); + } + + function getQueueSize(address user) public view returns (uint256) { + return rootBridge.getPendingWithdrawalsLength(user); + } + + function finaliseWithdrawal(address user, uint256 previousLen) public { + // Check if this withdrawal has hit rate limit + if (rootBridge.getPendingWithdrawalsLength(user) > previousLen) { + skip(86401); + vm.prank(user); + rootBridge.finaliseQueuedWithdrawal(user, previousLen); + } + + if (rootBridge.withdrawalQueueActivated()) { + vm.prank(admin); + rootBridge.deactivateWithdrawalQueue(); + } + } +}