From 12325dcc160ab4d60d542e6545df58bac648ea41 Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 11 Apr 2025 00:09:48 +0530 Subject: [PATCH 1/5] Use signature --- src/UniversalBridgeV1.sol | 125 ++++-- test/UniversalBridgeV1.t.sol | 786 ++++++++++++++++++++--------------- 2 files changed, 534 insertions(+), 377 deletions(-) diff --git a/src/UniversalBridgeV1.sol b/src/UniversalBridgeV1.sol index 6d1d895..a7fad14 100644 --- a/src/UniversalBridgeV1.sol +++ b/src/UniversalBridgeV1.sol @@ -3,8 +3,10 @@ pragma solidity ^0.8.22; /// @author thirdweb +import { EIP712 } from "lib/solady/src/utils/EIP712.sol"; import { SafeTransferLib } from "lib/solady/src/utils/SafeTransferLib.sol"; import { ReentrancyGuard } from "lib/solady/src/utils/ReentrancyGuard.sol"; +import { ECDSA } from "lib/solady/src/utils/ECDSA.sol"; import { Ownable } from "lib/solady/src/auth/Ownable.sol"; import { UUPSUpgradeable } from "lib/solady/src/utils/UUPSUpgradeable.sol"; import { Initializable } from "lib/solady/src/utils/Initializable.sol"; @@ -35,7 +37,9 @@ library UniversalBridgeStorage { } } -contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, ReentrancyGuard { +contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, Ownable, ReentrancyGuard { + using ECDSA for bytes32; + /*/////////////////////////////////////////////////////////////// State, constants, structs //////////////////////////////////////////////////////////////*/ @@ -43,6 +47,24 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 private constant MAX_PROTOCOL_FEE_BPS = 300; // 3% + struct TransactionRequest { + bytes32 transactionId; + address tokenAddress; + uint256 tokenAmount; + address payable forwardAddress; + address payable spenderAddress; + uint256 expirationTimestamp; + address payable developerFeeRecipient; + uint256 developerFeeBps; + bytes callData; + bytes extraData; + } + + bytes32 private constant TRANSACTION_REQUEST_TYPEHASH = + keccak256( + "TransactionRequest(bytes32 transactionId,address tokenAddress,uint256 tokenAmount,address forwardAddress,address spenderAddress,uint256 expirationTimestamp,address developerFeeRecipient,uint256 developerFeeBps,bytes callData,bytes extraData)" + ); + /*/////////////////////////////////////////////////////////////// Events //////////////////////////////////////////////////////////////*/ @@ -69,6 +91,7 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc error UniversalBridgeZeroAddress(); error UniversalBridgePaused(); error UniversalBridgeRestrictedAddress(); + error UniversalBridgeVerificationFailed(); constructor() { _disableInitializers(); @@ -136,69 +159,71 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc transactions. This function will allow us to standardize the logging and fee splitting across all providers. */ function initiateTransaction( - bytes32 transactionId, - address tokenAddress, - uint256 tokenAmount, - address payable forwardAddress, - address payable spenderAddress, - address payable developerFeeRecipient, - uint256 developerFeeBps, - bytes calldata callData, - bytes calldata extraData + TransactionRequest calldata req, + bytes calldata signature ) external payable nonReentrant onlyProxy { + // verify req + if (!_verifyTransactionReq(req, signature)) { + revert UniversalBridgeVerificationFailed(); + } + // mark the pay request as processed + _universalBridgeStorage().processed[req.transactionId] = true; + if (_universalBridgeStorage().isPaused) { revert UniversalBridgePaused(); } if ( - _universalBridgeStorage().isRestricted[forwardAddress] || - _universalBridgeStorage().isRestricted[tokenAddress] + _universalBridgeStorage().isRestricted[req.forwardAddress] || + _universalBridgeStorage().isRestricted[req.tokenAddress] ) { revert UniversalBridgeRestrictedAddress(); } // verify amount - if (tokenAmount == 0) { - revert UniversalBridgeInvalidAmount(tokenAmount); + if (req.tokenAmount == 0) { + revert UniversalBridgeInvalidAmount(req.tokenAmount); } - // mark the pay request as processed - _universalBridgeStorage().processed[transactionId] = true; - uint256 sendValue = msg.value; // includes bridge fee etc. (if any) // distribute fees - uint256 totalFeeAmount = _distributeFees(tokenAddress, tokenAmount, developerFeeRecipient, developerFeeBps); + uint256 totalFeeAmount = _distributeFees( + req.tokenAddress, + req.tokenAmount, + req.developerFeeRecipient, + req.developerFeeBps + ); - if (_isNativeToken(tokenAddress)) { + if (_isNativeToken(req.tokenAddress)) { sendValue = msg.value - totalFeeAmount; - if (sendValue < tokenAmount) { - revert UniversalBridgeMismatchedValue(tokenAmount, sendValue); + if (sendValue < req.tokenAmount) { + revert UniversalBridgeMismatchedValue(req.tokenAmount, sendValue); } - _call(forwardAddress, sendValue, callData); // calldata empty for direct transfer - } else if (callData.length == 0) { + _call(req.forwardAddress, sendValue, req.callData); // calldata empty for direct transfer + } else if (req.callData.length == 0) { if (msg.value != 0) { revert UniversalBridgeMsgValueNotZero(); } - SafeTransferLib.safeTransferFrom(tokenAddress, msg.sender, forwardAddress, tokenAmount); + SafeTransferLib.safeTransferFrom(req.tokenAddress, msg.sender, req.forwardAddress, req.tokenAmount); } else { // pull user funds - SafeTransferLib.safeTransferFrom(tokenAddress, msg.sender, address(this), tokenAmount); + SafeTransferLib.safeTransferFrom(req.tokenAddress, msg.sender, address(this), req.tokenAmount); // approve to spender address and call forward address -- both will be same in most cases - SafeTransferLib.safeApprove(tokenAddress, spenderAddress, tokenAmount); - _call(forwardAddress, sendValue, callData); + SafeTransferLib.safeApprove(req.tokenAddress, req.spenderAddress, req.tokenAmount); + _call(req.forwardAddress, sendValue, req.callData); } emit TransactionInitiated( msg.sender, - transactionId, - tokenAddress, - tokenAmount, - developerFeeRecipient, - developerFeeBps, - extraData + req.transactionId, + req.tokenAddress, + req.tokenAmount, + req.developerFeeRecipient, + req.developerFeeBps, + req.extraData ); } @@ -221,6 +246,35 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc Internal functions //////////////////////////////////////////////////////////////*/ + function _verifyTransactionReq( + TransactionRequest calldata req, + bytes calldata signature + ) private view returns (bool) { + bool processed = _universalBridgeStorage().processed[req.transactionId]; + + bytes32 structHash = keccak256( + abi.encode( + TRANSACTION_REQUEST_TYPEHASH, + req.transactionId, + req.tokenAddress, + req.tokenAmount, + req.forwardAddress, + req.spenderAddress, + req.expirationTimestamp, + req.developerFeeRecipient, + req.developerFeeBps, + keccak256(req.callData), + keccak256(req.extraData) + ) + ); + + bytes32 digest = _hashTypedData(structHash); + address recovered = digest.recover(signature); + bool valid = recovered == owner() && !processed; + + return valid; + } + function _distributeFees( address tokenAddress, uint256 tokenAmount, @@ -255,6 +309,11 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc return totalFeeAmount; } + function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { + name = "UniversalBridgeV1"; + version = "1"; + } + function _setProtocolFeeInfo(address payable feeRecipient, uint256 feeBps) internal { if (feeRecipient == address(0)) { revert UniversalBridgeZeroAddress(); diff --git a/test/UniversalBridgeV1.t.sol b/test/UniversalBridgeV1.t.sol index 88e3e73..3009966 100644 --- a/test/UniversalBridgeV1.t.sol +++ b/test/UniversalBridgeV1.t.sol @@ -44,6 +44,12 @@ contract UniversalBridgeTest is Test { uint256 internal expectedDeveloperFee; uint256 internal sendValueWithFees; + bytes32 internal typehashTransactionRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + function setUp() public { owner = payable(vm.addr(1)); protocolFeeRecipient = payable(vm.addr(2)); @@ -73,6 +79,17 @@ contract UniversalBridgeTest is Test { // fund the sender mockERC20.mint(sender, 1000 ether); vm.deal(sender, 1000 ether); + + // EIP712 + typehashTransactionRequest = keccak256( + "TransactionRequest(bytes32 transactionId,address tokenAddress,uint256 tokenAmount,address forwardAddress,address spenderAddress,uint256 expirationTimestamp,address developerFeeRecipient,uint256 developerFeeBps,bytes callData,bytes extraData)" + ); + nameHash = keccak256(bytes("UniversalBridgeV1")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(bridge))); } /*/////////////////////////////////////////////////////////////// @@ -89,6 +106,36 @@ contract UniversalBridgeTest is Test { data = abi.encode(_sender, _receiver, _token, _sendValue, _message); } + function _prepareAndSignData( + uint256 _operatorPrivateKey, + UniversalBridgeV1.TransactionRequest memory req + ) internal view returns (bytes memory signature) { + bytes memory dataToHash; + { + dataToHash = abi.encode( + typehashTransactionRequest, + req.transactionId, + req.tokenAddress, + req.tokenAmount, + req.forwardAddress, + req.spenderAddress, + req.expirationTimestamp, + req.developerFeeRecipient, + req.developerFeeBps, + keccak256(req.callData), + keccak256(req.extraData) + ); + } + + { + bytes32 _structHash = keccak256(dataToHash); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, _structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_operatorPrivateKey, typedDataHash); + + signature = abi.encodePacked(r, s, v); + } + } + /*/////////////////////////////////////////////////////////////// Test `initiateTransaction` //////////////////////////////////////////////////////////////*/ @@ -100,78 +147,26 @@ contract UniversalBridgeTest is Test { vm.prank(sender); mockERC20.approve(address(bridge), sendValueWithFees); + // create pay request + UniversalBridgeV1.TransactionRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); - // state/balances before sending transaction - uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); - uint256 developerBalanceBefore = mockERC20.balanceOf(developer); - uint256 senderBalanceBefore = mockERC20.balanceOf(sender); - uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); - - // send transaction - vm.prank(sender); - bridge.initiateTransaction( - _transactionId, - address(mockERC20), - sendValue, - payable(address(mockTarget)), - payable(address(mockTarget)), - developer, - developerFeeBps, - targetCalldata, - "" - ); - - // check balances after transaction - assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); - assertEq(mockERC20.balanceOf(developer), developerBalanceBefore + expectedDeveloperFee); - assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); - assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); - } - - function test_initiateTransaction_erc20_differentSpender() public { - bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); - - // approve amount to bridge contract - vm.prank(sender); - mockERC20.approve(address(bridge), sendValueWithFees); - - bytes32 _transactionId = keccak256("transaction ID"); - - // state/balances before sending transaction - uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); - uint256 developerBalanceBefore = mockERC20.balanceOf(developer); - uint256 senderBalanceBefore = mockERC20.balanceOf(sender); - uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); - - // send transaction - vm.prank(sender); - bridge.initiateTransaction( - _transactionId, - address(mockERC20), - sendValue, - payable(address(mockTargetNonSpender)), - payable(address(mockSpender)), - developer, - developerFeeBps, - targetCalldata, - "" + req.transactionId = _transactionId; + req.tokenAddress = address(mockERC20); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(mockTarget)); + req.spenderAddress = payable(address(mockTarget)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req ); - // check balances after transaction - assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); - assertEq(mockERC20.balanceOf(developer), developerBalanceBefore + expectedDeveloperFee); - assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); - assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); - } - - function test_initiateTransaction_erc20_directTransfer() public { - // approve amount to bridge contract - vm.prank(sender); - mockERC20.approve(address(bridge), sendValueWithFees); - - bytes32 _transactionId = keccak256("transaction ID"); - // state/balances before sending transaction uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); uint256 developerBalanceBefore = mockERC20.balanceOf(developer); @@ -180,17 +175,7 @@ contract UniversalBridgeTest is Test { // send transaction vm.prank(sender); - bridge.initiateTransaction( - _transactionId, - address(mockERC20), - sendValue, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - "", - "" - ); + bridge.initiateTransaction(req, _signature); // check balances after transaction assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); @@ -199,269 +184,382 @@ contract UniversalBridgeTest is Test { assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); } - function test_initiateTransaction_nativeToken() public { - bytes memory targetCalldata = _buildMockTargetCalldata( - sender, - receiver, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - "" - ); - - bytes32 _transactionId = keccak256("transaction ID"); - - // state/balances before sending transaction - uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; - uint256 developerBalanceBefore = developer.balance; - uint256 senderBalanceBefore = sender.balance; - uint256 receiverBalanceBefore = receiver.balance; - - // send transaction - vm.prank(sender); - bridge.initiateTransaction{ value: sendValueWithFees }( - _transactionId, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - payable(address(mockTarget)), - payable(address(mockTarget)), - developer, - developerFeeBps, - targetCalldata, - "" - ); - - // check balances after transaction - assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); - assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); - assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); - assertEq(receiver.balance, receiverBalanceBefore + sendValue); - } - - function test_initiateTransaction_nativeToken_differentSpender() public { - bytes memory targetCalldata = _buildMockTargetCalldata( - sender, - receiver, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - "" - ); - - bytes32 _transactionId = keccak256("transaction ID"); - - // state/balances before sending transaction - uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; - uint256 developerBalanceBefore = developer.balance; - uint256 senderBalanceBefore = sender.balance; - uint256 receiverBalanceBefore = receiver.balance; - - // send transaction - vm.prank(sender); - bridge.initiateTransaction{ value: sendValueWithFees }( - _transactionId, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - payable(address(mockTargetNonSpender)), - payable(address(mockSpender)), - developer, - developerFeeBps, - targetCalldata, - "" - ); - - // check balances after transaction - assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); - assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); - assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); - assertEq(receiver.balance, receiverBalanceBefore + sendValue); - } - - function test_initiateTransaction_nativeToken_directTransfer() public { - bytes memory targetCalldata = ""; - - bytes32 _transactionId = keccak256("transaction ID"); - - // state/balances before sending transaction - uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; - uint256 developerBalanceBefore = developer.balance; - uint256 senderBalanceBefore = sender.balance; - uint256 receiverBalanceBefore = receiver.balance; - - // send transaction - vm.prank(sender); - bridge.initiateTransaction{ value: sendValueWithFees }( - _transactionId, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - // true, - targetCalldata, - "" - ); - - // check balances after transaction - assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); - assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); - assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); - assertEq(receiver.balance, receiverBalanceBefore + sendValue); - } - - function test_initiateTransaction_events() public { - bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); - - // approve amount to bridge contract - vm.prank(sender); - mockERC20.approve(address(bridge), sendValueWithFees); - - bytes32 _transactionId = keccak256("transaction ID"); - - // send transaction - vm.prank(sender); - vm.expectEmit(true, true, false, true); - emit TransactionInitiated( - sender, - _transactionId, - address(mockERC20), - sendValue, - developer, - developerFeeBps, - "" - ); - bridge.initiateTransaction( - _transactionId, - address(mockERC20), - sendValue, - payable(address(mockTarget)), - payable(address(mockTarget)), - developer, - developerFeeBps, - targetCalldata, - "" - ); - } - - function test_revert_invalidAmount() public { - vm.prank(sender); - vm.expectRevert(abi.encodeWithSelector(UniversalBridgeV1.UniversalBridgeInvalidAmount.selector, 0)); - bridge.initiateTransaction( - bytes32(0), - address(mockERC20), - 0, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - "", - "" - ); - } - - function test_revert_mismatchedValue() public { - sendValueWithFees -= 1; // send less value than required - bytes memory targetCalldata = ""; - - bytes32 _transactionId = keccak256("transaction ID"); - - // send transaction - vm.prank(sender); - vm.expectRevert( - abi.encodeWithSelector(UniversalBridgeV1.UniversalBridgeMismatchedValue.selector, sendValue, sendValue - 1) - ); - bridge.initiateTransaction{ value: sendValueWithFees }( - _transactionId, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - targetCalldata, - "" - ); - } - - function test_revert_erc20_directTransfer_nonZeroMsgValue() public { - // approve amount to bridge contract - vm.prank(sender); - mockERC20.approve(address(bridge), sendValueWithFees); - - bytes32 _transactionId = keccak256("transaction ID"); - - // send transaction - vm.prank(sender); - vm.expectRevert(UniversalBridgeV1.UniversalBridgeMsgValueNotZero.selector); - bridge.initiateTransaction{ value: 1 }( // non-zero msg value - _transactionId, - address(mockERC20), - sendValue, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - // true, - "", - "" - ); - } - - function test_revert_paused() public { - vm.prank(owner); - bridge.pause(true); - - vm.prank(sender); - vm.expectRevert(UniversalBridgeV1.UniversalBridgePaused.selector); - bridge.initiateTransaction( - bytes32(0), - address(mockERC20), - 1, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - // true, - "", - "" - ); - } - - function test_revert_restrictedForwardAddress() public { - vm.prank(owner); - bridge.restrictAddress(address(receiver), true); - - vm.prank(sender); - vm.expectRevert(UniversalBridgeV1.UniversalBridgeRestrictedAddress.selector); - bridge.initiateTransaction( - bytes32(0), - address(mockERC20), - 1, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - // true, - "", - "" - ); - } - - function test_revert_restrictedTokenAddress() public { - vm.prank(owner); - bridge.restrictAddress(address(mockERC20), true); - - vm.prank(sender); - vm.expectRevert(UniversalBridgeV1.UniversalBridgeRestrictedAddress.selector); - bridge.initiateTransaction( - bytes32(0), - address(mockERC20), - 1, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - "", - "" - ); - } + // function test_initiateTransaction_erc20_differentSpender() public { + // bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); + + // // approve amount to bridge contract + // vm.prank(sender); + // mockERC20.approve(address(bridge), sendValueWithFees); + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // state/balances before sending transaction + // uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); + // uint256 developerBalanceBefore = mockERC20.balanceOf(developer); + // uint256 senderBalanceBefore = mockERC20.balanceOf(sender); + // uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); + + // // send transaction + // vm.prank(sender); + // bridge.initiateTransaction( + // _transactionId, + // address(mockERC20), + // sendValue, + // payable(address(mockTargetNonSpender)), + // payable(address(mockSpender)), + // developer, + // developerFeeBps, + // targetCalldata, + // "" + // ); + + // // check balances after transaction + // assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); + // assertEq(mockERC20.balanceOf(developer), developerBalanceBefore + expectedDeveloperFee); + // assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); + // assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); + // } + + // function test_initiateTransaction_erc20_directTransfer() public { + // // approve amount to bridge contract + // vm.prank(sender); + // mockERC20.approve(address(bridge), sendValueWithFees); + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // state/balances before sending transaction + // uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); + // uint256 developerBalanceBefore = mockERC20.balanceOf(developer); + // uint256 senderBalanceBefore = mockERC20.balanceOf(sender); + // uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); + + // // send transaction + // vm.prank(sender); + // bridge.initiateTransaction( + // _transactionId, + // address(mockERC20), + // sendValue, + // payable(address(receiver)), + // payable(address(0)), + // developer, + // developerFeeBps, + // "", + // "" + // ); + + // // check balances after transaction + // assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); + // assertEq(mockERC20.balanceOf(developer), developerBalanceBefore + expectedDeveloperFee); + // assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); + // assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); + // } + + // function test_initiateTransaction_nativeToken() public { + // bytes memory targetCalldata = _buildMockTargetCalldata( + // sender, + // receiver, + // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + // sendValue, + // "" + // ); + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // state/balances before sending transaction + // uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; + // uint256 developerBalanceBefore = developer.balance; + // uint256 senderBalanceBefore = sender.balance; + // uint256 receiverBalanceBefore = receiver.balance; + + // // send transaction + // vm.prank(sender); + // bridge.initiateTransaction{ value: sendValueWithFees }( + // _transactionId, + // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + // sendValue, + // payable(address(mockTarget)), + // payable(address(mockTarget)), + // developer, + // developerFeeBps, + // targetCalldata, + // "" + // ); + + // // check balances after transaction + // assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); + // assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); + // assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); + // assertEq(receiver.balance, receiverBalanceBefore + sendValue); + // } + + // function test_initiateTransaction_nativeToken_differentSpender() public { + // bytes memory targetCalldata = _buildMockTargetCalldata( + // sender, + // receiver, + // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + // sendValue, + // "" + // ); + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // state/balances before sending transaction + // uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; + // uint256 developerBalanceBefore = developer.balance; + // uint256 senderBalanceBefore = sender.balance; + // uint256 receiverBalanceBefore = receiver.balance; + + // // send transaction + // vm.prank(sender); + // bridge.initiateTransaction{ value: sendValueWithFees }( + // _transactionId, + // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + // sendValue, + // payable(address(mockTargetNonSpender)), + // payable(address(mockSpender)), + // developer, + // developerFeeBps, + // targetCalldata, + // "" + // ); + + // // check balances after transaction + // assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); + // assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); + // assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); + // assertEq(receiver.balance, receiverBalanceBefore + sendValue); + // } + + // function test_initiateTransaction_nativeToken_directTransfer() public { + // bytes memory targetCalldata = ""; + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // state/balances before sending transaction + // uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; + // uint256 developerBalanceBefore = developer.balance; + // uint256 senderBalanceBefore = sender.balance; + // uint256 receiverBalanceBefore = receiver.balance; + + // // send transaction + // vm.prank(sender); + // bridge.initiateTransaction{ value: sendValueWithFees }( + // _transactionId, + // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + // sendValue, + // payable(address(receiver)), + // payable(address(0)), + // developer, + // developerFeeBps, + // // true, + // targetCalldata, + // "" + // ); + + // // check balances after transaction + // assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); + // assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); + // assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); + // assertEq(receiver.balance, receiverBalanceBefore + sendValue); + // } + + // function test_initiateTransaction_events() public { + // bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); + + // // approve amount to bridge contract + // vm.prank(sender); + // mockERC20.approve(address(bridge), sendValueWithFees); + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // send transaction + // vm.prank(sender); + // vm.expectEmit(true, true, false, true); + // emit TransactionInitiated( + // sender, + // _transactionId, + // address(mockERC20), + // sendValue, + // developer, + // developerFeeBps, + // "" + // ); + // bridge.initiateTransaction( + // _transactionId, + // address(mockERC20), + // sendValue, + // payable(address(mockTarget)), + // payable(address(mockTarget)), + // developer, + // developerFeeBps, + // targetCalldata, + // "" + // ); + // } + + // function test_revert_invalidAmount() public { + // vm.prank(sender); + // vm.expectRevert(abi.encodeWithSelector(UniversalBridgeV1.UniversalBridgeInvalidAmount.selector, 0)); + // bridge.initiateTransaction( + // bytes32(0), + // address(mockERC20), + // 0, + // payable(address(receiver)), + // payable(address(0)), + // developer, + // developerFeeBps, + // "", + // "" + // ); + // } + + // function test_revert_mismatchedValue() public { + // sendValueWithFees -= 1; // send less value than required + // bytes memory targetCalldata = ""; + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // send transaction + // vm.prank(sender); + // vm.expectRevert( + // abi.encodeWithSelector(UniversalBridgeV1.UniversalBridgeMismatchedValue.selector, sendValue, sendValue - 1) + // ); + // bridge.initiateTransaction{ value: sendValueWithFees }( + // _transactionId, + // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + // sendValue, + // payable(address(receiver)), + // payable(address(0)), + // developer, + // developerFeeBps, + // targetCalldata, + // "" + // ); + // } + + // function test_revert_erc20_directTransfer_nonZeroMsgValue() public { + // // approve amount to bridge contract + // vm.prank(sender); + // mockERC20.approve(address(bridge), sendValueWithFees); + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // send transaction + // vm.prank(sender); + // vm.expectRevert(UniversalBridgeV1.UniversalBridgeMsgValueNotZero.selector); + // bridge.initiateTransaction{ value: 1 }( // non-zero msg value + // _transactionId, + // address(mockERC20), + // sendValue, + // payable(address(receiver)), + // payable(address(0)), + // developer, + // developerFeeBps, + // // true, + // "", + // "" + // ); + // } + + // function test_revert_paused() public { + // vm.prank(owner); + // bridge.pause(true); + + // vm.prank(sender); + // vm.expectRevert(UniversalBridgeV1.UniversalBridgePaused.selector); + // bridge.initiateTransaction( + // bytes32(0), + // address(mockERC20), + // 1, + // payable(address(receiver)), + // payable(address(0)), + // developer, + // developerFeeBps, + // // true, + // "", + // "" + // ); + // } + + // function test_revert_restrictedForwardAddress() public { + // vm.prank(owner); + // bridge.restrictAddress(address(receiver), true); + + // vm.prank(sender); + // vm.expectRevert(UniversalBridgeV1.UniversalBridgeRestrictedAddress.selector); + // bridge.initiateTransaction( + // bytes32(0), + // address(mockERC20), + // 1, + // payable(address(receiver)), + // payable(address(0)), + // developer, + // developerFeeBps, + // // true, + // "", + // "" + // ); + // } + + // function test_revert_restrictedTokenAddress() public { + // vm.prank(owner); + // bridge.restrictAddress(address(mockERC20), true); + + // vm.prank(sender); + // vm.expectRevert(UniversalBridgeV1.UniversalBridgeRestrictedAddress.selector); + // bridge.initiateTransaction( + // bytes32(0), + // address(mockERC20), + // 1, + // payable(address(receiver)), + // payable(address(0)), + // developer, + // developerFeeBps, + // "", + // "" + // ); + // } + + // function test_POC() public { + // // mock usdc + // MockERC20 usdc = new MockERC20("usdc", "usdc"); + // usdc.mint(sender, 100 ether); + // // approve usdc to bridge contract + // vm.prank(sender); + // usdc.approve(address(bridge), 95 ether); + + // // setup arbitrary token and malicious sender + // MockERC20 tokenU = new MockERC20("tokenU", "tokenU"); + // address initiator = payable(vm.addr(9)); + // address malicousSpender = payable(vm.addr(8)); + // tokenU.mint(initiator, 100 ether); + // // approve tokenU to bridge contract + // vm.prank(initiator); + // tokenU.approve(address(bridge), 100 ether); + + // bytes memory targetCalldata = abi.encodeWithSignature( + // "transferFrom(address,address,uint256)", + // sender, + // initiator, + // 95 ether + // ); + + // bytes32 _transactionId = keccak256("transaction ID"); + + // // send transaction + // vm.prank(initiator); + // bridge.initiateTransaction( + // _transactionId, + // address(tokenU), + // 100, + // payable(address(usdc)), + // payable(address(usdc)), + // developer, + // developerFeeBps, + // targetCalldata, + // "" + // ); + + // assertEq(usdc.balanceOf(initiator), 95 ether); + // } } From 0577392d8d8c691bdeeed2843a22357ff834dacb Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 11 Apr 2025 00:24:59 +0530 Subject: [PATCH 2/5] tests --- test/UniversalBridgeV1.t.sol | 660 +++++++++++++++++++---------------- 1 file changed, 366 insertions(+), 294 deletions(-) diff --git a/test/UniversalBridgeV1.t.sol b/test/UniversalBridgeV1.t.sol index 3009966..005a85c 100644 --- a/test/UniversalBridgeV1.t.sol +++ b/test/UniversalBridgeV1.t.sol @@ -147,7 +147,7 @@ contract UniversalBridgeTest is Test { vm.prank(sender); mockERC20.approve(address(bridge), sendValueWithFees); - // create pay request + // create transaction request UniversalBridgeV1.TransactionRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); @@ -184,341 +184,413 @@ contract UniversalBridgeTest is Test { assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); } - // function test_initiateTransaction_erc20_differentSpender() public { - // bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); + function test_initiateTransaction_erc20_differentSpender() public { + bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); - // // approve amount to bridge contract - // vm.prank(sender); - // mockERC20.approve(address(bridge), sendValueWithFees); + // approve amount to bridge contract + vm.prank(sender); + mockERC20.approve(address(bridge), sendValueWithFees); - // bytes32 _transactionId = keccak256("transaction ID"); + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); - // // state/balances before sending transaction - // uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); - // uint256 developerBalanceBefore = mockERC20.balanceOf(developer); - // uint256 senderBalanceBefore = mockERC20.balanceOf(sender); - // uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); + req.transactionId = _transactionId; + req.tokenAddress = address(mockERC20); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(mockTargetNonSpender)); + req.spenderAddress = payable(address(mockSpender)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; - // // send transaction - // vm.prank(sender); - // bridge.initiateTransaction( - // _transactionId, - // address(mockERC20), - // sendValue, - // payable(address(mockTargetNonSpender)), - // payable(address(mockSpender)), - // developer, - // developerFeeBps, - // targetCalldata, - // "" - // ); + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); - // // check balances after transaction - // assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); - // assertEq(mockERC20.balanceOf(developer), developerBalanceBefore + expectedDeveloperFee); - // assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); - // assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); - // } + // state/balances before sending transaction + uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); + uint256 developerBalanceBefore = mockERC20.balanceOf(developer); + uint256 senderBalanceBefore = mockERC20.balanceOf(sender); + uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); - // function test_initiateTransaction_erc20_directTransfer() public { - // // approve amount to bridge contract - // vm.prank(sender); - // mockERC20.approve(address(bridge), sendValueWithFees); + // send transaction + vm.prank(sender); + bridge.initiateTransaction(req, _signature); - // bytes32 _transactionId = keccak256("transaction ID"); + // check balances after transaction + assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); + assertEq(mockERC20.balanceOf(developer), developerBalanceBefore + expectedDeveloperFee); + assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); + assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); + } - // // state/balances before sending transaction - // uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); - // uint256 developerBalanceBefore = mockERC20.balanceOf(developer); - // uint256 senderBalanceBefore = mockERC20.balanceOf(sender); - // uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); + function test_initiateTransaction_erc20_directTransfer() public { + // approve amount to bridge contract + vm.prank(sender); + mockERC20.approve(address(bridge), sendValueWithFees); - // // send transaction - // vm.prank(sender); - // bridge.initiateTransaction( - // _transactionId, - // address(mockERC20), - // sendValue, - // payable(address(receiver)), - // payable(address(0)), - // developer, - // developerFeeBps, - // "", - // "" - // ); + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); - // // check balances after transaction - // assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); - // assertEq(mockERC20.balanceOf(developer), developerBalanceBefore + expectedDeveloperFee); - // assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); - // assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); - // } + req.transactionId = _transactionId; + req.tokenAddress = address(mockERC20); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(receiver)); + req.spenderAddress = payable(address(0)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; - // function test_initiateTransaction_nativeToken() public { - // bytes memory targetCalldata = _buildMockTargetCalldata( - // sender, - // receiver, - // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - // sendValue, - // "" - // ); + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); - // bytes32 _transactionId = keccak256("transaction ID"); + // state/balances before sending transaction + uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); + uint256 developerBalanceBefore = mockERC20.balanceOf(developer); + uint256 senderBalanceBefore = mockERC20.balanceOf(sender); + uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); - // // state/balances before sending transaction - // uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; - // uint256 developerBalanceBefore = developer.balance; - // uint256 senderBalanceBefore = sender.balance; - // uint256 receiverBalanceBefore = receiver.balance; + // send transaction + vm.prank(sender); + bridge.initiateTransaction(req, _signature); - // // send transaction - // vm.prank(sender); - // bridge.initiateTransaction{ value: sendValueWithFees }( - // _transactionId, - // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - // sendValue, - // payable(address(mockTarget)), - // payable(address(mockTarget)), - // developer, - // developerFeeBps, - // targetCalldata, - // "" - // ); + // check balances after transaction + assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); + assertEq(mockERC20.balanceOf(developer), developerBalanceBefore + expectedDeveloperFee); + assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); + assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); + } - // // check balances after transaction - // assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); - // assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); - // assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); - // assertEq(receiver.balance, receiverBalanceBefore + sendValue); - // } + function test_initiateTransaction_nativeToken() public { + bytes memory targetCalldata = _buildMockTargetCalldata( + sender, + receiver, + address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + sendValue, + "" + ); - // function test_initiateTransaction_nativeToken_differentSpender() public { - // bytes memory targetCalldata = _buildMockTargetCalldata( - // sender, - // receiver, - // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - // sendValue, - // "" - // ); + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); - // bytes32 _transactionId = keccak256("transaction ID"); + req.transactionId = _transactionId; + req.tokenAddress = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(mockTarget)); + req.spenderAddress = payable(address(mockTarget)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; - // // state/balances before sending transaction - // uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; - // uint256 developerBalanceBefore = developer.balance; - // uint256 senderBalanceBefore = sender.balance; - // uint256 receiverBalanceBefore = receiver.balance; + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); - // // send transaction - // vm.prank(sender); - // bridge.initiateTransaction{ value: sendValueWithFees }( - // _transactionId, - // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - // sendValue, - // payable(address(mockTargetNonSpender)), - // payable(address(mockSpender)), - // developer, - // developerFeeBps, - // targetCalldata, - // "" - // ); + // state/balances before sending transaction + uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; + uint256 developerBalanceBefore = developer.balance; + uint256 senderBalanceBefore = sender.balance; + uint256 receiverBalanceBefore = receiver.balance; - // // check balances after transaction - // assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); - // assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); - // assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); - // assertEq(receiver.balance, receiverBalanceBefore + sendValue); - // } + // send transaction + vm.prank(sender); + bridge.initiateTransaction{ value: sendValueWithFees }(req, _signature); - // function test_initiateTransaction_nativeToken_directTransfer() public { - // bytes memory targetCalldata = ""; + // check balances after transaction + assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); + assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); + assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); + assertEq(receiver.balance, receiverBalanceBefore + sendValue); + } - // bytes32 _transactionId = keccak256("transaction ID"); + function test_initiateTransaction_nativeToken_differentSpender() public { + bytes memory targetCalldata = _buildMockTargetCalldata( + sender, + receiver, + address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + sendValue, + "" + ); - // // state/balances before sending transaction - // uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; - // uint256 developerBalanceBefore = developer.balance; - // uint256 senderBalanceBefore = sender.balance; - // uint256 receiverBalanceBefore = receiver.balance; + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); - // // send transaction - // vm.prank(sender); - // bridge.initiateTransaction{ value: sendValueWithFees }( - // _transactionId, - // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - // sendValue, - // payable(address(receiver)), - // payable(address(0)), - // developer, - // developerFeeBps, - // // true, - // targetCalldata, - // "" - // ); + req.transactionId = _transactionId; + req.tokenAddress = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(mockTargetNonSpender)); + req.spenderAddress = payable(address(mockSpender)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; - // // check balances after transaction - // assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); - // assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); - // assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); - // assertEq(receiver.balance, receiverBalanceBefore + sendValue); - // } + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); - // function test_initiateTransaction_events() public { - // bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); + // state/balances before sending transaction + uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; + uint256 developerBalanceBefore = developer.balance; + uint256 senderBalanceBefore = sender.balance; + uint256 receiverBalanceBefore = receiver.balance; - // // approve amount to bridge contract - // vm.prank(sender); - // mockERC20.approve(address(bridge), sendValueWithFees); + // send transaction + vm.prank(sender); + bridge.initiateTransaction{ value: sendValueWithFees }(req, _signature); - // bytes32 _transactionId = keccak256("transaction ID"); + // check balances after transaction + assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); + assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); + assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); + assertEq(receiver.balance, receiverBalanceBefore + sendValue); + } - // // send transaction - // vm.prank(sender); - // vm.expectEmit(true, true, false, true); - // emit TransactionInitiated( - // sender, - // _transactionId, - // address(mockERC20), - // sendValue, - // developer, - // developerFeeBps, - // "" - // ); - // bridge.initiateTransaction( - // _transactionId, - // address(mockERC20), - // sendValue, - // payable(address(mockTarget)), - // payable(address(mockTarget)), - // developer, - // developerFeeBps, - // targetCalldata, - // "" - // ); - // } + function test_initiateTransaction_nativeToken_directTransfer() public { + bytes memory targetCalldata = ""; - // function test_revert_invalidAmount() public { - // vm.prank(sender); - // vm.expectRevert(abi.encodeWithSelector(UniversalBridgeV1.UniversalBridgeInvalidAmount.selector, 0)); - // bridge.initiateTransaction( - // bytes32(0), - // address(mockERC20), - // 0, - // payable(address(receiver)), - // payable(address(0)), - // developer, - // developerFeeBps, - // "", - // "" - // ); - // } + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); - // function test_revert_mismatchedValue() public { - // sendValueWithFees -= 1; // send less value than required - // bytes memory targetCalldata = ""; + req.transactionId = _transactionId; + req.tokenAddress = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(receiver)); + req.spenderAddress = payable(address(0)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; - // bytes32 _transactionId = keccak256("transaction ID"); + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); - // // send transaction - // vm.prank(sender); - // vm.expectRevert( - // abi.encodeWithSelector(UniversalBridgeV1.UniversalBridgeMismatchedValue.selector, sendValue, sendValue - 1) - // ); - // bridge.initiateTransaction{ value: sendValueWithFees }( - // _transactionId, - // address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - // sendValue, - // payable(address(receiver)), - // payable(address(0)), - // developer, - // developerFeeBps, - // targetCalldata, - // "" - // ); - // } + // state/balances before sending transaction + uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; + uint256 developerBalanceBefore = developer.balance; + uint256 senderBalanceBefore = sender.balance; + uint256 receiverBalanceBefore = receiver.balance; - // function test_revert_erc20_directTransfer_nonZeroMsgValue() public { - // // approve amount to bridge contract - // vm.prank(sender); - // mockERC20.approve(address(bridge), sendValueWithFees); + // send transaction + vm.prank(sender); + bridge.initiateTransaction{ value: sendValueWithFees }(req, _signature); - // bytes32 _transactionId = keccak256("transaction ID"); + // check balances after transaction + assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); + assertEq(developer.balance, developerBalanceBefore + expectedDeveloperFee); + assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); + assertEq(receiver.balance, receiverBalanceBefore + sendValue); + } - // // send transaction - // vm.prank(sender); - // vm.expectRevert(UniversalBridgeV1.UniversalBridgeMsgValueNotZero.selector); - // bridge.initiateTransaction{ value: 1 }( // non-zero msg value - // _transactionId, - // address(mockERC20), - // sendValue, - // payable(address(receiver)), - // payable(address(0)), - // developer, - // developerFeeBps, - // // true, - // "", - // "" - // ); - // } + function test_initiateTransaction_events() public { + bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); - // function test_revert_paused() public { - // vm.prank(owner); - // bridge.pause(true); + // approve amount to bridge contract + vm.prank(sender); + mockERC20.approve(address(bridge), sendValueWithFees); - // vm.prank(sender); - // vm.expectRevert(UniversalBridgeV1.UniversalBridgePaused.selector); - // bridge.initiateTransaction( - // bytes32(0), - // address(mockERC20), - // 1, - // payable(address(receiver)), - // payable(address(0)), - // developer, - // developerFeeBps, - // // true, - // "", - // "" - // ); - // } + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); - // function test_revert_restrictedForwardAddress() public { - // vm.prank(owner); - // bridge.restrictAddress(address(receiver), true); + req.transactionId = _transactionId; + req.tokenAddress = address(mockERC20); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(mockTarget)); + req.spenderAddress = payable(address(mockTarget)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; - // vm.prank(sender); - // vm.expectRevert(UniversalBridgeV1.UniversalBridgeRestrictedAddress.selector); - // bridge.initiateTransaction( - // bytes32(0), - // address(mockERC20), - // 1, - // payable(address(receiver)), - // payable(address(0)), - // developer, - // developerFeeBps, - // // true, - // "", - // "" - // ); - // } + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); - // function test_revert_restrictedTokenAddress() public { - // vm.prank(owner); - // bridge.restrictAddress(address(mockERC20), true); + // send transaction + vm.prank(sender); + vm.expectEmit(true, true, false, true); + emit TransactionInitiated( + sender, + _transactionId, + address(mockERC20), + sendValue, + developer, + developerFeeBps, + "" + ); + bridge.initiateTransaction(req, _signature); + } - // vm.prank(sender); - // vm.expectRevert(UniversalBridgeV1.UniversalBridgeRestrictedAddress.selector); - // bridge.initiateTransaction( - // bytes32(0), - // address(mockERC20), - // 1, - // payable(address(receiver)), - // payable(address(0)), - // developer, - // developerFeeBps, - // "", - // "" - // ); - // } + function test_revert_invalidAmount() public { + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); + + req.transactionId = _transactionId; + req.tokenAddress = address(mockERC20); + req.tokenAmount = 0; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); + + vm.prank(sender); + vm.expectRevert(abi.encodeWithSelector(UniversalBridgeV1.UniversalBridgeInvalidAmount.selector, 0)); + bridge.initiateTransaction(req, _signature); + } + + function test_revert_mismatchedValue() public { + sendValueWithFees -= 1; // send less value than required + bytes memory targetCalldata = ""; + + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); + + req.transactionId = _transactionId; + req.tokenAddress = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(receiver)); + req.spenderAddress = payable(address(0)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); + + // send transaction + vm.prank(sender); + vm.expectRevert( + abi.encodeWithSelector(UniversalBridgeV1.UniversalBridgeMismatchedValue.selector, sendValue, sendValue - 1) + ); + bridge.initiateTransaction{ value: sendValueWithFees }(req, _signature); + } + + function test_revert_erc20_directTransfer_nonZeroMsgValue() public { + // approve amount to bridge contract + vm.prank(sender); + mockERC20.approve(address(bridge), sendValueWithFees); + + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); + + req.transactionId = _transactionId; + req.tokenAddress = address(mockERC20); + req.tokenAmount = sendValue; + req.forwardAddress = payable(address(receiver)); + req.spenderAddress = payable(address(0)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = ""; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); + + // send transaction + vm.prank(sender); + vm.expectRevert(UniversalBridgeV1.UniversalBridgeMsgValueNotZero.selector); + bridge.initiateTransaction{ value: 1 }(req, _signature); // non-zero msg value + } + + function test_revert_paused() public { + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); + + req.transactionId = _transactionId; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); + + vm.prank(owner); + bridge.pause(true); + + vm.prank(sender); + vm.expectRevert(UniversalBridgeV1.UniversalBridgePaused.selector); + bridge.initiateTransaction(req, _signature); + } + + function test_revert_restrictedForwardAddress() public { + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); + + req.transactionId = _transactionId; + req.forwardAddress = payable(address(receiver)); + + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); + + vm.prank(owner); + bridge.restrictAddress(address(receiver), true); + + vm.prank(sender); + vm.expectRevert(UniversalBridgeV1.UniversalBridgeRestrictedAddress.selector); + bridge.initiateTransaction(req, _signature); + } + + function test_revert_restrictedTokenAddress() public { + // create transaction request + UniversalBridgeV1.TransactionRequest memory req; + bytes32 _transactionId = keccak256("transaction ID"); + + req.transactionId = _transactionId; + req.tokenAddress = address(mockERC20); + req.forwardAddress = payable(address(receiver)); + + // generate signature + bytes memory _signature = _prepareAndSignData( + 1, // sign with operator private key + req + ); + + vm.prank(owner); + bridge.restrictAddress(address(mockERC20), true); + + vm.prank(sender); + vm.expectRevert(UniversalBridgeV1.UniversalBridgeRestrictedAddress.selector); + bridge.initiateTransaction(req, _signature); + } // function test_POC() public { // // mock usdc From 6d03a4ec87aa85e0673ca38168ba969cd97683f4 Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 11 Apr 2025 00:28:36 +0530 Subject: [PATCH 3/5] check expiration timestamp --- src/UniversalBridgeV1.sol | 5 +++++ test/UniversalBridgeV1.t.sol | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/UniversalBridgeV1.sol b/src/UniversalBridgeV1.sol index a7fad14..7139aca 100644 --- a/src/UniversalBridgeV1.sol +++ b/src/UniversalBridgeV1.sol @@ -92,6 +92,7 @@ contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, Ownable, R error UniversalBridgePaused(); error UniversalBridgeRestrictedAddress(); error UniversalBridgeVerificationFailed(); + error UniversalBridgeRequestExpired(uint256 expirationTimestamp); constructor() { _disableInitializers(); @@ -250,6 +251,10 @@ contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, Ownable, R TransactionRequest calldata req, bytes calldata signature ) private view returns (bool) { + if (req.expirationTimestamp < block.timestamp) { + revert UniversalBridgeRequestExpired(req.expirationTimestamp); + } + bool processed = _universalBridgeStorage().processed[req.transactionId]; bytes32 structHash = keccak256( diff --git a/test/UniversalBridgeV1.t.sol b/test/UniversalBridgeV1.t.sol index 005a85c..76b5174 100644 --- a/test/UniversalBridgeV1.t.sol +++ b/test/UniversalBridgeV1.t.sol @@ -451,6 +451,7 @@ contract UniversalBridgeTest is Test { req.transactionId = _transactionId; req.tokenAddress = address(mockERC20); req.tokenAmount = 0; + req.expirationTimestamp = 1000; // generate signature bytes memory _signature = _prepareAndSignData( @@ -532,6 +533,7 @@ contract UniversalBridgeTest is Test { bytes32 _transactionId = keccak256("transaction ID"); req.transactionId = _transactionId; + req.expirationTimestamp = 1000; // generate signature bytes memory _signature = _prepareAndSignData( @@ -554,6 +556,7 @@ contract UniversalBridgeTest is Test { req.transactionId = _transactionId; req.forwardAddress = payable(address(receiver)); + req.expirationTimestamp = 1000; // generate signature bytes memory _signature = _prepareAndSignData( @@ -577,6 +580,7 @@ contract UniversalBridgeTest is Test { req.transactionId = _transactionId; req.tokenAddress = address(mockERC20); req.forwardAddress = payable(address(receiver)); + req.expirationTimestamp = 1000; // generate signature bytes memory _signature = _prepareAndSignData( From e73ae84fa331fef1735e56e816203f05bd364dbe Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 11 Apr 2025 00:39:24 +0530 Subject: [PATCH 4/5] operator role --- src/UniversalBridgeProxy.sol | 4 +++- src/UniversalBridgeV1.sol | 9 ++++++--- test/UniversalBridgeV1.t.sol | 30 ++++++++++++++++-------------- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/UniversalBridgeProxy.sol b/src/UniversalBridgeProxy.sol index 776edb3..2d647ce 100644 --- a/src/UniversalBridgeProxy.sol +++ b/src/UniversalBridgeProxy.sol @@ -13,6 +13,7 @@ contract UniversalBridgeProxy { constructor( address _implementation, address _owner, + address _operator, address payable _protocolFeeRecipient, uint256 _protocolFeeBps ) { @@ -33,8 +34,9 @@ contract UniversalBridgeProxy { } bytes memory data = abi.encodeWithSignature( - "initialize(address,address,uint256)", + "initialize(address,address,address,uint256)", _owner, + _operator, _protocolFeeRecipient, _protocolFeeBps ); diff --git a/src/UniversalBridgeV1.sol b/src/UniversalBridgeV1.sol index 7139aca..c6c103c 100644 --- a/src/UniversalBridgeV1.sol +++ b/src/UniversalBridgeV1.sol @@ -7,7 +7,7 @@ import { EIP712 } from "lib/solady/src/utils/EIP712.sol"; import { SafeTransferLib } from "lib/solady/src/utils/SafeTransferLib.sol"; import { ReentrancyGuard } from "lib/solady/src/utils/ReentrancyGuard.sol"; import { ECDSA } from "lib/solady/src/utils/ECDSA.sol"; -import { Ownable } from "lib/solady/src/auth/Ownable.sol"; +import { OwnableRoles } from "lib/solady/src/auth/OwnableRoles.sol"; import { UUPSUpgradeable } from "lib/solady/src/utils/UUPSUpgradeable.sol"; import { Initializable } from "lib/solady/src/utils/Initializable.sol"; @@ -37,7 +37,7 @@ library UniversalBridgeStorage { } } -contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, Ownable, ReentrancyGuard { +contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, OwnableRoles, ReentrancyGuard { using ECDSA for bytes32; /*/////////////////////////////////////////////////////////////// @@ -46,6 +46,7 @@ contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, Ownable, R address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 private constant MAX_PROTOCOL_FEE_BPS = 300; // 3% + uint256 private constant _OPERATOR_ROLE = 1 << 0; struct TransactionRequest { bytes32 transactionId; @@ -100,10 +101,12 @@ contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, Ownable, R function initialize( address _owner, + address _operator, address payable _protocolFeeRecipient, uint256 _protocolFeeBps ) external initializer { _initializeOwner(_owner); + _grantRoles(_operator, _OPERATOR_ROLE); _setProtocolFeeInfo(_protocolFeeRecipient, _protocolFeeBps); } @@ -275,7 +278,7 @@ contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, Ownable, R bytes32 digest = _hashTypedData(structHash); address recovered = digest.recover(signature); - bool valid = recovered == owner() && !processed; + bool valid = hasAllRoles(recovered, _OPERATOR_ROLE) && !processed; return valid; } diff --git a/test/UniversalBridgeV1.t.sol b/test/UniversalBridgeV1.t.sol index 76b5174..e412bbe 100644 --- a/test/UniversalBridgeV1.t.sol +++ b/test/UniversalBridgeV1.t.sol @@ -35,6 +35,7 @@ contract UniversalBridgeTest is Test { address payable internal sender; address payable internal receiver; address payable internal developer; + address internal operator; uint256 internal protocolFeeBps; uint256 internal developerFeeBps; @@ -56,6 +57,7 @@ contract UniversalBridgeTest is Test { sender = payable(vm.addr(3)); receiver = payable(vm.addr(4)); developer = payable(vm.addr(5)); + operator = payable(vm.addr(6)); protocolFeeBps = 30; // 0.3% developerFeeBps = 10; // 0.1% @@ -68,7 +70,7 @@ contract UniversalBridgeTest is Test { // deploy impl and proxy address impl = address(new UniversalBridgeV1()); bridge = UniversalBridgeV1( - address(new UniversalBridgeProxy(impl, owner, protocolFeeRecipient, protocolFeeBps)) + address(new UniversalBridgeProxy(impl, owner, operator, protocolFeeRecipient, protocolFeeBps)) ); mockERC20 = new MockERC20("Token", "TKN"); @@ -163,7 +165,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -207,7 +209,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -248,7 +250,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -294,7 +296,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -340,7 +342,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -380,7 +382,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -424,7 +426,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -455,7 +457,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -484,7 +486,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -517,7 +519,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -537,7 +539,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -560,7 +562,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); @@ -584,7 +586,7 @@ contract UniversalBridgeTest is Test { // generate signature bytes memory _signature = _prepareAndSignData( - 1, // sign with operator private key + 6, // sign with operator private key req ); From 348a4ff1cdcea9f8932f3fd48c451c06edd0d0ba Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 11 Apr 2025 01:44:23 +0530 Subject: [PATCH 5/5] revert if already processed --- src/UniversalBridgeV1.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/UniversalBridgeV1.sol b/src/UniversalBridgeV1.sol index c6c103c..b17b7f1 100644 --- a/src/UniversalBridgeV1.sol +++ b/src/UniversalBridgeV1.sol @@ -94,6 +94,7 @@ contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, OwnableRol error UniversalBridgeRestrictedAddress(); error UniversalBridgeVerificationFailed(); error UniversalBridgeRequestExpired(uint256 expirationTimestamp); + error UniversalBridgeTransactionAlreadyProcessed(); constructor() { _disableInitializers(); @@ -260,6 +261,10 @@ contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, OwnableRol bool processed = _universalBridgeStorage().processed[req.transactionId]; + if (processed) { + revert UniversalBridgeTransactionAlreadyProcessed(); + } + bytes32 structHash = keccak256( abi.encode( TRANSACTION_REQUEST_TYPEHASH, @@ -278,7 +283,7 @@ contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, OwnableRol bytes32 digest = _hashTypedData(structHash); address recovered = digest.recover(signature); - bool valid = hasAllRoles(recovered, _OPERATOR_ROLE) && !processed; + bool valid = hasAllRoles(recovered, _OPERATOR_ROLE); return valid; }