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 6d1d895..b17b7f1 100644 --- a/src/UniversalBridgeV1.sol +++ b/src/UniversalBridgeV1.sol @@ -3,9 +3,11 @@ 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 { Ownable } from "lib/solady/src/auth/Ownable.sol"; +import { ECDSA } from "lib/solady/src/utils/ECDSA.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"; @@ -35,13 +37,34 @@ library UniversalBridgeStorage { } } -contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, ReentrancyGuard { +contract UniversalBridgeV1 is EIP712, Initializable, UUPSUpgradeable, OwnableRoles, ReentrancyGuard { + using ECDSA for bytes32; + /*/////////////////////////////////////////////////////////////// State, constants, structs //////////////////////////////////////////////////////////////*/ 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; + 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 +92,9 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc error UniversalBridgeZeroAddress(); error UniversalBridgePaused(); error UniversalBridgeRestrictedAddress(); + error UniversalBridgeVerificationFailed(); + error UniversalBridgeRequestExpired(uint256 expirationTimestamp); + error UniversalBridgeTransactionAlreadyProcessed(); constructor() { _disableInitializers(); @@ -76,10 +102,12 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc function initialize( address _owner, + address _operator, address payable _protocolFeeRecipient, uint256 _protocolFeeBps ) external initializer { _initializeOwner(_owner); + _grantRoles(_operator, _OPERATOR_ROLE); _setProtocolFeeInfo(_protocolFeeRecipient, _protocolFeeBps); } @@ -136,69 +164,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 +251,43 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc Internal functions //////////////////////////////////////////////////////////////*/ + function _verifyTransactionReq( + 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]; + + if (processed) { + revert UniversalBridgeTransactionAlreadyProcessed(); + } + + 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 = hasAllRoles(recovered, _OPERATOR_ROLE); + + return valid; + } + function _distributeFees( address tokenAddress, uint256 tokenAmount, @@ -255,6 +322,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..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; @@ -44,12 +45,19 @@ 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)); 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% @@ -62,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"); @@ -73,6 +81,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 +108,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,8 +149,26 @@ contract UniversalBridgeTest is Test { 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(mockTarget)); + req.spenderAddress = payable(address(mockTarget)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + // state/balances before sending transaction uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); uint256 developerBalanceBefore = mockERC20.balanceOf(developer); @@ -110,17 +177,7 @@ contract UniversalBridgeTest is Test { // send transaction vm.prank(sender); - bridge.initiateTransaction( - _transactionId, - address(mockERC20), - sendValue, - payable(address(mockTarget)), - payable(address(mockTarget)), - developer, - developerFeeBps, - targetCalldata, - "" - ); + bridge.initiateTransaction(req, _signature); // check balances after transaction assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); @@ -136,8 +193,26 @@ contract UniversalBridgeTest is Test { 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(mockTargetNonSpender)); + req.spenderAddress = payable(address(mockSpender)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + // state/balances before sending transaction uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); uint256 developerBalanceBefore = mockERC20.balanceOf(developer); @@ -146,17 +221,7 @@ contract UniversalBridgeTest is Test { // send transaction vm.prank(sender); - bridge.initiateTransaction( - _transactionId, - address(mockERC20), - sendValue, - payable(address(mockTargetNonSpender)), - payable(address(mockSpender)), - developer, - developerFeeBps, - targetCalldata, - "" - ); + bridge.initiateTransaction(req, _signature); // check balances after transaction assertEq(mockERC20.balanceOf(protocolFeeRecipient), protocolFeeRecipientBalanceBefore + expectedProtocolFee); @@ -170,8 +235,25 @@ contract UniversalBridgeTest is Test { 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; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + // state/balances before sending transaction uint256 protocolFeeRecipientBalanceBefore = mockERC20.balanceOf(protocolFeeRecipient); uint256 developerBalanceBefore = mockERC20.balanceOf(developer); @@ -180,17 +262,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); @@ -208,8 +280,26 @@ contract UniversalBridgeTest is Test { "" ); + // 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(mockTarget)); + req.spenderAddress = payable(address(mockTarget)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + // state/balances before sending transaction uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; uint256 developerBalanceBefore = developer.balance; @@ -218,17 +308,7 @@ contract UniversalBridgeTest is Test { // send transaction vm.prank(sender); - bridge.initiateTransaction{ value: sendValueWithFees }( - _transactionId, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - payable(address(mockTarget)), - payable(address(mockTarget)), - developer, - developerFeeBps, - targetCalldata, - "" - ); + bridge.initiateTransaction{ value: sendValueWithFees }(req, _signature); // check balances after transaction assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); @@ -246,8 +326,26 @@ contract UniversalBridgeTest is Test { "" ); + // 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(mockTargetNonSpender)); + req.spenderAddress = payable(address(mockSpender)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + // state/balances before sending transaction uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; uint256 developerBalanceBefore = developer.balance; @@ -256,17 +354,7 @@ contract UniversalBridgeTest is Test { // send transaction vm.prank(sender); - bridge.initiateTransaction{ value: sendValueWithFees }( - _transactionId, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - payable(address(mockTargetNonSpender)), - payable(address(mockSpender)), - developer, - developerFeeBps, - targetCalldata, - "" - ); + bridge.initiateTransaction{ value: sendValueWithFees }(req, _signature); // check balances after transaction assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); @@ -278,8 +366,26 @@ contract UniversalBridgeTest is Test { function test_initiateTransaction_nativeToken_directTransfer() public { 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( + 6, // sign with operator private key + req + ); + // state/balances before sending transaction uint256 protocolFeeRecipientBalanceBefore = protocolFeeRecipient.balance; uint256 developerBalanceBefore = developer.balance; @@ -288,18 +394,7 @@ contract UniversalBridgeTest is Test { // send transaction vm.prank(sender); - bridge.initiateTransaction{ value: sendValueWithFees }( - _transactionId, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - payable(address(receiver)), - payable(address(0)), - developer, - developerFeeBps, - // true, - targetCalldata, - "" - ); + bridge.initiateTransaction{ value: sendValueWithFees }(req, _signature); // check balances after transaction assertEq(protocolFeeRecipient.balance, protocolFeeRecipientBalanceBefore + expectedProtocolFee); @@ -315,8 +410,26 @@ contract UniversalBridgeTest is Test { 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(mockTarget)); + req.spenderAddress = payable(address(mockTarget)); + req.expirationTimestamp = 1000; + req.developerFeeRecipient = developer; + req.developerFeeBps = developerFeeBps; + req.callData = targetCalldata; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + // send transaction vm.prank(sender); vm.expectEmit(true, true, false, true); @@ -329,57 +442,60 @@ contract UniversalBridgeTest is Test { developerFeeBps, "" ); - bridge.initiateTransaction( - _transactionId, - address(mockERC20), - sendValue, - payable(address(mockTarget)), - payable(address(mockTarget)), - developer, - developerFeeBps, - targetCalldata, - "" - ); + bridge.initiateTransaction(req, _signature); } 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; + req.expirationTimestamp = 1000; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + 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, - "", - "" - ); + 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( + 6, // 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, - "" - ); + bridge.initiateTransaction{ value: sendValueWithFees }(req, _signature); } function test_revert_erc20_directTransfer_nonZeroMsgValue() public { @@ -387,81 +503,141 @@ contract UniversalBridgeTest is Test { 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( + 6, // sign with operator private key + req + ); + // 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, - "", - "" - ); + 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; + req.expirationTimestamp = 1000; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + 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, - "", - "" - ); + 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)); + req.expirationTimestamp = 1000; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + 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, - "", - "" - ); + 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)); + req.expirationTimestamp = 1000; + + // generate signature + bytes memory _signature = _prepareAndSignData( + 6, // sign with operator private key + req + ); + 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, - "", - "" - ); + bridge.initiateTransaction(req, _signature); } + + // 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); + // } }