diff --git a/.gitmodules b/.gitmodules index ceb0d21ba..6a2a13a79 100644 --- a/.gitmodules +++ b/.gitmodules @@ -70,3 +70,9 @@ [submodule "protocol-units/tokens/mock/testnet/holesky/lib/openzeppelin-contracts"] path = protocol-units/tokens/mock/testnet/holesky/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/BokkyPooBahsDateTimeLibrary"] + path = lib/BokkyPooBahsDateTimeLibrary + url = https://github.com/bokkypoobah/BokkyPooBahsDateTimeLibrary +[submodule "protocol-units/bridge/contracts/lib/BokkyPooBahsDateTimeLibrary"] + path = protocol-units/bridge/contracts/lib/BokkyPooBahsDateTimeLibrary + url = https://github.com/bokkypoobah/BokkyPooBahsDateTimeLibrary diff --git a/protocol-units/bridge/contracts/foundry.toml b/protocol-units/bridge/contracts/foundry.toml index 081338885..1f445f4c6 100644 --- a/protocol-units/bridge/contracts/foundry.toml +++ b/protocol-units/bridge/contracts/foundry.toml @@ -3,7 +3,7 @@ src = "src" out = "out" libs = ["lib"] -solc = "0.8.26" +solc = "0.8.27" evm_version = "cancun" # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/protocol-units/bridge/contracts/remappings.txt b/protocol-units/bridge/contracts/remappings.txt index cba72cb70..b20cd1281 100644 --- a/protocol-units/bridge/contracts/remappings.txt +++ b/protocol-units/bridge/contracts/remappings.txt @@ -1,5 +1,9 @@ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/ -erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +@DateTimeLibrary/=lib/BokkyPooBahsDateTimeLibrary/ +ds-test/=lib/openzeppelin-contracts-upgradeable/lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/ forge-std/=lib/forge-std/src/ -openzeppelin-contracts/=lib/openzeppelin-contracts/ \ No newline at end of file +halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/ +openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ diff --git a/protocol-units/bridge/contracts/src/INativeBridge.sol b/protocol-units/bridge/contracts/src/INativeBridge.sol index f7c6d91bc..9ccb94b1b 100644 --- a/protocol-units/bridge/contracts/src/INativeBridge.sol +++ b/protocol-units/bridge/contracts/src/INativeBridge.sol @@ -19,6 +19,9 @@ interface INativeBridge { uint256 nonce ); + event InsuranceFundUpdated(address insuranceFund); + event PauseToggled(bool paused); + error ZeroAmount(); error MOVETransferFailed(); error ZeroAddress(); @@ -26,6 +29,8 @@ interface INativeBridge { error InvalidBridgeTransferId(); error CompletedBridgeTransferId(); error InvalidNonce(); + error OutboundRateLimitExceeded(); + error InboundRateLimitExceeded(); /** * @dev Creates a new bridge diff --git a/protocol-units/bridge/contracts/src/NativeBridge.sol b/protocol-units/bridge/contracts/src/NativeBridge.sol index db45c6327..42fb3c6c9 100644 --- a/protocol-units/bridge/contracts/src/NativeBridge.sol +++ b/protocol-units/bridge/contracts/src/NativeBridge.sol @@ -19,7 +19,10 @@ contract NativeBridge is AccessControlUpgradeable, PausableUpgradeable, INativeB mapping(uint256 nonce => OutboundTransfer) public noncesToOutboundTransfers; mapping(bytes32 bridgeTransferId => uint256 nonce) public idsToInboundNonces; + mapping(uint256 day => uint256 amount) public outboundRateLimitBudget; + mapping(uint256 day => uint256 amount) public inboundRateLimitBudget; + address public insuranceFund; IERC20 public moveToken; bytes32 public constant RELAYER_ROLE = keccak256(abi.encodePacked("RELAYER_ROLE")); uint256 private _nonce; @@ -37,12 +40,17 @@ contract NativeBridge is AccessControlUpgradeable, PausableUpgradeable, INativeB * @param _relayer The address of the relayer role * @param _maintainer The address of the maintainer role */ - function initialize(address _moveToken, address _admin, address _relayer, address _maintainer) public initializer { + function initialize(address _moveToken, address _admin, address _relayer, address _maintainer, address _insuranceFund) public initializer { require(_moveToken != address(0) && _admin != address(0) && _relayer != address(0), ZeroAddress()); __Pausable_init(); moveToken = IERC20(_moveToken); _grantRole(DEFAULT_ADMIN_ROLE, _admin); _grantRole(RELAYER_ROLE, _relayer); + + // Set insurance fund + insuranceFund = _insuranceFund; + + // Maintainer is optional _grantRole(RELAYER_ROLE, _maintainer); } @@ -60,7 +68,7 @@ contract NativeBridge is AccessControlUpgradeable, PausableUpgradeable, INativeB { // Ensure there is a valid amount require(amount > 0, ZeroAmount()); - // _l1l2RateLimit(amount); + _rateLimitOutbound(amount); address initiator = msg.sender; // Transfer the MOVE tokens from the user to the contract @@ -79,7 +87,7 @@ contract NativeBridge is AccessControlUpgradeable, PausableUpgradeable, INativeB /** * @dev Completes the bridging of funds. Only the relayer can call this function. * @param bridgeTransferId Unique identifier for the BridgeTransfer - * @param originator The address on the other chain that originated the transfer of funds + * @param initiator The address on the other chain that originated the transfer of funds * @param recipient The address on this chain to which to transfer funds * @param amount The amount to transfer * @param nonce The seed nonce to generate the bridgeTransferId @@ -129,7 +137,7 @@ contract NativeBridge is AccessControlUpgradeable, PausableUpgradeable, INativeB uint256 amount, uint256 nonce ) internal { - // _l2l1RateLimit(amount); + _rateLimitInbound(amount); // Ensure the bridge transfer has not already been completed require(nonce > 0, InvalidNonce()); require(idsToInboundNonces[bridgeTransferId] == 0, CompletedBridgeTransferId()); @@ -148,7 +156,25 @@ contract NativeBridge is AccessControlUpgradeable, PausableUpgradeable, INativeB emit BridgeTransferCompleted(bridgeTransferId, initiator, recipient, amount, nonce); } + function setInsuranceFund(address _insuranceFund) external onlyRole(DEFAULT_ADMIN_ROLE) { + insuranceFund = _insuranceFund; + emit InsuranceFundUpdated(_insuranceFund); + } + function togglePause() external onlyRole(DEFAULT_ADMIN_ROLE) { paused() ? _pause() : _unpause(); + emit PauseToggled(paused()); + } + + function _rateLimitOutbound(uint256 amount) internal { + uint256 day = block.timestamp / 1 days; + outboundRateLimitBudget[day] += amount; + require(outboundRateLimitBudget[day] < moveToken.balanceOf(insuranceFund) / 4, OutboundRateLimitExceeded()); + } + + function _rateLimitInbound(uint256 amount) internal { + uint256 day = block.timestamp / 1 days; + inboundRateLimitBudget[day] += amount; + require(inboundRateLimitBudget[day] < moveToken.balanceOf(insuranceFund) / 4, InboundRateLimitExceeded()); } } diff --git a/protocol-units/bridge/contracts/test/AtomicBridgeCounterpartyMOVE.t.sol b/protocol-units/bridge/contracts/test/AtomicBridgeCounterpartyMOVE.t.sol deleted file mode 100644 index 25be7357f..000000000 --- a/protocol-units/bridge/contracts/test/AtomicBridgeCounterpartyMOVE.t.sol +++ /dev/null @@ -1,267 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; -pragma abicoder v2; - -import {Test, console} from "forge-std/Test.sol"; -import {AtomicBridgeCounterpartyMOVE} from "../src/AtomicBridgeCounterpartyMOVE.sol"; -import {AtomicBridgeInitiatorMOVE} from "../src/AtomicBridgeInitiatorMOVE.sol"; -import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {MockMOVEToken} from "../src/MockMOVEToken.sol"; - -contract AtomicBridgeCounterpartyMOVETest is Test { - AtomicBridgeCounterpartyMOVE public atomicBridgeCounterpartyMOVEImplementation; - AtomicBridgeCounterpartyMOVE public atomicBridgeCounterpartyMOVE; - AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorMOVEImplementation; - AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorMOVE; - MockMOVEToken public moveToken; - ProxyAdmin public proxyAdmin; - TransparentUpgradeableProxy public proxy; - - address public deployer = address(0x1); - address public originator = address(1); - address public recipient = address(0x2); - address public otherUser = address(0x3); - bytes32 public hashLock = keccak256(abi.encodePacked("secret")); - uint256 public amount = 100 * 10 ** 8; // 100 MOVEToken (assuming 8 decimals) - uint256 public timeLock = 100; - bytes32 public initiator = keccak256(abi.encodePacked(deployer)); - bytes32 public bridgeTransferId = - keccak256( - abi.encodePacked( - block.timestamp, - initiator, - recipient, - amount, - hashLock, - timeLock - ) - ); - - uint256 public constant COUNTERPARTY_TIME_LOCK_DURATION = 24 * 60 * 60; // 24 hours - - function setUp() public { - // Deploy the MOVEToken contract and mint some tokens to the deployer - moveToken = new MockMOVEToken(); - moveToken.initialize(address(this)); // Contract will hold initial MOVE tokens - - // Time lock durations - uint256 initiatorTimeLockDuration = 48 * 60 * 60; // 48 hours for the initiator - uint256 counterpartyTimeLockDuration = 24 * 60 * 60; // 24 hours for the counterparty - - originator = vm.addr(uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)))); - - // Deploy the AtomicBridgeInitiator contract with a 48-hour time lock - atomicBridgeInitiatorMOVEImplementation = new AtomicBridgeInitiatorMOVE(); - proxyAdmin = new ProxyAdmin(deployer); - proxy = new TransparentUpgradeableProxy( - address(atomicBridgeInitiatorMOVEImplementation), - address(proxyAdmin), - abi.encodeWithSignature( - "initialize(address,address,uint256,uint256)", - address(moveToken), - deployer, - initiatorTimeLockDuration, - 0 ether // Initial pool balance - ) - ); - atomicBridgeInitiatorMOVE = AtomicBridgeInitiatorMOVE(address(proxy)); - - // Deploy the AtomicBridgeCounterparty contract with a 24-hour time lock - atomicBridgeCounterpartyMOVEImplementation = new AtomicBridgeCounterpartyMOVE(); - proxy = new TransparentUpgradeableProxy( - address(atomicBridgeCounterpartyMOVEImplementation), - address(proxyAdmin), - abi.encodeWithSignature( - "initialize(address,address,uint256)", - address(atomicBridgeInitiatorMOVE), - deployer, - counterpartyTimeLockDuration - ) - ); - atomicBridgeCounterpartyMOVE = AtomicBridgeCounterpartyMOVE(address(proxy)); - - // Set the counterparty contract in the AtomicBridgeInitiator contract - vm.startPrank(deployer); - atomicBridgeInitiatorMOVE.setCounterpartyAddress( - address(atomicBridgeCounterpartyMOVE) - ); - vm.stopPrank(); - } - - function testLockBridgeTransfer() public { - uint256 moveAmount = 100 * 10**8; - moveToken.transfer(originator, moveAmount); - vm.startPrank(originator); - - // Approve the AtomicBridgeInitiatorMOVE contract to spend MOVEToken - moveToken.approve(address(atomicBridgeInitiatorMOVE), amount); - - // Initiate the bridge transfer - atomicBridgeInitiatorMOVE.initiateBridgeTransfer( - amount, - initiator, - hashLock - ); - - vm.stopPrank(); - - vm.startPrank(deployer); // Only the owner (deployer) can call lockBridgeTransfer - atomicBridgeCounterpartyMOVE.lockBridgeTransfer( - initiator, - bridgeTransferId, - hashLock, - recipient, - amount - ); - vm.stopPrank(); - - ( - bytes32 pendingInitiator, - address pendingRecipient, - uint256 pendingAmount, - bytes32 pendingHashLock, - uint256 pendingTimelock, - AtomicBridgeCounterpartyMOVE.MessageState pendingState - ) = atomicBridgeCounterpartyMOVE.bridgeTransfers(bridgeTransferId); - - assertEq(pendingInitiator, initiator); - assertEq(pendingRecipient, recipient); - assertEq(pendingAmount, amount); - assertEq(pendingHashLock, hashLock); - assertGt(pendingTimelock, block.timestamp); - assertEq( - uint8(pendingState), - uint8(AtomicBridgeCounterpartyMOVE.MessageState.PENDING) - ); - } - - function testCompleteBridgeTransfer() public { - bytes32 preImage = "secret"; - bytes32 testHashLock = keccak256(abi.encodePacked(preImage)); - - uint256 moveAmount = 100 * 10**8; - moveToken.transfer(originator, moveAmount); - vm.startPrank(originator); - - // Approve the AtomicBridgeInitiatorMOVE contract to spend MOVEToken - moveToken.approve(address(atomicBridgeInitiatorMOVE), amount); - - // Initiate the bridge transfer - atomicBridgeInitiatorMOVE.initiateBridgeTransfer( - amount, - initiator, - testHashLock - ); - - vm.stopPrank(); - - vm.startPrank(deployer); // Only the owner (deployer) can call lockBridgeTransfer - atomicBridgeCounterpartyMOVE.lockBridgeTransfer( - initiator, - bridgeTransferId, - testHashLock, - recipient, - amount - ); - vm.stopPrank(); - - vm.startPrank(otherUser); - - atomicBridgeCounterpartyMOVE.completeBridgeTransfer( - bridgeTransferId, - preImage - ); - - ( - bytes32 completedInitiator, - address completedRecipient, - uint256 completedAmount, - bytes32 completedHashLock, - uint256 completedTimeLock, - AtomicBridgeCounterpartyMOVE.MessageState completedState - ) = atomicBridgeCounterpartyMOVE.bridgeTransfers(bridgeTransferId); - - assertEq(completedInitiator, initiator); - assertEq(completedRecipient, recipient); - assertEq(completedAmount, amount); - assertEq(completedHashLock, testHashLock); - assertGt(completedTimeLock, block.timestamp); - assertEq( - uint8(completedState), - uint8(AtomicBridgeCounterpartyMOVE.MessageState.COMPLETED) - ); - - vm.stopPrank(); - } - -function testAbortBridgeTransfer() public { - uint256 moveAmount = 100 * 10**8; - moveToken.transfer(originator, moveAmount); - vm.startPrank(originator); - - // Approve the AtomicBridgeInitiatorMOVE contract to spend MOVEToken - moveToken.approve(address(atomicBridgeInitiatorMOVE), amount); - - // Initiate the bridge transfer - atomicBridgeInitiatorMOVE.initiateBridgeTransfer( - amount, - initiator, - hashLock - ); - - vm.stopPrank(); - - vm.startPrank(deployer); - - atomicBridgeCounterpartyMOVE.lockBridgeTransfer( - initiator, - bridgeTransferId, - hashLock, - recipient, - amount - ); - - vm.stopPrank(); - - // Advance the block number to beyond the timelock period - vm.warp(block.timestamp + COUNTERPARTY_TIME_LOCK_DURATION + 1); - - // Try to abort as a malicious user (this should fail) - //vm.startPrank(otherUser); - //vm.expectRevert("Ownable: caller is not the owner"); - //atomicBridgeCounterpartyMOVE.abortBridgeTransfer(bridgeTransferId); - //vm.stopPrank(); - - // Abort as the owner (this should pass) - vm.startPrank(deployer); // The deployer is the owner - atomicBridgeCounterpartyMOVE.abortBridgeTransfer(bridgeTransferId); - - ( - bytes32 abortedInitiator, - address abortedRecipient, - uint256 abortedAmount, - bytes32 abortedHashLock, - uint256 abortedTimeLock, - AtomicBridgeCounterpartyMOVE.MessageState abortedState - ) = atomicBridgeCounterpartyMOVE.bridgeTransfers(bridgeTransferId); - - assertEq(abortedInitiator, initiator); - assertEq(abortedRecipient, recipient); - assertEq(abortedAmount, amount); - assertEq(abortedHashLock, hashLock); - assertLe( - abortedTimeLock, - block.timestamp, - "Timelock is not less than or equal to current timestamp" - ); - assertEq( - uint8(abortedState), - uint8(AtomicBridgeCounterpartyMOVE.MessageState.REFUNDED) - ); - - vm.stopPrank(); -} - - -} diff --git a/protocol-units/bridge/contracts/test/AtomicBridgeInitiatorMOVE.t.sol b/protocol-units/bridge/contracts/test/AtomicBridgeInitiatorMOVE.t.sol deleted file mode 100644 index f10a4d9cf..000000000 --- a/protocol-units/bridge/contracts/test/AtomicBridgeInitiatorMOVE.t.sol +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; -pragma abicoder v2; - -import {Test, console} from "forge-std/Test.sol"; -import {AtomicBridgeInitiatorMOVE, IAtomicBridgeInitiatorMOVE, OwnableUpgradeable} from "../src/AtomicBridgeInitiatorMOVE.sol"; -import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {MockMOVEToken} from "../src/MockMOVEToken.sol"; - -contract AtomicBridgeInitiatorMOVETest is Test { - AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorImplementation; - MockMOVEToken public moveToken; - ProxyAdmin public proxyAdmin; - TransparentUpgradeableProxy public proxy; - AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorMOVE; - - address public originator = address(1); - bytes32 public recipient = keccak256(abi.encodePacked(address(2))); - bytes32 public hashLock = keccak256(abi.encodePacked("secret")); - uint256 public amount = 1 ether; - uint256 public constant timeLockDuration = 48 * 60 * 60; // 48 hours in seconds - - function setUp() public { - // Deploy the MOVEToken contract and mint some tokens to the deployer - moveToken = new MockMOVEToken(); - moveToken.initialize(address(this)); // Contract will hold initial MOVE tokens - - originator = vm.addr(uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)))); - - // Deploy the AtomicBridgeInitiatorMOVE contract - atomicBridgeInitiatorImplementation = new AtomicBridgeInitiatorMOVE(); - proxyAdmin = new ProxyAdmin(msg.sender); - proxy = new TransparentUpgradeableProxy( - address(atomicBridgeInitiatorImplementation), - address(proxyAdmin), - abi.encodeWithSignature( - "initialize(address,address,uint256,uint256)", - address(moveToken), - address(this), - timeLockDuration, - 0 ether - ) - ); - - atomicBridgeInitiatorMOVE = AtomicBridgeInitiatorMOVE(address(proxy)); - } - - function testInitiateBridgeTransferWithMove() public { - uint256 moveAmount = 100 * 10**8; - - // Transfer moveAmount tokens to the originator and check initial balance - moveToken.transfer(originator, moveAmount); - uint256 initialBalance = moveToken.balanceOf(originator); - - vm.startPrank(originator); - moveToken.approve(address(atomicBridgeInitiatorMOVE), moveAmount); - - // Initiate the bridge transfer - bytes32 bridgeTransferId = atomicBridgeInitiatorMOVE.initiateBridgeTransfer( - moveAmount, - recipient, - hashLock - ); - - // Verify the bridge transfer details - ( - uint256 transferAmount, - address transferOriginator, - bytes32 transferRecipient, - bytes32 transferHashLock, - uint256 transferTimeLock, - AtomicBridgeInitiatorMOVE.MessageState transferState - ) = atomicBridgeInitiatorMOVE.bridgeTransfers(bridgeTransferId); - - assertEq(transferAmount, moveAmount); - assertEq(transferOriginator, originator); - assertEq(transferRecipient, recipient); - assertEq(transferHashLock, hashLock); - assertGt(transferTimeLock, block.timestamp); - assertEq(uint8(transferState), uint8(AtomicBridgeInitiatorMOVE.MessageState.INITIALIZED)); - - // Check the originator's MOVE balance after initiating the transfer - uint256 finalBalance = moveToken.balanceOf(originator); - assertEq(finalBalance, initialBalance - moveAmount); - - vm.stopPrank(); - } - - function testCompleteBridgeTransfer() public { - bytes32 secret = "secret"; - bytes32 testHashLock = keccak256(abi.encodePacked(secret)); - uint256 moveAmount = 100 * 10**8; // 100 MOVEToken - - // Transfer moveAmount tokens to the originator and check initial balance - moveToken.transfer(originator, moveAmount); - uint256 initialBalance = moveToken.balanceOf(originator); - - vm.startPrank(originator); - moveToken.approve(address(atomicBridgeInitiatorMOVE), moveAmount); - - // Initiate the bridge transfer - bytes32 bridgeTransferId = atomicBridgeInitiatorMOVE.initiateBridgeTransfer( - moveAmount, - recipient, - testHashLock - ); - - vm.stopPrank(); - - atomicBridgeInitiatorMOVE.completeBridgeTransfer(bridgeTransferId, secret); - - // Verify the bridge transfer details after completion - ( - uint256 completedAmount, - address completedOriginator, - bytes32 completedRecipient, - bytes32 completedHashLock, - uint256 completedTimeLock, - AtomicBridgeInitiatorMOVE.MessageState completedState - ) = atomicBridgeInitiatorMOVE.bridgeTransfers(bridgeTransferId); - - assertEq(completedAmount, moveAmount); - assertEq(completedOriginator, originator); - assertEq(completedRecipient, recipient); - assertEq(completedHashLock, testHashLock); - assertGt(completedTimeLock, block.timestamp); - assertEq(uint8(completedState), uint8(AtomicBridgeInitiatorMOVE.MessageState.COMPLETED)); - - // Ensure no changes to the originator's balance after the transfer is completed - uint256 finalBalance = moveToken.balanceOf(originator); - assertEq(finalBalance, initialBalance - moveAmount); - } - - function testRefundBridgeTransfer() public { - uint256 moveAmount = 100 * 10**8; // 100 MOVEToken - - // Transfer moveAmount tokens to the originator and check initial balance - moveToken.transfer(originator, moveAmount); - uint256 initialBalance = moveToken.balanceOf(originator); - - vm.startPrank(originator); - moveToken.approve(address(atomicBridgeInitiatorMOVE), moveAmount); - - // Initiate the bridge transfer - bytes32 bridgeTransferId = atomicBridgeInitiatorMOVE.initiateBridgeTransfer( - moveAmount, - recipient, - hashLock - ); - vm.stopPrank(); - - // Advance time and block height to ensure the time lock has expired - vm.warp(block.timestamp + timeLockDuration + 1); - - // Test that a non-owner cannot call refund - vm.startPrank(originator); - vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, originator)); - atomicBridgeInitiatorMOVE.refundBridgeTransfer(bridgeTransferId); - vm.stopPrank(); - - // Owner refunds the transfer - vm.expectEmit(); - emit IAtomicBridgeInitiatorMOVE.BridgeTransferRefunded(bridgeTransferId); - atomicBridgeInitiatorMOVE.refundBridgeTransfer(bridgeTransferId); - - // Verify that the originator receives the refund and the balance is restored - uint256 finalBalance = moveToken.balanceOf(originator); - assertEq(finalBalance, initialBalance, "MOVE balance mismatch"); - } -} - diff --git a/protocol-units/bridge/contracts/test/NativeBridge.t.sol b/protocol-units/bridge/contracts/test/NativeBridge.t.sol index a30c38277..b6d51cf2a 100644 --- a/protocol-units/bridge/contracts/test/NativeBridge.t.sol +++ b/protocol-units/bridge/contracts/test/NativeBridge.t.sol @@ -22,6 +22,7 @@ contract NativeBridgeTest is Test { address public relayer = address(0x8e1a7e8); address public recipient = address(0x2); address public otherUser = address(0x3); + address public insuranceFund = address(this); function setUp() public { moveToken = new MockMOVEToken(); @@ -33,7 +34,7 @@ contract NativeBridgeTest is Test { address(nativeBridgeImplementation), address(proxyAdmin), abi.encodeWithSignature( - "initialize(address,address,address,address)", address(moveToken), deployer, relayer, address(0) + "initialize(address,address,address,address,address)", address(moveToken), deployer, relayer, address(0), insuranceFund ) ); nativeBridge = NativeBridge(address(proxy)); @@ -44,7 +45,7 @@ contract NativeBridgeTest is Test { vm.assume(_originator != address(0)); vm.assume(_originator != deployer); - _amount = bound(_amount, 1, 10000000000 * 10 ** 8); + _amount = bound(_amount, 1, 100000000 * 10 ** 8); moveToken.transfer(_originator, _amount); vm.startPrank(_originator); // require approval @@ -61,7 +62,7 @@ contract NativeBridgeTest is Test { bytes32 bridgeTransferId = nativeBridge.initiateBridgeTransfer(_recipient, _amount); (bytes32 bridgeTransferId_, address originator, bytes32 recipient_, uint256 amount) = - nativeBridge.noncesToOutgoingTransfers(1); + nativeBridge.noncesToOutboundTransfers(1); assertEq(originator, _originator); assertEq(recipient_, _recipient); @@ -70,12 +71,54 @@ contract NativeBridgeTest is Test { vm.stopPrank(); } + function testOutboundRateLimitFuzz(address sender, uint256 _amount) public { + excludeSender(deployer); + _amount = bound(_amount, 3, 1000000000 * 10 ** 8); + moveToken.transfer(sender, _amount); + + vm.startPrank(sender); + moveToken.approve(address(nativeBridge), _amount); + nativeBridge.initiateBridgeTransfer(keccak256(abi.encodePacked(sender)), _amount / 2); + + vm.warp(1 days - 1); + if (_amount >= moveToken.balanceOf(insuranceFund) / 4) { + vm.expectRevert(INativeBridge.OutboundRateLimitExceeded.selector); + nativeBridge.initiateBridgeTransfer(keccak256(abi.encodePacked(sender)), _amount / 2); + vm.warp(1 days + 1); + nativeBridge.initiateBridgeTransfer(keccak256(abi.encodePacked(sender)), _amount / 2); + } else { + nativeBridge.initiateBridgeTransfer(keccak256(abi.encodePacked(sender)), _amount / 2); + } + } + + function testInboundRateLimitFuzz(address receiver, uint256 _amount) public { + _amount = bound(_amount, 3, 1000000000 * 10 ** 8); + moveToken.transfer(address(nativeBridge), _amount); + + bytes32 tx1BridgeTransferId = keccak256(abi.encodePacked(keccak256(abi.encodePacked(receiver)), receiver, _amount / 2, uint256(1))); + bytes32 tx2BridgeTransferId = keccak256(abi.encodePacked(keccak256(abi.encodePacked(receiver)), receiver, _amount / 2, uint256(2))); + + + vm.startPrank(relayer); + nativeBridge.completeBridgeTransfer(tx1BridgeTransferId, keccak256(abi.encodePacked(receiver)), receiver, _amount / 2, 1); + + vm.warp(1 days - 1); + if (_amount >= moveToken.balanceOf(insuranceFund) / 4) { + vm.expectRevert(INativeBridge.InboundRateLimitExceeded.selector); + nativeBridge.completeBridgeTransfer(tx2BridgeTransferId, keccak256(abi.encodePacked(receiver)), receiver, _amount / 2, 2); + vm.warp(1 days + 1); + nativeBridge.completeBridgeTransfer(tx2BridgeTransferId, keccak256(abi.encodePacked(receiver)), receiver, _amount / 2, 2); + } else { + nativeBridge.completeBridgeTransfer(tx2BridgeTransferId, keccak256(abi.encodePacked(receiver)), receiver, _amount / 2, 2); + } + } + function testCompleteBridgeFuzz(bytes32 _originator, address _recipient, uint256 _amount, uint256 _nonce) public { excludeSender(deployer); vm.assume(_recipient != address(0)); vm.assume(relayer != address(0)); - _amount = bound(_amount, 1, 10000000000 * 10 ** 8); + _amount = bound(_amount, 1, 100000000 * 10 ** 8); // nonce cannot be uint256 max because we are testing a +1 addition case to the nonce _nonce = bound(_nonce, 1, type(uint256).max - 1); @@ -112,7 +155,7 @@ contract NativeBridgeTest is Test { nativeBridge.completeBridgeTransfer(bridgeTransferId, _originator, _recipient, _amount, _nonce); uint256 nonce = - nativeBridge.idsToIncomingNonces(bridgeTransferId); + nativeBridge.idsToInboundNonces(bridgeTransferId); assertEq(nonce, _nonce); vm.stopPrank(); @@ -144,7 +187,7 @@ contract NativeBridgeTest is Test { for (uint256 i; i < length; i++) { uint256 nonce = - nativeBridge.idsToIncomingNonces(bridgeTransferIds[i]); + nativeBridge.idsToInboundNonces(bridgeTransferIds[i]); assertEq(nonce, nonces[i]); }