diff --git a/src/ModularPaymentsGateway.sol b/src/PayGateway.sol similarity index 93% rename from src/ModularPaymentsGateway.sol rename to src/PayGateway.sol index 5254471..d1c3655 100644 --- a/src/ModularPaymentsGateway.sol +++ b/src/PayGateway.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.22; import { ModularCore } from "lib/modular-contracts/src/ModularCore.sol"; import { Initializable } from "lib/solady/src/utils/Initializable.sol"; -contract ModularPaymentsGateway is ModularCore, Initializable { +contract PayGateway is ModularCore, Initializable { constructor() { _disableInitializers(); } diff --git a/src/PaymentsGatewayExtension.sol b/src/PayGatewayExtension.sol similarity index 91% rename from src/PaymentsGatewayExtension.sol rename to src/PayGatewayExtension.sol index 1a02418..8375bcb 100644 --- a/src/PaymentsGatewayExtension.sol +++ b/src/PayGatewayExtension.sol @@ -10,7 +10,7 @@ import { ECDSA } from "lib/solady/src/utils/ECDSA.sol"; import { ModularExtension } from "lib/modular-contracts/src/ModularExtension.sol"; import { Ownable } from "lib/solady/src/auth/Ownable.sol"; -library PaymentsGatewayExtensionStorage { +library PayGatewayExtensionStorage { /// @custom:storage-location erc7201:payments.gateway.extension bytes32 public constant PAYMENTS_GATEWAY_EXTENSION_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256("payments.gateway.extension")) - 1)) & ~bytes32(uint256(0xff)); @@ -28,7 +28,7 @@ library PaymentsGatewayExtensionStorage { } } -contract PaymentsGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { +contract PayGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { using ECDSA for bytes32; /*/////////////////////////////////////////////////////////////// @@ -113,11 +113,11 @@ contract PaymentsGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { Errors //////////////////////////////////////////////////////////////*/ - error PaymentsGatewayMismatchedValue(uint256 expected, uint256 actual); - error PaymentsGatewayInvalidAmount(uint256 amount); - error PaymentsGatewayVerificationFailed(); - error PaymentsGatewayFailedToForward(); - error PaymentsGatewayRequestExpired(uint256 expirationTimestamp); + error PayGatewayMismatchedValue(uint256 expected, uint256 actual); + error PayGatewayInvalidAmount(uint256 amount); + error PayGatewayVerificationFailed(); + error PayGatewayFailedToForward(); + error PayGatewayRequestExpired(uint256 expirationTimestamp); /*////////////////////////////////////////////////////////////// EXTENSION CONFIG @@ -178,27 +178,27 @@ contract PaymentsGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { function initiateTokenPurchase(PayRequest calldata req, bytes calldata signature) external payable nonReentrant { // verify amount if (req.tokenAmount == 0) { - revert PaymentsGatewayInvalidAmount(req.tokenAmount); + revert PayGatewayInvalidAmount(req.tokenAmount); } // verify expiration timestamp if (req.expirationTimestamp < block.timestamp) { - revert PaymentsGatewayRequestExpired(req.expirationTimestamp); + revert PayGatewayRequestExpired(req.expirationTimestamp); } // verify data if (!_verifyTransferStart(req, signature)) { - revert PaymentsGatewayVerificationFailed(); + revert PayGatewayVerificationFailed(); } if (_isTokenNative(req.tokenAddress)) { if (msg.value < req.tokenAmount) { - revert PaymentsGatewayMismatchedValue(req.tokenAmount, msg.value); + revert PayGatewayMismatchedValue(req.tokenAmount, msg.value); } } // mark the pay request as processed - PaymentsGatewayExtensionStorage.data().processed[req.transactionId] = true; + PayGatewayExtensionStorage.data().processed[req.transactionId] = true; // distribute fees uint256 totalFeeAmount = _distributeFees(req.tokenAddress, req.payouts); @@ -209,7 +209,7 @@ contract PaymentsGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { sendValue = msg.value - totalFeeAmount; if (sendValue < req.tokenAmount) { - revert PaymentsGatewayMismatchedValue(sendValue, req.tokenAmount); + revert PayGatewayMismatchedValue(sendValue, req.tokenAmount); } } @@ -229,7 +229,7 @@ contract PaymentsGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { revert(add(32, response), returndata_size) } } else { - revert PaymentsGatewayFailedToForward(); + revert PayGatewayFailedToForward(); } } } @@ -256,12 +256,12 @@ contract PaymentsGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { address payable receiverAddress ) external payable nonReentrant { if (tokenAmount == 0) { - revert PaymentsGatewayInvalidAmount(tokenAmount); + revert PayGatewayInvalidAmount(tokenAmount); } if (_isTokenNative(tokenAddress)) { if (msg.value < tokenAmount) { - revert PaymentsGatewayMismatchedValue(tokenAmount, msg.value); + revert PayGatewayMismatchedValue(tokenAmount, msg.value); } } @@ -280,7 +280,7 @@ contract PaymentsGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { //////////////////////////////////////////////////////////////*/ function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { - name = "PaymentsGateway"; + name = "PayGateway"; version = "1"; } @@ -323,7 +323,7 @@ contract PaymentsGatewayExtension is EIP712, ModularExtension, ReentrancyGuard { } function _verifyTransferStart(PayRequest calldata req, bytes calldata signature) private view returns (bool) { - bool processed = PaymentsGatewayExtensionStorage.data().processed[req.transactionId]; + bool processed = PayGatewayExtensionStorage.data().processed[req.transactionId]; bytes32 payoutsHash = _hashPayoutInfo(req.payouts); bytes32 structHash = keccak256( diff --git a/src/PaymentsGateway.sol b/src/PaymentsGateway.sol deleted file mode 100644 index 7536cae..0000000 --- a/src/PaymentsGateway.sol +++ /dev/null @@ -1,332 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.22; - -/// @author thirdweb - -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import { EIP712 } from "./utils/EIP712.sol"; - -import { SafeTransferLib } from "./lib/SafeTransferLib.sol"; -import { ECDSA } from "./lib/ECDSA.sol"; - -contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard { - using ECDSA for bytes32; - - /*/////////////////////////////////////////////////////////////// - State, constants, structs - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant PAYOUTINFO_TYPEHASH = - keccak256("PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)"); - bytes32 private constant REQUEST_TYPEHASH = - keccak256( - "PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)" - ); - address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - - /// @dev Mapping from pay request UID => whether the pay request is processed. - mapping(bytes32 => bool) private processed; - - /** - * @notice Info of fee payout recipients. - * - * @param clientId ClientId of fee recipient - * @param payoutAddress Recipient address - * @param feeBPS The fee basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%) - */ - struct PayoutInfo { - bytes32 clientId; - address payable payoutAddress; - uint256 feeBPS; - } - - /** - * @notice The body of a request to purchase tokens. - * - * @param clientId Thirdweb clientId for logging attribution data - * @param transactionId Acts as a uid and a key to lookup associated swap provider - * @param tokenAddress Address of the currency used for purchase - * @param tokenAmount Currency amount being sent - * @param expirationTimestamp The unix timestamp at which the request expires - * @param payouts Array of Payout struct - containing fee recipients' info - * @param forwardAddress Address of swap provider contract - * @param data Calldata for swap provider - */ - struct PayRequest { - bytes32 clientId; - bytes32 transactionId; - address tokenAddress; - uint256 tokenAmount; - uint256 expirationTimestamp; - PayoutInfo[] payouts; - address payable forwardAddress; - bytes data; - } - - /*/////////////////////////////////////////////////////////////// - Events - //////////////////////////////////////////////////////////////*/ - - event TokenPurchaseInitiated( - bytes32 indexed clientId, - address indexed sender, - bytes32 transactionId, - address tokenAddress, - uint256 tokenAmount - ); - - event TokenPurchaseCompleted( - bytes32 indexed clientId, - address indexed receiver, - bytes32 transactionId, - address tokenAddress, - uint256 tokenAmount - ); - - event FeePayout( - bytes32 indexed clientId, - address indexed sender, - address payoutAddress, - address tokenAddress, - uint256 feeAmount, - uint256 feeBPS - ); - - /*/////////////////////////////////////////////////////////////// - Errors - //////////////////////////////////////////////////////////////*/ - - error PaymentsGatewayMismatchedValue(uint256 expected, uint256 actual); - error PaymentsGatewayInvalidAmount(uint256 amount); - error PaymentsGatewayVerificationFailed(); - error PaymentsGatewayFailedToForward(); - error PaymentsGatewayRequestExpired(uint256 expirationTimestamp); - - /*/////////////////////////////////////////////////////////////// - Constructor - //////////////////////////////////////////////////////////////*/ - - constructor(address contractOwner) Ownable(contractOwner) {} - - /*/////////////////////////////////////////////////////////////// - External / public functions - //////////////////////////////////////////////////////////////*/ - - /// @notice some bridges may refund need a way to get funds back to user - function withdrawTo( - address tokenAddress, - uint256 tokenAmount, - address payable receiver - ) public onlyOwner nonReentrant { - if (_isTokenERC20(tokenAddress)) { - SafeTransferLib.safeTransferFrom(tokenAddress, address(this), receiver, tokenAmount); - } else { - SafeTransferLib.safeTransferETH(receiver, tokenAmount); - } - } - - function withdraw(address tokenAddress, uint256 tokenAmount) external onlyOwner nonReentrant { - withdrawTo(tokenAddress, tokenAmount, payable(msg.sender)); - } - - /** - @notice - The purpose of initiateTokenPurchase is to be the entrypoint for all thirdweb pay swap / bridge - transactions. This function will allow us to standardize the logging and fee splitting across all providers. - - Requirements: - 1. Verify the parameters are the same parameters sent from thirdweb pay service by requiring a backend signature - 2. Log transfer start allowing us to link onchain and offchain data - 3. distribute the fees to all the payees (thirdweb, developer, swap provider (?)) - 4. forward the user funds to the swap provider (forwardAddress) - */ - - function initiateTokenPurchase(PayRequest calldata req, bytes calldata signature) external payable nonReentrant { - // verify amount - if (req.tokenAmount == 0) { - revert PaymentsGatewayInvalidAmount(req.tokenAmount); - } - - // verify expiration timestamp - if (req.expirationTimestamp < block.timestamp) { - revert PaymentsGatewayRequestExpired(req.expirationTimestamp); - } - - // verify data - if (!_verifyTransferStart(req, signature)) { - revert PaymentsGatewayVerificationFailed(); - } - - if (_isTokenNative(req.tokenAddress)) { - if (msg.value < req.tokenAmount) { - revert PaymentsGatewayMismatchedValue(req.tokenAmount, msg.value); - } - } - - // mark the pay request as processed - processed[req.transactionId] = true; - - // distribute fees - uint256 totalFeeAmount = _distributeFees(req.tokenAddress, req.tokenAmount, req.payouts); - - // determine native value to send - uint256 sendValue = msg.value; // includes bridge fee etc. (if any) - if (_isTokenNative(req.tokenAddress)) { - sendValue = msg.value - totalFeeAmount; - - if (sendValue < req.tokenAmount) { - revert PaymentsGatewayMismatchedValue(sendValue, req.tokenAmount); - } - } - - if (_isTokenERC20(req.tokenAddress)) { - // pull user funds - SafeTransferLib.safeTransferFrom(req.tokenAddress, msg.sender, address(this), req.tokenAmount); - SafeTransferLib.safeApprove(req.tokenAddress, req.forwardAddress, req.tokenAmount); - } - - { - (bool success, bytes memory response) = req.forwardAddress.call{ value: sendValue }(req.data); - if (!success) { - // If there is return data, the delegate call reverted with a reason or a custom error, which we bubble up. - if (response.length > 0) { - assembly { - let returndata_size := mload(response) - revert(add(32, response), returndata_size) - } - } else { - revert PaymentsGatewayFailedToForward(); - } - } - } - - emit TokenPurchaseInitiated(req.clientId, msg.sender, req.transactionId, req.tokenAddress, req.tokenAmount); - } - - /** - @notice - The purpose of completeTokenPurchase is to provide a forwarding contract call - on the destination chain. For some swap providers, they can only guarantee the toAmount - if we use a contract call. This allows us to call the endTransfer function and forward the - funds to the end user. - - Requirements: - 1. Log the transfer end - 2. forward the user funds - */ - function completeTokenPurchase( - bytes32 clientId, - bytes32 transactionId, - address tokenAddress, - uint256 tokenAmount, - address payable receiverAddress - ) external payable nonReentrant { - if (tokenAmount == 0) { - revert PaymentsGatewayInvalidAmount(tokenAmount); - } - - if (_isTokenNative(tokenAddress)) { - if (msg.value < tokenAmount) { - revert PaymentsGatewayMismatchedValue(tokenAmount, msg.value); - } - } - - // pull user funds - if (_isTokenERC20(tokenAddress)) { - SafeTransferLib.safeTransferFrom(tokenAddress, msg.sender, receiverAddress, tokenAmount); - } else { - SafeTransferLib.safeTransferETH(receiverAddress, tokenAmount); - } - - emit TokenPurchaseCompleted(clientId, receiverAddress, transactionId, tokenAddress, tokenAmount); - } - - /*/////////////////////////////////////////////////////////////// - Internal functions - //////////////////////////////////////////////////////////////*/ - - function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { - name = "PaymentsGateway"; - version = "1"; - } - - function _hashPayoutInfo(PayoutInfo[] calldata payouts) private pure returns (bytes32) { - bytes32[] memory payoutsHashes = new bytes32[](payouts.length); - for (uint i = 0; i < payouts.length; i++) { - payoutsHashes[i] = keccak256( - abi.encode(PAYOUTINFO_TYPEHASH, payouts[i].clientId, payouts[i].payoutAddress, payouts[i].feeBPS) - ); - } - return keccak256(abi.encodePacked(payoutsHashes)); - } - - function _distributeFees( - address tokenAddress, - uint256 tokenAmount, - PayoutInfo[] calldata payouts - ) private returns (uint256) { - uint256 totalFeeAmount = 0; - - for (uint32 payeeIdx = 0; payeeIdx < payouts.length; payeeIdx++) { - uint256 feeAmount = _calculateFee(tokenAmount, payouts[payeeIdx].feeBPS); - totalFeeAmount += feeAmount; - - emit FeePayout( - payouts[payeeIdx].clientId, - msg.sender, - payouts[payeeIdx].payoutAddress, - tokenAddress, - feeAmount, - payouts[payeeIdx].feeBPS - ); - if (_isTokenNative(tokenAddress)) { - SafeTransferLib.safeTransferETH(payouts[payeeIdx].payoutAddress, feeAmount); - } else { - SafeTransferLib.safeTransferFrom(tokenAddress, msg.sender, payouts[payeeIdx].payoutAddress, feeAmount); - } - } - - if (totalFeeAmount > tokenAmount) { - revert PaymentsGatewayMismatchedValue(totalFeeAmount, tokenAmount); - } - return totalFeeAmount; - } - - function _verifyTransferStart(PayRequest calldata req, bytes calldata signature) private view returns (bool) { - bytes32 payoutsHash = _hashPayoutInfo(req.payouts); - bytes32 structHash = keccak256( - abi.encode( - REQUEST_TYPEHASH, - req.clientId, - req.transactionId, - req.tokenAddress, - req.tokenAmount, - req.expirationTimestamp, - payoutsHash, - req.forwardAddress, - keccak256(req.data) - ) - ); - - bytes32 digest = _hashTypedData(structHash); - address recovered = digest.recover(signature); - bool valid = recovered == owner() && !processed[req.transactionId]; - - return valid; - } - - function _isTokenERC20(address tokenAddress) private pure returns (bool) { - return tokenAddress != NATIVE_TOKEN_ADDRESS; - } - - function _isTokenNative(address tokenAddress) private pure returns (bool) { - return tokenAddress == NATIVE_TOKEN_ADDRESS; - } - - function _calculateFee(uint256 amount, uint256 feeBPS) private pure returns (uint256) { - uint256 feeAmount = (amount * feeBPS) / 10_000; - return feeAmount; - } -} diff --git a/test/ModularPaymentsGateway.t.sol b/test/PayGateway.t.sol similarity index 89% rename from test/ModularPaymentsGateway.t.sol rename to test/PayGateway.t.sol index 7212eaf..1465bd9 100644 --- a/test/ModularPaymentsGateway.t.sol +++ b/test/PayGateway.t.sol @@ -3,15 +3,15 @@ pragma solidity ^0.8.0; import { Test, console } from "forge-std/Test.sol"; -import { ModularPaymentsGateway } from "src/ModularPaymentsGateway.sol"; -import { PaymentsGatewayExtension } from "src/PaymentsGatewayExtension.sol"; +import { PayGateway } from "src/PayGateway.sol"; +import { PayGatewayExtension } from "src/PayGatewayExtension.sol"; import { IExtensionConfig } from "lib/modular-contracts/src/interface/IExtensionConfig.sol"; import { IModularCore } from "lib/modular-contracts/src/interface/IModularCore.sol"; import { LibClone } from "lib/solady/src/utils/LibClone.sol"; import { MockERC20 } from "./utils/MockERC20.sol"; import { MockTarget } from "./utils/MockTarget.sol"; -contract ModularPaymentsGatewayTest is Test { +contract PayGatewayTest is Test { event TokenPurchaseInitiated( bytes32 indexed clientId, address indexed sender, @@ -39,7 +39,7 @@ contract ModularPaymentsGatewayTest is Test { event OperatorChanged(address indexed previousOperator, address indexed newOperator); - PaymentsGatewayExtension internal gateway; + PayGatewayExtension internal gateway; MockERC20 internal mockERC20; MockTarget internal mockTarget; @@ -56,7 +56,7 @@ contract ModularPaymentsGatewayTest is Test { uint256 internal clientFeeAmount; uint256 internal totalFeeAmount; - PaymentsGatewayExtension.PayoutInfo[] internal payouts; + PayGatewayExtension.PayoutInfo[] internal payouts; bytes32 internal typehashPayRequest; bytes32 internal typehashPayoutInfo; @@ -79,16 +79,16 @@ contract ModularPaymentsGatewayTest is Test { clientFeeAmount = 10; // deploy and install extension - address impl = address(new ModularPaymentsGateway()); - address extension = address(new PaymentsGatewayExtension()); + address impl = address(new PayGateway()); + address extension = address(new PayGatewayExtension()); address[] memory extensions = new address[](1); bytes[] memory extensionData = new bytes[](1); extensions[0] = address(extension); extensionData[0] = ""; - gateway = PaymentsGatewayExtension(LibClone.clone(impl)); - ModularPaymentsGateway(payable(address(gateway))).initialize(operator, extensions, extensionData); + gateway = PayGatewayExtension(LibClone.clone(impl)); + PayGateway(payable(address(gateway))).initialize(operator, extensions, extensionData); mockERC20 = new MockERC20("Token", "TKN"); mockTarget = new MockTarget(); @@ -99,18 +99,10 @@ contract ModularPaymentsGatewayTest is Test { // build payout info payouts.push( - PaymentsGatewayExtension.PayoutInfo({ - clientId: ownerClientId, - payoutAddress: owner, - feeAmount: ownerFeeAmount - }) + PayGatewayExtension.PayoutInfo({ clientId: ownerClientId, payoutAddress: owner, feeAmount: ownerFeeAmount }) ); payouts.push( - PaymentsGatewayExtension.PayoutInfo({ - clientId: clientId, - payoutAddress: client, - feeAmount: clientFeeAmount - }) + PayGatewayExtension.PayoutInfo({ clientId: clientId, payoutAddress: client, feeAmount: clientFeeAmount }) ); for (uint256 i = 0; i < payouts.length; i++) { @@ -122,7 +114,7 @@ contract ModularPaymentsGatewayTest is Test { typehashPayRequest = keccak256( "PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeAmount)" ); - nameHash = keccak256(bytes("PaymentsGateway")); + nameHash = keccak256(bytes("PayGateway")); versionHash = keccak256(bytes("1")); typehashEip712 = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" @@ -144,7 +136,7 @@ contract ModularPaymentsGatewayTest is Test { data = abi.encode(_sender, _receiver, _token, _sendValue, _message); } - function _hashPayoutInfo(PaymentsGatewayExtension.PayoutInfo[] memory _payouts) private view returns (bytes32) { + function _hashPayoutInfo(PayGatewayExtension.PayoutInfo[] memory _payouts) private view returns (bytes32) { bytes32 payoutHash = typehashPayoutInfo; bytes32[] memory payoutsHashes = new bytes32[](_payouts.length); @@ -158,7 +150,7 @@ contract ModularPaymentsGatewayTest is Test { function _prepareAndSignData( uint256 _operatorPrivateKey, - PaymentsGatewayExtension.PayRequest memory req + PayGatewayExtension.PayRequest memory req ) internal view returns (bytes memory signature) { bytes memory dataToHash; { @@ -199,7 +191,7 @@ contract ModularPaymentsGatewayTest is Test { mockERC20.approve(address(gateway), sendValueWithFees); // create pay request - PaymentsGatewayExtension.PayRequest memory req; + PayGatewayExtension.PayRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); req.clientId = clientId; @@ -246,7 +238,7 @@ contract ModularPaymentsGatewayTest is Test { ); // create pay request - PaymentsGatewayExtension.PayRequest memory req; + PayGatewayExtension.PayRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); req.clientId = clientId; @@ -297,7 +289,7 @@ contract ModularPaymentsGatewayTest is Test { mockERC20.approve(address(gateway), sendValueWithFees); // create pay request - PaymentsGatewayExtension.PayRequest memory req; + PayGatewayExtension.PayRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); req.clientId = clientId; @@ -332,7 +324,7 @@ contract ModularPaymentsGatewayTest is Test { mockERC20.approve(address(gateway), sendValueWithFees); // create pay request - PaymentsGatewayExtension.PayRequest memory req; + PayGatewayExtension.PayRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); req.clientId = clientId; @@ -352,7 +344,7 @@ contract ModularPaymentsGatewayTest is Test { // send transaction vm.prank(sender); - vm.expectRevert(abi.encodeWithSelector(PaymentsGatewayExtension.PaymentsGatewayVerificationFailed.selector)); + vm.expectRevert(abi.encodeWithSelector(PayGatewayExtension.PayGatewayVerificationFailed.selector)); gateway.initiateTokenPurchase(req, _signature); } @@ -361,7 +353,7 @@ contract ModularPaymentsGatewayTest is Test { bytes memory targetCalldata = ""; // create pay request - PaymentsGatewayExtension.PayRequest memory req; + PayGatewayExtension.PayRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); req.clientId = clientId; @@ -380,10 +372,7 @@ contract ModularPaymentsGatewayTest is Test { // send transaction vm.prank(sender); vm.expectRevert( - abi.encodeWithSelector( - PaymentsGatewayExtension.PaymentsGatewayRequestExpired.selector, - req.expirationTimestamp - ) + abi.encodeWithSelector(PayGatewayExtension.PayGatewayRequestExpired.selector, req.expirationTimestamp) ); gateway.initiateTokenPurchase(req, _signature); } diff --git a/test/PaymentsGateway.t.sol b/test/PaymentsGateway.t.sol deleted file mode 100644 index 068cfd7..0000000 --- a/test/PaymentsGateway.t.sol +++ /dev/null @@ -1,435 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import { Test, console, console2 } from "forge-std/Test.sol"; -import { PaymentsGateway } from "src/PaymentsGateway.sol"; -import { MockERC20 } from "./utils/MockERC20.sol"; -import { MockTarget } from "./utils/MockTarget.sol"; - -contract PaymentsGatewayTest is Test { - event TokenPurchaseInitiated( - bytes32 indexed clientId, - address indexed sender, - bytes32 transactionId, - address tokenAddress, - uint256 tokenAmount - ); - - event TokenPurchaseCompleted( - bytes32 indexed clientId, - address indexed receiver, - bytes32 transactionId, - address tokenAddress, - uint256 tokenAmount - ); - - event FeePayout( - bytes32 indexed clientId, - address indexed sender, - address payoutAddress, - address tokenAddress, - uint256 feeAmount, - uint256 feeBPS - ); - - event OperatorChanged(address indexed previousOperator, address indexed newOperator); - - PaymentsGateway internal gateway; - MockERC20 internal mockERC20; - MockTarget internal mockTarget; - - address payable internal owner; - address payable internal operator; - address payable internal sender; - address payable internal receiver; - address payable internal client; - - bytes32 internal ownerClientId; - bytes32 internal clientId; - - uint256 internal ownerFeeBps; - uint256 internal clientFeeBps; - uint256 internal totalFeeBps; - - PaymentsGateway.PayoutInfo[] internal payouts; - - bytes32 internal typehashPayRequest; - bytes32 internal typehashPayoutInfo; - bytes32 internal nameHash; - bytes32 internal versionHash; - bytes32 internal typehashEip712; - bytes32 internal domainSeparator; - - function setUp() public { - owner = payable(vm.addr(1)); - operator = payable(vm.addr(2)); - sender = payable(vm.addr(3)); - receiver = payable(vm.addr(4)); - client = payable(vm.addr(5)); - - ownerClientId = keccak256("owner"); - clientId = keccak256("client"); - - ownerFeeBps = 200; - clientFeeBps = 100; - - gateway = new PaymentsGateway(operator); - mockERC20 = new MockERC20("Token", "TKN"); - mockTarget = new MockTarget(); - - // fund the sender - mockERC20.mint(sender, 10 ether); - vm.deal(sender, 10 ether); - - // build payout info - payouts.push( - PaymentsGateway.PayoutInfo({ clientId: ownerClientId, payoutAddress: owner, feeBPS: ownerFeeBps }) - ); - payouts.push(PaymentsGateway.PayoutInfo({ clientId: clientId, payoutAddress: client, feeBPS: clientFeeBps })); - - // console.logBytes32(clientId); - // console.log(client); - // console.log(clientFeeBps); - console.log(address(gateway)); - for (uint256 i = 0; i < payouts.length; i++) { - totalFeeBps += payouts[i].feeBPS; - } - - // EIP712 - typehashPayoutInfo = keccak256("PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)"); - typehashPayRequest = keccak256( - "PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)" - ); - nameHash = keccak256(bytes("PaymentsGateway")); - 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(gateway))); - } - - /*/////////////////////////////////////////////////////////////// - internal util functions - //////////////////////////////////////////////////////////////*/ - - function _buildMockTargetCalldata( - address _sender, - address _receiver, - address _token, - uint256 _sendValue, - string memory _message - ) internal pure returns (bytes memory data) { - data = abi.encode(_sender, _receiver, _token, _sendValue, _message); - } - - function _hashPayoutInfo(PaymentsGateway.PayoutInfo[] memory _payouts) private view returns (bytes32) { - bytes32 payoutHash = typehashPayoutInfo; - - bytes32[] memory payoutsHashes = new bytes32[](_payouts.length); - for (uint i = 0; i < payouts.length; i++) { - payoutsHashes[i] = keccak256( - abi.encode(payoutHash, _payouts[i].clientId, _payouts[i].payoutAddress, _payouts[i].feeBPS) - ); - } - return keccak256(abi.encodePacked(payoutsHashes)); - } - - function _prepareAndSignData( - uint256 _operatorPrivateKey, - PaymentsGateway.PayRequest memory req - ) internal view returns (bytes memory signature) { - bytes memory dataToHash; - { - bytes32 _payoutsHash = _hashPayoutInfo(req.payouts); - dataToHash = abi.encode( - typehashPayRequest, - req.clientId, - req.transactionId, - req.tokenAddress, - req.tokenAmount, - req.expirationTimestamp, - _payoutsHash, - req.forwardAddress, - keccak256(req.data) - ); - } - - { - 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 `initiateTokenPurchase` - //////////////////////////////////////////////////////////////*/ - - function test_initiateTokenPurchase_erc20() public { - uint256 sendValue = 1 ether; - uint256 sendValueWithFees = sendValue + (sendValue * totalFeeBps) / 10_000; - bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); - - // approve amount to gateway contract - vm.prank(sender); - mockERC20.approve(address(gateway), sendValueWithFees); - - // create pay request - PaymentsGateway.PayRequest memory req; - bytes32 _transactionId = keccak256("transaction ID"); - - req.clientId = clientId; - req.transactionId = _transactionId; - req.tokenAddress = address(mockERC20); - req.tokenAmount = sendValue; - req.forwardAddress = payable(address(mockTarget)); - req.expirationTimestamp = 1000; - req.data = targetCalldata; - req.payouts = payouts; - - // generate signature - bytes memory _signature = _prepareAndSignData( - 2, // sign with operator private key, i.e. 2 - req - ); - - // state/balances before sending transaction - uint256 ownerBalanceBefore = mockERC20.balanceOf(owner); - uint256 clientBalanceBefore = mockERC20.balanceOf(client); - uint256 senderBalanceBefore = mockERC20.balanceOf(sender); - uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); - - // send transaction - vm.prank(sender); - gateway.initiateTokenPurchase(req, _signature); - - // check balances after transaction - assertEq(mockERC20.balanceOf(owner), ownerBalanceBefore + (sendValue * ownerFeeBps) / 10_000); - assertEq(mockERC20.balanceOf(client), clientBalanceBefore + (sendValue * clientFeeBps) / 10_000); - assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValueWithFees); - assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); - } - - function test_initiateTokenPurchase_nativeToken() public { - uint256 sendValue = 1 ether; - uint256 sendValueWithFees = sendValue + (sendValue * totalFeeBps) / 10_000; - bytes memory targetCalldata = _buildMockTargetCalldata( - sender, - receiver, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - "" - ); - - // create pay request - PaymentsGateway.PayRequest memory req; - bytes32 _transactionId = keccak256("transaction ID"); - - req.clientId = clientId; - req.transactionId = _transactionId; - req.tokenAddress = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); - req.tokenAmount = sendValue; - req.forwardAddress = payable(address(mockTarget)); - req.expirationTimestamp = 1000; - req.data = targetCalldata; - req.payouts = payouts; - - console.logBytes32(clientId); - console.logBytes32(_transactionId); - console.log(sendValue); - console.log(address(mockTarget)); - console.logBytes(targetCalldata); - - // generate signature - bytes memory _signature = _prepareAndSignData( - 2, // sign with operator private key, i.e. 2 - req - ); - - console.logBytes(_signature); - console.log(address(uint160(gateway._cachedThis()))); - - // state/balances before sending transaction - uint256 ownerBalanceBefore = owner.balance; - uint256 clientBalanceBefore = client.balance; - uint256 senderBalanceBefore = sender.balance; - uint256 receiverBalanceBefore = receiver.balance; - - // send transaction - vm.prank(sender); - gateway.initiateTokenPurchase{ value: sendValueWithFees }(req, _signature); - - // check balances after transaction - assertEq(owner.balance, ownerBalanceBefore + (sendValue * ownerFeeBps) / 10_000); - assertEq(client.balance, clientBalanceBefore + (sendValue * clientFeeBps) / 10_000); - assertEq(sender.balance, senderBalanceBefore - sendValueWithFees); - assertEq(receiver.balance, receiverBalanceBefore + sendValue); - } - - function test_initiateTokenPurchase_events() public { - uint256 sendValue = 1 ether; - uint256 sendValueWithFees = sendValue + (sendValue * totalFeeBps) / 10_000; - bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); - - // approve amount to gateway contract - vm.prank(sender); - mockERC20.approve(address(gateway), sendValueWithFees); - - // create pay request - PaymentsGateway.PayRequest memory req; - bytes32 _transactionId = keccak256("transaction ID"); - - req.clientId = clientId; - req.transactionId = _transactionId; - req.tokenAddress = address(mockERC20); - req.tokenAmount = sendValue; - req.forwardAddress = payable(address(mockTarget)); - req.expirationTimestamp = 1000; - req.data = targetCalldata; - req.payouts = payouts; - - // generate signature - bytes memory _signature = _prepareAndSignData( - 2, // sign with operator private key, i.e. 2 - req - ); - - // send transaction - vm.prank(sender); - vm.expectEmit(true, true, false, true); - emit TokenPurchaseInitiated(req.clientId, sender, _transactionId, req.tokenAddress, req.tokenAmount); - gateway.initiateTokenPurchase(req, _signature); - } - - function test_revert_initiateTokenPurchase_invalidSignature() public { - uint256 sendValue = 1 ether; - uint256 sendValueWithFees = sendValue + (sendValue * totalFeeBps) / 10_000; - bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); - - // approve amount to gateway contract - vm.prank(sender); - mockERC20.approve(address(gateway), sendValueWithFees); - - // create pay request - PaymentsGateway.PayRequest memory req; - bytes32 _transactionId = keccak256("transaction ID"); - - req.clientId = clientId; - req.transactionId = _transactionId; - req.tokenAddress = address(mockERC20); - req.tokenAmount = sendValue; - req.forwardAddress = payable(address(mockTarget)); - req.expirationTimestamp = 1000; - req.data = targetCalldata; - req.payouts = payouts; - - // generate signature - bytes memory _signature = _prepareAndSignData( - 123, // sign with random private key - req - ); - - // send transaction - vm.prank(sender); - vm.expectRevert(abi.encodeWithSelector(PaymentsGateway.PaymentsGatewayVerificationFailed.selector)); - gateway.initiateTokenPurchase(req, _signature); - } - - function test_revert_initiateTokenPurchase_requestExpired() public { - uint256 sendValue = 1 ether; - bytes memory targetCalldata = ""; - - // create pay request - PaymentsGateway.PayRequest memory req; - bytes32 _transactionId = keccak256("transaction ID"); - - req.clientId = clientId; - req.transactionId = _transactionId; - req.tokenAddress = address(mockERC20); - req.tokenAmount = sendValue; - req.forwardAddress = payable(address(mockTarget)); - req.expirationTimestamp = 1000; - req.data = targetCalldata; - req.payouts = payouts; - - // generate signature - bytes memory _signature = _prepareAndSignData(2, req); - - vm.warp(req.expirationTimestamp + 1); - // send transaction - vm.prank(sender); - vm.expectRevert( - abi.encodeWithSelector(PaymentsGateway.PaymentsGatewayRequestExpired.selector, req.expirationTimestamp) - ); - gateway.initiateTokenPurchase(req, _signature); - } - - // /*/////////////////////////////////////////////////////////////// - // Test `completeTokenPurchase` - // //////////////////////////////////////////////////////////////*/ - - function test_completeTokenPurchase_erc20() public { - uint256 sendValue = 1 ether; - - // approve amount to gateway contract - vm.prank(sender); - mockERC20.approve(address(gateway), sendValue); - - // state/balances before sending transaction - uint256 ownerBalanceBefore = mockERC20.balanceOf(owner); - uint256 senderBalanceBefore = mockERC20.balanceOf(sender); - uint256 receiverBalanceBefore = mockERC20.balanceOf(receiver); - - // send transaction - bytes32 _transactionId = keccak256("transaction ID"); - vm.prank(sender); - gateway.completeTokenPurchase(clientId, _transactionId, address(mockERC20), sendValue, receiver); - - // check balances after transaction - assertEq(mockERC20.balanceOf(owner), ownerBalanceBefore); - assertEq(mockERC20.balanceOf(sender), senderBalanceBefore - sendValue); - assertEq(mockERC20.balanceOf(receiver), receiverBalanceBefore + sendValue); - } - - function test_completeTokenPurchase_nativeToken() public { - uint256 sendValue = 1 ether; - - // state/balances before sending transaction - uint256 ownerBalanceBefore = owner.balance; - uint256 senderBalanceBefore = sender.balance; - uint256 receiverBalanceBefore = receiver.balance; - - // send transaction - bytes32 _transactionId = keccak256("transaction ID"); - vm.prank(sender); - gateway.completeTokenPurchase{ value: sendValue }( - clientId, - _transactionId, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - receiver - ); - - // check balances after transaction - assertEq(owner.balance, ownerBalanceBefore); - assertEq(sender.balance, senderBalanceBefore - sendValue); - assertEq(receiver.balance, receiverBalanceBefore + sendValue); - } - - function test_completeTokenPurchase_events() public { - uint256 sendValue = 1 ether; - - // approve amount to gateway contract - vm.prank(sender); - mockERC20.approve(address(gateway), sendValue); - - // send transaction - bytes32 _transactionId = keccak256("transaction ID"); - vm.prank(sender); - vm.expectEmit(true, true, false, true); - emit TokenPurchaseCompleted(clientId, receiver, _transactionId, address(mockERC20), sendValue); - gateway.completeTokenPurchase(clientId, _transactionId, address(mockERC20), sendValue, receiver); - } -} diff --git a/test/benchmarks/BenchmarkModularPaymentsGateway.t.sol b/test/benchmarks/BenchmarkPayGateway.t.sol similarity index 84% rename from test/benchmarks/BenchmarkModularPaymentsGateway.t.sol rename to test/benchmarks/BenchmarkPayGateway.t.sol index bb398de..d0933b0 100644 --- a/test/benchmarks/BenchmarkModularPaymentsGateway.t.sol +++ b/test/benchmarks/BenchmarkPayGateway.t.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.13; import { Test, console } from "forge-std/Test.sol"; -import { ModularPaymentsGateway } from "src/ModularPaymentsGateway.sol"; -import { PaymentsGatewayExtension } from "src/PaymentsGatewayExtension.sol"; +import { PayGateway } from "src/PayGateway.sol"; +import { PayGatewayExtension } from "src/PayGatewayExtension.sol"; import { LibClone } from "lib/solady/src/utils/LibClone.sol"; import { MockERC20 } from "../utils/MockERC20.sol"; import { MockTarget } from "../utils/MockTarget.sol"; -contract BenchmarkModularPaymentsGatewayTest is Test { - PaymentsGatewayExtension internal gateway; +contract BenchmarkPayGatewayTest is Test { + PayGatewayExtension internal gateway; MockERC20 internal mockERC20; MockTarget internal mockTarget; @@ -26,7 +26,7 @@ contract BenchmarkModularPaymentsGatewayTest is Test { uint256 internal clientFeeAmount; uint256 internal totalFeeAmount; - PaymentsGatewayExtension.PayoutInfo[] internal payouts; + PayGatewayExtension.PayoutInfo[] internal payouts; bytes32 internal typehashPayRequest; bytes32 internal typehashPayoutInfo; @@ -49,16 +49,16 @@ contract BenchmarkModularPaymentsGatewayTest is Test { clientFeeAmount = 10; // deploy and install extension - address impl = address(new ModularPaymentsGateway()); - address extension = address(new PaymentsGatewayExtension()); + address impl = address(new PayGateway()); + address extension = address(new PayGatewayExtension()); address[] memory extensions = new address[](1); bytes[] memory extensionData = new bytes[](1); extensions[0] = address(extension); extensionData[0] = ""; - gateway = PaymentsGatewayExtension(LibClone.clone(impl)); - ModularPaymentsGateway(payable(address(gateway))).initialize(operator, extensions, extensionData); + gateway = PayGatewayExtension(LibClone.clone(impl)); + PayGateway(payable(address(gateway))).initialize(operator, extensions, extensionData); mockERC20 = new MockERC20("Token", "TKN"); mockTarget = new MockTarget(); @@ -69,18 +69,10 @@ contract BenchmarkModularPaymentsGatewayTest is Test { // build payout info payouts.push( - PaymentsGatewayExtension.PayoutInfo({ - clientId: ownerClientId, - payoutAddress: owner, - feeAmount: ownerFeeAmount - }) + PayGatewayExtension.PayoutInfo({ clientId: ownerClientId, payoutAddress: owner, feeAmount: ownerFeeAmount }) ); payouts.push( - PaymentsGatewayExtension.PayoutInfo({ - clientId: clientId, - payoutAddress: client, - feeAmount: clientFeeAmount - }) + PayGatewayExtension.PayoutInfo({ clientId: clientId, payoutAddress: client, feeAmount: clientFeeAmount }) ); for (uint256 i = 0; i < payouts.length; i++) { @@ -92,7 +84,7 @@ contract BenchmarkModularPaymentsGatewayTest is Test { typehashPayRequest = keccak256( "PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeAmount)" ); - nameHash = keccak256(bytes("PaymentsGateway")); + nameHash = keccak256(bytes("PayGateway")); versionHash = keccak256(bytes("1")); typehashEip712 = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" @@ -114,7 +106,7 @@ contract BenchmarkModularPaymentsGatewayTest is Test { data = abi.encode(_sender, _receiver, _token, _sendValue, _message); } - function _hashPayoutInfo(PaymentsGatewayExtension.PayoutInfo[] memory _payouts) private view returns (bytes32) { + function _hashPayoutInfo(PayGatewayExtension.PayoutInfo[] memory _payouts) private view returns (bytes32) { bytes32 payoutHash = typehashPayoutInfo; bytes32[] memory payoutsHashes = new bytes32[](_payouts.length); @@ -128,7 +120,7 @@ contract BenchmarkModularPaymentsGatewayTest is Test { function _prepareAndSignData( uint256 _operatorPrivateKey, - PaymentsGatewayExtension.PayRequest memory req + PayGatewayExtension.PayRequest memory req ) internal view returns (bytes memory signature) { bytes memory dataToHash; { @@ -170,7 +162,7 @@ contract BenchmarkModularPaymentsGatewayTest is Test { mockERC20.approve(address(gateway), sendValueWithFees); // create pay request - PaymentsGatewayExtension.PayRequest memory req; + PayGatewayExtension.PayRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); req.clientId = clientId; @@ -207,7 +199,7 @@ contract BenchmarkModularPaymentsGatewayTest is Test { ); // create pay request - PaymentsGatewayExtension.PayRequest memory req; + PayGatewayExtension.PayRequest memory req; bytes32 _transactionId = keccak256("transaction ID"); req.clientId = clientId; diff --git a/test/benchmarks/BenchmarkPaymentsGateway.t.sol b/test/benchmarks/BenchmarkPaymentsGateway.t.sol deleted file mode 100644 index f5d45ce..0000000 --- a/test/benchmarks/BenchmarkPaymentsGateway.t.sol +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import { Test, console, console2 } from "forge-std/Test.sol"; -import { PaymentsGateway } from "src/PaymentsGateway.sol"; -import { MockERC20 } from "../utils/MockERC20.sol"; -import { MockTarget } from "../utils/MockTarget.sol"; - -contract BenchmarkPaymentsGatewayTest is Test { - PaymentsGateway internal gateway; - MockERC20 internal mockERC20; - MockTarget internal mockTarget; - - address payable internal owner; - address payable internal operator; - address payable internal sender; - address payable internal receiver; - address payable internal client; - - bytes32 internal ownerClientId; - bytes32 internal clientId; - - uint256 internal ownerFeeBps; - uint256 internal clientFeeBps; - uint256 internal totalFeeBps; - - PaymentsGateway.PayoutInfo[] internal payouts; - - bytes32 internal typehashPayRequest; - bytes32 internal typehashPayoutInfo; - bytes32 internal nameHash; - bytes32 internal versionHash; - bytes32 internal typehashEip712; - bytes32 internal domainSeparator; - - function setUp() public { - owner = payable(vm.addr(1)); - operator = payable(vm.addr(2)); - sender = payable(vm.addr(3)); - receiver = payable(vm.addr(4)); - client = payable(vm.addr(5)); - - ownerClientId = keccak256("owner"); - clientId = keccak256("client"); - - ownerFeeBps = 200; - clientFeeBps = 100; - - gateway = new PaymentsGateway(operator); - mockERC20 = new MockERC20("Token", "TKN"); - mockTarget = new MockTarget(); - - // fund the sender - mockERC20.mint(sender, 10 ether); - vm.deal(sender, 10 ether); - - // build payout info - payouts.push( - PaymentsGateway.PayoutInfo({ clientId: ownerClientId, payoutAddress: owner, feeBPS: ownerFeeBps }) - ); - payouts.push(PaymentsGateway.PayoutInfo({ clientId: clientId, payoutAddress: client, feeBPS: clientFeeBps })); - for (uint256 i = 0; i < payouts.length; i++) { - totalFeeBps += payouts[i].feeBPS; - } - - // EIP712 - typehashPayoutInfo = keccak256("PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)"); - typehashPayRequest = keccak256( - "PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)" - ); - nameHash = keccak256(bytes("PaymentsGateway")); - 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(gateway))); - } - - /*/////////////////////////////////////////////////////////////// - internal util functions - //////////////////////////////////////////////////////////////*/ - - function _buildMockTargetCalldata( - address _sender, - address _receiver, - address _token, - uint256 _sendValue, - string memory _message - ) internal pure returns (bytes memory data) { - data = abi.encode(_sender, _receiver, _token, _sendValue, _message); - } - - function _hashPayoutInfo(PaymentsGateway.PayoutInfo[] memory _payouts) private view returns (bytes32) { - bytes32 payoutHash = typehashPayoutInfo; - - bytes32[] memory payoutsHashes = new bytes32[](_payouts.length); - for (uint i = 0; i < payouts.length; i++) { - payoutsHashes[i] = keccak256( - abi.encode(payoutHash, _payouts[i].clientId, _payouts[i].payoutAddress, _payouts[i].feeBPS) - ); - } - return keccak256(abi.encodePacked(payoutsHashes)); - } - - function _prepareAndSignData( - uint256 _operatorPrivateKey, - PaymentsGateway.PayRequest memory req - ) internal view returns (bytes memory signature) { - bytes memory dataToHash; - { - bytes32 _payoutsHash = _hashPayoutInfo(req.payouts); - dataToHash = abi.encode( - typehashPayRequest, - req.clientId, - req.transactionId, - req.tokenAddress, - req.tokenAmount, - req.expirationTimestamp, - _payoutsHash, - req.forwardAddress, - keccak256(req.data) - ); - } - - { - 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 `initiateTokenPurchase` - //////////////////////////////////////////////////////////////*/ - - function test_initiateTokenPurchase_erc20() public { - vm.pauseGasMetering(); - uint256 sendValue = 1 ether; - uint256 sendValueWithFees = sendValue + (sendValue * totalFeeBps) / 10_000; - bytes memory targetCalldata = _buildMockTargetCalldata(sender, receiver, address(mockERC20), sendValue, ""); - - // approve amount to gateway contract - vm.prank(sender); - mockERC20.approve(address(gateway), sendValueWithFees); - - // create pay request - PaymentsGateway.PayRequest memory req; - bytes32 _transactionId = keccak256("transaction ID"); - - req.clientId = clientId; - req.transactionId = _transactionId; - req.tokenAddress = address(mockERC20); - req.tokenAmount = sendValue; - req.forwardAddress = payable(address(mockTarget)); - req.expirationTimestamp = 1000; - req.data = targetCalldata; - req.payouts = payouts; - - // generate signature - bytes memory _signature = _prepareAndSignData( - 2, // sign with operator private key, i.e. 2 - req - ); - - // send transaction - vm.prank(sender); - vm.resumeGasMetering(); - gateway.initiateTokenPurchase(req, _signature); - } - - function test_initiateTokenPurchase_nativeToken() public { - vm.pauseGasMetering(); - uint256 sendValue = 1 ether; - uint256 sendValueWithFees = sendValue + (sendValue * totalFeeBps) / 10_000; - bytes memory targetCalldata = _buildMockTargetCalldata( - sender, - receiver, - address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - sendValue, - "" - ); - - // create pay request - PaymentsGateway.PayRequest memory req; - bytes32 _transactionId = keccak256("transaction ID"); - - req.clientId = clientId; - req.transactionId = _transactionId; - req.tokenAddress = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); - req.tokenAmount = sendValue; - req.forwardAddress = payable(address(mockTarget)); - req.expirationTimestamp = 1000; - req.data = targetCalldata; - req.payouts = payouts; - - // generate signature - bytes memory _signature = _prepareAndSignData( - 2, // sign with operator private key, i.e. 2 - req - ); - - // send transaction - vm.prank(sender); - vm.resumeGasMetering(); - gateway.initiateTokenPurchase{ value: sendValueWithFees }(req, _signature); - } -}