diff --git a/protocol-units/bridge/contracts/src/AtomicBridgeCounterpartyMOVE.sol b/protocol-units/bridge/contracts/src/AtomicBridgeCounterpartyMOVE.sol index fa2ca4f73..c6bd066a1 100644 --- a/protocol-units/bridge/contracts/src/AtomicBridgeCounterpartyMOVE.sol +++ b/protocol-units/bridge/contracts/src/AtomicBridgeCounterpartyMOVE.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.22; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IAtomicBridgeCounterpartyMOVE} from "./IAtomicBridgeCounterpartyMOVE.sol"; import {AtomicBridgeInitiatorMOVE} from "./AtomicBridgeInitiatorMOVE.sol"; +import {RateLimiter} from "./RateLimiter.sol"; contract AtomicBridgeCounterpartyMOVE is IAtomicBridgeCounterpartyMOVE, OwnableUpgradeable { enum MessageState { @@ -22,17 +23,26 @@ contract AtomicBridgeCounterpartyMOVE is IAtomicBridgeCounterpartyMOVE, OwnableU } AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorMOVE; + RateLimiter public rateLimiter; mapping(bytes32 => BridgeTransferDetails) public bridgeTransfers; // Configurable time lock duration uint256 public counterpartyTimeLockDuration; - function initialize(address _atomicBridgeInitiator, address owner, uint256 _timeLockDuration) public initializer { + // Initialize with initiator, RateLimiter, owner, and time lock duration + function initialize( + address _atomicBridgeInitiator, + address _rateLimiter, + address owner, + uint256 _timeLockDuration + ) public initializer { if (_atomicBridgeInitiator == address(0)) revert ZeroAddress(); + if (_rateLimiter == address(0)) revert ZeroAddress(); + atomicBridgeInitiatorMOVE = AtomicBridgeInitiatorMOVE(_atomicBridgeInitiator); + rateLimiter = RateLimiter(_rateLimiter); __Ownable_init(owner); - // Set the configurable time lock duration counterpartyTimeLockDuration = _timeLockDuration; } @@ -41,6 +51,11 @@ contract AtomicBridgeCounterpartyMOVE is IAtomicBridgeCounterpartyMOVE, OwnableU atomicBridgeInitiatorMOVE = AtomicBridgeInitiatorMOVE(_atomicBridgeInitiator); } + function setRateLimiter(address _rateLimiter) external onlyOwner { + if (_rateLimiter == address(0)) revert ZeroAddress(); + rateLimiter = RateLimiter(_rateLimiter); + } + function setTimeLockDuration(uint256 _timeLockDuration) external onlyOwner { counterpartyTimeLockDuration = _timeLockDuration; } @@ -55,7 +70,12 @@ contract AtomicBridgeCounterpartyMOVE is IAtomicBridgeCounterpartyMOVE, OwnableU if (amount == 0) revert ZeroAmount(); if (atomicBridgeInitiatorMOVE.poolBalance() < amount) revert InsufficientMOVEBalance(); - // The time lock is now based on the configurable duration + bool isWithinRateLimit = rateLimiter.initiateTransfer(amount, RateLimiter.TransferDirection.L2_TO_L1); + if (!isWithinRateLimit) { + revert("RATE_LIMIT_EXCEEDED"); + } + + // The time lock is based on the configurable duration uint256 timeLock = block.timestamp + counterpartyTimeLockDuration; bridgeTransfers[bridgeTransferId] = BridgeTransferDetails({ diff --git a/protocol-units/bridge/contracts/src/AtomicBridgeInitiatorMOVE.sol b/protocol-units/bridge/contracts/src/AtomicBridgeInitiatorMOVE.sol index e9e1226f7..db2752766 100644 --- a/protocol-units/bridge/contracts/src/AtomicBridgeInitiatorMOVE.sol +++ b/protocol-units/bridge/contracts/src/AtomicBridgeInitiatorMOVE.sol @@ -5,6 +5,7 @@ import {IAtomicBridgeInitiatorMOVE} from "./IAtomicBridgeInitiatorMOVE.sol"; import {MockMOVEToken} from "./MockMOVEToken.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {RateLimiter} from "./RateLimiter.sol"; contract AtomicBridgeInitiatorMOVE is IAtomicBridgeInitiatorMOVE, OwnableUpgradeable { enum MessageState { @@ -35,23 +36,25 @@ contract AtomicBridgeInitiatorMOVE is IAtomicBridgeInitiatorMOVE, OwnableUpgrade // Configurable time lock duration uint256 public initiatorTimeLockDuration; - // Initialize the contract with MOVE token address, owner, custom time lock duration, and initial pool balance + // RateLimiter contract instance + RateLimiter public rateLimiter; + + // Initialize the contract with MOVE token address, owner, custom time lock duration, initial pool balance, and RateLimiter contract address function initialize( address _moveToken, address owner, uint256 _timeLockDuration, - uint256 _initialPoolBalance + uint256 _initialPoolBalance, + address _rateLimiter ) public initializer { - if (_moveToken == address(0)) { - revert ZeroAddress(); - } + if (_moveToken == address(0)) revert ZeroAddress(); + if (_rateLimiter == address(0)) revert ZeroAddress(); + moveToken = ERC20Upgradeable(_moveToken); + rateLimiter = RateLimiter(_rateLimiter); __Ownable_init(owner); - // Set the custom time lock duration initiatorTimeLockDuration = _timeLockDuration; - - // Set the initial pool balance poolBalance = _initialPoolBalance; } @@ -67,8 +70,11 @@ contract AtomicBridgeInitiatorMOVE is IAtomicBridgeInitiatorMOVE, OwnableUpgrade address originator = msg.sender; // Ensure there is a valid amount - if (moveAmount == 0) { - revert ZeroAmount(); + if (moveAmount == 0) revert ZeroAmount(); + + // Check the rate limit before proceeding with the transfer + if (!rateLimiter.initiateTransfer(moveAmount, RateLimiter.TransferDirection.L1_TO_L2)) { + revert("RATE_LIMIT_EXCEEDED"); } // Transfer the MOVE tokens from the user to the contract diff --git a/protocol-units/bridge/contracts/src/RateLimiter.sol b/protocol-units/bridge/contracts/src/RateLimiter.sol new file mode 100644 index 000000000..dbaa05a67 --- /dev/null +++ b/protocol-units/bridge/contracts/src/RateLimiter.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract RateLimiter is OwnableUpgradeable { + enum TransferDirection { + L1_TO_L2, + L2_TO_L1 + } + + // Maximum amount that can be transferred in each direction within the risk period + uint256 public rateLimitL1L2; + uint256 public rateLimitL2L1; + + // Track the accumulated budget per transfer direction + uint256 public budgetL1L2; + uint256 public budgetL2L1; + + // Risk period for rate limiting (in seconds) + uint256 public riskPeriod; + + // Security fund balance + uint256 public securityFund; + + event RateLimitExceeded(TransferDirection direction); + event RateLimitUpdated(uint256 newRateLimitL1L2, uint256 newRateLimitL2L1); + event SecurityFundUpdated(uint256 newSecurityFund); + + // Initialize the contract with initial rate limits and risk period + function initialize(address owner, uint256 _riskPeriod, uint256 _securityFund) public initializer { + riskPeriod = _riskPeriod; + securityFund = _securityFund; + __Ownable_init(owner); + _updateRateLimits(); + } + + // Modifier to check if a transfer exceeds the rate limit + modifier withinRateLimit(uint256 amount, TransferDirection direction) { + uint256 currentBudget = (direction == TransferDirection.L1_TO_L2) ? budgetL1L2 : budgetL2L1; + uint256 rateLimit = (direction == TransferDirection.L1_TO_L2) ? rateLimitL1L2 : rateLimitL2L1; + + require(currentBudget + amount <= rateLimit, "RATE_LIMIT_EXCEEDED"); + _; + } + + function initiateTransfer(uint256 amount, TransferDirection direction) external returns (bool) { + uint256 currentBudget = (direction == TransferDirection.L1_TO_L2) ? budgetL1L2 : budgetL2L1; + uint256 rateLimit = (direction == TransferDirection.L1_TO_L2) ? rateLimitL1L2 : rateLimitL2L1; + + if (currentBudget + amount > rateLimit) { + emit RateLimitExceeded(direction); + return false; + } + + // Update the budget for the specified direction + if (direction == TransferDirection.L1_TO_L2) { + budgetL1L2 += amount; + } else { + budgetL2L1 += amount; + } + + return true; + } + + // Update the security fund and recalculate rate limits + function updateSecurityFund(uint256 newSecurityFund) external onlyOwner { + securityFund = newSecurityFund; + _updateRateLimits(); + emit SecurityFundUpdated(newSecurityFund); + } + + // Private function to update the rate limits based on the security fund and risk period + function _updateRateLimits() private { + rateLimitL1L2 = (securityFund * 5) / (riskPeriod * 10); // 0.5 * securityFund / riskPeriod + rateLimitL2L1 = (securityFund * 5) / (riskPeriod * 10); // Same calculation as for L1 to L2 + + emit RateLimitUpdated(rateLimitL1L2, rateLimitL2L1); + } + + // Reset the budget for each direction; this could be called periodically or by governance if all transfers are confirmed + function resetBudget() external onlyOwner { + budgetL1L2 = 0; + budgetL2L1 = 0; + } +} diff --git a/protocol-units/bridge/contracts/test/AtomicBridgeCounterpartyMOVE.t.sol b/protocol-units/bridge/contracts/test/AtomicBridgeCounterpartyMOVE.t.sol index 2a7aae30e..08c9e631a 100644 --- a/protocol-units/bridge/contracts/test/AtomicBridgeCounterpartyMOVE.t.sol +++ b/protocol-units/bridge/contracts/test/AtomicBridgeCounterpartyMOVE.t.sol @@ -8,12 +8,14 @@ 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"; +import {RateLimiter} from "../src/RateLimiter.sol"; contract AtomicBridgeCounterpartyMOVETest is Test { AtomicBridgeCounterpartyMOVE public atomicBridgeCounterpartyMOVEImplementation; AtomicBridgeCounterpartyMOVE public atomicBridgeCounterpartyMOVE; AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorMOVEImplementation; AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorMOVE; + RateLimiter public rateLimiter; MockMOVEToken public moveToken; ProxyAdmin public proxyAdmin; TransparentUpgradeableProxy public proxy; @@ -23,7 +25,7 @@ contract AtomicBridgeCounterpartyMOVETest is Test { 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 amount = 100 * 10 ** 8; uint256 public timeLock = 100; bytes32 public initiator = keccak256(abi.encodePacked(deployer)); bytes32 public bridgeTransferId = @@ -51,37 +53,45 @@ contract AtomicBridgeCounterpartyMOVETest is Test { originator = vm.addr(uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)))); - // Deploy the AtomicBridgeInitiator contract with a 48-hour time lock + // Deploy and initialize the RateLimiter contract + rateLimiter = new RateLimiter(); + uint256 riskPeriod = 24 * 60 * 60; // 24 hours in seconds + uint256 securityFund = 5 ether; + rateLimiter.initialize(address(this), riskPeriod, securityFund); + + // Deploy the AtomicBridgeInitiatorMOVE contract with a 48-hour time lock and RateLimiter instance atomicBridgeInitiatorMOVEImplementation = new AtomicBridgeInitiatorMOVE(); proxyAdmin = new ProxyAdmin(deployer); proxy = new TransparentUpgradeableProxy( address(atomicBridgeInitiatorMOVEImplementation), address(proxyAdmin), abi.encodeWithSignature( - "initialize(address,address,uint256,uint256)", + "initialize(address,address,uint256,uint256,address)", address(moveToken), - deployer, + deployer, initiatorTimeLockDuration, - 0 ether // Initial pool balance + 0 ether, // Initial pool balance + address(rateLimiter) ) ); atomicBridgeInitiatorMOVE = AtomicBridgeInitiatorMOVE(address(proxy)); - // Deploy the AtomicBridgeCounterparty contract with a 24-hour time lock + // Deploy the AtomicBridgeCounterpartyMOVE contract with a 24-hour time lock and RateLimiter instance atomicBridgeCounterpartyMOVEImplementation = new AtomicBridgeCounterpartyMOVE(); proxy = new TransparentUpgradeableProxy( address(atomicBridgeCounterpartyMOVEImplementation), address(proxyAdmin), abi.encodeWithSignature( - "initialize(address,address,uint256)", + "initialize(address,address,address,uint256)", address(atomicBridgeInitiatorMOVE), + address(rateLimiter), deployer, counterpartyTimeLockDuration ) ); atomicBridgeCounterpartyMOVE = AtomicBridgeCounterpartyMOVE(address(proxy)); - // Set the counterparty contract in the AtomicBridgeInitiator contract + // Set the counterparty contract in the AtomicBridgeInitiatorMOVE contract vm.startPrank(deployer); atomicBridgeInitiatorMOVE.setCounterpartyAddress( address(atomicBridgeCounterpartyMOVE) @@ -89,6 +99,7 @@ contract AtomicBridgeCounterpartyMOVETest is Test { vm.stopPrank(); } + function testLockBridgeTransfer() public { uint256 moveAmount = 100 * 10**8; moveToken.transfer(originator, moveAmount); @@ -180,89 +191,152 @@ contract AtomicBridgeCounterpartyMOVETest is Test { uint256 completedAmount, bytes32 completedHashLock, uint256 completedTimeLock, - AtomicBridgeCounterpartyMOVE.MessageState completedState + 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(completedInitiator, initiator); - assertEq(completedRecipient, recipient); - assertEq(completedAmount, amount); - assertEq(completedHashLock, testHashLock); - assertGt(completedTimeLock, block.timestamp); + 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(completedState), - uint8(AtomicBridgeCounterpartyMOVE.MessageState.COMPLETED) + uint8(abortedState), + uint8(AtomicBridgeCounterpartyMOVE.MessageState.REFUNDED) ); 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(); -} + function testRateLimitExceeded() public { + uint256 moveAmount = 100 * 10**8; // 100 MOVEToken + moveToken.transfer(originator, moveAmount); + + vm.startPrank(originator); + moveToken.approve(address(atomicBridgeInitiatorMOVE), moveAmount); + + // Initiate the first bridge transfer through the initiator + atomicBridgeInitiatorMOVE.initiateBridgeTransfer( + moveAmount / 2, + initiator, + hashLock + ); + vm.stopPrank(); + + // Start locking the transfer on the counterparty side + vm.startPrank(deployer); + // Lock the first transfer (within rate limit) + atomicBridgeCounterpartyMOVE.lockBridgeTransfer( + initiator, + bridgeTransferId, + hashLock, + recipient, + moveAmount / 2 + ); + + // Verify the first transfer was locked successfully + ( + bytes32 lockedInitiator, + address lockedRecipient, + uint256 lockedAmount, + bytes32 lockedHashLock, + uint256 lockedTimeLock, + AtomicBridgeCounterpartyMOVE.MessageState lockedState + ) = atomicBridgeCounterpartyMOVE.bridgeTransfers(bridgeTransferId); + assertEq(lockedInitiator, initiator); + assertEq(lockedRecipient, recipient); + assertEq(lockedAmount, moveAmount / 2); + assertEq(lockedHashLock, hashLock); + assertGt(lockedTimeLock, block.timestamp); + assertEq( + uint8(lockedState), + uint8(AtomicBridgeCounterpartyMOVE.MessageState.PENDING) + ); + + // Attempt a second transfer to exceed the rate limit + bytes32 secondBridgeTransferId = keccak256( + abi.encodePacked(block.timestamp, initiator, recipient, moveAmount, hashLock, timeLock) + ); + + // Expect the second transfer to exceed the rate limit and revert + vm.expectRevert("RATE_LIMIT_EXCEEDED"); + atomicBridgeCounterpartyMOVE.lockBridgeTransfer( + initiator, + secondBridgeTransferId, + hashLock, + recipient, + moveAmount + ); + + vm.stopPrank(); + } } diff --git a/protocol-units/bridge/contracts/test/AtomicBridgeInitiatorMOVE.t.sol b/protocol-units/bridge/contracts/test/AtomicBridgeInitiatorMOVE.t.sol index 0f676f154..639df61bb 100644 --- a/protocol-units/bridge/contracts/test/AtomicBridgeInitiatorMOVE.t.sol +++ b/protocol-units/bridge/contracts/test/AtomicBridgeInitiatorMOVE.t.sol @@ -8,6 +8,7 @@ import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.s import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {MockMOVEToken} from "../src/MockMOVEToken.sol"; import {console} from "forge-std/console.sol"; +import {RateLimiter} from "../src/RateLimiter.sol"; contract AtomicBridgeInitiatorMOVETest is Test { AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorImplementation; @@ -15,6 +16,7 @@ contract AtomicBridgeInitiatorMOVETest is Test { ProxyAdmin public proxyAdmin; TransparentUpgradeableProxy public proxy; AtomicBridgeInitiatorMOVE public atomicBridgeInitiatorMOVE; + RateLimiter public rateLimiter; address public originator = address(1); bytes32 public recipient = keccak256(abi.encodePacked(address(2))); @@ -27,26 +29,39 @@ contract AtomicBridgeInitiatorMOVETest is Test { moveToken = new MockMOVEToken(); moveToken.initialize(address(this)); // Contract will hold initial MOVE tokens + // Set up the originator with an address derived from a hash originator = vm.addr(uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)))); - // Deploy the AtomicBridgeInitiatorMOVE contract + // Deploy and initialize the RateLimiter contract + rateLimiter = new RateLimiter(); + uint256 riskPeriod = 24 * 60 * 60; // 24 hours in seconds + uint256 securityFund = 5 ether; + rateLimiter.initialize(address(this), riskPeriod, securityFund); + + // Deploy the AtomicBridgeInitiatorMOVE contract with the RateLimiter instance atomicBridgeInitiatorImplementation = new AtomicBridgeInitiatorMOVE(); proxyAdmin = new ProxyAdmin(msg.sender); proxy = new TransparentUpgradeableProxy( address(atomicBridgeInitiatorImplementation), address(proxyAdmin), abi.encodeWithSignature( - "initialize(address,address,uint256,uint256)", + "initialize(address,address,uint256,uint256,address)", address(moveToken), address(this), timeLockDuration, - 0 ether + 0 ether, + address(rateLimiter) ) ); atomicBridgeInitiatorMOVE = AtomicBridgeInitiatorMOVE(address(proxy)); + + // Fund the originator for testing with initial MOVE balance + uint256 moveAmount = 100 * 10**8; // 100 MOVEToken + moveToken.transfer(originator, moveAmount); } + function testInitiateBridgeTransferWithMove() public { uint256 moveAmount = 100 * 10**8; @@ -169,5 +184,47 @@ contract AtomicBridgeInitiatorMOVETest is Test { uint256 finalBalance = moveToken.balanceOf(originator); assertEq(finalBalance, initialBalance, "MOVE balance mismatch"); } -} + function testRateLimitExceeded() public { + uint256 moveAmount = 100 * 10**8; // 100 MOVEToken + + vm.startPrank(originator); + moveToken.approve(address(atomicBridgeInitiatorMOVE), moveAmount); + + // First transfer: within the rate limit + uint256 initialBalance = moveToken.balanceOf(originator); + bytes32 bridgeTransferId1 = atomicBridgeInitiatorMOVE.initiateBridgeTransfer( + moveAmount / 2, + recipient, + hashLock + ); + + // Verify that the first transfer succeeded + ( + uint256 transferAmount1, + , + , + , + , + AtomicBridgeInitiatorMOVE.MessageState transferState1 + ) = atomicBridgeInitiatorMOVE.bridgeTransfers(bridgeTransferId1); + console.log("initiated"); + assertEq(transferAmount1, moveAmount / 2); + assertEq(uint8(transferState1), uint8(AtomicBridgeInitiatorMOVE.MessageState.INITIALIZED)); + + // Second transfer: attempt to exceed the rate limit + vm.expectRevert("RATE_LIMIT_EXCEEDED"); + atomicBridgeInitiatorMOVE.initiateBridgeTransfer( + 5 ether, + recipient, + hashLock + ); + + + // Verify that the originator’s balance reflects only the first transfer + uint256 finalBalance = moveToken.balanceOf(originator); + assertEq(finalBalance, initialBalance - (moveAmount / 2)); + + vm.stopPrank(); + } +} diff --git a/protocol-units/bridge/move-modules/sources/placeholder.move b/protocol-units/bridge/move-modules/sources/placeholder.move new file mode 100644 index 000000000..b45972a31 --- /dev/null +++ b/protocol-units/bridge/move-modules/sources/placeholder.move @@ -0,0 +1,6 @@ +//sources/ with a .move file is required for the CLI to build the scripts +module atomic_bridge::some_module { + public fun some_function() { + return + } +}