From e85881b8e38ff924119091f0b3f57f5858041684 Mon Sep 17 00:00:00 2001 From: parodime Date: Thu, 12 Sep 2024 16:04:46 -0400 Subject: [PATCH 01/16] init. solidity ^. FbV2 relay/prove/claim overloads --- .vscode/settings.json | 2 +- packages/contracts-rfq/contracts/Admin.sol | 2 +- .../contracts-rfq/contracts/FastBridgeV2.sol | 286 ++++++++++++++++++ .../contracts/interfaces/IAdmin.sol | 2 +- .../contracts/interfaces/IFastBridge.sol | 2 +- .../contracts/interfaces/IMulticallTarget.sol | 2 +- .../contracts-rfq/contracts/libs/Errors.sol | 2 +- .../contracts/libs/UniversalToken.sol | 2 +- .../contracts/utils/MulticallTarget.sol | 2 +- .../script/ConfigureFastBridge.s.sol | 2 +- .../script/DeployFastBridge.CREATE2.s.sol | 2 +- 11 files changed, 296 insertions(+), 10 deletions(-) create mode 100644 packages/contracts-rfq/contracts/FastBridgeV2.sol diff --git a/.vscode/settings.json b/.vscode/settings.json index 116a5a5174..f4ccb368ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,7 @@ "files.trimTrailingWhitespace": true, "solidity.packageDefaultDependenciesContractsDirectory": "contracts", "solidity.packageDefaultDependenciesDirectory": "lib", - "solidity.compileUsingRemoteVersion": "v0.8.17+commit.8df45f5f", + "solidity.compileUsingRemoteVersion": "v0.8.24+commit.e11b9ed9", "solidity.formatter": "prettier", "solidity.defaultCompiler": "remote", "tailwindCSS.classAttributes": [ diff --git a/packages/contracts-rfq/contracts/Admin.sol b/packages/contracts-rfq/contracts/Admin.sol index 881b5bea6d..ffb352b28a 100644 --- a/packages/contracts-rfq/contracts/Admin.sol +++ b/packages/contracts-rfq/contracts/Admin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol new file mode 100644 index 0000000000..0418dc1df6 --- /dev/null +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "./libs/Errors.sol"; +import {UniversalTokenLib} from "./libs/UniversalToken.sol"; + +import {Admin} from "./Admin.sol"; +import {IFastBridge} from "./interfaces/IFastBridge.sol"; + +contract FastBridge is IFastBridge, Admin { + using SafeERC20 for IERC20; + using UniversalTokenLib for address; + + /// @notice Dispute period for relayed transactions + uint256 public constant DISPUTE_PERIOD = 30 minutes; + + /// @notice Delay for a transaction after which it could be permisionlessly refunded + uint256 public constant REFUND_DELAY = 7 days; + + /// @notice Minimum deadline period to relay a requested bridge transaction + uint256 public constant MIN_DEADLINE_PERIOD = 30 minutes; + + enum BridgeStatus { + NULL, // doesn't exist yet + REQUESTED, + RELAYER_PROVED, + RELAYER_CLAIMED, + REFUNDED + } + + /// @notice Status of the bridge tx on origin chain + mapping(bytes32 => BridgeStatus) public bridgeStatuses; + /// @notice Proof of relayed bridge tx on origin chain + mapping(bytes32 => BridgeProof) public bridgeProofs; + /// @notice Whether bridge has been relayed on destination chain + mapping(bytes32 => bool) public bridgeRelays; + + /// @dev to prevent replays + uint256 public nonce; + // @dev the block the contract was deployed at + uint256 public immutable deployBlock; + + constructor(address _owner) Admin(_owner) { + deployBlock = block.number; + } + + /// @notice Pulls a requested token from the user to the requested recipient. + /// @dev Be careful of re-entrancy issues when msg.value > 0 and recipient != address(this) + function _pullToken(address recipient, address token, uint256 amount) internal returns (uint256 amountPulled) { + if (token != UniversalTokenLib.ETH_ADDRESS) { + token.assertIsContract(); + // Record token balance before transfer + amountPulled = IERC20(token).balanceOf(recipient); + // Token needs to be pulled only if msg.value is zero + // This way user can specify WETH as the origin asset + IERC20(token).safeTransferFrom(msg.sender, recipient, amount); + // Use the difference between the recorded balance and the current balance as the amountPulled + amountPulled = IERC20(token).balanceOf(recipient) - amountPulled; + } else { + // Otherwise, we need to check that ETH amount matches msg.value + if (amount != msg.value) revert MsgValueIncorrect(); + // Transfer value to recipient if not this address + if (recipient != address(this)) token.universalTransfer(recipient, amount); + // We will forward msg.value in the external call later, if recipient is not this contract + amountPulled = msg.value; + } + } + + /// @inheritdoc IFastBridge + function getBridgeTransaction(bytes memory request) public pure returns (BridgeTransaction memory) { + return abi.decode(request, (BridgeTransaction)); + } + + /// @inheritdoc IFastBridge + function bridge(BridgeParams memory params) external payable { + // check bridge params + if (params.dstChainId == block.chainid) revert ChainIncorrect(); + if (params.originAmount == 0 || params.destAmount == 0) revert AmountIncorrect(); + if (params.originToken == address(0) || params.destToken == address(0)) revert ZeroAddress(); + if (params.deadline < block.timestamp + MIN_DEADLINE_PERIOD) revert DeadlineTooShort(); + + // transfer tokens to bridge contract + // @dev use returned originAmount in request in case of transfer fees + uint256 originAmount = _pullToken(address(this), params.originToken, params.originAmount); + + // track amount of origin token owed to protocol + uint256 originFeeAmount; + if (protocolFeeRate > 0) originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS; + originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers + + // set status to requested + bytes memory request = abi.encode( + BridgeTransaction({ + originChainId: uint32(block.chainid), + destChainId: params.dstChainId, + originSender: params.sender, + destRecipient: params.to, + originToken: params.originToken, + destToken: params.destToken, + originAmount: originAmount, + destAmount: params.destAmount, + originFeeAmount: originFeeAmount, + sendChainGas: params.sendChainGas, + deadline: params.deadline, + nonce: nonce++ // increment nonce on every bridge + }) + ); + bytes32 transactionId = keccak256(request); + bridgeStatuses[transactionId] = BridgeStatus.REQUESTED; + + emit BridgeRequested( + transactionId, + params.sender, + request, + params.dstChainId, + params.originToken, + params.destToken, + originAmount, + params.destAmount, + params.sendChainGas + ); + } + + /// @inheritdoc IFastBridge + function relay(bytes memory request) external { + relay(request, msg.sender); + } + + /// @inheritdoc IFastBridge + function relay(bytes memory request, address relayer) external payable { + bytes32 transactionId = keccak256(request); + BridgeTransaction memory transaction = getBridgeTransaction(request); + if (transaction.destChainId != uint32(block.chainid)) revert ChainIncorrect(); + + // check haven't exceeded deadline for relay to happen + if (block.timestamp > transaction.deadline) revert DeadlineExceeded(); + + // mark bridge transaction as relayed + if (bridgeRelays[transactionId]) revert TransactionRelayed(); + bridgeRelays[transactionId] = true; + + // transfer tokens to recipient on destination chain and gas rebate if requested + address to = transaction.destRecipient; + address token = transaction.destToken; + uint256 amount = transaction.destAmount; + + uint256 rebate = chainGasAmount; + if (!transaction.sendChainGas) { + // forward erc20 + rebate = 0; + _pullToken(to, token, amount); + } else if (token == UniversalTokenLib.ETH_ADDRESS) { + // lump in gas rebate into amount in native gas token + _pullToken(to, token, amount + rebate); + } else { + // forward erc20 then forward gas rebate in native gas token + _pullToken(to, token, amount); + _pullToken(to, UniversalTokenLib.ETH_ADDRESS, rebate); + } + + emit BridgeRelayed( + transactionId, + relayer, + to, + transaction.originChainId, + transaction.originToken, + transaction.destToken, + transaction.originAmount, + transaction.destAmount, + rebate + ); + } + + /// @inheritdoc IFastBridge + function prove(bytes memory request, bytes32 destTxHash) external { + prove(request, destTxHash, msg.sender); + } + + /// @inheritdoc IFastBridge + function prove(bytes memory request, bytes32 destTxHash, address relayer) external onlyRole(RELAYER_ROLE) { + bytes32 transactionId = keccak256(request); + // update bridge tx status given proof provided + if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect(); + bridgeStatuses[transactionId] = BridgeStatus.RELAYER_PROVED; + bridgeProofs[transactionId] = BridgeProof({timestamp: uint96(block.timestamp), relayer: relayer}); // overflow ok + + emit BridgeProofProvided(transactionId, relayer, destTxHash); + } + + /// @notice Calculates time since proof submitted + /// @dev proof.timestamp stores casted uint96(block.timestamp) block timestamps for gas optimization + /// _timeSince(proof) can accomodate rollover case when block.timestamp > type(uint96).max but + /// proof.timestamp < type(uint96).max via unchecked statement + /// @param proof The bridge proof + /// @return delta Time delta since proof submitted + function _timeSince(BridgeProof memory proof) internal view returns (uint256 delta) { + unchecked { + delta = uint96(block.timestamp) - proof.timestamp; + } + } + + /// @inheritdoc IFastBridge + function canClaim(bytes32 transactionId, address relayer) external view returns (bool) { + if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); + BridgeProof memory proof = bridgeProofs[transactionId]; + if (proof.relayer != relayer) revert SenderIncorrect(); + return _timeSince(proof) > DISPUTE_PERIOD; + } + + /// @inheritdoc IFastBridge + function claim(bytes memory request) external { + claim(request, 0); + } + + /// @inheritdoc IFastBridge + function claim(bytes memory request, address to) external { + bytes32 transactionId = keccak256(request); + BridgeTransaction memory transaction = getBridgeTransaction(request); + + // update bridge tx status if able to claim origin collateral + if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); + + BridgeProof memory proof = bridgeProofs[transactionId]; + + // if "to" is zero addr, permissionlessly send funds to proven relayer + if (to == address(0)) { + to = proof.relayer; + } else if (proof.relayer != msg.sender) { + revert SenderIncorrect(); + } + + if (_timeSince(proof) <= DISPUTE_PERIOD) revert DisputePeriodNotPassed(); + + bridgeStatuses[transactionId] = BridgeStatus.RELAYER_CLAIMED; + + // update protocol fees if origin fee amount exists + if (transaction.originFeeAmount > 0) protocolFees[transaction.originToken] += transaction.originFeeAmount; + + // transfer origin collateral less fee to specified address + address token = transaction.originToken; + uint256 amount = transaction.originAmount; + token.universalTransfer(to, amount); + + emit BridgeDepositClaimed(transactionId, proof.relayer, to, token, amount); + } + + /// @inheritdoc IFastBridge + function dispute(bytes32 transactionId) external onlyRole(GUARD_ROLE) { + if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); + if (_timeSince(bridgeProofs[transactionId]) > DISPUTE_PERIOD) revert DisputePeriodPassed(); + + // @dev relayer gets slashed effectively if dest relay has gone thru + bridgeStatuses[transactionId] = BridgeStatus.REQUESTED; + delete bridgeProofs[transactionId]; + + emit BridgeProofDisputed(transactionId, msg.sender); + } + + /// @inheritdoc IFastBridge + function refund(bytes memory request) external { + bytes32 transactionId = keccak256(request); + BridgeTransaction memory transaction = getBridgeTransaction(request); + + if (hasRole(REFUNDER_ROLE, msg.sender)) { + // Refunder can refund if deadline has passed + if (block.timestamp <= transaction.deadline) revert DeadlineNotExceeded(); + } else { + // Permissionless refund is allowed after REFUND_DELAY + if (block.timestamp <= transaction.deadline + REFUND_DELAY) revert DeadlineNotExceeded(); + } + + // set status to refunded if still in requested state + if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect(); + bridgeStatuses[transactionId] = BridgeStatus.REFUNDED; + + // transfer origin collateral back to original sender + address to = transaction.originSender; + address token = transaction.originToken; + uint256 amount = transaction.originAmount + transaction.originFeeAmount; + token.universalTransfer(to, amount); + + emit BridgeDepositRefunded(transactionId, to, token, amount); + } +} diff --git a/packages/contracts-rfq/contracts/interfaces/IAdmin.sol b/packages/contracts-rfq/contracts/interfaces/IAdmin.sol index 10d866d499..c6f398191d 100644 --- a/packages/contracts-rfq/contracts/interfaces/IAdmin.sol +++ b/packages/contracts-rfq/contracts/interfaces/IAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; interface IAdmin { // ============ Events ============ diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridge.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridge.sol index 0176d557bb..b691dfb5b4 100644 --- a/packages/contracts-rfq/contracts/interfaces/IFastBridge.sol +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridge.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; interface IFastBridge { struct BridgeTransaction { diff --git a/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol index 1f48e59609..d112057ad6 100644 --- a/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol +++ b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; /// @notice Interface for a contract that can be called multiple times by the same caller. Inspired by MulticallV3: /// https://github.com/mds1/multicall/blob/master/src/Multicall3.sol diff --git a/packages/contracts-rfq/contracts/libs/Errors.sol b/packages/contracts-rfq/contracts/libs/Errors.sol index f2efb94304..b165dd46d4 100644 --- a/packages/contracts-rfq/contracts/libs/Errors.sol +++ b/packages/contracts-rfq/contracts/libs/Errors.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.20; +pragma solidity ^0.8.20; error DeadlineExceeded(); error DeadlineNotExceeded(); diff --git a/packages/contracts-rfq/contracts/libs/UniversalToken.sol b/packages/contracts-rfq/contracts/libs/UniversalToken.sol index 98e6de0325..c57bf141e0 100644 --- a/packages/contracts-rfq/contracts/libs/UniversalToken.sol +++ b/packages/contracts-rfq/contracts/libs/UniversalToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import {TokenNotContract} from "./Errors.sol"; diff --git a/packages/contracts-rfq/contracts/utils/MulticallTarget.sol b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol index bed9266c33..d259b6c3db 100644 --- a/packages/contracts-rfq/contracts/utils/MulticallTarget.sol +++ b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {IMulticallTarget} from "../interfaces/IMulticallTarget.sol"; diff --git a/packages/contracts-rfq/script/ConfigureFastBridge.s.sol b/packages/contracts-rfq/script/ConfigureFastBridge.s.sol index 4349fae615..4be61d98b1 100644 --- a/packages/contracts-rfq/script/ConfigureFastBridge.s.sol +++ b/packages/contracts-rfq/script/ConfigureFastBridge.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import {FastBridge} from "../contracts/FastBridge.sol"; diff --git a/packages/contracts-rfq/script/DeployFastBridge.CREATE2.s.sol b/packages/contracts-rfq/script/DeployFastBridge.CREATE2.s.sol index 3369914b69..5d42a636b3 100644 --- a/packages/contracts-rfq/script/DeployFastBridge.CREATE2.s.sol +++ b/packages/contracts-rfq/script/DeployFastBridge.CREATE2.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import {FastBridge} from "../contracts/FastBridge.sol"; From b4b77832f6251e05a210b92eef69a72f81020f18 Mon Sep 17 00:00:00 2001 From: parodime Date: Fri, 13 Sep 2024 11:15:19 -0400 Subject: [PATCH 02/16] +IFastBridgeV2, explicit address0 cast, func scope & inheritdoc fixes --- .../contracts-rfq/contracts/FastBridgeV2.sol | 19 +++++++-------- .../contracts/interfaces/IFastBridgeV2.sol | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index 0418dc1df6..1b4eaf3439 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -8,8 +8,9 @@ import {UniversalTokenLib} from "./libs/UniversalToken.sol"; import {Admin} from "./Admin.sol"; import {IFastBridge} from "./interfaces/IFastBridge.sol"; +import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol"; -contract FastBridge is IFastBridge, Admin { +contract FastBridge is IFastBridge, IFastBridgeV2, Admin { using SafeERC20 for IERC20; using UniversalTokenLib for address; @@ -124,12 +125,12 @@ contract FastBridge is IFastBridge, Admin { } /// @inheritdoc IFastBridge - function relay(bytes memory request) external { + function relay(bytes memory request) external payable { relay(request, msg.sender); } - /// @inheritdoc IFastBridge - function relay(bytes memory request, address relayer) external payable { + /// @inheritdoc IFastBridgeV2 + function relay(bytes memory request, address relayer) public payable { bytes32 transactionId = keccak256(request); BridgeTransaction memory transaction = getBridgeTransaction(request); if (transaction.destChainId != uint32(block.chainid)) revert ChainIncorrect(); @@ -178,8 +179,8 @@ contract FastBridge is IFastBridge, Admin { prove(request, destTxHash, msg.sender); } - /// @inheritdoc IFastBridge - function prove(bytes memory request, bytes32 destTxHash, address relayer) external onlyRole(RELAYER_ROLE) { + /// @inheritdoc IFastBridgeV2 + function prove(bytes memory request, bytes32 destTxHash, address relayer) public onlyRole(RELAYER_ROLE) { bytes32 transactionId = keccak256(request); // update bridge tx status given proof provided if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect(); @@ -209,13 +210,13 @@ contract FastBridge is IFastBridge, Admin { return _timeSince(proof) > DISPUTE_PERIOD; } - /// @inheritdoc IFastBridge + /// @inheritdoc IFastBridgeV2 function claim(bytes memory request) external { - claim(request, 0); + claim(request, address(0)); } /// @inheritdoc IFastBridge - function claim(bytes memory request, address to) external { + function claim(bytes memory request, address to) public { bytes32 transactionId = keccak256(request); BridgeTransaction memory transaction = getBridgeTransaction(request); diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol new file mode 100644 index 0000000000..979551919a --- /dev/null +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IFastBridge} from "./IFastBridge.sol"; + +interface IFastBridgeV2 is IFastBridge { + + /// @notice Relays destination side of bridge transaction by off-chain relayer + /// @param request The encoded bridge transaction to relay on destination chain + /// @param relayer The address of the relaying entity which should have control of the origin funds when claimed + function relay(bytes memory request, address relayer) external payable; + + /// @notice Provides proof on origin side that relayer provided funds on destination side of bridge transaction + /// @param request The encoded bridge transaction to prove on origin chain + /// @param destTxHash The destination tx hash proving bridge transaction was relayed + /// @param relayer The address of the relaying entity which should have control of the origin funds when claimed + function prove(bytes memory request, bytes32 destTxHash, address relayer) external; + + /// @notice Completes bridge transaction on origin chain by claiming originally deposited capital. Can only send funds to the relayer address on the proof. + /// @param request The encoded bridge transaction to claim on origin chain + function claim(bytes memory request) external; + +} From 1a521d671e1d93890a2ce9ea5029b91a5c565917 Mon Sep 17 00:00:00 2001 From: parodime Date: Fri, 13 Sep 2024 11:39:07 -0400 Subject: [PATCH 03/16] pragma lock, contract relabel --- packages/contracts-rfq/contracts/FastBridgeV2.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index 1b4eaf3439..a298f8e659 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity 0.8.24; import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -10,7 +10,7 @@ import {Admin} from "./Admin.sol"; import {IFastBridge} from "./interfaces/IFastBridge.sol"; import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol"; -contract FastBridge is IFastBridge, IFastBridgeV2, Admin { +contract FastBridgeV2 is IFastBridge, IFastBridgeV2, Admin { using SafeERC20 for IERC20; using UniversalTokenLib for address; From 0fd674bdf37048fa36c67acce60bfdec57fa4e94 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:06:14 +0100 Subject: [PATCH 04/16] feat: start scoping V2 tests --- packages/contracts-rfq/test/FastBridge.t.sol | 10 +++++++--- packages/contracts-rfq/test/FastBridgeV2.t.sol | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 packages/contracts-rfq/test/FastBridgeV2.t.sol diff --git a/packages/contracts-rfq/test/FastBridge.t.sol b/packages/contracts-rfq/test/FastBridge.t.sol index 264ebe3b79..1c04adfb59 100644 --- a/packages/contracts-rfq/test/FastBridge.t.sol +++ b/packages/contracts-rfq/test/FastBridge.t.sol @@ -28,15 +28,19 @@ contract FastBridgeTest is Test { MockERC20 arbUSDC; MockERC20 ethUSDC; - function setUp() public { + function setUp() public virtual { vm.chainId(42_161); - fastBridge = new FastBridge(owner); + fastBridge = FastBridge(deployFastBridge()); arbUSDC = new MockERC20("arbUSDC", 6); ethUSDC = new MockERC20("ethUSDC", 6); _mintTokensToActors(); } - function _mintTokensToActors() internal { + function deployFastBridge() internal virtual returns (address) { + return address(new FastBridge(owner)); + } + + function _mintTokensToActors() internal virtual { arbUSDC.mint(relayer, 100 * 10 ** 6); arbUSDC.mint(guard, 100 * 10 ** 6); arbUSDC.mint(user, 100 * 10 ** 6); diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol new file mode 100644 index 0000000000..e0bdc33f0a --- /dev/null +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FastBridgeTest} from "./FastBridge.t.sol"; + +contract FastBridgeV2Test is FastBridgeTest { + function deployFastBridge() internal virtual override returns (address) { + // TODO: change to FastBridgeV2 + return super.deployFastBridge(); + } +} \ No newline at end of file From 9e8a6283a3598f239d8bc2f956184de1a39bb67e Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:07:02 +0100 Subject: [PATCH 05/16] test: override relayer role scenarios, no longer enforced by V2 --- packages/contracts-rfq/test/FastBridge.t.sol | 6 ++-- .../contracts-rfq/test/FastBridgeV2.t.sol | 32 +++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/contracts-rfq/test/FastBridge.t.sol b/packages/contracts-rfq/test/FastBridge.t.sol index 1c04adfb59..2d90779c96 100644 --- a/packages/contracts-rfq/test/FastBridge.t.sol +++ b/packages/contracts-rfq/test/FastBridge.t.sol @@ -1301,7 +1301,7 @@ contract FastBridgeTest is Test { vm.stopPrank(); } - function test_failedRelayNotRelayer() public { + function test_failedRelayNotRelayer() public virtual { // Set up the roles for the test setUpRoles(); @@ -1646,7 +1646,7 @@ contract FastBridgeTest is Test { vm.stopPrank(); } - function test_failedClaimNotOldRelayer() public { + function test_failedClaimNotOldRelayer() public virtual { setUpRoles(); test_successfulBridge(); @@ -1683,7 +1683,7 @@ contract FastBridgeTest is Test { vm.stopPrank(); } - function test_failedClaimNotRelayer() public { + function test_failedClaimNotRelayer() public virtual { setUpRoles(); test_successfulRelayProof(); diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol index e0bdc33f0a..bd4789d0c0 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -1,11 +1,39 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {FastBridgeTest} from "./FastBridge.t.sol"; +import {FastBridgeTest, SenderIncorrect} from "./FastBridge.t.sol"; contract FastBridgeV2Test is FastBridgeTest { + address public anotherRelayer = makeAddr("Another Relayer"); + function deployFastBridge() internal virtual override returns (address) { // TODO: change to FastBridgeV2 return super.deployFastBridge(); } -} \ No newline at end of file + + /// @notice Relay function is no longer permissioned, so we skip this test + function test_failedRelayNotRelayer() public virtual override { + vm.skip(true); + } + + /// @notice Claim function is no longer permissioned by the role (but still by proven address), + /// so we skip this test + function test_failedClaimNotRelayer() public virtual override { + vm.skip(true); + } + + /// @notice Claim function is no longer permissioned by the role (but still by proven address), + /// so we modify the parent test by removing the role assignment. + function test_failedClaimNotOldRelayer() public virtual override { + setUpRoles(); + test_successfulBridge(); + (bytes memory request,) = _getBridgeRequestAndId(block.chainid, 0, 0); + vm.warp(block.timestamp + 31 minutes); + vm.prank(relayer); + fastBridge.prove(request, bytes32("0x04")); + + vm.expectRevert(abi.encodeWithSelector(SenderIncorrect.selector)); + vm.prank(anotherRelayer); + fastBridge.claim(request, relayer); + } +} From 1d28940bdad528ca5136d369ea70bd45d927524d Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:52:24 +0100 Subject: [PATCH 06/16] test: finish the parity test --- .../{FastBridgeV2.t.sol => FastBridgeV2.Parity.t.sol} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename packages/contracts-rfq/test/{FastBridgeV2.t.sol => FastBridgeV2.Parity.t.sol} (82%) diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Parity.t.sol similarity index 82% rename from packages/contracts-rfq/test/FastBridgeV2.t.sol rename to packages/contracts-rfq/test/FastBridgeV2.Parity.t.sol index bd4789d0c0..d92e4f8ac5 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Parity.t.sol @@ -1,14 +1,15 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity 0.8.20; import {FastBridgeTest, SenderIncorrect} from "./FastBridge.t.sol"; -contract FastBridgeV2Test is FastBridgeTest { +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2ParityTest is FastBridgeTest { address public anotherRelayer = makeAddr("Another Relayer"); function deployFastBridge() internal virtual override returns (address) { - // TODO: change to FastBridgeV2 - return super.deployFastBridge(); + // Use the cheatcode to deploy 0.8.24 contract within a 0.8.20 test + return deployCode({what: "FastBridgeV2", args: abi.encode(owner)}); } /// @notice Relay function is no longer permissioned, so we skip this test From b3a0eb9942908d0269c1b27ba1bcb2836f45c9d2 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:38:45 +0100 Subject: [PATCH 07/16] test: the management methods --- .../test/FastBridgeV2.Management.t.sol | 141 ++++++++++++++++++ .../contracts-rfq/test/FastBridgeV2.t.sol | 51 +++++++ 2 files changed, 192 insertions(+) create mode 100644 packages/contracts-rfq/test/FastBridgeV2.Management.t.sol create mode 100644 packages/contracts-rfq/test/FastBridgeV2.t.sol diff --git a/packages/contracts-rfq/test/FastBridgeV2.Management.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Management.t.sol new file mode 100644 index 0000000000..58502618b9 --- /dev/null +++ b/packages/contracts-rfq/test/FastBridgeV2.Management.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FastBridgeV2, FastBridgeV2Test} from "./FastBridgeV2.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2ManagementTest is FastBridgeV2Test { + uint256 public constant FEE_RATE_MAX = 1e4; // 1% + bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE"); + + address public admin = makeAddr("Admin"); + address public governorA = makeAddr("Governor A"); + + event FeeRateUpdated(uint256 oldFeeRate, uint256 newFeeRate); + event FeesSwept(address token, address recipient, uint256 amount); + event ChainGasAmountUpdated(uint256 oldChainGasAmount, uint256 newChainGasAmount); + + function deployFastBridge() public override returns (FastBridgeV2) { + return new FastBridgeV2(admin); + } + + function configureFastBridge() public override { + setGovernor(admin, governor); + } + + function mintTokens() public override { + srcToken.mint(address(fastBridge), 100); + deal(address(fastBridge), 200); + cheatCollectedProtocolFees(address(srcToken), 100); + cheatCollectedProtocolFees(ETH_ADDRESS, 200); + } + + function setGovernor(address caller, address newGovernor) public { + vm.prank(caller); + fastBridge.grantRole(GOVERNOR_ROLE, newGovernor); + } + + function setProtocolFeeRate(address caller, uint256 newFeeRate) public { + vm.prank(caller); + fastBridge.setProtocolFeeRate(newFeeRate); + } + + function sweepProtocolFees(address caller, address token, address recipient) public { + vm.prank(caller); + fastBridge.sweepProtocolFees(token, recipient); + } + + function setChainGasAmount(address caller, uint256 newChainGasAmount) public { + vm.prank(caller); + fastBridge.setChainGasAmount(newChainGasAmount); + } + + function test_grantGovernorRole() public { + assertFalse(fastBridge.hasRole(GOVERNOR_ROLE, governorA)); + setGovernor(admin, governorA); + assertTrue(fastBridge.hasRole(GOVERNOR_ROLE, governorA)); + } + + function test_grantGovernorRole_revertNotAdmin(address caller) public { + vm.assume(caller != admin); + expectUnauthorized(caller, fastBridge.DEFAULT_ADMIN_ROLE()); + setGovernor(caller, governorA); + } + + // ═══════════════════════════════════════════ SET PROTOCOL FEE RATE ═══════════════════════════════════════════════ + + function test_setProtocolFeeRate() public { + vm.expectEmit(address(fastBridge)); + emit FeeRateUpdated(0, 123); + setProtocolFeeRate(governor, 123); + assertEq(fastBridge.protocolFeeRate(), 123); + } + + function test_setProtocolFeeRate_twice() public { + test_setProtocolFeeRate(); + vm.expectEmit(address(fastBridge)); + emit FeeRateUpdated(123, FEE_RATE_MAX); + setProtocolFeeRate(governor, FEE_RATE_MAX); + assertEq(fastBridge.protocolFeeRate(), FEE_RATE_MAX); + } + + function test_setProtocolFeeRate_revert_tooHigh() public { + vm.expectRevert("newFeeRate > max"); + setProtocolFeeRate(governor, FEE_RATE_MAX + 1); + } + + function test_setProtocolFeeRate_revert_notGovernor(address caller) public { + vm.assume(caller != governor); + expectUnauthorized(caller, fastBridge.GOVERNOR_ROLE()); + setProtocolFeeRate(caller, 123); + } + + // ════════════════════════════════════════════ SWEEP PROTOCOL FEES ════════════════════════════════════════════════ + + function test_sweepProtocolFees_erc20() public { + vm.expectEmit(address(fastBridge)); + emit FeesSwept(address(srcToken), governorA, 100); + sweepProtocolFees(governor, address(srcToken), governorA); + assertEq(srcToken.balanceOf(address(fastBridge)), 0); + assertEq(srcToken.balanceOf(governorA), 100); + assertEq(fastBridge.protocolFees(address(srcToken)), 0); + } + + function test_sweepProtocolFees_eth() public { + vm.expectEmit(address(fastBridge)); + emit FeesSwept(ETH_ADDRESS, governorA, 200); + sweepProtocolFees(governor, ETH_ADDRESS, governorA); + assertEq(address(fastBridge).balance, 0); + assertEq(governorA.balance, 200); + assertEq(fastBridge.protocolFees(ETH_ADDRESS), 0); + } + + function test_sweepProtocolFees_revertNotGovernor(address caller) public { + vm.assume(caller != governor); + expectUnauthorized(caller, fastBridge.GOVERNOR_ROLE()); + sweepProtocolFees(caller, address(srcToken), governorA); + } + + // ═══════════════════════════════════════════ SET CHAIN GAS AMOUNT ════════════════════════════════════════════════ + + function test_setChainGasAmount() public { + vm.expectEmit(address(fastBridge)); + emit ChainGasAmountUpdated(0, 123); + setChainGasAmount(governor, 123); + assertEq(fastBridge.chainGasAmount(), 123); + } + + function test_setChainGasAmount_twice() public { + test_setChainGasAmount(); + vm.expectEmit(address(fastBridge)); + emit ChainGasAmountUpdated(123, 456); + setChainGasAmount(governor, 456); + assertEq(fastBridge.chainGasAmount(), 456); + } + + function test_setChainGasAmount_revertNotGovernor(address caller) public { + vm.assume(caller != governor); + expectUnauthorized(caller, fastBridge.GOVERNOR_ROLE()); + setChainGasAmount(caller, 123); + } +} diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol new file mode 100644 index 0000000000..592edb2b40 --- /dev/null +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FastBridgeV2} from "../contracts/FastBridgeV2.sol"; + +import {MockERC20} from "./MockERC20.sol"; + +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {Test} from "forge-std/Test.sol"; +import {stdStorage, StdStorage} from "forge-std/Test.sol"; + +// solhint-disable no-empty-blocks +abstract contract FastBridgeV2Test is Test { + using stdStorage for StdStorage; + + address public constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + FastBridgeV2 public fastBridge; + MockERC20 public srcToken; + MockERC20 public dstToken; + + address public relayerA = makeAddr("Relayer A"); + address public relayerB = makeAddr("Relayer B"); + address public guard = makeAddr("Guard"); + address public userA = makeAddr("User A"); + address public userB = makeAddr("User B"); + address public governor = makeAddr("Governor"); + address public refunder = makeAddr("Refunder"); + + function setUp() public virtual { + srcToken = new MockERC20("SrcToken", 6); + dstToken = new MockERC20("DstToken", 6); + fastBridge = deployFastBridge(); + configureFastBridge(); + mintTokens(); + } + + function deployFastBridge() public virtual returns (FastBridgeV2); + + function configureFastBridge() public virtual {} + + function mintTokens() public virtual {} + + function expectUnauthorized(address caller, bytes32 role) public { + vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, role)); + } + + function cheatCollectedProtocolFees(address token, uint256 amount) public { + stdstore.target(address(fastBridge)).sig("protocolFees(address)").with_key(token).checked_write(amount); + } +} From c4d22357fe6b89b5cffa076d9d853e52f32f1c39 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:49:02 +0100 Subject: [PATCH 08/16] test: dst chain scenarios --- .../contracts-rfq/test/FastBridgeV2.Dst.t.sol | 155 ++++++++++++++++++ .../contracts-rfq/test/FastBridgeV2.t.sol | 70 ++++++++ 2 files changed, 225 insertions(+) create mode 100644 packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol new file mode 100644 index 0000000000..926f05c8c0 --- /dev/null +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ChainIncorrect, DeadlineExceeded, TransactionRelayed} from "../contracts/libs/Errors.sol"; + +import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2DstTest is FastBridgeV2Test { + event BridgeRelayed( + bytes32 indexed transactionId, + address indexed relayer, + address indexed to, + uint32 originChainId, + address originToken, + address destToken, + uint256 originAmount, + uint256 destAmount, + uint256 chainGasAmount + ); + + uint256 public constant LEFTOVER_BALANCE = 1 ether; + + function setUp() public override { + vm.chainId(DST_CHAIN_ID); + super.setUp(); + } + + function deployFastBridge() public override returns (FastBridgeV2) { + return new FastBridgeV2(address(this)); + } + + function mintTokens() public override { + dstToken.mint(address(relayerA), LEFTOVER_BALANCE + tokenParams.destAmount); + deal(relayerB, LEFTOVER_BALANCE + ethParams.destAmount); + vm.prank(relayerA); + dstToken.approve(address(fastBridge), type(uint256).max); + } + + function expectBridgeRelayed(IFastBridge.BridgeTransaction memory bridgeTx, bytes32 txId, address relayer) public { + vm.expectEmit(address(fastBridge)); + emit BridgeRelayed({ + transactionId: txId, + relayer: relayer, + to: bridgeTx.destRecipient, + originChainId: bridgeTx.originChainId, + originToken: bridgeTx.originToken, + destToken: bridgeTx.destToken, + originAmount: bridgeTx.originAmount, + destAmount: bridgeTx.destAmount, + chainGasAmount: 0 + }); + } + + function relay(address caller, uint256 msgValue, IFastBridge.BridgeTransaction memory bridgeTx) public { + bytes memory request = abi.encode(bridgeTx); + vm.prank(caller); + fastBridge.relay{value: msgValue}(request); + } + + function relayWithAddress( + address caller, + address relayer, + uint256 msgValue, + IFastBridge.BridgeTransaction memory bridgeTx + ) + public + { + bytes memory request = abi.encode(bridgeTx); + vm.prank(caller); + fastBridge.relay{value: msgValue}(request, relayer); + } + + /// @notice RelayerA completes the ERC20 bridge request + function test_relay_token() public { + bytes32 txId = getTxId(tokenTx); + expectBridgeRelayed(tokenTx, txId, address(relayerA)); + relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); + assertTrue(fastBridge.bridgeRelays(txId)); + assertEq(dstToken.balanceOf(address(userB)), tokenParams.destAmount); + assertEq(dstToken.balanceOf(address(relayerA)), LEFTOVER_BALANCE); + assertEq(dstToken.balanceOf(address(fastBridge)), 0); + } + + /// @notice RelayerA completes the ERC20 bridge request, using relayerB's address + function test_relay_token_withRelayerAddress() public { + bytes32 txId = getTxId(tokenTx); + expectBridgeRelayed(tokenTx, txId, address(relayerB)); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: 0, bridgeTx: tokenTx}); + assertTrue(fastBridge.bridgeRelays(txId)); + assertEq(dstToken.balanceOf(address(userB)), tokenParams.destAmount); + assertEq(dstToken.balanceOf(address(relayerA)), LEFTOVER_BALANCE); + assertEq(dstToken.balanceOf(address(fastBridge)), 0); + } + + /// @notice RelayerB completes the ETH bridge request + function test_relay_eth() public { + bytes32 txId = getTxId(ethTx); + expectBridgeRelayed(ethTx, txId, address(relayerB)); + relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx}); + assertTrue(fastBridge.bridgeRelays(txId)); + assertEq(address(userB).balance, ethParams.destAmount); + assertEq(address(relayerB).balance, LEFTOVER_BALANCE); + assertEq(address(fastBridge).balance, 0); + } + + /// @notice RelayerB completes the ETH bridge request, using relayerA's address + function test_relay_eth_withRelayerAddress() public { + bytes32 txId = getTxId(ethTx); + expectBridgeRelayed(ethTx, txId, address(relayerA)); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: ethParams.destAmount, bridgeTx: ethTx}); + assertTrue(fastBridge.bridgeRelays(txId)); + assertEq(address(userB).balance, ethParams.destAmount); + assertEq(address(relayerB).balance, LEFTOVER_BALANCE); + assertEq(address(fastBridge).balance, 0); + } + + // ══════════════════════════════════════════════════ REVERTS ══════════════════════════════════════════════════════ + + function test_relay_revert_chainIncorrect() public { + vm.chainId(SRC_CHAIN_ID); + vm.expectRevert(ChainIncorrect.selector); + relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_revert_transactionRelayed() public { + relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); + vm.expectRevert(TransactionRelayed.selector); + relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_revert_deadlineExceeded() public { + skip(DEADLINE + 1); + vm.expectRevert(DeadlineExceeded.selector); + relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_withRelayerAddress_revert_chainIncorrect() public { + vm.chainId(SRC_CHAIN_ID); + vm.expectRevert(ChainIncorrect.selector); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_withRelayerAddress_revert_transactionRelayed() public { + relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); + vm.expectRevert(TransactionRelayed.selector); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_withRelayerAddress_revert_deadlineExceeded() public { + skip(DEADLINE + 1); + vm.expectRevert(DeadlineExceeded.selector); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: 0, bridgeTx: tokenTx}); + } +} diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol index 592edb2b40..fa669169ee 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import {IFastBridge} from "../contracts/interfaces/IFastBridge.sol"; import {FastBridgeV2} from "../contracts/FastBridgeV2.sol"; import {MockERC20} from "./MockERC20.sol"; @@ -13,6 +14,9 @@ import {stdStorage, StdStorage} from "forge-std/Test.sol"; abstract contract FastBridgeV2Test is Test { using stdStorage for StdStorage; + uint32 public constant SRC_CHAIN_ID = 1337; + uint32 public constant DST_CHAIN_ID = 7331; + uint256 public constant DEADLINE = 1 days; address public constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; FastBridgeV2 public fastBridge; @@ -27,9 +31,15 @@ abstract contract FastBridgeV2Test is Test { address public governor = makeAddr("Governor"); address public refunder = makeAddr("Refunder"); + IFastBridge.BridgeTransaction public tokenTx; + IFastBridge.BridgeTransaction public ethTx; + IFastBridge.BridgeParams public tokenParams; + IFastBridge.BridgeParams public ethParams; + function setUp() public virtual { srcToken = new MockERC20("SrcToken", 6); dstToken = new MockERC20("DstToken", 6); + createFixtures(); fastBridge = deployFastBridge(); configureFastBridge(); mintTokens(); @@ -41,6 +51,66 @@ abstract contract FastBridgeV2Test is Test { function mintTokens() public virtual {} + function createFixtures() public virtual { + tokenParams = IFastBridge.BridgeParams({ + dstChainId: DST_CHAIN_ID, + sender: userA, + to: userB, + originToken: address(srcToken), + destToken: address(dstToken), + originAmount: 1e6, + destAmount: 0.99e6, + sendChainGas: false, + deadline: block.timestamp + DEADLINE + }); + ethParams = IFastBridge.BridgeParams({ + dstChainId: DST_CHAIN_ID, + sender: userA, + to: userB, + originToken: ETH_ADDRESS, + destToken: ETH_ADDRESS, + originAmount: 1 ether, + destAmount: 0.99 ether, + sendChainGas: false, + deadline: block.timestamp + DEADLINE + }); + + tokenTx = IFastBridge.BridgeTransaction({ + originChainId: SRC_CHAIN_ID, + destChainId: DST_CHAIN_ID, + originSender: userA, + destRecipient: userB, + originToken: address(srcToken), + destToken: address(dstToken), + originAmount: 1e6, + destAmount: 0.99e6, + // override this in tests with protocol fees + originFeeAmount: 0, + sendChainGas: false, + deadline: block.timestamp + DEADLINE, + nonce: 0 + }); + ethTx = IFastBridge.BridgeTransaction({ + originChainId: SRC_CHAIN_ID, + destChainId: DST_CHAIN_ID, + originSender: userA, + destRecipient: userB, + originToken: ETH_ADDRESS, + destToken: ETH_ADDRESS, + originAmount: 1 ether, + destAmount: 0.99 ether, + // override this in tests with protocol fees + originFeeAmount: 0, + sendChainGas: false, + deadline: block.timestamp + DEADLINE, + nonce: 1 + }); + } + + function getTxId(IFastBridge.BridgeTransaction memory bridgeTx) public returns (bytes32) { + return keccak256(abi.encode(bridgeTx)); + } + function expectUnauthorized(address caller, bytes32 role) public { vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, role)); } From e368351ae8cf2732e72bf82f06ebf8d40c0c810e Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:53:05 +0100 Subject: [PATCH 09/16] test: bridge --- .../test/FastBridgeV2.Src.ProtocolFees.t.sol | 25 +++ .../contracts-rfq/test/FastBridgeV2.Src.t.sol | 156 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 packages/contracts-rfq/test/FastBridgeV2.Src.ProtocolFees.t.sol create mode 100644 packages/contracts-rfq/test/FastBridgeV2.Src.t.sol diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.ProtocolFees.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.ProtocolFees.t.sol new file mode 100644 index 0000000000..d9a5324ed1 --- /dev/null +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.ProtocolFees.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FastBridgeV2SrcTest} from "./FastBridgeV2.Src.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2SrcProtocolFeesTest is FastBridgeV2SrcTest { + function configureFastBridge() public virtual override { + super.configureFastBridge(); + fastBridge.grantRole(fastBridge.GOVERNOR_ROLE(), address(this)); + fastBridge.setProtocolFeeRate(1e4); // 1% + } + + function createFixtures() public virtual override { + super.createFixtures(); + tokenTx.originFeeAmount = 0.01e6; + tokenTx.originAmount = 0.99e6; + tokenTx.destAmount = 0.98e6; + tokenParams.destAmount = 0.98e6; + ethTx.originFeeAmount = 0.01 ether; + ethTx.originAmount = 0.99 ether; + ethTx.destAmount = 0.98 ether; + ethParams.destAmount = 0.98 ether; + } +} diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol new file mode 100644 index 0000000000..266b666aaa --- /dev/null +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {MsgValueIncorrect} from "../contracts/libs/Errors.sol"; + +import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2SrcTest is FastBridgeV2Test { + event BridgeRequested( + bytes32 indexed transactionId, + address indexed sender, + bytes request, + uint32 destChainId, + address originToken, + address destToken, + uint256 originAmount, + uint256 destAmount, + bool sendChainGas + ); + + uint256 public constant LEFTOVER_BALANCE = 1 ether; + uint256 public constant INITIAL_PROTOCOL_FEES_TOKEN = 456_789; + uint256 public constant INITIAL_PROTOCOL_FEES_ETH = 0.123 ether; + + function setUp() public override { + vm.chainId(SRC_CHAIN_ID); + super.setUp(); + } + + function deployFastBridge() public override returns (FastBridgeV2) { + return new FastBridgeV2(address(this)); + } + + function mintTokens() public override { + // Prior Protocol fees + srcToken.mint(address(fastBridge), INITIAL_PROTOCOL_FEES_TOKEN); + deal(address(fastBridge), INITIAL_PROTOCOL_FEES_ETH); + cheatCollectedProtocolFees(address(srcToken), INITIAL_PROTOCOL_FEES_TOKEN); + cheatCollectedProtocolFees(ETH_ADDRESS, INITIAL_PROTOCOL_FEES_ETH); + // Users + srcToken.mint(userA, LEFTOVER_BALANCE + tokenParams.originAmount); + srcToken.mint(userB, LEFTOVER_BALANCE + tokenParams.originAmount); + deal(userA, LEFTOVER_BALANCE + ethParams.originAmount); + deal(userB, LEFTOVER_BALANCE + ethParams.originAmount); + vm.prank(userA); + srcToken.approve(address(fastBridge), type(uint256).max); + vm.prank(userB); + srcToken.approve(address(fastBridge), type(uint256).max); + } + + function bridge(address caller, uint256 msgValue, IFastBridge.BridgeParams memory params) public { + vm.prank(caller); + fastBridge.bridge{value: msgValue}(params); + } + + function expectBridgeRequested(IFastBridge.BridgeTransaction memory bridgeTx, bytes32 txId) public { + vm.expectEmit(address(fastBridge)); + emit BridgeRequested({ + transactionId: txId, + sender: bridgeTx.originSender, + request: abi.encode(bridgeTx), + destChainId: bridgeTx.destChainId, + originToken: bridgeTx.originToken, + destToken: bridgeTx.destToken, + originAmount: bridgeTx.originAmount, + destAmount: bridgeTx.destAmount, + sendChainGas: bridgeTx.sendChainGas + }); + } + + function assertEq(FastBridgeV2.BridgeStatus a, FastBridgeV2.BridgeStatus b) public pure { + assertEq(uint8(a), uint8(b)); + } + + // ══════════════════════════════════════════════════ BRIDGE ═══════════════════════════════════════════════════════ + + function checkTokenBalancesAfterBridge(address caller) public { + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); + assertEq(srcToken.balanceOf(caller), LEFTOVER_BALANCE); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount); + } + + function test_bridge_token() public { + bytes32 txId = getTxId(tokenTx); + expectBridgeRequested(tokenTx, txId); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + checkTokenBalancesAfterBridge(userA); + } + + function test_bridge_token_diffSender() public { + bytes32 txId = getTxId(tokenTx); + expectBridgeRequested(tokenTx, txId); + bridge({caller: userB, msgValue: 0, params: tokenParams}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount); + checkTokenBalancesAfterBridge(userB); + } + + function checkEthBalancesAfterBridge(address caller) public { + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); + assertEq(address(caller).balance, LEFTOVER_BALANCE); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount); + } + + function test_bridge_eth() public { + // bridge token first to match the nonce + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + expectBridgeRequested(ethTx, txId); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + checkEthBalancesAfterBridge(userA); + } + + function test_bridge_eth_diffSender() public { + // bridge token first to match the nonce + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + expectBridgeRequested(ethTx, txId); + bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + assertEq(userA.balance, LEFTOVER_BALANCE + ethParams.originAmount); + checkEthBalancesAfterBridge(userB); + } + + function test_bridge_userSpecificNonce() public { + vm.skip(true); // TODO: unskip when implemented + bridge({caller: userA, msgValue: 0, params: tokenParams}); + // UserB nonce is 0 + ethTx.nonce = 0; + ethParams.sender = userB; + ethTx.originSender = userB; + bytes32 txId = getTxId(ethTx); + expectBridgeRequested(ethTx, txId); + bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + checkEthBalancesAfterBridge(userB); + } + + function test_bridge_eth_revert_lowerMsgValue() public { + vm.expectRevert(MsgValueIncorrect.selector); + bridge({caller: userA, msgValue: ethParams.originAmount - 1, params: ethParams}); + } + + function test_bridge_eth_revert_higherMsgValue() public { + vm.expectRevert(MsgValueIncorrect.selector); + bridge({caller: userA, msgValue: ethParams.originAmount + 1, params: ethParams}); + } + + function test_bridge_eth_revert_zeroMsgValue() public { + vm.expectRevert(MsgValueIncorrect.selector); + bridge({caller: userA, msgValue: 0, params: ethParams}); + } +} From 7a5c181035c7db4af0bf2daf713135a1b6238389 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:24:52 +0100 Subject: [PATCH 10/16] test: prove --- .../contracts-rfq/test/FastBridgeV2.Src.t.sol | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index 266b666aaa..d77dbb0f28 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {MsgValueIncorrect} from "../contracts/libs/Errors.sol"; +import {MsgValueIncorrect, StatusIncorrect} from "../contracts/libs/Errors.sol"; import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol"; @@ -19,6 +19,10 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { bool sendChainGas ); + event BridgeProofProvided(bytes32 indexed transactionId, address indexed relayer, bytes32 transactionHash); + + uint256 public constant CLAIM_DELAY = 30 minutes; + uint256 public constant LEFTOVER_BALANCE = 1 ether; uint256 public constant INITIAL_PROTOCOL_FEES_TOKEN = 456_789; uint256 public constant INITIAL_PROTOCOL_FEES_ETH = 0.123 ether; @@ -32,6 +36,13 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { return new FastBridgeV2(address(this)); } + function configureFastBridge() public virtual override { + fastBridge.grantRole(fastBridge.RELAYER_ROLE(), relayerA); + fastBridge.grantRole(fastBridge.RELAYER_ROLE(), relayerB); + fastBridge.grantRole(fastBridge.GUARD_ROLE(), guard); + fastBridge.grantRole(fastBridge.REFUNDER_ROLE(), refunder); + } + function mintTokens() public override { // Prior Protocol fees srcToken.mint(address(fastBridge), INITIAL_PROTOCOL_FEES_TOKEN); @@ -54,6 +65,26 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { fastBridge.bridge{value: msgValue}(params); } + function prove(address caller, IFastBridge.BridgeTransaction memory bridgeTx, bytes32 destTxHash) public { + vm.prank(caller); + fastBridge.prove(abi.encode(bridgeTx), destTxHash); + } + + function claim(address caller, IFastBridge.BridgeTransaction memory bridgeTx, address to) public { + vm.prank(caller); + fastBridge.claim(abi.encode(bridgeTx), to); + } + + function dispute(address caller, bytes32 txId) public { + vm.prank(caller); + fastBridge.dispute(txId); + } + + function refund(address caller, IFastBridge.BridgeTransaction memory bridgeTx) public { + vm.prank(caller); + fastBridge.refund(abi.encode(bridgeTx)); + } + function expectBridgeRequested(IFastBridge.BridgeTransaction memory bridgeTx, bytes32 txId) public { vm.expectEmit(address(fastBridge)); emit BridgeRequested({ @@ -69,6 +100,11 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { }); } + function expectBridgeProofProvided(bytes32 txId, address relayer, bytes32 destTxHash) public { + vm.expectEmit(address(fastBridge)); + emit BridgeProofProvided({transactionId: txId, relayer: relayer, transactionHash: destTxHash}); + } + function assertEq(FastBridgeV2.BridgeStatus a, FastBridgeV2.BridgeStatus b) public pure { assertEq(uint8(a), uint8(b)); } @@ -153,4 +189,68 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { vm.expectRevert(MsgValueIncorrect.selector); bridge({caller: userA, msgValue: 0, params: ethParams}); } + + // ═══════════════════════════════════════════════════ PROVE ═══════════════════════════════════════════════════════ + + function test_prove_token() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + (uint96 timestamp, address relayer) = fastBridge.bridgeProofs(txId); + assertEq(timestamp, block.timestamp); + assertEq(relayer, relayerA); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount); + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); + } + + function test_prove_eth() public { + // bridge token first to match the nonce + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"}); + prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); + (uint96 timestamp, address relayer) = fastBridge.bridgeProofs(txId); + assertEq(timestamp, block.timestamp); + assertEq(relayer, relayerA); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount); + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); + } + + function test_prove_revert_statusNull() public { + vm.expectRevert(StatusIncorrect.selector); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + } + + function test_prove_revert_statusProved() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + vm.expectRevert(StatusIncorrect.selector); + prove({caller: relayerB, bridgeTx: tokenTx, destTxHash: hex"02"}); + } + + function test_prove_revert_statusClaimed() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + vm.expectRevert(StatusIncorrect.selector); + prove({caller: relayerB, bridgeTx: tokenTx, destTxHash: hex"02"}); + } + + function test_prove_revert_statusRefunded() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE + 1); + refund({caller: refunder, bridgeTx: tokenTx}); + vm.expectRevert(StatusIncorrect.selector); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + } + + function test_prove_revert_callerNotRelayer(address caller) public { + vm.assume(caller != relayerA && caller != relayerB); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + expectUnauthorized(caller, fastBridge.RELAYER_ROLE()); + prove({caller: caller, bridgeTx: tokenTx, destTxHash: hex"01"}); + } } From 8d387540af73627bd53959fd42038b7f46d9705a Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:44:22 +0100 Subject: [PATCH 11/16] test: claim --- .../contracts-rfq/test/FastBridgeV2.Src.t.sol | 237 +++++++++++++++++- .../contracts-rfq/test/FastBridgeV2.t.sol | 2 +- 2 files changed, 237 insertions(+), 2 deletions(-) diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index d77dbb0f28..0871c51d8b 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {MsgValueIncorrect, StatusIncorrect} from "../contracts/libs/Errors.sol"; +import { + DisputePeriodNotPassed, MsgValueIncorrect, SenderIncorrect, StatusIncorrect +} from "../contracts/libs/Errors.sol"; import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol"; @@ -21,12 +23,18 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { event BridgeProofProvided(bytes32 indexed transactionId, address indexed relayer, bytes32 transactionHash); + event BridgeDepositClaimed( + bytes32 indexed transactionId, address indexed relayer, address indexed to, address token, uint256 amount + ); + uint256 public constant CLAIM_DELAY = 30 minutes; uint256 public constant LEFTOVER_BALANCE = 1 ether; uint256 public constant INITIAL_PROTOCOL_FEES_TOKEN = 456_789; uint256 public constant INITIAL_PROTOCOL_FEES_ETH = 0.123 ether; + address public claimTo = makeAddr("Claim To"); + function setUp() public override { vm.chainId(SRC_CHAIN_ID); super.setUp(); @@ -70,6 +78,11 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { fastBridge.prove(abi.encode(bridgeTx), destTxHash); } + function claim(address caller, IFastBridge.BridgeTransaction memory bridgeTx) public { + vm.prank(caller); + fastBridge.claim(abi.encode(bridgeTx)); + } + function claim(address caller, IFastBridge.BridgeTransaction memory bridgeTx, address to) public { vm.prank(caller); fastBridge.claim(abi.encode(bridgeTx), to); @@ -105,6 +118,24 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { emit BridgeProofProvided({transactionId: txId, relayer: relayer, transactionHash: destTxHash}); } + function expectBridgeDepositClaimed( + IFastBridge.BridgeTransaction memory bridgeTx, + bytes32 txId, + address relayer, + address to + ) + public + { + vm.expectEmit(address(fastBridge)); + emit BridgeDepositClaimed({ + transactionId: txId, + relayer: relayer, + to: to, + token: bridgeTx.originToken, + amount: bridgeTx.originAmount + }); + } + function assertEq(FastBridgeV2.BridgeStatus a, FastBridgeV2.BridgeStatus b) public pure { assertEq(uint8(a), uint8(b)); } @@ -253,4 +284,208 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { expectUnauthorized(caller, fastBridge.RELAYER_ROLE()); prove({caller: caller, bridgeTx: tokenTx, destTxHash: hex"01"}); } + + // ═══════════════════════════════════════════════════ CLAIM ═══════════════════════════════════════════════════════ + + function checkTokenBalancesAfterClaim(address relayer) public { + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN + tokenTx.originFeeAmount); + assertEq(srcToken.balanceOf(relayer), tokenTx.originAmount); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenTx.originFeeAmount); + } + + function test_claim_token() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + assertTrue(fastBridge.canClaim(txId, relayerA)); + expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: relayerA}); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkTokenBalancesAfterClaim(relayerA); + } + + function test_claim_token_permissionless(address caller) public { + vm.assume(caller != relayerA); + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: relayerA}); + claim({caller: caller, bridgeTx: tokenTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkTokenBalancesAfterClaim(relayerA); + } + + function test_claim_token_permissionless_toZeroAddress(address caller) public { + vm.assume(caller != relayerA); + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: relayerA}); + claim({caller: caller, bridgeTx: tokenTx, to: address(0)}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkTokenBalancesAfterClaim(relayerA); + } + + function test_claim_token_toDiffAddress() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: claimTo}); + claim({caller: relayerA, bridgeTx: tokenTx, to: claimTo}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + assertEq(srcToken.balanceOf(relayerA), 0); + checkTokenBalancesAfterClaim(claimTo); + } + + function test_claim_token_longDelay() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 30 days); + expectBridgeDepositClaimed({bridgeTx: tokenTx, txId: txId, relayer: relayerA, to: relayerA}); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkTokenBalancesAfterClaim(relayerA); + } + + function checkEthBalancesAfterClaim(address relayer) public { + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH + ethTx.originFeeAmount); + assertEq(address(relayer).balance, ethTx.originAmount); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethTx.originFeeAmount); + } + + function test_claim_eth() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + assertTrue(fastBridge.canClaim(txId, relayerA)); + expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: relayerA}); + claim({caller: relayerA, bridgeTx: ethTx, to: relayerA}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkEthBalancesAfterClaim(relayerA); + } + + function test_claim_eth_permissionless(address caller) public { + vm.assume(caller != relayerA); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: relayerA}); + claim({caller: caller, bridgeTx: ethTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkEthBalancesAfterClaim(relayerA); + } + + function test_claim_eth_permissionless_toZeroAddress(address caller) public { + vm.assume(caller != relayerA); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: relayerA}); + claim({caller: caller, bridgeTx: ethTx, to: address(0)}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkEthBalancesAfterClaim(relayerA); + } + + function test_claim_eth_toDiffAddress() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: claimTo}); + claim({caller: relayerA, bridgeTx: ethTx, to: claimTo}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkEthBalancesAfterClaim(claimTo); + } + + function test_claim_eth_longDelay() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 30 days); + expectBridgeDepositClaimed({bridgeTx: ethTx, txId: txId, relayer: relayerA, to: relayerA}); + claim({caller: relayerA, bridgeTx: ethTx, to: relayerA}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.RELAYER_CLAIMED); + checkEthBalancesAfterClaim(relayerA); + } + + function test_claim_revert_zeroDelay() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + assertFalse(fastBridge.canClaim(getTxId(tokenTx), relayerA)); + vm.expectRevert(DisputePeriodNotPassed.selector); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + } + + function test_claim_revert_smallerDelay() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY); + assertFalse(fastBridge.canClaim(getTxId(tokenTx), relayerA)); + vm.expectRevert(DisputePeriodNotPassed.selector); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + } + + function test_claim_revert_callerNotProven(address caller, address to) public { + vm.assume(caller != relayerA && to != address(0)); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + vm.expectRevert(SenderIncorrect.selector); + fastBridge.canClaim(getTxId(tokenTx), caller); + vm.expectRevert(SenderIncorrect.selector); + claim({caller: caller, bridgeTx: tokenTx, to: to}); + } + + function test_claim_revert_statusNull() public { + bytes32 txId = getTxId(tokenTx); + vm.expectRevert(StatusIncorrect.selector); + fastBridge.canClaim(txId, relayerA); + vm.expectRevert(StatusIncorrect.selector); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + } + + function test_claim_revert_statusRequested() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + vm.expectRevert(StatusIncorrect.selector); + fastBridge.canClaim(txId, relayerA); + vm.expectRevert(StatusIncorrect.selector); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + } + + function test_claim_revert_statusClaimed() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + vm.expectRevert(StatusIncorrect.selector); + fastBridge.canClaim(txId, relayerA); + vm.expectRevert(StatusIncorrect.selector); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + } + + function test_claim_revert_statusRefunded() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE + 1); + refund({caller: refunder, bridgeTx: tokenTx}); + vm.expectRevert(StatusIncorrect.selector); + fastBridge.canClaim(txId, relayerA); + vm.expectRevert(StatusIncorrect.selector); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol index fa669169ee..4fe357e3f2 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -107,7 +107,7 @@ abstract contract FastBridgeV2Test is Test { }); } - function getTxId(IFastBridge.BridgeTransaction memory bridgeTx) public returns (bytes32) { + function getTxId(IFastBridge.BridgeTransaction memory bridgeTx) public pure returns (bytes32) { return keccak256(abi.encode(bridgeTx)); } From 9f42c6901c47a034556c49432f4409c1d444dc62 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:52:50 +0100 Subject: [PATCH 12/16] test: dispute --- .../contracts-rfq/test/FastBridgeV2.Src.t.sol | 114 +++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index 0871c51d8b..9a50d3478a 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -2,7 +2,11 @@ pragma solidity ^0.8.20; import { - DisputePeriodNotPassed, MsgValueIncorrect, SenderIncorrect, StatusIncorrect + DisputePeriodNotPassed, + DisputePeriodPassed, + MsgValueIncorrect, + SenderIncorrect, + StatusIncorrect } from "../contracts/libs/Errors.sol"; import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol"; @@ -27,6 +31,8 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { bytes32 indexed transactionId, address indexed relayer, address indexed to, address token, uint256 amount ); + event BridgeProofDisputed(bytes32 indexed transactionId, address indexed relayer); + uint256 public constant CLAIM_DELAY = 30 minutes; uint256 public constant LEFTOVER_BALANCE = 1 ether; @@ -136,6 +142,12 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { }); } + function expectBridgeProofDisputed(bytes32 txId, address guard) public { + vm.expectEmit(address(fastBridge)); + // Note: BridgeProofDisputed event has a mislabeled address parameter, this is actually the guard + emit BridgeProofDisputed({transactionId: txId, relayer: guard}); + } + function assertEq(FastBridgeV2.BridgeStatus a, FastBridgeV2.BridgeStatus b) public pure { assertEq(uint8(a), uint8(b)); } @@ -488,4 +500,104 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { vm.expectRevert(StatusIncorrect.selector); claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); } + + // ══════════════════════════════════════════════════ DISPUTE ══════════════════════════════════════════════════════ + + function test_dispute_token() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + expectBridgeProofDisputed({txId: txId, guard: guard}); + dispute({caller: guard, txId: txId}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount); + } + + function test_dispute_token_justBeforeDeadline() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY); + expectBridgeProofDisputed({txId: txId, guard: guard}); + dispute({caller: guard, txId: txId}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount); + } + + function test_dispute_eth() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); + expectBridgeProofDisputed({txId: txId, guard: guard}); + dispute({caller: guard, txId: txId}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount); + } + + function test_dispute_eth_justBeforeDeadline() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY); + expectBridgeProofDisputed({txId: txId, guard: guard}); + dispute({caller: guard, txId: txId}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REQUESTED); + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount); + } + + function test_dispute_revert_afterDeadline() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + vm.expectRevert(DisputePeriodPassed.selector); + dispute({caller: guard, txId: txId}); + } + + function test_dispute_revert_callerNotGuard(address caller) public { + vm.assume(caller != guard); + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + expectUnauthorized(caller, fastBridge.GUARD_ROLE()); + dispute({caller: caller, txId: txId}); + } + + function test_dispute_revert_statusNull() public { + bytes32 txId = getTxId(tokenTx); + vm.expectRevert(StatusIncorrect.selector); + dispute({caller: guard, txId: txId}); + } + + function test_dispute_revert_statusRequested() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + vm.expectRevert(StatusIncorrect.selector); + dispute({caller: guard, txId: txId}); + } + + function test_dispute_revert_statusClaimed() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + vm.expectRevert(StatusIncorrect.selector); + dispute({caller: guard, txId: txId}); + } + + function test_dispute_revert_statusRefunded() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE + 1); + refund({caller: refunder, bridgeTx: tokenTx}); + vm.expectRevert(StatusIncorrect.selector); + dispute({caller: guard, txId: txId}); + } } From 2cff786e02323e376713a8e728dbda960820f94b Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 16 Sep 2024 19:19:12 +0100 Subject: [PATCH 13/16] test: refund --- .../contracts-rfq/test/FastBridgeV2.Src.t.sol | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index 9a50d3478a..c3b3bb4d84 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import { DisputePeriodNotPassed, DisputePeriodPassed, + DeadlineNotExceeded, MsgValueIncorrect, SenderIncorrect, StatusIncorrect @@ -33,7 +34,10 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { event BridgeProofDisputed(bytes32 indexed transactionId, address indexed relayer); + event BridgeDepositRefunded(bytes32 indexed transactionId, address indexed to, address token, uint256 amount); + uint256 public constant CLAIM_DELAY = 30 minutes; + uint256 public constant PERMISSIONLESS_REFUND_DELAY = 7 days; uint256 public constant LEFTOVER_BALANCE = 1 ether; uint256 public constant INITIAL_PROTOCOL_FEES_TOKEN = 456_789; @@ -148,6 +152,16 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { emit BridgeProofDisputed({transactionId: txId, relayer: guard}); } + function expectBridgeDepositRefunded(IFastBridge.BridgeParams memory bridgeParams, bytes32 txId) public { + vm.expectEmit(address(fastBridge)); + emit BridgeDepositRefunded({ + transactionId: txId, + to: bridgeParams.sender, + token: bridgeParams.originToken, + amount: bridgeParams.originAmount + }); + } + function assertEq(FastBridgeV2.BridgeStatus a, FastBridgeV2.BridgeStatus b) public pure { assertEq(uint8(a), uint8(b)); } @@ -600,4 +614,165 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { vm.expectRevert(StatusIncorrect.selector); dispute({caller: guard, txId: txId}); } + + // ══════════════════════════════════════════════════ REFUND ═══════════════════════════════════════════════════════ + + function test_refund_token() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE + 1); + expectBridgeDepositRefunded({bridgeParams: tokenParams, txId: txId}); + refund({caller: refunder, bridgeTx: tokenTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REFUNDED); + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); + assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN); + } + + /// @notice Deposit should be refunded to the BridgeParams.sender, regardless of the actual caller + function test_refund_token_diffSender() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userB, msgValue: 0, params: tokenParams}); + skip(DEADLINE + 1); + expectBridgeDepositRefunded({bridgeParams: tokenParams, txId: txId}); + refund({caller: refunder, bridgeTx: tokenTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REFUNDED); + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); + assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + 2 * tokenParams.originAmount); + assertEq(srcToken.balanceOf(userB), LEFTOVER_BALANCE); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN); + } + + function test_refund_token_longDelay() public { + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE + 30 days); + expectBridgeDepositRefunded({bridgeParams: tokenParams, txId: txId}); + refund({caller: refunder, bridgeTx: tokenTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REFUNDED); + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); + assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN); + } + + function test_refund_token_permisionless(address caller) public { + vm.assume(caller != refunder); + bytes32 txId = getTxId(tokenTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE + PERMISSIONLESS_REFUND_DELAY + 1); + expectBridgeDepositRefunded({bridgeParams: tokenParams, txId: txId}); + refund({caller: caller, bridgeTx: tokenTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REFUNDED); + assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); + assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount); + assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN); + } + + function test_refund_eth() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + skip(DEADLINE + 1); + expectBridgeDepositRefunded({bridgeParams: ethParams, txId: txId}); + refund({caller: refunder, bridgeTx: ethTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REFUNDED); + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); + assertEq(address(userA).balance, LEFTOVER_BALANCE + ethParams.originAmount); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH); + } + + /// @notice Deposit should be refunded to the BridgeParams.sender, regardless of the actual caller + function test_refund_eth_diffSender() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams}); + skip(DEADLINE + 1); + expectBridgeDepositRefunded({bridgeParams: ethParams, txId: txId}); + refund({caller: refunder, bridgeTx: ethTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REFUNDED); + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); + assertEq(address(userA).balance, LEFTOVER_BALANCE + 2 * ethParams.originAmount); + assertEq(address(userB).balance, LEFTOVER_BALANCE); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH); + } + + function test_refund_eth_longDelay() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + skip(DEADLINE + 30 days); + expectBridgeDepositRefunded({bridgeParams: ethParams, txId: txId}); + refund({caller: refunder, bridgeTx: ethTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REFUNDED); + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); + assertEq(address(userA).balance, LEFTOVER_BALANCE + ethParams.originAmount); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH); + } + + function test_refund_eth_permisionless(address caller) public { + vm.assume(caller != refunder); + bytes32 txId = getTxId(ethTx); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + skip(DEADLINE + PERMISSIONLESS_REFUND_DELAY + 1); + expectBridgeDepositRefunded({bridgeParams: ethParams, txId: txId}); + refund({caller: caller, bridgeTx: ethTx}); + assertEq(fastBridge.bridgeStatuses(txId), FastBridgeV2.BridgeStatus.REFUNDED); + assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); + assertEq(address(userA).balance, LEFTOVER_BALANCE + ethParams.originAmount); + assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH); + } + + function test_refund_revert_zeroDelay() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + vm.expectRevert(DeadlineNotExceeded.selector); + refund({caller: refunder, bridgeTx: ethTx}); + } + + function test_refund_revert_justBeforeDeadline() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE); + vm.expectRevert(DeadlineNotExceeded.selector); + refund({caller: refunder, bridgeTx: ethTx}); + } + + function test_refund_revert_justBeforeDeadline_permisionless(address caller) public { + vm.assume(caller != refunder); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE + PERMISSIONLESS_REFUND_DELAY); + vm.expectRevert(DeadlineNotExceeded.selector); + refund({caller: caller, bridgeTx: ethTx}); + } + + function test_refund_revert_statusNull() public { + vm.skip(true); // TODO: unskip when fixed + vm.expectRevert(StatusIncorrect.selector); + refund({caller: refunder, bridgeTx: ethTx}); + } + + function test_refund_revert_statusProven() public { + vm.skip(true); // TODO: unskip when fixed + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + vm.expectRevert(StatusIncorrect.selector); + refund({caller: refunder, bridgeTx: tokenTx}); + } + + function test_refund_revert_statusClaimed() public { + vm.skip(true); // TODO: unskip when fixed + bridge({caller: userA, msgValue: 0, params: tokenParams}); + prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); + skip(CLAIM_DELAY + 1); + claim({caller: relayerA, bridgeTx: tokenTx, to: relayerA}); + vm.expectRevert(StatusIncorrect.selector); + refund({caller: refunder, bridgeTx: tokenTx}); + } + + function test_refund_revert_statusRefunded() public { + bridge({caller: userA, msgValue: 0, params: tokenParams}); + skip(DEADLINE + 1); + refund({caller: refunder, bridgeTx: tokenTx}); + vm.expectRevert(StatusIncorrect.selector); + refund({caller: refunder, bridgeTx: tokenTx}); + } } From 0f99c3e32d9be24f7d8ee601c07dbd1c26ec9b35 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:50:51 +0100 Subject: [PATCH 14/16] test: bridge reverts --- .../contracts-rfq/test/FastBridgeV2.Src.t.sol | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index c3b3bb4d84..8a0106d0c0 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -2,12 +2,16 @@ pragma solidity ^0.8.20; import { + AmountIncorrect, + ChainIncorrect, DisputePeriodNotPassed, DisputePeriodPassed, DeadlineNotExceeded, + DeadlineTooShort, MsgValueIncorrect, SenderIncorrect, - StatusIncorrect + StatusIncorrect, + ZeroAddress } from "../contracts/libs/Errors.sol"; import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol"; @@ -36,6 +40,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { event BridgeDepositRefunded(bytes32 indexed transactionId, address indexed to, address token, uint256 amount); + uint256 public constant MIN_DEADLINE = 30 minutes; uint256 public constant CLAIM_DELAY = 30 minutes; uint256 public constant PERMISSIONLESS_REFUND_DELAY = 7 days; @@ -168,7 +173,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { // ══════════════════════════════════════════════════ BRIDGE ═══════════════════════════════════════════════════════ - function checkTokenBalancesAfterBridge(address caller) public { + function checkTokenBalancesAfterBridge(address caller) public view { assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); assertEq(srcToken.balanceOf(caller), LEFTOVER_BALANCE); assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenParams.originAmount); @@ -191,7 +196,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { checkTokenBalancesAfterBridge(userB); } - function checkEthBalancesAfterBridge(address caller) public { + function checkEthBalancesAfterBridge(address caller) public view { assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); assertEq(address(caller).balance, LEFTOVER_BALANCE); assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethParams.originAmount); @@ -247,6 +252,56 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { bridge({caller: userA, msgValue: 0, params: ethParams}); } + function test_bridge_revert_sameDestinationChain() public { + tokenParams.dstChainId = SRC_CHAIN_ID; + vm.expectRevert(ChainIncorrect.selector); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + } + + function test_bridge_revert_zeroOriginAmount() public { + tokenParams.originAmount = 0; + vm.expectRevert(AmountIncorrect.selector); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + } + + function test_bridge_revert_zeroDestAmount() public { + tokenParams.destAmount = 0; + vm.expectRevert(AmountIncorrect.selector); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + } + + function test_bridge_revert_zeroOriginToken() public { + tokenParams.originToken = address(0); + vm.expectRevert(ZeroAddress.selector); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + } + + function test_bridge_revert_zeroDestToken() public { + tokenParams.destToken = address(0); + vm.expectRevert(ZeroAddress.selector); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + } + + function test_bridge_revert_zeroSender() public { + vm.skip(true); // TODO: unskip when fixed + tokenParams.sender = address(0); + vm.expectRevert(ZeroAddress.selector); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + } + + function test_bridge_revert_zeroRecipient() public { + vm.skip(true); // TODO: unskip when fixed + tokenParams.to = address(0); + vm.expectRevert(ZeroAddress.selector); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + } + + function test_bridge_revert_deadlineTooClose() public { + tokenParams.deadline = block.timestamp + MIN_DEADLINE - 1; + vm.expectRevert(DeadlineTooShort.selector); + bridge({caller: userA, msgValue: 0, params: tokenParams}); + } + // ═══════════════════════════════════════════════════ PROVE ═══════════════════════════════════════════════════════ function test_prove_token() public { @@ -313,7 +368,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { // ═══════════════════════════════════════════════════ CLAIM ═══════════════════════════════════════════════════════ - function checkTokenBalancesAfterClaim(address relayer) public { + function checkTokenBalancesAfterClaim(address relayer) public view { assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN + tokenTx.originFeeAmount); assertEq(srcToken.balanceOf(relayer), tokenTx.originAmount); assertEq(srcToken.balanceOf(address(fastBridge)), INITIAL_PROTOCOL_FEES_TOKEN + tokenTx.originFeeAmount); @@ -378,7 +433,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2Test { checkTokenBalancesAfterClaim(relayerA); } - function checkEthBalancesAfterClaim(address relayer) public { + function checkEthBalancesAfterClaim(address relayer) public view { assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH + ethTx.originFeeAmount); assertEq(address(relayer).balance, ethTx.originAmount); assertEq(address(fastBridge).balance, INITIAL_PROTOCOL_FEES_ETH + ethTx.originFeeAmount); From 69aa8707fd54018513daee66d710f9c927afa443 Mon Sep 17 00:00:00 2001 From: parodime Date: Fri, 20 Sep 2024 15:01:19 -0400 Subject: [PATCH 15/16] remove redundant extend. rearrange inherit list --- packages/contracts-rfq/contracts/FastBridgeV2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index a298f8e659..7f938f28f1 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -10,7 +10,7 @@ import {Admin} from "./Admin.sol"; import {IFastBridge} from "./interfaces/IFastBridge.sol"; import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol"; -contract FastBridgeV2 is IFastBridge, IFastBridgeV2, Admin { +contract FastBridgeV2 is Admin, IFastBridgeV2 { using SafeERC20 for IERC20; using UniversalTokenLib for address; From a98353db3cf45b499bece7ed8b38fc3cbc36a8f2 Mon Sep 17 00:00:00 2001 From: parodime Date: Fri, 20 Sep 2024 15:57:19 -0400 Subject: [PATCH 16/16] revert 0.8.20 in favor of user (non-ws) setting --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f4ccb368ce..116a5a5174 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,7 @@ "files.trimTrailingWhitespace": true, "solidity.packageDefaultDependenciesContractsDirectory": "contracts", "solidity.packageDefaultDependenciesDirectory": "lib", - "solidity.compileUsingRemoteVersion": "v0.8.24+commit.e11b9ed9", + "solidity.compileUsingRemoteVersion": "v0.8.17+commit.8df45f5f", "solidity.formatter": "prettier", "solidity.defaultCompiler": "remote", "tailwindCSS.classAttributes": [