From a63e6cc6a9baeacba77be9ba7db49335b9903c8d Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Thu, 8 Feb 2024 07:59:06 +1000 Subject: [PATCH] Add fuzz testing --- .../fuzz/child/ChildAxelarBridgeAdaptor.t.sol | 94 +++++ test/fuzz/child/ChildERC20.t.sol | 55 +++ test/fuzz/child/ChildERC20Bridge.t.sol | 322 ++++++++++++++++ test/fuzz/child/WIMX.t.sol | 161 ++++++++ test/fuzz/root/FlowRateDetection.t.sol | 62 +++ test/fuzz/root/RootAxelarBridgeAdaptor.t.sol | 94 +++++ test/fuzz/root/RootERC20Bridge.t.sol | 363 ++++++++++++++++++ test/fuzz/root/RootERC20BridgeFlowRate.t.sol | 69 ++++ 8 files changed, 1220 insertions(+) create mode 100644 test/fuzz/child/ChildAxelarBridgeAdaptor.t.sol create mode 100644 test/fuzz/child/ChildERC20.t.sol create mode 100644 test/fuzz/child/ChildERC20Bridge.t.sol create mode 100644 test/fuzz/child/WIMX.t.sol create mode 100644 test/fuzz/root/FlowRateDetection.t.sol create mode 100644 test/fuzz/root/RootAxelarBridgeAdaptor.t.sol create mode 100644 test/fuzz/root/RootERC20Bridge.t.sol create mode 100644 test/fuzz/root/RootERC20BridgeFlowRate.t.sol diff --git a/test/fuzz/child/ChildAxelarBridgeAdaptor.t.sol b/test/fuzz/child/ChildAxelarBridgeAdaptor.t.sol new file mode 100644 index 00000000..16824341 --- /dev/null +++ b/test/fuzz/child/ChildAxelarBridgeAdaptor.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + IChildAxelarBridgeAdaptorErrors, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptor +} from "../../../src/interfaces/child/IChildAxelarBridgeAdaptor.sol"; +import {ChildAxelarBridgeAdaptor} from "../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {MockChildERC20Bridge} from "../../mocks/child/MockChildERC20Bridge.sol"; +import {MockChildAxelarGateway} from "../../mocks/child/MockChildAxelarGateway.sol"; +import {MockChildAxelarGasService} from "../../mocks/child/MockChildAxelarGasService.sol"; + +contract ChildAxelarBridgeAdaptorTest is Test, IChildAxelarBridgeAdaptorErrors, IChildAxelarBridgeAdaptorEvents { + string public constant ROOT_CHAIN_NAME = "root"; + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + + ChildAxelarBridgeAdaptor public axelarAdaptor; + MockChildERC20Bridge public mockChildERC20Bridge; + MockChildAxelarGateway public mockChildAxelarGateway; + MockChildAxelarGasService public mockChildAxelarGasService; + + function setUp() public { + IChildAxelarBridgeAdaptor.InitializationRoles memory roles = IChildAxelarBridgeAdaptor.InitializationRoles({ + defaultAdmin: address(this), + bridgeManager: address(this), + gasServiceManager: address(this), + targetManager: address(this) + }); + + mockChildERC20Bridge = new MockChildERC20Bridge(); + mockChildAxelarGateway = new MockChildAxelarGateway(); + mockChildAxelarGasService = new MockChildAxelarGasService(); + + axelarAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway), address(this)); + axelarAdaptor.initialize( + roles, + address(mockChildERC20Bridge), + ROOT_CHAIN_NAME, + ROOT_BRIDGE_ADAPTOR, + address(mockChildAxelarGasService) + ); + } + + function testFuzz_SendMessage(uint256 callValue, bytes calldata payload, address refundRecipient) public { + vm.assume(callValue > 0 && callValue < type(uint256).max); + + vm.startPrank(address(mockChildERC20Bridge)); + vm.deal(address(mockChildERC20Bridge), callValue); + + // Send message called with insufficient balance should revert + vm.expectRevert(); + axelarAdaptor.sendMessage{value: callValue + 1}(payload, refundRecipient); + + // Send message correctly should call gas service and gateway with expected data + vm.expectCall( + address(mockChildAxelarGasService), + callValue, + abi.encodeWithSelector( + mockChildAxelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + ROOT_CHAIN_NAME, + axelarAdaptor.rootBridgeAdaptor(), + payload, + refundRecipient + ) + ); + vm.expectCall( + address(mockChildAxelarGateway), + abi.encodeWithSelector( + mockChildAxelarGateway.callContract.selector, + ROOT_CHAIN_NAME, + axelarAdaptor.rootBridgeAdaptor(), + payload + ) + ); + axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); + + vm.stopPrank(); + } + + function testFuzz_Execute(bytes32 commandId, bytes calldata payload) public { + // Execute should emit event and call bridge. + vm.expectEmit(); + emit AdaptorExecute(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, payload); + vm.expectCall( + address(mockChildERC20Bridge), + abi.encodeWithSelector(mockChildERC20Bridge.onMessageReceive.selector, payload) + ); + axelarAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, payload); + } +} diff --git a/test/fuzz/child/ChildERC20.t.sol b/test/fuzz/child/ChildERC20.t.sol new file mode 100644 index 00000000..33faba5d --- /dev/null +++ b/test/fuzz/child/ChildERC20.t.sol @@ -0,0 +1,55 @@ +// 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"; + +contract ChildERC20Test is Test { + ChildERC20 public childToken; + + address constant DEFAULT_ROOT_ADDRESS = address(111); + string constant DEFAULT_NAME = "Test ERC20"; + string constant DEFAULT_SYMBOL = "TEST"; + uint8 constant DEFAULT_DECIMALS = 18; + + function setUp() public { + childToken = new ChildERC20(); + childToken.initialize(DEFAULT_ROOT_ADDRESS, DEFAULT_NAME, DEFAULT_SYMBOL, DEFAULT_DECIMALS); + } + + function testFuzz_Mint(address user, uint256 amount) public { + vm.assume(user != address(0)); + + assertEq(childToken.balanceOf(user), 0, "User should not have balance before mint"); + + // Unauthorised mint should revert + vm.prank(user); + vm.expectRevert("ChildERC20: Only bridge can call"); + childToken.mint(user, amount); + + childToken.mint(user, amount); + assertEq(childToken.balanceOf(user), amount, "User should have given amount of balance after mint"); + } + + function testFuzz_Burn(address user, uint256 balance, uint256 burnAmt) public { + vm.assume(user != address(0)); + vm.assume(balance < type(uint256).max); + vm.assume(burnAmt < balance); + + childToken.mint(user, balance); + assertEq(childToken.balanceOf(user), balance, "User should have given amount of balance before burn"); + + // Unauthorised burn should revert + vm.prank(user); + vm.expectRevert("ChildERC20: Only bridge can call"); + childToken.burn(user, burnAmt); + + // Over burn should revert + vm.expectRevert("ERC20: burn amount exceeds balance"); + childToken.burn(user, balance + 1); + + // Burn should decrease balance + childToken.burn(user, burnAmt); + assertEq(childToken.balanceOf(user), balance - burnAmt, "User should have balance - burnAmt after burn"); + } +} diff --git a/test/fuzz/child/ChildERC20Bridge.t.sol b/test/fuzz/child/ChildERC20Bridge.t.sol new file mode 100644 index 00000000..dfddc41f --- /dev/null +++ b/test/fuzz/child/ChildERC20Bridge.t.sol @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import { + ChildERC20Bridge, + IChildERC20Bridge, + IChildERC20BridgeEvents, + IChildERC20BridgeErrors +} from "../../../src/child/ChildERC20Bridge.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../mocks/root/MockAdaptor.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; +import {IChildERC20} from "../../../src/interfaces/child/IChildERC20.sol"; + +contract ChildERC20BridgeTest is Test, IChildERC20BridgeEvents { + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); + address public constant NATIVE_ETH = address(0xeee); + address public constant NATIVE_IMX = address(0xfff); + + address constant ROOT_IMX_TOKEN = address(0xccc); + ChildERC20 public childTokenTemplate; + WIMX public wIMX; + MockAdaptor public mockAdaptor; + + ChildERC20Bridge bridge; + + receive() external payable {} + + function setUp() public { + IChildERC20Bridge.InitializationRoles memory roles = IChildERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + adaptorManager: address(this), + initialDepositor: address(this), + treasuryManager: address(this) + }); + + bridge = new ChildERC20Bridge(address(this)); + + wIMX = new WIMX(); + + mockAdaptor = new MockAdaptor(); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + bridge.initialize(roles, address(mockAdaptor), address(childTokenTemplate), ROOT_IMX_TOKEN, address(wIMX)); + } + + function testFuzz_MapToken(address rootToken, string memory name, string memory symbol, uint8 decimals) public { + vm.assume(rootToken != address(0) && bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + + // Map token on L1 triggers call on child bridge. + bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, name, symbol, decimals); + + address childTokenAddress = Clones.predictDeterministicAddress( + address(childTokenTemplate), keccak256(abi.encodePacked(rootToken)), address(bridge) + ); + vm.expectEmit(address(bridge)); + emit L2TokenMapped(rootToken, childTokenAddress); + + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + assertEq( + bridge.rootTokenToChildToken(rootToken), + childTokenAddress, + "Child actual token address should match predicated address" + ); + + vm.stopPrank(); + } + + function testFuzz_DepositIMX(address sender, address recipient, uint256 depositAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); + vm.deal(address(bridge), depositAmt); + + assertEq(address(bridge).balance, depositAmt, "Bridge should have depositAmt of IMX"); + assertEq(recipient.balance, 0, "Recipient should have 0 IMX"); + + // Deposit IMX on L1 triggers call on child bridge. + bytes memory data = abi.encode(DEPOSIT_SIG, bridge.rootIMXToken(), sender, recipient, depositAmt); + + vm.expectEmit(address(bridge)); + emit IMXDeposit(bridge.rootIMXToken(), sender, recipient, depositAmt); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + assertEq(address(bridge).balance, 0, "Bridge should have 0 IMX"); + assertEq(recipient.balance, depositAmt, "User should have depositAmt of IMX"); + } + + function testFuzz_WithdrawIMX(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { + vm.assume(user != address(0)); + vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance < type(uint256).max - gasAmt); + vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt); + + // Fund user + vm.deal(user, balance); + vm.startPrank(user); + + // Before withdraw + assertEq(user.balance, balance, "User should have given balance of IMX"); + assertEq(address(bridge).balance, 0, "Bridge should have 0 balance of IMX"); + + // Over-withdraw should fail + vm.expectRevert(); + bridge.withdrawIMX{value: gasAmt + balance}(balance); + + // Normal withdraw should succeed + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, bridge.rootIMXToken(), user, user, withdrawAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + vm.expectEmit(address(bridge)); + emit ChildChainNativeIMXWithdraw(bridge.rootIMXToken(), user, user, withdrawAmt); + bridge.withdrawIMX{value: gasAmt + withdrawAmt}(withdrawAmt); + + assertEq( + user.balance, balance - gasAmt - withdrawAmt, "User should have balance - gasAmt - withdrawAmt of balance" + ); + + vm.stopPrank(); + } + + function testFuzz_WithdrawWIMX(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { + vm.assume(user != address(0)); + vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance < type(uint256).max); + vm.assume(balance > withdrawAmt && balance - withdrawAmt > gasAmt); + + // Fund user + vm.deal(user, balance); + vm.startPrank(user); + + // Wrap IMX + wIMX.deposit{value: balance}(); + + vm.deal(user, gasAmt); + + assertEq(user.balance, gasAmt, "User should have gasAmt of balance"); + assertEq(wIMX.balanceOf(user), balance, "User should have given balance"); + + // Withdraw without approval should fail + vm.expectRevert(); + bridge.withdrawWIMX{value: gasAmt}(withdrawAmt); + + // Over-withdraw should fail + wIMX.approve(address(bridge), balance + 1); + vm.expectRevert(); + bridge.withdrawWIMX{value: gasAmt}(balance + 1); + + // Withdraw within balance and allowance should go through + wIMX.approve(address(bridge), withdrawAmt); + + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, bridge.rootIMXToken(), user, user, withdrawAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + + vm.expectEmit(address(bridge)); + emit ChildChainWrappedIMXWithdraw(bridge.rootIMXToken(), user, user, withdrawAmt); + + bridge.withdrawWIMX{value: gasAmt}(withdrawAmt); + + assertEq(user.balance, 0, "User should have 0 balance"); + assertEq(wIMX.balanceOf(user), balance - withdrawAmt, "User should have balance - withdrawAmt of wIMX"); + + vm.stopPrank(); + } + + function testFuzz_DepositETH(address sender, address recipient, uint256 depositAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); + + assertEq(IChildERC20(bridge.childETHToken()).balanceOf(recipient), 0, "Recipient should have 0 ETH"); + + // Deposit ETH on L1 triggers call on child bridge. + bytes memory data = abi.encode(DEPOSIT_SIG, bridge.NATIVE_ETH(), sender, recipient, depositAmt); + + vm.expectEmit(address(bridge)); + emit NativeEthDeposit(bridge.NATIVE_ETH(), bridge.childETHToken(), sender, recipient, depositAmt); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + assertEq( + IChildERC20(bridge.childETHToken()).balanceOf(recipient), + depositAmt, + "Recipient should have depositAmt of ETH" + ); + } + + function testFuzz_WithdrawETH(address user, uint256 balance, uint256 gasAmt, uint256 withdrawAmt) public { + vm.assume(user != address(0)); + vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance < type(uint256).max); + vm.assume(balance > withdrawAmt); + + // Fund user + vm.deal(user, gasAmt); + // Mint token to user + vm.startPrank(address(bridge)); + IChildERC20 childETH = IChildERC20(bridge.childETHToken()); + childETH.mint(user, balance); + + assertEq(user.balance, gasAmt, "User should have given gasAmt of balance"); + assertEq(childETH.balanceOf(user), balance, "User should have given balance of ETH"); + + vm.startPrank(user); + + // Over-withdraw should fail + vm.expectRevert(); + bridge.withdrawETH{value: gasAmt}(balance + 1); + + // Withdraw within balance + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, bridge.NATIVE_ETH(), user, user, withdrawAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + + vm.expectEmit(address(bridge)); + emit ChildChainEthWithdraw(user, user, withdrawAmt); + + bridge.withdrawETH{value: gasAmt}(withdrawAmt); + + assertEq(user.balance, 0, "User should have 0 balance"); + assertEq(childETH.balanceOf(user), balance - withdrawAmt, "User should have balance - withdrawAmt of ETH"); + + vm.stopPrank(); + } + + function testFuzz_DepositERC20(address rootToken, address sender, address recipient, uint256 depositAmt) public { + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + vm.assume(rootToken != address(0) && rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + vm.assume(sender != address(0) && recipient != address(0) && depositAmt > 0); + + // Map + bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, "Test token", "Test", 18); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + address childTokenAddr = bridge.rootTokenToChildToken(rootToken); + IChildERC20 childToken = IChildERC20(childTokenAddr); + + assertEq(childToken.balanceOf(recipient), 0, "Recipient should have 0 token"); + + // Map token on L1 triggers call on child bridge. + data = abi.encode(DEPOSIT_SIG, rootToken, sender, recipient, depositAmt); + + vm.expectEmit(address(bridge)); + emit ChildChainERC20Deposit(rootToken, childTokenAddr, sender, recipient, depositAmt); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + assertEq(childToken.balanceOf(recipient), depositAmt, "Recipient should have depositAmt token"); + } + + function testFuzz_WithdrawERC20( + address rootToken, + address user, + uint256 balance, + uint256 gasAmt, + uint256 withdrawAmt + ) public { + vm.assume(rootToken != bridge.NATIVE_ETH() && rootToken != bridge.NATIVE_IMX()); + vm.assume(rootToken != address(0) && user != address(0)); + vm.assume(balance > 0 && withdrawAmt > 0 && gasAmt > 0); + vm.assume(balance < type(uint256).max); + vm.assume(balance > withdrawAmt); + + // Map + bytes memory data = abi.encode(MAP_TOKEN_SIG, rootToken, "Test token", "Test", 18); + vm.startPrank(address(mockAdaptor)); + bridge.onMessageReceive(data); + + address childTokenAddr = bridge.rootTokenToChildToken(rootToken); + + vm.deal(user, gasAmt); + // Mint token to user + vm.startPrank(address(bridge)); + IChildERC20 childToken = IChildERC20(childTokenAddr); + childToken.mint(user, balance); + + assertEq(user.balance, gasAmt, "User should have given gasAmt of balance"); + assertEq(childToken.balanceOf(user), balance, "User should have given balance of token"); + + // Over-withdraw + vm.startPrank(user); + vm.expectRevert(); + bridge.withdraw{value: gasAmt}(childToken, balance + 1); + + // Withdraw within balance + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, rootToken, user, user, withdrawAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + + vm.expectEmit(address(bridge)); + emit ChildChainERC20Withdraw(rootToken, childTokenAddr, user, user, withdrawAmt); + + bridge.withdraw{value: gasAmt}(childToken, withdrawAmt); + + assertEq(user.balance, 0, "User should have 0 balance"); + assertEq(childToken.balanceOf(user), balance - withdrawAmt, "User should have balance - withdrawAmt of token"); + + vm.stopPrank(); + } +} diff --git a/test/fuzz/child/WIMX.t.sol b/test/fuzz/child/WIMX.t.sol new file mode 100644 index 00000000..75866a01 --- /dev/null +++ b/test/fuzz/child/WIMX.t.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {WIMX} from "../../../src/child/WIMX.sol"; + +contract WIMXTest is Test { + WIMX public wIMX; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + function setUp() public { + wIMX = new WIMX(); + } + + function testFuzz_Deposit(uint256 depositAmt) public { + // Create a user and fund it + address user = address(1234); + vm.deal(user, depositAmt); + vm.startPrank(user); + + // Before deposit + assertEq(user.balance, depositAmt, "User should have given depositAmt of IMX"); + assertEq(wIMX.balanceOf(user), 0, "User should have 0 wIMX"); + assertEq(wIMX.totalSupply(), 0, "Total supply should be 0"); + + if (depositAmt != type(uint256).max) { + vm.expectRevert(); + wIMX.deposit{value: depositAmt + 1}(); + } + + // Deposit + vm.expectEmit(address(wIMX)); + emit Deposit(user, depositAmt); + wIMX.deposit{value: depositAmt}(); + + // After deposit + assertEq(user.balance, 0, "User should have 0 of IMX"); + assertEq(wIMX.balanceOf(user), depositAmt, "User should have given depositAmt wIMX"); + assertEq(wIMX.totalSupply(), depositAmt, "Total supply should be given depositAmt"); + + vm.stopPrank(); + } + + function testFuzz_Withdraw(uint256 depositAmt, uint256 withdrawAmt) public { + vm.assume(depositAmt >= withdrawAmt); + + // Create a user and fund it + address user = address(1234); + vm.deal(user, depositAmt); + vm.startPrank(user); + + // Deposit + wIMX.deposit{value: depositAmt}(); + + assertEq(wIMX.totalSupply(), depositAmt, "Total supply should be given depositAmt"); + + // Withdraw more than depositAmt + if (depositAmt != type(uint256).max) { + vm.expectRevert("Wrapped IMX: Insufficient balance"); + wIMX.withdraw(depositAmt + 1); + } + + vm.expectEmit(address(wIMX)); + emit Withdrawal(user, withdrawAmt); + wIMX.withdraw(withdrawAmt); + + assertEq(user.balance, withdrawAmt, "User should have withdrawAmt of IMX"); + assertEq(wIMX.balanceOf(user), depositAmt - withdrawAmt, "User should have depositAmt - withdrawAmt wIMX"); + assertEq(wIMX.totalSupply(), depositAmt - withdrawAmt, "Total supply should be depositAmt - withdrawAmt"); + + vm.stopPrank(); + } + + function testFuzz_Approve(address user, address approved, uint256 approvalAmt) public { + vm.startPrank(user); + + // Approve + vm.expectEmit(address(wIMX)); + emit Approval(user, approved, approvalAmt); + wIMX.approve(approved, approvalAmt); + + assertEq(wIMX.allowance(user, approved), approvalAmt, "Allowance should be given approvalAmt"); + + vm.stopPrank(); + } + + function testFuzz_Transfer(address from, address to, uint256 depositAmt, uint256 transferAmt) public { + vm.assume(depositAmt >= transferAmt); + vm.assume(from != to); + + // Fund sender + vm.deal(from, depositAmt); + vm.startPrank(from); + + // Deposit + wIMX.deposit{value: depositAmt}(); + + // Transfer out of balance + if (depositAmt != type(uint256).max) { + vm.expectRevert("Wrapped IMX: Insufficient balance"); + wIMX.transfer(to, depositAmt + 1); + } + + vm.expectEmit(address(wIMX)); + emit Transfer(from, to, transferAmt); + wIMX.transfer(to, transferAmt); + + assertEq(wIMX.balanceOf(from), depositAmt - transferAmt, "Sender should have depositAmt - transferAmt of IMX"); + assertEq(wIMX.balanceOf(to), transferAmt, "User should have transferAmt wIMX"); + assertEq(wIMX.totalSupply(), depositAmt, "Total supply should be depositAmt"); + + vm.stopPrank(); + } + + function testFuzz_TransferFrom(address from, address to, address operator, uint256 depositAmt, uint256 transferAmt) + public + { + vm.assume(depositAmt != type(uint256).max && depositAmt >= transferAmt && transferAmt > 1); + vm.assume(from != to && from != operator && to != operator); + + // Fund sender + vm.deal(from, depositAmt); + vm.startPrank(from); + + // Deposit + wIMX.deposit{value: depositAmt}(); + + // Insufficient allowance + wIMX.approve(operator, transferAmt - 1); + + // Transfer + vm.startPrank(operator); + + vm.expectRevert("Wrapped IMX: Insufficient allowance"); + wIMX.transferFrom(from, to, transferAmt); + + // Approve sufficient amount + vm.startPrank(from); + wIMX.approve(operator, depositAmt); + + vm.startPrank(operator); + vm.expectEmit(address(wIMX)); + emit Transfer(from, to, transferAmt); + wIMX.transferFrom(from, to, transferAmt); + + assertEq(wIMX.balanceOf(from), depositAmt - transferAmt, "Sender should have depositAmt - transferAmt of IMX"); + assertEq(wIMX.balanceOf(to), transferAmt, "User should have transferAmt wIMX"); + assertEq(wIMX.totalSupply(), depositAmt, "Total supply should be depositAmt"); + assertEq( + wIMX.allowance(from, operator), + depositAmt - transferAmt, + "Allowance should have depositAmt - transferAmt of IMX" + ); + + vm.stopPrank(); + } +} diff --git a/test/fuzz/root/FlowRateDetection.t.sol b/test/fuzz/root/FlowRateDetection.t.sol new file mode 100644 index 00000000..dffee508 --- /dev/null +++ b/test/fuzz/root/FlowRateDetection.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; + +import {FlowRateDetection} from "../../../src/root/flowrate/FlowRateDetection.sol"; + +contract FlowRateDetectionTest is Test, FlowRateDetection { + function activateWithdrawalQueue() external { + _activateWithdrawalQueue(); + } + + function deactivateWithdrawalQueue() external { + _deactivateWithdrawalQueue(); + } + + function setFlowRateThreshold(address token, uint256 capacity, uint256 refillRate) external { + _setFlowRateThreshold(token, capacity, refillRate); + } + + function updateFlowRateBucket(address token, uint256 amount) external returns (bool delayWithdrawal) { + return _updateFlowRateBucket(token, amount); + } + + function setUp() public {} + + function testFuzz_SetFlowRateThreshold(address token, uint256 capacity, uint256 refillRate) public { + vm.assume(token != address(0)); + vm.assume(capacity > 0); + vm.assume(refillRate > 0); + + this.setFlowRateThreshold(token, capacity, refillRate); + (uint256 currentCapacity,,, uint256 currentRefillRate) = this.flowRateBuckets(token); + assertEq(currentCapacity, capacity, "Capacity should match"); + assertEq(currentRefillRate, refillRate, "Refill rate should match"); + } + + function testFuzz_RateLimit(address token, uint256 capacity, uint256 refillRate) public { + vm.assume(token != address(0)); + vm.assume(refillRate > 0 && refillRate < type(uint256).max / 86400); + vm.assume(capacity > 86400 * refillRate && capacity < type(uint256).max / 86400); + + this.setFlowRateThreshold(token, capacity, refillRate); + (, uint256 depth,,) = this.flowRateBuckets(token); + assertEq(depth, capacity, "Depth should match capacity"); + assertFalse(this.withdrawalQueueActivated(), "Withdrawal queue should not activate"); + + // Use half capacity + bool delay = this.updateFlowRateBucket(token, capacity / 2); + assertFalse(delay, "Should not be delayed"); + (, depth,,) = this.flowRateBuckets(token); + assertEq(depth, capacity - capacity / 2, "Depth should match half capacity"); + assertFalse(this.withdrawalQueueActivated(), "Withdrawal queue should not activate"); + + // Use the other half capacity + delay = this.updateFlowRateBucket(token, capacity / 2 + 2); + assertFalse(delay, "Should not be delayed"); + (, depth,,) = this.flowRateBuckets(token); + assertEq(depth, 0, "Depth should be 0"); + assertTrue(this.withdrawalQueueActivated(), "Withdrawal queue should activate"); + } +} diff --git a/test/fuzz/root/RootAxelarBridgeAdaptor.t.sol b/test/fuzz/root/RootAxelarBridgeAdaptor.t.sol new file mode 100644 index 00000000..7af7e469 --- /dev/null +++ b/test/fuzz/root/RootAxelarBridgeAdaptor.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + RootAxelarBridgeAdaptor, + IRootAxelarBridgeAdaptorEvents, + IRootAxelarBridgeAdaptorErrors, + IRootAxelarBridgeAdaptor +} from "../../../src/root/RootAxelarBridgeAdaptor.sol"; +import {MockAxelarGateway} from "../../mocks/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../mocks/root/MockAxelarGasService.sol"; +import {StubRootBridge} from "../../mocks/root/StubRootBridge.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; + +contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorErrors, IRootAxelarBridgeAdaptorEvents { + string public constant CHILD_CHAIN_NAME = "child"; + string public CHILD_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + + RootAxelarBridgeAdaptor public axelarAdaptor; + MockAxelarGateway public mockRootAxelarGateway; + MockAxelarGasService public mockRootAxelarGasService; + StubRootBridge public mockRootERC20Bridge; + + function setUp() public { + IRootAxelarBridgeAdaptor.InitializationRoles memory roles = IRootAxelarBridgeAdaptor.InitializationRoles({ + defaultAdmin: address(this), + bridgeManager: address(this), + gasServiceManager: address(this), + targetManager: address(this) + }); + + mockRootERC20Bridge = new StubRootBridge(); + mockRootAxelarGateway = new MockAxelarGateway(); + mockRootAxelarGasService = new MockAxelarGasService(); + + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockRootAxelarGateway), address(this)); + axelarAdaptor.initialize( + roles, + address(mockRootERC20Bridge), + CHILD_CHAIN_NAME, + CHILD_BRIDGE_ADAPTOR, + address(mockRootAxelarGasService) + ); + } + + function testFuzz_SendMessage(uint256 callValue, bytes calldata payload, address refundRecipient) public { + vm.assume(callValue > 0 && callValue < type(uint256).max); + + vm.startPrank(address(mockRootERC20Bridge)); + vm.deal(address(mockRootERC20Bridge), callValue); + + // Send message called with insufficient balance should revert + vm.expectRevert(); + axelarAdaptor.sendMessage{value: callValue + 1}(payload, refundRecipient); + + // Send message correctly should call gas service and gateway with expected data + vm.expectCall( + address(mockRootAxelarGasService), + callValue, + abi.encodeWithSelector( + mockRootAxelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + CHILD_CHAIN_NAME, + axelarAdaptor.childBridgeAdaptor(), + payload, + refundRecipient + ) + ); + vm.expectCall( + address(mockRootAxelarGateway), + abi.encodeWithSelector( + mockRootAxelarGateway.callContract.selector, + CHILD_CHAIN_NAME, + axelarAdaptor.childBridgeAdaptor(), + payload + ) + ); + axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); + + vm.stopPrank(); + } + + function testFuzz_Execute(bytes32 commandId, bytes calldata payload) public { + // Execute should emit event and call bridge. + vm.expectEmit(); + emit AdaptorExecute(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR, payload); + vm.expectCall( + address(mockRootERC20Bridge), abi.encodeWithSelector(mockRootERC20Bridge.onMessageReceive.selector, payload) + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR, payload); + } +} diff --git a/test/fuzz/root/RootERC20Bridge.t.sol b/test/fuzz/root/RootERC20Bridge.t.sol new file mode 100644 index 00000000..8c23662b --- /dev/null +++ b/test/fuzz/root/RootERC20Bridge.t.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import { + RootERC20Bridge, + IRootERC20BridgeEvents, + IERC20Metadata, + IRootERC20BridgeErrors, + IRootERC20Bridge +} from "../../../src/root/RootERC20Bridge.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {WETH} from "../../../src/lib/WETH.sol"; +import {MockAdaptor} from "../../mocks/root/MockAdaptor.sol"; + +contract RootERC20BridgeTest is Test, IRootERC20BridgeEvents { + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); + address constant CHILD_BRIDGE = address(3); + uint256 constant IMX_DEPOSITS_LIMIT = 10000 ether; + + ChildERC20 childTokenTemplate; + ChildERC20 imxToken; + WETH wETH; + MockAdaptor mockAdaptor; + RootERC20Bridge bridge; + + function setUp() public { + mockAdaptor = new MockAdaptor(); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + imxToken = new ChildERC20(); + imxToken.initialize(address(234), "IMX Token", "IMX", 18); + + wETH = new WETH(); + + IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + variableManager: address(this), + adaptorManager: address(this) + }); + + bridge = new RootERC20Bridge(address(this)); + bridge.initialize( + roles, + address(mockAdaptor), + CHILD_BRIDGE, + address(childTokenTemplate), + address(imxToken), + address(wETH), + IMX_DEPOSITS_LIMIT + ); + } + + function testFuzz_MapToken(address user, uint256 gasAmt, string memory name, string memory symbol, uint8 decimals) + public + { + vm.assume(user != address(0)); + vm.assume(gasAmt > 0); + vm.assume(bytes(name).length != 0 && bytes(symbol).length != 0 && decimals > 0); + + ChildERC20 rootToken = new ChildERC20(); + rootToken.initialize(address(123), name, symbol, decimals); + + // Map token on L1 triggers call to child bridge. + vm.deal(user, gasAmt); + vm.startPrank(user); + + address childTokenAddress = Clones.predictDeterministicAddress( + address(childTokenTemplate), keccak256(abi.encodePacked(rootToken)), CHILD_BRIDGE + ); + + bytes memory predictedPayload = abi.encode(MAP_TOKEN_SIG, address(rootToken), name, symbol, decimals); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, user) + ); + vm.expectEmit(address(bridge)); + emit L1TokenMapped(address(rootToken), childTokenAddress); + bridge.mapToken{value: gasAmt}(IERC20Metadata(address(rootToken))); + + vm.stopPrank(); + } + + function testFuzz_DepositIMX(address sender, address recipient, uint256 balance, uint256 gasAmt, uint256 depositAmt) + public + { + vm.assume(sender != address(0) && recipient != address(0)); + vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); + vm.assume(balance > depositAmt && balance < type(uint256).max); + vm.assume(depositAmt <= IMX_DEPOSITS_LIMIT); + + // Fund user + vm.deal(sender, gasAmt); + imxToken.mint(sender, balance); + vm.startPrank(sender); + + // Before deposit + assertEq(sender.balance, gasAmt, "Sender should have gasAmt of balance"); + assertEq(imxToken.balanceOf(sender), balance, "Sender should have given balance of IMX"); + + // Deposit without approval should fail + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(imxToken)), recipient, depositAmt); + + // Deposit out of balance should fail + imxToken.approve(address(bridge), balance + 1); + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(imxToken)), recipient, balance + 1); + + // Deposit within balance and allowance should go through + imxToken.approve(address(bridge), depositAmt); + + bytes memory predictedPayload = abi.encode(DEPOSIT_SIG, address(imxToken), sender, recipient, depositAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, sender) + ); + vm.expectEmit(address(bridge)); + emit IMXDeposit(address(imxToken), sender, recipient, depositAmt); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(imxToken)), recipient, depositAmt); + + // After deposit + assertEq(sender.balance, 0, "Sender should have 0 balance"); + assertEq(imxToken.balanceOf(sender), balance - depositAmt, "Sender should have balance - depositAmt of IMX"); + + assertEq(address(imxToken), bridge.rootIMXToken()); + + vm.stopPrank(); + } + + function testFuzz_WithdrawIMX(address sender, address recipient, uint256 withdrawAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + + imxToken.mint(address(bridge), withdrawAmt); + + assertEq(imxToken.balanceOf(address(bridge)), withdrawAmt, "Bridge should have withdrawAmt balance"); + assertEq(imxToken.balanceOf(recipient), 0, "Recipient should have 0 balance"); + + bytes memory data = abi.encode(WITHDRAW_SIG, address(imxToken), sender, recipient, withdrawAmt); + + vm.expectEmit(address(bridge)); + emit RootChainERC20Withdraw(address(imxToken), bridge.NATIVE_IMX(), sender, recipient, withdrawAmt); + + vm.startPrank(address(mockAdaptor)); + + bridge.onMessageReceive(data); + + assertEq(imxToken.balanceOf(address(bridge)), 0, "Bridge should have 0 balance"); + assertEq(imxToken.balanceOf(recipient), withdrawAmt, "Recipient should have withdrawAmt balance"); + + vm.stopPrank(); + } + + function testFuzz_DepositETH(address sender, address recipient, uint256 balance, uint256 gasAmt, uint256 depositAmt) + public + { + vm.assume(sender != address(0) && recipient != address(0)); + vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); + vm.assume(balance > depositAmt && balance < type(uint256).max - gasAmt && balance - depositAmt > gasAmt); + + // Fund user + vm.deal(sender, balance); + vm.startPrank(sender); + + // Before deposit + assertEq(sender.balance, balance, "Sender should have given balance"); + assertEq(address(bridge).balance, 0, "Bridge should have 0 balance"); + + // Deposit out of balance should fail + vm.expectRevert(); + bridge.depositToETH{value: balance + gasAmt + 1}(recipient, balance + 1); + + // Deposit within balance should go through + bytes memory predictedPayload = abi.encode(DEPOSIT_SIG, bridge.NATIVE_ETH(), sender, recipient, depositAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, sender) + ); + vm.expectEmit(address(bridge)); + emit NativeEthDeposit(bridge.NATIVE_ETH(), bridge.childETHToken(), sender, recipient, depositAmt); + bridge.depositToETH{value: depositAmt + gasAmt}(recipient, depositAmt); + + // Before deposit + assertEq(sender.balance, balance - gasAmt - depositAmt, "Sender should have balance - gasAmt - depositAmt"); + assertEq(address(bridge).balance, depositAmt, "Bridge should have depositAmt"); + + vm.stopPrank(); + } + + function testFuzz_DepositWETH( + address sender, + address recipient, + uint256 balance, + uint256 gasAmt, + uint256 depositAmt + ) public { + vm.assume(sender != address(0) && recipient != address(0)); + vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); + vm.assume(balance > depositAmt && balance < type(uint256).max - gasAmt && balance - depositAmt > gasAmt); + + // Fund user + vm.deal(sender, balance); + vm.startPrank(sender); + wETH.deposit{value: balance}(); + + vm.deal(sender, gasAmt); + + // Before deposit + assertEq(sender.balance, gasAmt, "Sender should have gasAmt"); + assertEq(wETH.balanceOf(sender), balance, "Sender should have given balance of WETH"); + assertEq(wETH.balanceOf(address(bridge)), 0, "Bridge should have 0 balance of WETH"); + + // Deposit without approval should fail + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(wETH)), recipient, depositAmt); + + // Deposit out of balance should fail + wETH.approve(address(bridge), balance + 1); + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(wETH)), recipient, balance + 1); + + // Deposit within balance and allowance should go through + wETH.approve(address(bridge), depositAmt); + bytes memory predictedPayload = abi.encode(DEPOSIT_SIG, bridge.NATIVE_ETH(), sender, recipient, depositAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, sender) + ); + vm.expectEmit(address(bridge)); + emit WETHDeposit(address(wETH), bridge.childETHToken(), sender, recipient, depositAmt); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(wETH)), recipient, depositAmt); + + // After deposit + assertEq(sender.balance, 0, "Sender should have 0"); + assertEq(wETH.balanceOf(sender), balance - depositAmt, "Sender should have balance - depositAmt of WETH"); + assertEq(address(bridge).balance, depositAmt, "Bridge should have depositAmt of ETH"); + + vm.stopPrank(); + } + + function testFuzz_WithdrawETH(address sender, address recipient, uint256 withdrawAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + + vm.deal(address(bridge), withdrawAmt); + + assertEq(address(bridge).balance, withdrawAmt, "Bridge should have withdrawAmt balance"); + assertEq(recipient.balance, 0, "Recipient should have 0 balance"); + + bytes memory data = abi.encode(WITHDRAW_SIG, bridge.NATIVE_ETH(), sender, recipient, withdrawAmt); + + vm.expectEmit(address(bridge)); + emit RootChainETHWithdraw(bridge.NATIVE_ETH(), bridge.childETHToken(), sender, recipient, withdrawAmt); + + vm.startPrank(address(mockAdaptor)); + + bridge.onMessageReceive(data); + + assertEq(address(bridge).balance, 0, "Bridge should have 0 balance"); + assertEq(recipient.balance, withdrawAmt, "Recipient should have withdrawAmt balance"); + + vm.stopPrank(); + } + + function testFuzz_DepositERC20( + address sender, + address recipient, + uint256 balance, + uint256 gasAmt, + uint256 depositAmt + ) public { + vm.assume(sender != address(0) && recipient != address(0)); + vm.assume(balance > 0 && depositAmt > 0 && gasAmt > 0); + vm.assume(balance > depositAmt && balance < type(uint256).max); + vm.assume(gasAmt < 100); + + // Map token + ChildERC20 rootToken = new ChildERC20(); + rootToken.initialize(address(123), "Test token", "TEST", 18); + + vm.deal(sender, gasAmt); + rootToken.mint(sender, balance); + vm.startPrank(sender); + + bridge.mapToken{value: gasAmt}(IERC20Metadata(address(rootToken))); + + vm.deal(sender, gasAmt); + + // Before deposit + assertEq(rootToken.balanceOf(sender), balance, "Sender should have given balance of ERC20"); + assertEq(rootToken.balanceOf(address(bridge)), 0, "Bridge should have 0 balance of ERC20"); + + // Deposit without approval should fail + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(rootToken)), recipient, depositAmt); + + // Deposit out of balance should fail + rootToken.approve(address(bridge), balance + 1); + vm.expectRevert(); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(rootToken)), recipient, balance + 1); + + // Deposit within balance and allowance should go through + rootToken.approve(address(bridge), depositAmt); + bytes memory predictedPayload = abi.encode(DEPOSIT_SIG, address(rootToken), sender, recipient, depositAmt); + vm.expectCall( + address(mockAdaptor), + gasAmt, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, sender) + ); + vm.expectEmit(address(bridge)); + emit ChildChainERC20Deposit( + address(rootToken), bridge.rootTokenToChildToken(address(rootToken)), sender, recipient, depositAmt + ); + bridge.depositTo{value: gasAmt}(IERC20Metadata(address(rootToken)), recipient, depositAmt); + + // After deposit + assertEq(rootToken.balanceOf(sender), balance - depositAmt, "Sender should have balance - depositAmt of ERC20"); + assertEq(rootToken.balanceOf(address(bridge)), depositAmt, "Bridge should have depositAmt of ERC20"); + + vm.stopPrank(); + } + + function testFuzz_WithdrawERC20(address sender, address recipient, uint256 withdrawAmt) public { + vm.assume(sender != address(0) && recipient != address(0) && withdrawAmt > 0); + + // Map token + ChildERC20 rootToken = new ChildERC20(); + rootToken.initialize(address(123), "Test token", "TEST", 18); + rootToken.mint(address(bridge), withdrawAmt); + vm.deal(sender, 100); + vm.startPrank(sender); + bridge.mapToken{value: 100}(IERC20Metadata(address(rootToken))); + + address childTokenAddr = bridge.rootTokenToChildToken(address(rootToken)); + + assertEq(rootToken.balanceOf(recipient), 0, "Recipient should have 0 balance of ERC20"); + assertEq(rootToken.balanceOf(address(bridge)), withdrawAmt, "Bridge should have withdrawAmt of ERC20"); + + bytes memory data = abi.encode(WITHDRAW_SIG, address(rootToken), sender, recipient, withdrawAmt); + + vm.expectEmit(address(bridge)); + emit RootChainERC20Withdraw(address(rootToken), childTokenAddr, sender, recipient, withdrawAmt); + + vm.startPrank(address(mockAdaptor)); + + bridge.onMessageReceive(data); + + assertEq(rootToken.balanceOf(recipient), withdrawAmt, "Recipient should have withdrawAmt of ERC20"); + assertEq(rootToken.balanceOf(address(bridge)), 0, "Bridge should have 0 of ERC20"); + + vm.stopPrank(); + } +} diff --git a/test/fuzz/root/RootERC20BridgeFlowRate.t.sol b/test/fuzz/root/RootERC20BridgeFlowRate.t.sol new file mode 100644 index 00000000..051beac7 --- /dev/null +++ b/test/fuzz/root/RootERC20BridgeFlowRate.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import { + IRootERC20BridgeEvents, + IERC20Metadata, + IRootERC20BridgeErrors, + IRootERC20Bridge +} from "../../../src/root/RootERC20Bridge.sol"; +import {RootERC20BridgeFlowRate} from "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import {ChildERC20} from "../../../src/child/ChildERC20.sol"; +import {WETH} from "../../../src/lib/WETH.sol"; +import {MockAdaptor} from "../../mocks/root/MockAdaptor.sol"; + +contract RootERC20BridgeFlowRateTest is Test { + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); + address constant CHILD_BRIDGE = address(3); + uint256 constant IMX_DEPOSITS_LIMIT = 10000 ether; + + ChildERC20 childTokenTemplate; + ChildERC20 imxToken; + WETH wETH; + MockAdaptor mockAdaptor; + RootERC20BridgeFlowRate bridge; + + function setUp() public { + mockAdaptor = new MockAdaptor(); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + imxToken = new ChildERC20(); + imxToken.initialize(address(234), "IMX Token", "IMX", 18); + + wETH = new WETH(); + + IRootERC20Bridge.InitializationRoles memory roles = IRootERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + variableManager: address(this), + adaptorManager: address(this) + }); + + bridge = new RootERC20BridgeFlowRate(address(this)); + bridge.initialize( + roles, + address(mockAdaptor), + CHILD_BRIDGE, + address(childTokenTemplate), + address(imxToken), + address(wETH), + IMX_DEPOSITS_LIMIT, + address(this) + ); + } + + function testFuzz_RateLimitForIMX(uint256 capacity) public { + vm.assume(capacity < 1000 ether && capacity > 86400); + uint256 refillRate = capacity / 86400; + uint256 largeTransferThreshold = capacity / 2; + + // bridge.setRateControlThreshold(, capacity, refillRate, largeTransferThreshold); + } +}