From 7a84e1747c3e0140ec475d759d58a837cff6d8fd Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 17 Jun 2024 12:58:57 +1200 Subject: [PATCH 01/17] Add payments --- src/payments/IPayments.sol | 73 +++++++++++ src/payments/Payments.sol | 131 ++++++++++++++++++++ test/payments/Payments.t.sol | 229 +++++++++++++++++++++++++++++++++++ 3 files changed, 433 insertions(+) create mode 100644 src/payments/IPayments.sol create mode 100644 src/payments/Payments.sol create mode 100644 test/payments/Payments.t.sol diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol new file mode 100644 index 0000000..3b888ce --- /dev/null +++ b/src/payments/IPayments.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.19; + +interface IPaymentsFunctions { + enum TokenType { + ERC20, + ERC721, + ERC1155 + } + + struct PaymentDetails { + // Unique ID for this purchase + uint256 purchaseId; + // Recipient of the purchased product + address productRecipient; + // Type of payment token + TokenType tokenType; + // Token address to use for payment + address tokenAddress; + // Token ID to use for payment. Used for ERC-721 and 1155 payments + uint256 tokenId; + // Amount to pay + uint256 amount; + // Address to send the funds to + address fundsRecipient; + // Expiration time of the payment + uint64 expiration; + // ID of the product + string productId; + // Unspecified additional data for the payment + bytes additionalData; + } + + /** + * Make a payment for a product. + * @param paymentDetails The payment details. + * @param signature The signature of the payment. + */ + function makePayment(PaymentDetails calldata paymentDetails, bytes calldata signature) external payable; + + /** + * Check is a signature is valid. + * @param paymentDetails The payment details. + * @param signature The signature of the payment. + * @return isValid True if the signature is valid. + */ + function isValidSignature(PaymentDetails calldata paymentDetails, bytes calldata signature) + external + view + returns (bool); +} + +interface IPaymentsSignals { + + /// @notice Emitted when a payment is already accepted. This prevents double spending. + error PaymentAlreadyAccepted(); + + /// @notice Emitted when a signature is invalid. + error InvalidSignature(); + + /// @notice Emitted when a payment has expired. + error PaymentExpired(); + + /// @notice Emitted when a token transfer is invalid. + error InvalidTokenTransfer(); + + /// @notice Emitted when a payment is made. + event PaymentMade( + address indexed spender, address indexed productRecipient, uint256 indexed purchaseId, string productId + ); +} + +interface IPayments is IPaymentsFunctions, IPaymentsSignals {} diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol new file mode 100644 index 0000000..01935fc --- /dev/null +++ b/src/payments/Payments.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.19; + +import {IPayments, IPaymentsFunctions} from "./IPayments.sol"; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; + +import {IERC721Transfer} from "../tokens/common/IERC721Transfer.sol"; +import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; + +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; + +contract Payments is Ownable, IPayments, IERC165 { + using ECDSA for bytes32; + + address public signer; + + // Payment accepted. Works as a nonce. + mapping(uint256 => bool) public paymentAccepted; + + constructor(address _owner, address _signer) { + Ownable._transferOwnership(_owner); + signer = _signer; + } + + /** + * Update the signer address. + */ + function updateSigner(address newSigner) external onlyOwner { + signer = newSigner; + } + + /// @inheritdoc IPaymentsFunctions + function makePayment(PaymentDetails calldata paymentDetails, bytes calldata signature) external payable { + // Check if payment is already accepted + if (paymentAccepted[paymentDetails.purchaseId]) { + revert PaymentAlreadyAccepted(); + } + if (!isValidSignature(paymentDetails, signature)) { + revert InvalidSignature(); + } + if (block.timestamp > paymentDetails.expiration) { + revert PaymentExpired(); + } + paymentAccepted[paymentDetails.purchaseId] = true; + + address spender = msg.sender; + + _takePayment( + paymentDetails.tokenType, + paymentDetails.tokenAddress, + spender, + paymentDetails.fundsRecipient, + paymentDetails.tokenId, + paymentDetails.amount + ); + //TODO Take cut? + + // Emit event + emit PaymentMade(spender, paymentDetails.productRecipient, paymentDetails.purchaseId, paymentDetails.productId); + } + + /// @inheritdoc IPaymentsFunctions + /// @notice A valid signature does not guarantee that the payment will be accepted. + function isValidSignature(PaymentDetails calldata paymentDetails, bytes calldata signature) + public + view + returns (bool) + { + // Check if signature is valid + bytes32 messageHash = keccak256( + abi.encode( + paymentDetails.purchaseId, + paymentDetails.productRecipient, + paymentDetails.tokenType, + paymentDetails.tokenAddress, + paymentDetails.tokenId, + paymentDetails.amount, + paymentDetails.fundsRecipient, + paymentDetails.expiration, + paymentDetails.productId, + paymentDetails.additionalData + ) + ); + //FIXME Check this + address sigSigner = messageHash.recoverCalldata(signature); + return sigSigner == signer; + } + + /** + * Take payment in the given token. + */ + function _takePayment( + TokenType tokenType, + address tokenAddr, + address from, + address to, + uint256 tokenId, + uint256 amount + ) internal { + if (tokenType == TokenType.ERC1155) { + if (amount == 0) { + revert InvalidTokenTransfer(); + } + // ERC-1155 + IERC1155(tokenAddr).safeTransferFrom(from, to, tokenId, amount, ""); + } else if (tokenType == TokenType.ERC721) { + // ERC-721 + if (amount != 1) { + revert InvalidTokenTransfer(); + } + IERC721Transfer(tokenAddr).safeTransferFrom(from, to, tokenId); + } else if (tokenType == TokenType.ERC20) { + // ERC-20 + if (tokenId != 0 || amount == 0) { + revert InvalidTokenTransfer(); + } + SafeTransferLib.safeTransferFrom(tokenAddr, from, to, amount); + } else { + revert InvalidTokenTransfer(); + } + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 _interfaceID) public view virtual returns (bool) { + return _interfaceID == type(IPayments).interfaceId || _interfaceID == type(IPaymentsFunctions).interfaceId + || _interfaceID == type(IERC165).interfaceId; + } +} diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol new file mode 100644 index 0000000..51b93ab --- /dev/null +++ b/test/payments/Payments.t.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import {Payments, IERC165} from "src/payments/Payments.sol"; +import {IPayments, IPaymentsFunctions, IPaymentsSignals} from "src/payments/IPayments.sol"; + +import {ERC1155Mock} from "test/_mocks/ERC1155Mock.sol"; +import {ERC20Mock} from "test/_mocks/ERC20Mock.sol"; +import {ERC721Mock} from "test/_mocks/ERC721Mock.sol"; +import {IGenericToken} from "test/_mocks/IGenericToken.sol"; + +contract PaymentsTest is Test, IPaymentsSignals { + Payments public payments; + address public owner; + address public signer; + uint256 public signerPk; + + ERC20Mock public erc20; + ERC721Mock public erc721; + ERC1155Mock public erc1155; + + function setUp() public { + owner = makeAddr("owner"); + (signer, signerPk) = makeAddrAndKey("signer"); + payments = new Payments(owner, signer); + + erc20 = new ERC20Mock(address(this)); + erc721 = new ERC721Mock(address(this), "baseURI"); + erc1155 = new ERC1155Mock(address(this), "baseURI"); + } + + struct DetailsInput { + uint256 purchaseId; + address productRecipient; + uint8 tokenType; + address tokenAddress; + uint256 tokenId; + uint256 amount; + address fundsRecipient; + uint64 expiration; + string productId; + bytes additionalData; + } + + function _toTokenType(uint8 tokenType) internal pure returns (IPaymentsFunctions.TokenType) { + tokenType = tokenType % 3; + if (tokenType == 0) { + return IPaymentsFunctions.TokenType.ERC20; + } + if (tokenType == 1) { + return IPaymentsFunctions.TokenType.ERC721; + } + return IPaymentsFunctions.TokenType.ERC1155; + } + + function _validTokenParams(IPaymentsFunctions.TokenType tokenType, uint256 tokenId, uint256 amount) + internal + view + returns (address, uint256, uint256) + { + if (tokenType == IPaymentsFunctions.TokenType.ERC20) { + return (address(erc20), 0, bound(amount, 1, type(uint256).max)); + } + if (tokenType == IPaymentsFunctions.TokenType.ERC721) { + return (address(erc721), bound(tokenId, 1, type(uint256).max), 1); + } + return (address(erc1155), tokenId, bound(amount, 1, type(uint128).max)); + } + + function testMakePaymentSuccess(address caller, DetailsInput calldata input) + public + safeAddress(caller) + safeAddress(input.fundsRecipient) + { + IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.amount); + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + amount, + input.fundsRecipient, + expiration, + input.productId, + input.additionalData + ); + + // Mint required tokens + IGenericToken(tokenAddr).mint(caller, tokenId, amount); + IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); + + // Sign it + bytes32 messageHash = _hashPaymentDetails(details); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + + // Send it + vm.expectEmit(true, true, true, true, address(payments)); + emit PaymentMade(caller, input.productRecipient, input.purchaseId, input.productId); + vm.prank(caller); + payments.makePayment(details, sig); + + assertEq(IGenericToken(tokenAddr).balanceOf(input.fundsRecipient, tokenId), amount); + + // Duplicate call fails + vm.expectRevert(PaymentAlreadyAccepted.selector); + vm.prank(caller); + payments.makePayment(details, sig); + } + + function testMakePaymentInvalidSignature(address caller, DetailsInput calldata input, bytes memory signature) + public + { + IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.amount); + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + amount, + input.fundsRecipient, + expiration, + input.productId, + input.additionalData + ); + + // Send it + vm.expectRevert(InvalidSignature.selector); + vm.prank(caller); + payments.makePayment(details, signature); + } + + function testMakePaymentExpired(address caller, DetailsInput calldata input, uint64 expiration, uint64 blockTimestamp) + public + safeAddress(caller) + safeAddress(input.fundsRecipient) + { + vm.assume(blockTimestamp > expiration); + vm.warp(blockTimestamp); + + IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.amount); + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + amount, + input.fundsRecipient, + expiration, + input.productId, + input.additionalData + ); + + // Mint required tokens + IGenericToken(tokenAddr).mint(caller, tokenId, amount); + IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); + + // Sign it + bytes32 messageHash = _hashPaymentDetails(details); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + + // Send it + vm.expectRevert(PaymentExpired.selector); + vm.prank(caller); + payments.makePayment(details, sig); + } + + function _hashPaymentDetails(IPaymentsFunctions.PaymentDetails memory details) internal pure returns (bytes32) { + return keccak256( + abi.encode( + details.purchaseId, + details.productRecipient, + details.tokenType, + details.tokenAddress, + details.tokenId, + details.amount, + details.fundsRecipient, + details.expiration, + details.productId, + details.additionalData + ) + ); + } + + // Update signer + + function testUpdateSignerSuccess(address newSigner) public { + vm.prank(owner); + payments.updateSigner(newSigner); + assertEq(payments.signer(), newSigner); + } + + function testUpdateSignerInvalidSender(address caller, address newSigner) public { + vm.assume(caller != owner); + + vm.expectRevert(); + vm.prank(caller); + payments.updateSigner(newSigner); + } + + // Supports interface + + function testSupportsInterface() public view { + assertTrue(payments.supportsInterface(type(IPayments).interfaceId)); + assertTrue(payments.supportsInterface(type(IPaymentsFunctions).interfaceId)); + assertTrue(payments.supportsInterface(type(IERC165).interfaceId)); + } + + // Helper + + modifier safeAddress(address addr) { + vm.assume(addr != address(0)); + vm.assume(addr.code.length <= 2); + assumeNotPrecompile(addr); + assumeNotForgeAddress(addr); + _; + } +} From 7a6cc46e273f384c06a2709fbef9fcd23427fdff Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 18 Jun 2024 09:40:25 +1200 Subject: [PATCH 02/17] Payment can have multiple payment recipients --- src/payments/IPayments.sol | 13 +++-- src/payments/Payments.sol | 24 ++++---- test/payments/Payments.t.sol | 105 +++++++++++++++++++++++++++-------- 3 files changed, 103 insertions(+), 39 deletions(-) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index 3b888ce..cbd2c0b 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -8,6 +8,13 @@ interface IPaymentsFunctions { ERC1155 } + struct PaymentRecipient { + // Payment recipient + address recipient; + // Payment amount + uint256 amount; + } + struct PaymentDetails { // Unique ID for this purchase uint256 purchaseId; @@ -19,10 +26,8 @@ interface IPaymentsFunctions { address tokenAddress; // Token ID to use for payment. Used for ERC-721 and 1155 payments uint256 tokenId; - // Amount to pay - uint256 amount; - // Address to send the funds to - address fundsRecipient; + // Payment receipients + PaymentRecipient[] paymentRecipients; // Expiration time of the payment uint64 expiration; // ID of the product diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index 01935fc..c2e31cc 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -48,15 +48,18 @@ contract Payments is Ownable, IPayments, IERC165 { address spender = msg.sender; - _takePayment( - paymentDetails.tokenType, - paymentDetails.tokenAddress, - spender, - paymentDetails.fundsRecipient, - paymentDetails.tokenId, - paymentDetails.amount - ); - //TODO Take cut? + for (uint256 i = 0; i < paymentDetails.paymentRecipients.length; i++) { + // We don't check length == 0. Will only be signed if length 0 is a valid input. + PaymentRecipient calldata recipient = paymentDetails.paymentRecipients[i]; + _takePayment( + paymentDetails.tokenType, + paymentDetails.tokenAddress, + spender, + recipient.recipient, + paymentDetails.tokenId, + recipient.amount + ); + } // Emit event emit PaymentMade(spender, paymentDetails.productRecipient, paymentDetails.purchaseId, paymentDetails.productId); @@ -77,8 +80,7 @@ contract Payments is Ownable, IPayments, IERC165 { paymentDetails.tokenType, paymentDetails.tokenAddress, paymentDetails.tokenId, - paymentDetails.amount, - paymentDetails.fundsRecipient, + paymentDetails.paymentRecipients, paymentDetails.expiration, paymentDetails.productId, paymentDetails.additionalData diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index 51b93ab..66cf3a1 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -37,8 +37,7 @@ contract PaymentsTest is Test, IPaymentsSignals { uint8 tokenType; address tokenAddress; uint256 tokenId; - uint256 amount; - address fundsRecipient; + IPaymentsFunctions.PaymentRecipient paymentRecipient; uint64 expiration; string productId; bytes additionalData; @@ -60,31 +59,35 @@ contract PaymentsTest is Test, IPaymentsSignals { view returns (address, uint256, uint256) { + // / 10 to avoid overflow when paying multiple if (tokenType == IPaymentsFunctions.TokenType.ERC20) { - return (address(erc20), 0, bound(amount, 1, type(uint256).max)); + return (address(erc20), 0, bound(amount, 1, type(uint256).max / 10)); } if (tokenType == IPaymentsFunctions.TokenType.ERC721) { - return (address(erc721), bound(tokenId, 1, type(uint256).max), 1); + return (address(erc721), bound(tokenId, 1, type(uint256).max / 10), 1); } - return (address(erc1155), tokenId, bound(amount, 1, type(uint128).max)); + return (address(erc1155), tokenId, bound(amount, 1, type(uint128).max / 10)); } function testMakePaymentSuccess(address caller, DetailsInput calldata input) public safeAddress(caller) - safeAddress(input.fundsRecipient) + safeAddress(input.paymentRecipient.recipient) { - IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.amount); uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = amount; + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( input.purchaseId, input.productRecipient, tokenType, tokenAddr, tokenId, - amount, - input.fundsRecipient, + paymentRecipients, expiration, input.productId, input.additionalData @@ -105,7 +108,7 @@ contract PaymentsTest is Test, IPaymentsSignals { vm.prank(caller); payments.makePayment(details, sig); - assertEq(IGenericToken(tokenAddr).balanceOf(input.fundsRecipient, tokenId), amount); + assertEq(IGenericToken(tokenAddr).balanceOf(input.paymentRecipient.recipient, tokenId), amount); // Duplicate call fails vm.expectRevert(PaymentAlreadyAccepted.selector); @@ -113,20 +116,72 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } - function testMakePaymentInvalidSignature(address caller, DetailsInput calldata input, bytes memory signature) + function testMakePaymentSuccessMultiplePaymentRecips(address caller, DetailsInput calldata input, address recip2) public + safeAddress(caller) + safeAddress(input.paymentRecipient.recipient) + safeAddress(recip2) { + vm.assume(input.paymentRecipient.recipient != recip2); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.amount); + vm.assume(tokenType != IPaymentsFunctions.TokenType.ERC721); // ERC-721 not supported for multi payments + + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](2); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = amount; + paymentRecipients[1] = IPaymentsFunctions.PaymentRecipient(recip2, amount); + + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + paymentRecipients, + expiration, + input.productId, + input.additionalData + ); + + // Mint required tokens + IGenericToken(tokenAddr).mint(caller, tokenId, amount * 2); + IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount * 2); + + // Sign it + bytes32 messageHash = _hashPaymentDetails(details); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + + // Send it + vm.expectEmit(true, true, true, true, address(payments)); + emit PaymentMade(caller, input.productRecipient, input.purchaseId, input.productId); + vm.prank(caller); + payments.makePayment(details, sig); + + assertEq(IGenericToken(tokenAddr).balanceOf(input.paymentRecipient.recipient, tokenId), amount); + assertEq(IGenericToken(tokenAddr).balanceOf(recip2, tokenId), amount); + } + + function testMakePaymentInvalidSignature(address caller, DetailsInput calldata input, bytes memory signature) + public + { uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = amount; + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( input.purchaseId, input.productRecipient, tokenType, tokenAddr, tokenId, - amount, - input.fundsRecipient, + paymentRecipients, expiration, input.productId, input.additionalData @@ -138,25 +193,28 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, signature); } - function testMakePaymentExpired(address caller, DetailsInput calldata input, uint64 expiration, uint64 blockTimestamp) + function testMakePaymentExpired(address caller, DetailsInput calldata input, uint64 blockTimestamp) public safeAddress(caller) - safeAddress(input.fundsRecipient) + safeAddress(input.paymentRecipient.recipient) { - vm.assume(blockTimestamp > expiration); + vm.assume(blockTimestamp > input.expiration); vm.warp(blockTimestamp); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = amount; + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( input.purchaseId, input.productRecipient, tokenType, tokenAddr, tokenId, - amount, - input.fundsRecipient, - expiration, + paymentRecipients, + input.expiration, input.productId, input.additionalData ); @@ -184,8 +242,7 @@ contract PaymentsTest is Test, IPaymentsSignals { details.tokenType, details.tokenAddress, details.tokenId, - details.amount, - details.fundsRecipient, + details.paymentRecipients, details.expiration, details.productId, details.additionalData From b9366efb1cbf2533e3692c86680db871a52d330d Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 25 Jun 2024 08:07:57 +1200 Subject: [PATCH 03/17] Add payments to deployables --- scripts/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/constants.ts b/scripts/constants.ts index 116756c..7ba954a 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -8,6 +8,7 @@ export const DEPLOYABLE_CONTRACT_NAMES = [ 'ERC1155SaleFactory', 'ERC1155SoulboundFactory', 'PaymentCombiner', + 'Payments', 'Clawback', 'ClawbackMetadata', ] From 27450ae04f4acc44f4b30ea9e4a6fa762c65e1f6 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 25 Jun 2024 08:08:38 +1200 Subject: [PATCH 04/17] Payments hashing on contract. Includes chainid --- src/payments/IPayments.sol | 10 ++++++++-- src/payments/Payments.sol | 15 ++++++++++----- test/payments/Payments.t.sol | 22 +++------------------- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index cbd2c0b..198fc2c 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -52,11 +52,17 @@ interface IPaymentsFunctions { function isValidSignature(PaymentDetails calldata paymentDetails, bytes calldata signature) external view - returns (bool); + returns (bool isValid); + + /** + * Returns the hash of the payment details. + * @param paymentDetails The payment details. + * @return paymentHash The hash of the payment details for signing. + */ + function hashPaymentDetails(PaymentDetails calldata paymentDetails) external view returns (bytes32 paymentHash); } interface IPaymentsSignals { - /// @notice Emitted when a payment is already accepted. This prevents double spending. error PaymentAlreadyAccepted(); diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index c2e31cc..e93cd3b 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -72,9 +72,17 @@ contract Payments is Ownable, IPayments, IERC165 { view returns (bool) { - // Check if signature is valid - bytes32 messageHash = keccak256( + bytes32 messageHash = hashPaymentDetails(paymentDetails); + address sigSigner = messageHash.recoverCalldata(signature); + return sigSigner == signer; + } + + /// @inheritdoc IPaymentsFunctions + /// @dev This hash includes the chain ID. + function hashPaymentDetails(PaymentDetails calldata paymentDetails) public view returns (bytes32) { + return keccak256( abi.encode( + block.chainid, paymentDetails.purchaseId, paymentDetails.productRecipient, paymentDetails.tokenType, @@ -86,9 +94,6 @@ contract Payments is Ownable, IPayments, IERC165 { paymentDetails.additionalData ) ); - //FIXME Check this - address sigSigner = messageHash.recoverCalldata(signature); - return sigSigner == signer; } /** diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index 66cf3a1..0ed9d0d 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -98,7 +98,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); // Sign it - bytes32 messageHash = _hashPaymentDetails(details); + bytes32 messageHash = payments.hashPaymentDetails(details); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); bytes memory sig = abi.encodePacked(r, s, v); @@ -151,7 +151,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount * 2); // Sign it - bytes32 messageHash = _hashPaymentDetails(details); + bytes32 messageHash = payments.hashPaymentDetails(details); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); bytes memory sig = abi.encodePacked(r, s, v); @@ -224,7 +224,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); // Sign it - bytes32 messageHash = _hashPaymentDetails(details); + bytes32 messageHash = payments.hashPaymentDetails(details); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); bytes memory sig = abi.encodePacked(r, s, v); @@ -234,22 +234,6 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } - function _hashPaymentDetails(IPaymentsFunctions.PaymentDetails memory details) internal pure returns (bytes32) { - return keccak256( - abi.encode( - details.purchaseId, - details.productRecipient, - details.tokenType, - details.tokenAddress, - details.tokenId, - details.paymentRecipients, - details.expiration, - details.productId, - details.additionalData - ) - ); - } - // Update signer function testUpdateSignerSuccess(address newSigner) public { From e580808000e10d63faa4a1b823f68c1880cdf49e Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Fri, 28 Jun 2024 12:46:34 +1200 Subject: [PATCH 05/17] Allow zero payments --- src/payments/Payments.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index e93cd3b..e68eb29 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -108,20 +108,17 @@ contract Payments is Ownable, IPayments, IERC165 { uint256 amount ) internal { if (tokenType == TokenType.ERC1155) { - if (amount == 0) { - revert InvalidTokenTransfer(); - } // ERC-1155 IERC1155(tokenAddr).safeTransferFrom(from, to, tokenId, amount, ""); } else if (tokenType == TokenType.ERC721) { // ERC-721 - if (amount != 1) { + if (amount > 1) { revert InvalidTokenTransfer(); } IERC721Transfer(tokenAddr).safeTransferFrom(from, to, tokenId); } else if (tokenType == TokenType.ERC20) { // ERC-20 - if (tokenId != 0 || amount == 0) { + if (tokenId != 0) { revert InvalidTokenTransfer(); } SafeTransferLib.safeTransferFrom(tokenAddr, from, to, amount); From 052fa9a1a8d414d7588e6d3590d3958508d30404 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 9 Jul 2024 11:38:56 +1200 Subject: [PATCH 06/17] payment accepted in interface --- src/payments/IPayments.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index 198fc2c..f71466a 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -60,6 +60,13 @@ interface IPaymentsFunctions { * @return paymentHash The hash of the payment details for signing. */ function hashPaymentDetails(PaymentDetails calldata paymentDetails) external view returns (bytes32 paymentHash); + + /** + * Check if a payment has been accepted. + * @param purchaseId The ID of the purchase. + * @return accepted True if the payment has been accepted. + */ + function paymentAccepted(uint256 purchaseId) external view returns (bool); } interface IPaymentsSignals { From 8d79c461d4e00288fcc39f4941bc10647b04d6d9 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 15 Jul 2024 15:03:16 +1200 Subject: [PATCH 07/17] Allow payment chaining --- src/payments/IPayments.sol | 9 ++++- src/payments/Payments.sol | 11 +++++- test/payments/Payments.t.sol | 71 +++++++++++++++++++++++++++++++++--- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index f71466a..54c9257 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -32,8 +32,10 @@ interface IPaymentsFunctions { uint64 expiration; // ID of the product string productId; - // Unspecified additional data for the payment - bytes additionalData; + // Address for chained call + address chainedCallAddress; + // Data for chained call + bytes chainedCallData; } /** @@ -82,6 +84,9 @@ interface IPaymentsSignals { /// @notice Emitted when a token transfer is invalid. error InvalidTokenTransfer(); + /// @notice Emitted when a chained call fails. + error ChainedCallFailed(); + /// @notice Emitted when a payment is made. event PaymentMade( address indexed spender, address indexed productRecipient, uint256 indexed purchaseId, string productId diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index e68eb29..8020380 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -63,6 +63,14 @@ contract Payments is Ownable, IPayments, IERC165 { // Emit event emit PaymentMade(spender, paymentDetails.productRecipient, paymentDetails.purchaseId, paymentDetails.productId); + + // Perform chained call + if (paymentDetails.chainedCallAddress != address(0)) { + (bool success, ) = paymentDetails.chainedCallAddress.call{value: 0}(paymentDetails.chainedCallData); + if (!success) { + revert ChainedCallFailed(); + } + } } /// @inheritdoc IPaymentsFunctions @@ -91,7 +99,8 @@ contract Payments is Ownable, IPayments, IERC165 { paymentDetails.paymentRecipients, paymentDetails.expiration, paymentDetails.productId, - paymentDetails.additionalData + paymentDetails.chainedCallAddress, + paymentDetails.chainedCallData ) ); } diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index 0ed9d0d..d8cbc0c 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -40,7 +40,6 @@ contract PaymentsTest is Test, IPaymentsSignals { IPaymentsFunctions.PaymentRecipient paymentRecipient; uint64 expiration; string productId; - bytes additionalData; } function _toTokenType(uint8 tokenType) internal pure returns (IPaymentsFunctions.TokenType) { @@ -90,7 +89,8 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - input.additionalData + address(0), + "" ); // Mint required tokens @@ -116,6 +116,64 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } + function testMakePaymentSuccessChainedCall(address caller, DetailsInput calldata input) + public + safeAddress(caller) + safeAddress(input.paymentRecipient.recipient) + safeAddress(input.productRecipient) + { + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = amount; + + // Will mint the next token type + IPaymentsFunctions.TokenType chainedTokenType; + if (tokenType == IPaymentsFunctions.TokenType.ERC20) { + chainedTokenType = IPaymentsFunctions.TokenType.ERC721; + } else if (tokenType == IPaymentsFunctions.TokenType.ERC721) { + chainedTokenType = IPaymentsFunctions.TokenType.ERC1155; + } else { + chainedTokenType = IPaymentsFunctions.TokenType.ERC20; + } + (address chainedTokenAddr, uint256 chainedTokenId, uint256 chainedAmount) = _validTokenParams(chainedTokenType, input.tokenId, input.paymentRecipient.amount); + bytes memory chainedData = abi.encodeWithSelector(IGenericToken.mint.selector, input.productRecipient, chainedTokenId, chainedAmount); + + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + paymentRecipients, + expiration, + input.productId, + chainedTokenAddr, + chainedData + ); + + // Mint required tokens + IGenericToken(tokenAddr).mint(caller, tokenId, amount); + IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); + + // Sign it + bytes32 messageHash = payments.hashPaymentDetails(details); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + + // Send it + vm.expectEmit(true, true, true, true, address(payments)); + emit PaymentMade(caller, input.productRecipient, input.purchaseId, input.productId); + vm.prank(caller); + payments.makePayment(details, sig); + + assertEq(IGenericToken(tokenAddr).balanceOf(input.paymentRecipient.recipient, tokenId), amount); + // Check chaining worked + assertEq(IGenericToken(chainedTokenAddr).balanceOf(input.productRecipient, chainedTokenId), chainedAmount); + } + function testMakePaymentSuccessMultiplePaymentRecips(address caller, DetailsInput calldata input, address recip2) public safeAddress(caller) @@ -143,7 +201,8 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - input.additionalData + address(0), + "" ); // Mint required tokens @@ -184,7 +243,8 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - input.additionalData + address(0), + "" ); // Send it @@ -216,7 +276,8 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, input.expiration, input.productId, - input.additionalData + address(0), + "" ); // Mint required tokens From fc2c8563888a3694a1fcfbdcd30f5941bc22de03 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 15 Jul 2024 15:09:33 +1200 Subject: [PATCH 08/17] Update gas snapshot --- .gas-snapshot | 227 ++++++++++++++++++++++++++++---------------------- 1 file changed, 129 insertions(+), 98 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index f50e71b..39111cb 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,66 +1,66 @@ ClawbackMetadataTest:testDurationAndUnlocksAt() (gas: 500605) -ClawbackMetadataTest:testMetadataPropertiesERC1155((uint8,uint32,uint56,uint256),(bool,bool,uint56,address)) (runs: 1024, μ: 157884, ~: 147382) -ClawbackMetadataTest:testMetadataPropertiesERC20((uint8,uint32,uint56,uint256),(bool,bool,uint56,address)) (runs: 1024, μ: 160168, ~: 160139) -ClawbackMetadataTest:testMetadataPropertiesERC721((uint8,uint32,uint56,uint256),(bool,bool,uint56,address)) (runs: 1024, μ: 258473, ~: 254753) +ClawbackMetadataTest:testMetadataPropertiesERC1155((uint8,uint32,uint56,uint256),(bool,bool,uint56,address)) (runs: 1024, μ: 157419, ~: 146784) +ClawbackMetadataTest:testMetadataPropertiesERC20((uint8,uint32,uint56,uint256),(bool,bool,uint56,address)) (runs: 1024, μ: 160105, ~: 160041) +ClawbackMetadataTest:testMetadataPropertiesERC721((uint8,uint32,uint56,uint256),(bool,bool,uint56,address)) (runs: 1024, μ: 258274, ~: 254433) ClawbackMetadataTest:testSupportsInterface() (gas: 9612) ClawbackTest:testAddTemplate(address,uint56,bool,bool) (runs: 1024, μ: 46910, ~: 46910) ClawbackTest:testAddTemplateTransferer(address,address) (runs: 1024, μ: 77227, ~: 77227) ClawbackTest:testAddTemplateTransfererInvalidCaller(address,address,bool,address) (runs: 1024, μ: 62924, ~: 75750) -ClawbackTest:testAddToWrap(address,uint8,uint256,uint256,uint56,address,uint64) (runs: 1024, μ: 285089, ~: 260572) +ClawbackTest:testAddToWrap(address,uint8,uint256,uint256,uint56,address,uint64) (runs: 1024, μ: 286286, ~: 260571) ClawbackTest:testAddToWrapInvalidWrappedId(uint256,uint256,address) (runs: 1024, μ: 13962, ~: 13962) -ClawbackTest:testClawback(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 279425, ~: 300635) -ClawbackTest:testClawbackAfterTransfer(uint8,uint256,uint256,uint56,address,address,address) (runs: 1024, μ: 306002, ~: 324613) -ClawbackTest:testClawbackDestructionOnly(uint8,uint256,uint256,uint56,address) (runs: 1024, μ: 280166, ~: 305452) -ClawbackTest:testClawbackDestructionOnlyInvalidReceiver(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 269170, ~: 292765) -ClawbackTest:testClawbackInvalidCaller(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 235662, ~: 259070) -ClawbackTest:testClawbackInvalidUnlocked(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 262973, ~: 286568) -ClawbackTest:testEmergencyClawback(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 291479, ~: 317524) -ClawbackTest:testEmergencyClawbackAfterTransfer(uint8,uint256,uint256,uint56,address,address,address) (runs: 1024, μ: 314150, ~: 340998) -ClawbackTest:testEmergencyClawbackDestructionOnly(uint8,uint256,uint256,uint56,address) (runs: 1024, μ: 290872, ~: 321459) -ClawbackTest:testEmergencyClawbackDestructionOnlyInvalidReceiver(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 269690, ~: 293285) -ClawbackTest:testEmergencyClawbackInvalidCaller(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 234708, ~: 258116) -ClawbackTest:testEmergencyClawbackInvalidUnlocked(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 263383, ~: 286978) -ClawbackTest:testPreventsOnERC1155Received(uint256,uint256) (runs: 1024, μ: 100436, ~: 100354) -ClawbackTest:testPreventsOnERC1155Received(uint256,uint256,uint256,uint256) (runs: 1024, μ: 159373, ~: 159370) +ClawbackTest:testClawback(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 279155, ~: 300635) +ClawbackTest:testClawbackAfterTransfer(uint8,uint256,uint256,uint56,address,address,address) (runs: 1024, μ: 305713, ~: 324613) +ClawbackTest:testClawbackDestructionOnly(uint8,uint256,uint256,uint56,address) (runs: 1024, μ: 280677, ~: 305452) +ClawbackTest:testClawbackDestructionOnlyInvalidReceiver(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 268878, ~: 292765) +ClawbackTest:testClawbackInvalidCaller(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 235304, ~: 259070) +ClawbackTest:testClawbackInvalidUnlocked(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 262681, ~: 286568) +ClawbackTest:testEmergencyClawback(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 291147, ~: 317524) +ClawbackTest:testEmergencyClawbackAfterTransfer(uint8,uint256,uint256,uint56,address,address,address) (runs: 1024, μ: 313729, ~: 340998) +ClawbackTest:testEmergencyClawbackDestructionOnly(uint8,uint256,uint256,uint56,address) (runs: 1024, μ: 291491, ~: 321459) +ClawbackTest:testEmergencyClawbackDestructionOnlyInvalidReceiver(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 269398, ~: 293285) +ClawbackTest:testEmergencyClawbackInvalidCaller(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 234350, ~: 258116) +ClawbackTest:testEmergencyClawbackInvalidUnlocked(uint8,uint256,uint256,uint56,address,address) (runs: 1024, μ: 263091, ~: 286978) +ClawbackTest:testPreventsOnERC1155Received(uint256,uint256) (runs: 1024, μ: 100437, ~: 100354) +ClawbackTest:testPreventsOnERC1155Received(uint256,uint256,uint256,uint256) (runs: 1024, μ: 159372, ~: 159370) ClawbackTest:testPreventsOnERC721Received(uint256) (runs: 1024, μ: 118375, ~: 118374) ClawbackTest:testSupportsInterface() (gas: 13448) -ClawbackTest:testTransferByTransfererAsFrom(uint8,uint256,uint256,uint56,uint64,bool,address,bool) (runs: 1024, μ: 276537, ~: 300766) -ClawbackTest:testTransferByTransfererAsOperator(uint8,uint256,uint256,uint56,uint64,bool,address,address,bool) (runs: 1024, μ: 301420, ~: 326941) -ClawbackTest:testTransferByTransfererAsTo(uint8,uint256,uint256,uint56,uint64,bool,address,bool) (runs: 1024, μ: 277110, ~: 301339) -ClawbackTest:testTransferByTransfererNotApproved(uint8,uint256,uint256,uint56,uint64,bool,address,address,bool) (runs: 1024, μ: 268184, ~: 294638) -ClawbackTest:testTransferInvalidOperator(uint8,uint256,uint256,uint56,address,address,bool) (runs: 1024, μ: 269383, ~: 294463) -ClawbackTest:testTransferOpen(uint8,uint256,uint256,uint56,uint64,address) (runs: 1024, μ: 251219, ~: 275183) -ClawbackTest:testUnwrap(address,uint8,uint256,uint256,uint56,uint64) (runs: 1024, μ: 236990, ~: 243165) -ClawbackTest:testUnwrapAfterTransfer(address,uint8,uint256,uint256,uint56,uint64,address) (runs: 1024, μ: 273806, ~: 288749) -ClawbackTest:testUnwrapByInvalidOperator(address,address,uint8,uint256,uint256,uint56) (runs: 1024, μ: 233229, ~: 259415) -ClawbackTest:testUnwrapByOperator(address,address,uint8,uint256,uint256,uint56,uint64) (runs: 1024, μ: 268073, ~: 276271) -ClawbackTest:testUnwrapInvalidAmount(address,uint8,uint256,uint256,uint256,uint56) (runs: 1024, μ: 232938, ~: 258241) -ClawbackTest:testUnwrapInvalidToken(address,uint8,uint256,uint256,uint256,uint56) (runs: 1024, μ: 238423, ~: 263539) -ClawbackTest:testUnwrapTokenLocked(address,uint8,uint256,uint256,uint56) (runs: 1024, μ: 229980, ~: 255596) +ClawbackTest:testTransferByTransfererAsFrom(uint8,uint256,uint256,uint56,uint64,bool,address,bool) (runs: 1024, μ: 277753, ~: 300766) +ClawbackTest:testTransferByTransfererAsOperator(uint8,uint256,uint256,uint56,uint64,bool,address,address,bool) (runs: 1024, μ: 301841, ~: 326941) +ClawbackTest:testTransferByTransfererAsTo(uint8,uint256,uint256,uint56,uint64,bool,address,bool) (runs: 1024, μ: 278326, ~: 301339) +ClawbackTest:testTransferByTransfererNotApproved(uint8,uint256,uint256,uint56,uint64,bool,address,address,bool) (runs: 1024, μ: 268606, ~: 294638) +ClawbackTest:testTransferInvalidOperator(uint8,uint256,uint256,uint56,address,address,bool) (runs: 1024, μ: 268819, ~: 294463) +ClawbackTest:testTransferOpen(uint8,uint256,uint256,uint56,uint64,address) (runs: 1024, μ: 249969, ~: 275183) +ClawbackTest:testUnwrap(address,uint8,uint256,uint256,uint56,uint64) (runs: 1024, μ: 237480, ~: 243165) +ClawbackTest:testUnwrapAfterTransfer(address,uint8,uint256,uint256,uint56,uint64,address) (runs: 1024, μ: 273543, ~: 288742) +ClawbackTest:testUnwrapByInvalidOperator(address,address,uint8,uint256,uint256,uint56) (runs: 1024, μ: 232873, ~: 259415) +ClawbackTest:testUnwrapByOperator(address,address,uint8,uint256,uint256,uint56,uint64) (runs: 1024, μ: 269345, ~: 276271) +ClawbackTest:testUnwrapInvalidAmount(address,uint8,uint256,uint256,uint256,uint56) (runs: 1024, μ: 231156, ~: 258241) +ClawbackTest:testUnwrapInvalidToken(address,uint8,uint256,uint256,uint256,uint56) (runs: 1024, μ: 236708, ~: 263539) +ClawbackTest:testUnwrapTokenLocked(address,uint8,uint256,uint256,uint56) (runs: 1024, μ: 229749, ~: 255596) ClawbackTest:testUpdateTemplateAdmin(address,address,uint56,bool,bool) (runs: 1024, μ: 76260, ~: 76261) ClawbackTest:testUpdateTemplateAdminInvalidAdmin() (gas: 49852) -ClawbackTest:testUpdateTemplateAdminInvalidCaller(address,address,bool) (runs: 1024, μ: 63348, ~: 76224) -ClawbackTest:testUpdateTemplateInvalidCaller(address,address,bool,uint56,bool,bool) (runs: 1024, μ: 62632, ~: 50111) +ClawbackTest:testUpdateTemplateAdminInvalidCaller(address,address,bool) (runs: 1024, μ: 63373, ~: 76224) +ClawbackTest:testUpdateTemplateInvalidCaller(address,address,bool,uint56,bool,bool) (runs: 1024, μ: 62607, ~: 50111) ClawbackTest:testUpdateTemplateInvalidDestructionOnly() (gas: 49760) ClawbackTest:testUpdateTemplateInvalidDuration(address,uint56,bool,bool,uint56) (runs: 1024, μ: 51268, ~: 51268) ClawbackTest:testUpdateTemplateInvalidTransferOpen() (gas: 50208) -ClawbackTest:testUpdateTemplateOperator(address,address,bool) (runs: 1024, μ: 67054, ~: 76985) -ClawbackTest:testUpdateTemplateOperatorInvalidCaller(address,address,bool,address,bool) (runs: 1024, μ: 63108, ~: 50285) -ClawbackTest:testUpdateTemplateValid(address,uint56,bool,bool,uint56,bool,bool) (runs: 1024, μ: 59336, ~: 59458) -ClawbackTest:testWrap(address,uint8,uint256,uint256,address) (runs: 1024, μ: 256053, ~: 281166) -ClawbackTest:testWrapInvalidAmount(address,uint8,uint256,uint256,address) (runs: 1024, μ: 210087, ~: 221861) -ClawbackTest:testWrapInvalidRewrapping(uint8,uint256,uint256,uint56,address) (runs: 1024, μ: 227976, ~: 254805) -ClawbackTest:testWrapInvalidTemplate(uint32,uint8,uint256,uint256,address) (runs: 1024, μ: 106233, ~: 115140) -ClawbackTest:testWrapInvalidTokenType(address,uint8,uint256,uint256) (runs: 1024, μ: 145171, ~: 152283) -ERC1155ItemsTest:testBatchMintOwner(uint256[],uint256[]) (runs: 1024, μ: 576935, ~: 587455) -ERC1155ItemsTest:testBatchMintWithRole(address,uint256[],uint256[]) (runs: 1024, μ: 661940, ~: 671839) -ERC1155ItemsTest:testBurnBatchInvalidOwnership(address,uint256[],uint256[]) (runs: 1024, μ: 231087, ~: 233624) -ERC1155ItemsTest:testBurnBatchSuccess(address,uint256[],uint256[],uint256[]) (runs: 1024, μ: 197974, ~: 196202) +ClawbackTest:testUpdateTemplateOperator(address,address,bool) (runs: 1024, μ: 67074, ~: 76985) +ClawbackTest:testUpdateTemplateOperatorInvalidCaller(address,address,bool,address,bool) (runs: 1024, μ: 63134, ~: 50285) +ClawbackTest:testUpdateTemplateValid(address,uint56,bool,bool,uint56,bool,bool) (runs: 1024, μ: 59338, ~: 59459) +ClawbackTest:testWrap(address,uint8,uint256,uint256,address) (runs: 1024, μ: 255421, ~: 281166) +ClawbackTest:testWrapInvalidAmount(address,uint8,uint256,uint256,address) (runs: 1024, μ: 209490, ~: 221861) +ClawbackTest:testWrapInvalidRewrapping(uint8,uint256,uint256,uint56,address) (runs: 1024, μ: 228525, ~: 254805) +ClawbackTest:testWrapInvalidTemplate(uint32,uint8,uint256,uint256,address) (runs: 1024, μ: 106055, ~: 115140) +ClawbackTest:testWrapInvalidTokenType(address,uint8,uint256,uint256) (runs: 1024, μ: 144924, ~: 152283) +ERC1155ItemsTest:testBatchMintOwner(uint256[],uint256[]) (runs: 1024, μ: 576938, ~: 587455) +ERC1155ItemsTest:testBatchMintWithRole(address,uint256[],uint256[]) (runs: 1024, μ: 661955, ~: 671874) +ERC1155ItemsTest:testBurnBatchInvalidOwnership(address,uint256[],uint256[]) (runs: 1024, μ: 230920, ~: 233658) +ERC1155ItemsTest:testBurnBatchSuccess(address,uint256[],uint256[],uint256[]) (runs: 1024, μ: 197915, ~: 196194) ERC1155ItemsTest:testBurnInvalidOwnership(address,uint256,uint256,uint256) (runs: 1024, μ: 109179, ~: 110870) ERC1155ItemsTest:testBurnSuccess(address,uint256,uint256,uint256) (runs: 1024, μ: 125849, ~: 125879) ERC1155ItemsTest:testContractURI() (gas: 83132) ERC1155ItemsTest:testDefaultRoyalty(address,uint96,uint256) (runs: 1024, μ: 41468, ~: 41468) -ERC1155ItemsTest:testFactoryDetermineAddress(address,address,string,string,string,address,uint96) (runs: 1024, μ: 6720772, ~: 6720833) +ERC1155ItemsTest:testFactoryDetermineAddress(address,address,string,string,string,address,uint96) (runs: 1024, μ: 6719774, ~: 6720586) ERC1155ItemsTest:testMetadataInvalid(address) (runs: 1024, μ: 66770, ~: 66770) ERC1155ItemsTest:testMetadataOwner() (gas: 43862) ERC1155ItemsTest:testMetadataWithRole(address) (runs: 1024, μ: 117392, ~: 117392) @@ -70,40 +70,53 @@ ERC1155ItemsTest:testMintWithRole(address,uint256,uint256) (runs: 1024, μ: 1904 ERC1155ItemsTest:testOwnerHasRoles() (gas: 46365) ERC1155ItemsTest:testReinitializeFails() (gas: 27986) ERC1155ItemsTest:testRoyaltyInvalidRole(address,uint256,address,uint96,uint256) (runs: 1024, μ: 121054, ~: 121054) -ERC1155ItemsTest:testRoyaltyWithRole(address,uint256,address,uint96,uint256) (runs: 1024, μ: 154232, ~: 154355) +ERC1155ItemsTest:testRoyaltyWithRole(address,uint256,address,uint96,uint256) (runs: 1024, μ: 154173, ~: 154355) ERC1155ItemsTest:testSelectorCollision() (gas: 82659) ERC1155ItemsTest:testSupportsInterface() (gas: 34330) ERC1155ItemsTest:testTokenRoyalty(uint256,address,uint96,uint256) (runs: 1024, μ: 65161, ~: 65161) -ERC1155SaleTest:testERC20Mint(bool,address,uint256,uint256) (runs: 1024, μ: 3403798, ~: 716548) -ERC1155SaleTest:testERC20MintFailPaidETH(bool,address,uint256,uint256) (runs: 1024, μ: 3318488, ~: 625622) +ERC1155SaleTest:testERC20Mint(bool,address,uint256,uint256) (runs: 1024, μ: 3409205, ~: 716548) +ERC1155SaleTest:testERC20MintFailPaidETH(bool,address,uint256,uint256) (runs: 1024, μ: 3323907, ~: 625622) ERC1155SaleTest:testFactoryDetermineAddress(address,address,address) (runs: 1024, μ: 5465831, ~: 5465831) -ERC1155SaleTest:testFreeGlobalMint(bool,address,uint256,uint256) (runs: 1024, μ: 2849078, ~: 162516) -ERC1155SaleTest:testFreeTokenMint(bool,address,uint256,uint256) (runs: 1024, μ: 2896586, ~: 209295) -ERC1155SaleTest:testMerkleFailBadProof(address[],address,uint256,bool) (runs: 1024, μ: 282253, ~: 280388) -ERC1155SaleTest:testMerkleFailNoProof(address[],address,uint256,bool) (runs: 1024, μ: 278157, ~: 276235) -ERC1155SaleTest:testMerkleReuseFail(address[],uint256,uint256,bool) (runs: 1024, μ: 404732, ~: 405619) -ERC1155SaleTest:testMerkleSuccess(address[],uint256,uint256,bool) (runs: 1024, μ: 396161, ~: 396909) -ERC1155SaleTest:testMerkleSuccessGlobalMultiple(address[],uint256,uint256[]) (runs: 1024, μ: 475005, ~: 484422) +ERC1155SaleTest:testFreeGlobalMint(bool,address,uint256,uint256) (runs: 1024, μ: 2854484, ~: 162516) +ERC1155SaleTest:testFreeTokenMint(bool,address,uint256,uint256) (runs: 1024, μ: 2901994, ~: 209295) +ERC1155SaleTest:testMerkleFailBadProof(address[],address,uint256,bool) (runs: 1024, μ: 282258, ~: 280434) +ERC1155SaleTest:testMerkleFailNoProof(address[],address,uint256,bool) (runs: 1024, μ: 278162, ~: 276267) +ERC1155SaleTest:testMerkleReuseFail(address[],uint256,uint256,bool) (runs: 1024, μ: 404586, ~: 405192) +ERC1155SaleTest:testMerkleSuccess(address[],uint256,uint256,bool) (runs: 1024, μ: 396015, ~: 396482) +ERC1155SaleTest:testMerkleSuccessGlobalMultiple(address[],uint256,uint256[]) (runs: 1024, μ: 474665, ~: 483219) ERC1155SaleTest:testMintExpiredGlobalFail(bool,address,uint256,uint256,uint64,uint64) (runs: 1024, μ: 2795878, ~: 99797) -ERC1155SaleTest:testMintExpiredSingleFail(bool,address,uint256,uint256,uint64,uint64) (runs: 1024, μ: 2796169, ~: 100087) -ERC1155SaleTest:testMintFailMaxTotal(bool,address,uint256,uint256) (runs: 1024, μ: 2882377, ~: 188007) -ERC1155SaleTest:testMintFailWrongPaymentToken(bool,address,uint256,uint256,address) (runs: 1024, μ: 3425257, ~: 6139498) -ERC1155SaleTest:testMintGlobalSuccess(bool,address,uint256,uint256) (runs: 1024, μ: 2877929, ~: 191367) -ERC1155SaleTest:testMintGlobalSupplyExceeded(bool,address,uint256,uint256,uint256) (runs: 1024, μ: 2943369, ~: 5669458) -ERC1155SaleTest:testMintGroupSuccess(bool,address,uint256,uint256) (runs: 1024, μ: 2983535, ~: 296242) -ERC1155SaleTest:testMintInactiveFail(bool,address,uint256,uint256) (runs: 1024, μ: 2742331, ~: 51398) -ERC1155SaleTest:testMintInactiveInGroupFail(bool,address,uint256,uint256) (runs: 1024, μ: 2799413, ~: 108722) -ERC1155SaleTest:testMintInactiveSingleFail(bool,address,uint256,uint256) (runs: 1024, μ: 2797206, ~: 106515) -ERC1155SaleTest:testMintSingleSuccess(bool,address,uint256,uint256) (runs: 1024, μ: 2877985, ~: 191420) -ERC1155SaleTest:testMintTokenSupplyExceeded(bool,address,uint256,uint256,uint256) (runs: 1024, μ: 2943464, ~: 5669556) +ERC1155SaleTest:testMintExpiredSingleFail(bool,address,uint256,uint256,uint64,uint64) (runs: 1024, μ: 2796170, ~: 100087) +ERC1155SaleTest:testMintFailMaxTotal(bool,address,uint256,uint256) (runs: 1024, μ: 2887799, ~: 188007) +ERC1155SaleTest:testMintFailWrongPaymentToken(bool,address,uint256,uint256,address) (runs: 1024, μ: 3430636, ~: 6139498) +ERC1155SaleTest:testMintGlobalSuccess(bool,address,uint256,uint256) (runs: 1024, μ: 2883335, ~: 191367) +ERC1155SaleTest:testMintGlobalSupplyExceeded(bool,address,uint256,uint256,uint256) (runs: 1024, μ: 2943368, ~: 5669458) +ERC1155SaleTest:testMintGroupSuccess(bool,address,uint256,uint256) (runs: 1024, μ: 2988942, ~: 296242) +ERC1155SaleTest:testMintInactiveFail(bool,address,uint256,uint256) (runs: 1024, μ: 2747745, ~: 51398) +ERC1155SaleTest:testMintInactiveInGroupFail(bool,address,uint256,uint256) (runs: 1024, μ: 2804827, ~: 108722) +ERC1155SaleTest:testMintInactiveSingleFail(bool,address,uint256,uint256) (runs: 1024, μ: 2802620, ~: 106515) +ERC1155SaleTest:testMintSingleSuccess(bool,address,uint256,uint256) (runs: 1024, μ: 2883391, ~: 191420) +ERC1155SaleTest:testMintTokenSupplyExceeded(bool,address,uint256,uint256,uint256) (runs: 1024, μ: 2943463, ~: 5669556) ERC1155SaleTest:testSelectorCollision() (gas: 51971) ERC1155SaleTest:testSupportsInterface() (gas: 10893) -ERC1155SaleTest:testWithdrawERC20(bool,address,uint256) (runs: 1024, μ: 3293262, ~: 6000742) -ERC1155SaleTest:testWithdrawETH(bool,address,uint256) (runs: 1024, μ: 2884989, ~: 5593690) -ERC1155SaleTest:testWithdrawFail(bool,address,uint256) (runs: 1024, μ: 3242654, ~: 5928350) +ERC1155SaleTest:testWithdrawERC20(bool,address,uint256) (runs: 1024, μ: 3282455, ~: 6000763) +ERC1155SaleTest:testWithdrawETH(bool,address,uint256) (runs: 1024, μ: 2900412, ~: 5625346) +ERC1155SaleTest:testWithdrawFail(bool,address,uint256) (runs: 1024, μ: 3242678, ~: 5928374) +ERC1155SoulboundTest:testBatchBurnBlocked(uint256[],uint256[]) (runs: 1024, μ: 47728, ~: 47797) +ERC1155SoulboundTest:testBatchMintAllowed(address,uint256) (runs: 1024, μ: 110185, ~: 110185) +ERC1155SoulboundTest:testBurnBlocked(uint256,uint256) (runs: 1024, μ: 25924, ~: 25924) +ERC1155SoulboundTest:testFactoryDetermineAddress(address,address,string,string,string,string,address,uint96) (runs: 1024, μ: 7042640, ~: 7041442) +ERC1155SoulboundTest:testOwnerHasRoles() (gas: 52087) +ERC1155SoulboundTest:testReinitializeFails() (gas: 34614) +ERC1155SoulboundTest:testSelectorCollision() (gas: 90031) +ERC1155SoulboundTest:testSupportsInterface() (gas: 31363) +ERC1155SoulboundTest:testTransferLocked(address,address,uint256) (runs: 1024, μ: 115056, ~: 115056) +ERC1155SoulboundTest:testTransferLockedOperator(address,address,address,uint256) (runs: 1024, μ: 148322, ~: 148322) +ERC1155SoulboundTest:testTransferUnlocked(address,address,uint256) (runs: 1024, μ: 131318, ~: 131318) +ERC1155SoulboundTest:testTransferUnlockedOperator(address,address,address,uint256) (runs: 1024, μ: 165064, ~: 165064) +ERC1155SoulboundTest:testUnlockInvalidRole(address) (runs: 1024, μ: 51725, ~: 51725) ERC20ItemsTest:testBurnInsufficient(address,uint256,uint256) (runs: 1024, μ: 81152, ~: 82008) ERC20ItemsTest:testBurnSuccess(address,uint256,uint256) (runs: 1024, μ: 90239, ~: 90239) -ERC20ItemsTest:testFactoryDetermineAddress(address,address,string,string,uint8) (runs: 1024, μ: 5252882, ~: 5251395) +ERC20ItemsTest:testFactoryDetermineAddress(address,address,string,string,uint8) (runs: 1024, μ: 5250919, ~: 5251395) ERC20ItemsTest:testInitValues() (gas: 38152) ERC20ItemsTest:testMintInvalidRole(address,uint256) (runs: 1024, μ: 67321, ~: 67321) ERC20ItemsTest:testMintOwner(uint256) (runs: 1024, μ: 80602, ~: 80602) @@ -118,7 +131,7 @@ ERC721ItemsTest:testBurnInvalidOwnership(address) (runs: 1024, μ: 109892, ~: 10 ERC721ItemsTest:testBurnSuccess(address) (runs: 1024, μ: 138130, ~: 138130) ERC721ItemsTest:testContractURI() (gas: 85853) ERC721ItemsTest:testDefaultRoyalty(address,uint96,uint256) (runs: 1024, μ: 41445, ~: 41445) -ERC721ItemsTest:testFactoryDetermineAddress(address,address,string,string,string,string,address,uint96) (runs: 1024, μ: 6794789, ~: 6793149) +ERC721ItemsTest:testFactoryDetermineAddress(address,address,string,string,string,string,address,uint96) (runs: 1024, μ: 6794578, ~: 6793149) ERC721ItemsTest:testMetadataInvalid(address) (runs: 1024, μ: 69188, ~: 69188) ERC721ItemsTest:testMetadataOwner() (gas: 130207) ERC721ItemsTest:testMetadataWithRole(address) (runs: 1024, μ: 117459, ~: 117459) @@ -130,40 +143,58 @@ ERC721ItemsTest:testNameAndSymbol() (gas: 90129) ERC721ItemsTest:testOwnerHasRoles() (gas: 46710) ERC721ItemsTest:testReinitializeFails() (gas: 28551) ERC721ItemsTest:testRoyaltyInvalidRole(address,uint256,address,uint96,uint256) (runs: 1024, μ: 125741, ~: 125741) -ERC721ItemsTest:testRoyaltyWithRole(address,uint256,address,uint96,uint256) (runs: 1024, μ: 154132, ~: 154333) +ERC721ItemsTest:testRoyaltyWithRole(address,uint256,address,uint96,uint256) (runs: 1024, μ: 154151, ~: 154333) ERC721ItemsTest:testSelectorCollision() (gas: 95069) ERC721ItemsTest:testSupportsInterface() (gas: 37278) ERC721ItemsTest:testTokenMetadata() (gas: 174823) ERC721ItemsTest:testTokenRoyalty(uint256,address,uint96,uint256) (runs: 1024, μ: 65223, ~: 65223) -ERC721SaleTest:testERC20Mint(bool,address,uint256) (runs: 1024, μ: 3160418, ~: 720351) -ERC721SaleTest:testERC20MintFailMaxTotal(bool,address,uint256) (runs: 1024, μ: 3048389, ~: 583551) -ERC721SaleTest:testERC20MintFailPaidETH(bool,address,uint256) (runs: 1024, μ: 3032284, ~: 567446) -ERC721SaleTest:testETHMintFailMaxTotal(bool,address,uint256) (runs: 1024, μ: 2551323, ~: 86420) +ERC721SaleTest:testERC20Mint(bool,address,uint256) (runs: 1024, μ: 3170452, ~: 720351) +ERC721SaleTest:testERC20MintFailMaxTotal(bool,address,uint256) (runs: 1024, μ: 3058429, ~: 583551) +ERC721SaleTest:testERC20MintFailPaidETH(bool,address,uint256) (runs: 1024, μ: 3042324, ~: 567446) +ERC721SaleTest:testETHMintFailMaxTotal(bool,address,uint256) (runs: 1024, μ: 2561364, ~: 86420) ERC721SaleTest:testFactoryDetermineAddress(address,address,address) (runs: 1024, μ: 5065069, ~: 5065069) -ERC721SaleTest:testFreeMint(bool,address,uint256) (runs: 1024, μ: 2607658, ~: 167521) -ERC721SaleTest:testMerkleFailBadProof(address[],address) (runs: 1024, μ: 282442, ~: 278506) -ERC721SaleTest:testMerkleFailNoProof(address[],address) (runs: 1024, μ: 278488, ~: 274565) -ERC721SaleTest:testMerkleReuseFail(address[],uint256) (runs: 1024, μ: 382525, ~: 378954) -ERC721SaleTest:testMerkleSuccess(address[],uint256) (runs: 1024, μ: 375700, ~: 372140) -ERC721SaleTest:testMintExpiredFail(bool,address,uint256,uint64,uint64) (runs: 1024, μ: 2696471, ~: 5233189) -ERC721SaleTest:testMintFailWrongPaymentToken(bool,address,uint256,address) (runs: 1024, μ: 3005394, ~: 540556) -ERC721SaleTest:testMintInactiveFail(bool,address,uint256) (runs: 1024, μ: 2504732, ~: 39587) -ERC721SaleTest:testMintSuccess(bool,address,uint256) (runs: 1024, μ: 2636738, ~: 196601) -ERC721SaleTest:testMintSupplyExceeded(bool,address,uint256,uint256) (runs: 1024, μ: 2616314, ~: 131387) +ERC721SaleTest:testFreeMint(bool,address,uint256) (runs: 1024, μ: 2617693, ~: 167521) +ERC721SaleTest:testMerkleFailBadProof(address[],address) (runs: 1024, μ: 282150, ~: 278510) +ERC721SaleTest:testMerkleFailNoProof(address[],address) (runs: 1024, μ: 278491, ~: 274625) +ERC721SaleTest:testMerkleReuseFail(address[],uint256) (runs: 1024, μ: 382656, ~: 378984) +ERC721SaleTest:testMerkleSuccess(address[],uint256) (runs: 1024, μ: 375831, ~: 372170) +ERC721SaleTest:testMintExpiredFail(bool,address,uint256,uint64,uint64) (runs: 1024, μ: 2681372, ~: 2672822) +ERC721SaleTest:testMintFailWrongPaymentToken(bool,address,uint256,address) (runs: 1024, μ: 3020493, ~: 540556) +ERC721SaleTest:testMintInactiveFail(bool,address,uint256) (runs: 1024, μ: 2514773, ~: 39587) +ERC721SaleTest:testMintSuccess(bool,address,uint256) (runs: 1024, μ: 2646773, ~: 196601) +ERC721SaleTest:testMintSupplyExceeded(bool,address,uint256,uint256) (runs: 1024, μ: 2611293, ~: 131387) ERC721SaleTest:testSelectorCollision() (gas: 44619) ERC721SaleTest:testSupportsInterface() (gas: 10884) -ERC721SaleTest:testWithdrawERC20(bool,address,uint256) (runs: 1024, μ: 3092352, ~: 5599117) -ERC721SaleTest:testWithdrawETH(bool,address,uint256) (runs: 1024, μ: 2684392, ~: 5192376) -ERC721SaleTest:testWithdrawFail(bool,address,uint256) (runs: 1024, μ: 3038633, ~: 5527463) -PaymentCombinerTest:testDetermineAddress(address[],uint256[]) (runs: 1024, μ: 646925, ~: 652205) -PaymentCombinerTest:testListPayeeSplitters(address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1258653, ~: 1269538) -PaymentCombinerTest:testListPayeeSplittersOffsetLimit(address,address[][],uint256[],uint256,uint256) (runs: 1024, μ: 11982755, ~: 4814944) -PaymentCombinerTest:testListReleasableERC20(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1368952, ~: 1380680) -PaymentCombinerTest:testListReleasableNative(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1297385, ~: 1309113) -PaymentCombinerTest:testListReleaseERC20(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1502846, ~: 1514574) -PaymentCombinerTest:testListReleaseNative(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1438278, ~: 1450031) -PaymentCombinerTest:testRepeatedDeploysFail(address[],uint256[]) (runs: 1024, μ: 1040378921, ~: 1040385358) +ERC721SaleTest:testWithdrawERC20(bool,address,uint256) (runs: 1024, μ: 3082327, ~: 5599138) +ERC721SaleTest:testWithdrawETH(bool,address,uint256) (runs: 1024, μ: 2696046, ~: 5224032) +ERC721SaleTest:testWithdrawFail(bool,address,uint256) (runs: 1024, μ: 3038657, ~: 5527487) +ERC721SoulboundTest:testFactoryDetermineAddress(address,address,string,string,string,string,address,uint96) (runs: 1024, μ: 7056525, ~: 7054935) +ERC721SoulboundTest:testOwnerHasRoles() (gas: 52377) +ERC721SoulboundTest:testReinitializeFails() (gas: 35172) +ERC721SoulboundTest:testSelectorCollision() (gas: 102395) +ERC721SoulboundTest:testSupportsInterface() (gas: 40208) +ERC721SoulboundTest:testTransferLocked(address,address) (runs: 1024, μ: 115439, ~: 115439) +ERC721SoulboundTest:testTransferLockedOperator(address,address,address) (runs: 1024, μ: 148251, ~: 148256) +ERC721SoulboundTest:testTransferUnlocked(address,address) (runs: 1024, μ: 151384, ~: 151384) +ERC721SoulboundTest:testTransferUnlockedOperator(address,address,address) (runs: 1024, μ: 184145, ~: 184150) +ERC721SoulboundTest:testUnlockInvalidRole(address) (runs: 1024, μ: 54197, ~: 54197) +PaymentCombinerTest:testDetermineAddress(address[],uint256[]) (runs: 1024, μ: 814888, ~: 822377) +PaymentCombinerTest:testListPayeeSplitters(address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1595183, ~: 1610209) +PaymentCombinerTest:testListPayeeSplittersOffsetLimit(address,address[][],uint256[],uint256,uint256) (runs: 1024, μ: 9966436, ~: 4812798) +PaymentCombinerTest:testListReleasableERC20(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1704648, ~: 1721055) +PaymentCombinerTest:testListReleasableNative(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1633081, ~: 1649487) +PaymentCombinerTest:testListReleaseERC20(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1838542, ~: 1854950) +PaymentCombinerTest:testListReleaseNative(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1773950, ~: 1790406) +PaymentCombinerTest:testRepeatedDeploysFail(address[],uint256[]) (runs: 1024, μ: 1040384595, ~: 1040390920) PaymentCombinerTest:testSupportsInterface() (gas: 10684) +PaymentsTest:testMakePaymentExpired(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),uint64) (runs: 1024, μ: 132598, ~: 141327) +PaymentsTest:testMakePaymentInvalidSignature(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bytes) (runs: 1024, μ: 25974, ~: 25945) +PaymentsTest:testMakePaymentSuccess(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 164803, ~: 180713) +PaymentsTest:testMakePaymentSuccessChainedCall(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 238699, ~: 237353) +PaymentsTest:testMakePaymentSuccessMultiplePaymentRecips(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),address) (runs: 1024, μ: 190134, ~: 172742) +PaymentsTest:testSupportsInterface() (gas: 10748) +PaymentsTest:testUpdateSignerInvalidSender(address,address) (runs: 1024, μ: 13748, ~: 13748) +PaymentsTest:testUpdateSignerSuccess(address) (runs: 1024, μ: 19171, ~: 19204) SequenceProxyFactoryTest:testAddressCompute() (gas: 1021270) SequenceProxyFactoryTest:testDeployProxy() (gas: 1021715) SequenceProxyFactoryTest:testDeployProxyAfterUpgrade() (gas: 1033634) From ebd0e818925136d3847593d0293739c2e77aa3d6 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 16 Jul 2024 08:35:24 +1200 Subject: [PATCH 09/17] Add ability for signer make chained call --- src/payments/IPayments.sol | 11 +++++ src/payments/Payments.sol | 26 +++++++++-- test/payments/Payments.t.sol | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index 54c9257..20d939e 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -45,6 +45,14 @@ interface IPaymentsFunctions { */ function makePayment(PaymentDetails calldata paymentDetails, bytes calldata signature) external payable; + /** + * Complete a chained call. + * @param chainedCallAddress The address of the chained call. + * @param chainedCallData The data for the chained call. + * @notice This is only callable by an authorised party. + */ + function performChainedCall(address chainedCallAddress, bytes calldata chainedCallData) external; + /** * Check is a signature is valid. * @param paymentDetails The payment details. @@ -75,6 +83,9 @@ interface IPaymentsSignals { /// @notice Emitted when a payment is already accepted. This prevents double spending. error PaymentAlreadyAccepted(); + /// @notice Emitted when a sender is invalid. + error InvalidSender(); + /// @notice Emitted when a signature is invalid. error InvalidSignature(); diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index 8020380..bc27250 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -66,10 +66,7 @@ contract Payments is Ownable, IPayments, IERC165 { // Perform chained call if (paymentDetails.chainedCallAddress != address(0)) { - (bool success, ) = paymentDetails.chainedCallAddress.call{value: 0}(paymentDetails.chainedCallData); - if (!success) { - revert ChainedCallFailed(); - } + _performChainedCall(paymentDetails.chainedCallAddress, paymentDetails.chainedCallData); } } @@ -105,6 +102,27 @@ contract Payments is Ownable, IPayments, IERC165 { ); } + /// @inheritdoc IPaymentsFunctions + /// @notice This can only be called by the signer. + /// @dev As the signer can validate any payment (including zero) this function does not increase the security surface. + function performChainedCall(address chainedCallAddress, bytes calldata chainedCallData) external override { + // Check authorization + if (msg.sender != signer) { + revert InvalidSender(); + } + _performChainedCall(chainedCallAddress, chainedCallData); + } + + /** + * Perform a chained call and revert on error. + */ + function _performChainedCall(address chainedCallAddress, bytes calldata chainedCallData) internal { + (bool success, ) = chainedCallAddress.call{value: 0}(chainedCallData); + if (!success) { + revert ChainedCallFailed(); + } + } + /** * Take payment in the given token. */ diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index d8cbc0c..9fda23e 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -295,6 +295,93 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } + // Note there is a VERY slim chance this test can fail if the fuzzed data makes a valid call + function testMakePaymentFailedChainedCall(address caller, DetailsInput calldata input, bytes memory chainedCallData) + public + safeAddress(caller) + safeAddress(input.paymentRecipient.recipient) + safeAddress(input.productRecipient) + { + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = amount; + + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + paymentRecipients, + expiration, + input.productId, + address(payments), // Chained call to payments will fail + chainedCallData + ); + + // Mint required tokens + IGenericToken(tokenAddr).mint(caller, tokenId, amount); + IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); + + // Sign it + bytes32 messageHash = payments.hashPaymentDetails(details); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + + // Send it + vm.expectRevert(ChainedCallFailed.selector); + vm.prank(caller); + payments.makePayment(details, sig); + } + + // Chained call + + function testPerformChainedCallSuccess(uint8 tokenTypeInt, uint256 tokenId, uint256 amount, address recipient) + public + safeAddress(recipient) + { + IPaymentsFunctions.TokenType tokenType = _toTokenType(tokenTypeInt); + address tokenAddr; + (tokenAddr, tokenId, amount) = _validTokenParams(tokenType, tokenId, amount); + + bytes memory callData = abi.encodeWithSelector(IGenericToken.mint.selector, recipient, tokenId, amount); + + // Send it + vm.prank(signer); + payments.performChainedCall(tokenAddr, callData); + + assertEq(IGenericToken(tokenAddr).balanceOf(recipient, tokenId), amount); + } + + function testPerformChainedCallInvalidCaller(address caller, uint8 tokenTypeInt, uint256 tokenId, uint256 amount, address recipient) + public + safeAddress(recipient) + { + IPaymentsFunctions.TokenType tokenType = _toTokenType(tokenTypeInt); + address tokenAddr; + (tokenAddr, tokenId, amount) = _validTokenParams(tokenType, tokenId, amount); + + bytes memory callData = abi.encodeWithSelector(IGenericToken.mint.selector, recipient, tokenId, amount); + + // Send it + vm.expectRevert(InvalidSender.selector); + vm.prank(caller); + payments.performChainedCall(tokenAddr, callData); + } + + // Note there is a slim chance this test can fail if the fuzzed data makes a valid call + function testPerformChainedCallInvalidCall(bytes memory chainedCallData) + public + { + vm.expectRevert(ChainedCallFailed.selector); + vm.prank(signer); + // Chained call to payments will fail + payments.performChainedCall(address(payments), chainedCallData); + } + // Update signer function testUpdateSignerSuccess(address newSigner) public { From 5934fc8e432d9113a8766ccd90166d5756123f1c Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 16 Jul 2024 08:55:41 +1200 Subject: [PATCH 10/17] Additional payments tests --- test/payments/Payments.t.sol | 113 +++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index 9fda23e..b09e4f9 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -295,6 +295,119 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } + function testMakePaymentInvalidPayment(address caller, DetailsInput calldata input) + public + safeAddress(caller) + safeAddress(input.paymentRecipient.recipient) + { + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = amount; + + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + paymentRecipients, + expiration, + input.productId, + address(0), + "" + ); + + // Do not mint required tokens + + // Sign it + bytes32 messageHash = payments.hashPaymentDetails(details); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + + // Send it + vm.expectRevert(); + vm.prank(caller); + payments.makePayment(details, sig); + } + + function testMakePaymentInvalidTokenSettingsERC20(address caller, DetailsInput calldata input, uint256 tokenId) + public + safeAddress(caller) + safeAddress(input.paymentRecipient.recipient) + { + tokenId = _bound(tokenId, 1, type(uint256).max); // Non-zero + + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.TokenType tokenType = IPaymentsFunctions.TokenType.ERC20; + (address tokenAddr,, uint256 amount) = _validTokenParams(tokenType, tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = amount; + + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + paymentRecipients, + expiration, + input.productId, + address(0), + "" + ); + + // Sign it + bytes32 messageHash = payments.hashPaymentDetails(details); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + + // Send it + vm.expectRevert(InvalidTokenTransfer.selector); + vm.prank(caller); + payments.makePayment(details, sig); + } + + function testMakePaymentInvalidTokenSettingsERC721(address caller, DetailsInput calldata input) + public + safeAddress(caller) + safeAddress(input.paymentRecipient.recipient) + { + + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); + IPaymentsFunctions.TokenType tokenType = IPaymentsFunctions.TokenType.ERC721; + (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); + paymentRecipients[0] = input.paymentRecipient; + paymentRecipients[0].amount = _bound(amount, 2, type(uint256).max); // Invalid amount + + IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( + input.purchaseId, + input.productRecipient, + tokenType, + tokenAddr, + tokenId, + paymentRecipients, + expiration, + input.productId, + address(0), + "" + ); + + // Sign it + bytes32 messageHash = payments.hashPaymentDetails(details); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + + // Send it + vm.expectRevert(InvalidTokenTransfer.selector); + vm.prank(caller); + payments.makePayment(details, sig); + } + // Note there is a VERY slim chance this test can fail if the fuzzed data makes a valid call function testMakePaymentFailedChainedCall(address caller, DetailsInput calldata input, bytes memory chainedCallData) public From 1927514f5b220664f24311c5a00323ff8961b4fb Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 16 Jul 2024 08:57:27 +1200 Subject: [PATCH 11/17] Update gas snapshot --- .gas-snapshot | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 39111cb..9de8b22 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -187,14 +187,21 @@ PaymentCombinerTest:testListReleaseERC20(uint256,address[],address[],uint256[],u PaymentCombinerTest:testListReleaseNative(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1773950, ~: 1790406) PaymentCombinerTest:testRepeatedDeploysFail(address[],uint256[]) (runs: 1024, μ: 1040384595, ~: 1040390920) PaymentCombinerTest:testSupportsInterface() (gas: 10684) -PaymentsTest:testMakePaymentExpired(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),uint64) (runs: 1024, μ: 132598, ~: 141327) -PaymentsTest:testMakePaymentInvalidSignature(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bytes) (runs: 1024, μ: 25974, ~: 25945) -PaymentsTest:testMakePaymentSuccess(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 164803, ~: 180713) -PaymentsTest:testMakePaymentSuccessChainedCall(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 238699, ~: 237353) -PaymentsTest:testMakePaymentSuccessMultiplePaymentRecips(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),address) (runs: 1024, μ: 190134, ~: 172742) -PaymentsTest:testSupportsInterface() (gas: 10748) -PaymentsTest:testUpdateSignerInvalidSender(address,address) (runs: 1024, μ: 13748, ~: 13748) -PaymentsTest:testUpdateSignerSuccess(address) (runs: 1024, μ: 19171, ~: 19204) +PaymentsTest:testMakePaymentExpired(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),uint64) (runs: 1024, μ: 132853, ~: 141494) +PaymentsTest:testMakePaymentFailedChainedCall(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bytes) (runs: 1024, μ: 161245, ~: 179033) +PaymentsTest:testMakePaymentInvalidPayment(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 71571, ~: 71420) +PaymentsTest:testMakePaymentInvalidSignature(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bytes) (runs: 1024, μ: 26209, ~: 26182) +PaymentsTest:testMakePaymentInvalidTokenSettingsERC20(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),uint256) (runs: 1024, μ: 65392, ~: 65456) +PaymentsTest:testMakePaymentInvalidTokenSettingsERC721(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 65166, ~: 65224) +PaymentsTest:testMakePaymentSuccess(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 164567, ~: 180604) +PaymentsTest:testMakePaymentSuccessChainedCall(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 238662, ~: 237362) +PaymentsTest:testMakePaymentSuccessMultiplePaymentRecips(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),address) (runs: 1024, μ: 189205, ~: 172813) +PaymentsTest:testPerformChainedCallInvalidCall(bytes) (runs: 1024, μ: 14950, ~: 14906) +PaymentsTest:testPerformChainedCallInvalidCaller(address,uint8,uint256,uint256,address) (runs: 1024, μ: 22959, ~: 22922) +PaymentsTest:testPerformChainedCallSuccess(uint8,uint256,uint256,address) (runs: 1024, μ: 88806, ~: 98550) +PaymentsTest:testSupportsInterface() (gas: 10757) +PaymentsTest:testUpdateSignerInvalidSender(address,address) (runs: 1024, μ: 13776, ~: 13776) +PaymentsTest:testUpdateSignerSuccess(address) (runs: 1024, μ: 19182, ~: 19229) SequenceProxyFactoryTest:testAddressCompute() (gas: 1021270) SequenceProxyFactoryTest:testDeployProxy() (gas: 1021715) SequenceProxyFactoryTest:testDeployProxyAfterUpgrade() (gas: 1033634) From 08ee48e62a87e4a97e41d5e3a7696668fd274293 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 16 Jul 2024 09:23:10 +1200 Subject: [PATCH 12/17] Test stability --- test/payments/Payments.t.sol | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index b09e4f9..3cdde0b 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -408,13 +408,16 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } - // Note there is a VERY slim chance this test can fail if the fuzzed data makes a valid call function testMakePaymentFailedChainedCall(address caller, DetailsInput calldata input, bytes memory chainedCallData) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) safeAddress(input.productRecipient) { + // Check the call will fail + (bool success,) = address(payments).call(chainedCallData); + vm.assume(!success); + uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); @@ -473,6 +476,8 @@ contract PaymentsTest is Test, IPaymentsSignals { public safeAddress(recipient) { + vm.assume(caller != signer); + IPaymentsFunctions.TokenType tokenType = _toTokenType(tokenTypeInt); address tokenAddr; (tokenAddr, tokenId, amount) = _validTokenParams(tokenType, tokenId, amount); @@ -485,10 +490,13 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.performChainedCall(tokenAddr, callData); } - // Note there is a slim chance this test can fail if the fuzzed data makes a valid call function testPerformChainedCallInvalidCall(bytes memory chainedCallData) public { + // Check the call will fail + (bool success,) = address(payments).call(chainedCallData); + vm.assume(!success); + vm.expectRevert(ChainedCallFailed.selector); vm.prank(signer); // Chained call to payments will fail From c6769c6737d04bbc06702627993013708ef4ad90 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Wed, 17 Jul 2024 07:12:12 +1200 Subject: [PATCH 13/17] Add signer to payments interface --- src/payments/IPayments.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index 20d939e..84975f9 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -77,6 +77,12 @@ interface IPaymentsFunctions { * @return accepted True if the payment has been accepted. */ function paymentAccepted(uint256 purchaseId) external view returns (bool); + + /** + * Get the signer address. + * @return signer The signer address. + */ + function signer() external view returns (address); } interface IPaymentsSignals { From 2a2cfe97e86999315e69f56310c1930ed8da82ce Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Wed, 17 Jul 2024 08:00:42 +1200 Subject: [PATCH 14/17] Parity for chained calls --- src/payments/IPayments.sol | 66 +++++++++++++++++++++++------------- src/payments/Payments.sol | 48 ++++++++++++++++++-------- test/payments/Payments.t.sol | 58 +++++++++++++++++-------------- 3 files changed, 109 insertions(+), 63 deletions(-) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index 84975f9..edfdebb 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -15,6 +15,13 @@ interface IPaymentsFunctions { uint256 amount; } + struct ChainedCallDetails { + // Address for chained call + address chainedCallAddress; + // Data for chained call + bytes chainedCallData; + } + struct PaymentDetails { // Unique ID for this purchase uint256 purchaseId; @@ -32,44 +39,34 @@ interface IPaymentsFunctions { uint64 expiration; // ID of the product string productId; - // Address for chained call - address chainedCallAddress; - // Data for chained call - bytes chainedCallData; + // Chained call details + ChainedCallDetails chainedCallDetails; } /** - * Make a payment for a product. + * Returns the hash of the payment details. * @param paymentDetails The payment details. - * @param signature The signature of the payment. - */ - function makePayment(PaymentDetails calldata paymentDetails, bytes calldata signature) external payable; - - /** - * Complete a chained call. - * @param chainedCallAddress The address of the chained call. - * @param chainedCallData The data for the chained call. - * @notice This is only callable by an authorised party. + * @return paymentHash The hash of the payment details for signing. */ - function performChainedCall(address chainedCallAddress, bytes calldata chainedCallData) external; + function hashPaymentDetails(PaymentDetails calldata paymentDetails) external view returns (bytes32 paymentHash); /** - * Check is a signature is valid. + * Check is a payment signature is valid. * @param paymentDetails The payment details. * @param signature The signature of the payment. * @return isValid True if the signature is valid. */ - function isValidSignature(PaymentDetails calldata paymentDetails, bytes calldata signature) + function isValidPaymentSignature(PaymentDetails calldata paymentDetails, bytes calldata signature) external view returns (bool isValid); /** - * Returns the hash of the payment details. + * Make a payment for a product. * @param paymentDetails The payment details. - * @return paymentHash The hash of the payment details for signing. + * @param signature The signature of the payment. */ - function hashPaymentDetails(PaymentDetails calldata paymentDetails) external view returns (bytes32 paymentHash); + function makePayment(PaymentDetails calldata paymentDetails, bytes calldata signature) external payable; /** * Check if a payment has been accepted. @@ -78,6 +75,32 @@ interface IPaymentsFunctions { */ function paymentAccepted(uint256 purchaseId) external view returns (bool); + /** + * Returns the hash of the chained call. + * @param chainedCallDetails The chained call details. + * @return callHash The hash of the chained call for signing. + */ + function hashChainedCallDetails(ChainedCallDetails calldata chainedCallDetails) external view returns (bytes32 callHash); + + /** + * Complete a chained call. + * @param chainedCallDetails The chained call details. + * @param signature The signature of the chained call. + * @dev This is called when a payment is accepted off/cross chain. + */ + function performChainedCall(ChainedCallDetails calldata chainedCallDetails, bytes calldata signature) external; + + /** + * Check is a chained call signature is valid. + * @param chainedCallDetails The chained call details. + * @param signature The signature of the chained call. + * @return isValid True if the signature is valid. + */ + function isValidChainedCallSignature(ChainedCallDetails calldata chainedCallDetails, bytes calldata signature) + external + view + returns (bool isValid); + /** * Get the signer address. * @return signer The signer address. @@ -89,9 +112,6 @@ interface IPaymentsSignals { /// @notice Emitted when a payment is already accepted. This prevents double spending. error PaymentAlreadyAccepted(); - /// @notice Emitted when a sender is invalid. - error InvalidSender(); - /// @notice Emitted when a signature is invalid. error InvalidSignature(); diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index bc27250..3ec47c3 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -38,7 +38,7 @@ contract Payments is Ownable, IPayments, IERC165 { if (paymentAccepted[paymentDetails.purchaseId]) { revert PaymentAlreadyAccepted(); } - if (!isValidSignature(paymentDetails, signature)) { + if (!isValidPaymentSignature(paymentDetails, signature)) { revert InvalidSignature(); } if (block.timestamp > paymentDetails.expiration) { @@ -65,14 +65,14 @@ contract Payments is Ownable, IPayments, IERC165 { emit PaymentMade(spender, paymentDetails.productRecipient, paymentDetails.purchaseId, paymentDetails.productId); // Perform chained call - if (paymentDetails.chainedCallAddress != address(0)) { - _performChainedCall(paymentDetails.chainedCallAddress, paymentDetails.chainedCallData); + if (paymentDetails.chainedCallDetails.chainedCallAddress != address(0)) { + _performChainedCall(paymentDetails.chainedCallDetails); } } /// @inheritdoc IPaymentsFunctions /// @notice A valid signature does not guarantee that the payment will be accepted. - function isValidSignature(PaymentDetails calldata paymentDetails, bytes calldata signature) + function isValidPaymentSignature(PaymentDetails calldata paymentDetails, bytes calldata signature) public view returns (bool) @@ -96,28 +96,48 @@ contract Payments is Ownable, IPayments, IERC165 { paymentDetails.paymentRecipients, paymentDetails.expiration, paymentDetails.productId, - paymentDetails.chainedCallAddress, - paymentDetails.chainedCallData + paymentDetails.chainedCallDetails ) ); } /// @inheritdoc IPaymentsFunctions - /// @notice This can only be called by the signer. /// @dev As the signer can validate any payment (including zero) this function does not increase the security surface. - function performChainedCall(address chainedCallAddress, bytes calldata chainedCallData) external override { - // Check authorization - if (msg.sender != signer) { - revert InvalidSender(); + function performChainedCall(ChainedCallDetails calldata chainedCallDetails, bytes calldata signature) external override { + if (!isValidChainedCallSignature(chainedCallDetails, signature)) { + revert InvalidSignature(); } - _performChainedCall(chainedCallAddress, chainedCallData); + _performChainedCall(chainedCallDetails); + } + + /// @inheritdoc IPaymentsFunctions + function isValidChainedCallSignature(ChainedCallDetails calldata chainedCallDetails, bytes calldata signature) + public + view + returns (bool) + { + bytes32 messageHash = hashChainedCallDetails(chainedCallDetails); + address sigSigner = messageHash.recoverCalldata(signature); + return sigSigner == signer; + } + + /// @inheritdoc IPaymentsFunctions + /// @dev This hash includes the chain ID. + function hashChainedCallDetails(ChainedCallDetails calldata chainedCallDetails) public view returns (bytes32) { + return keccak256( + abi.encode( + block.chainid, + chainedCallDetails.chainedCallAddress, + chainedCallDetails.chainedCallData + ) + ); } /** * Perform a chained call and revert on error. */ - function _performChainedCall(address chainedCallAddress, bytes calldata chainedCallData) internal { - (bool success, ) = chainedCallAddress.call{value: 0}(chainedCallData); + function _performChainedCall(ChainedCallDetails calldata chainedCallDetails) internal { + (bool success, ) = chainedCallDetails.chainedCallAddress.call{value: 0}(chainedCallDetails.chainedCallData); if (!success) { revert ChainedCallFailed(); } diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index 3cdde0b..33bf872 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -89,8 +89,10 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - address(0), - "" + IPaymentsFunctions.ChainedCallDetails( + address(0), + "" + ) ); // Mint required tokens @@ -150,8 +152,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - chainedTokenAddr, - chainedData + IPaymentsFunctions.ChainedCallDetails(chainedTokenAddr, chainedData) ); // Mint required tokens @@ -201,8 +202,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - address(0), - "" + IPaymentsFunctions.ChainedCallDetails(address(0), "") ); // Mint required tokens @@ -243,8 +243,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - address(0), - "" + IPaymentsFunctions.ChainedCallDetails(address(0), "") ); // Send it @@ -276,8 +275,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, input.expiration, input.productId, - address(0), - "" + IPaymentsFunctions.ChainedCallDetails(address(0), "") ); // Mint required tokens @@ -316,8 +314,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - address(0), - "" + IPaymentsFunctions.ChainedCallDetails(address(0), "") ); // Do not mint required tokens @@ -356,8 +353,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - address(0), - "" + IPaymentsFunctions.ChainedCallDetails(address(0), "") ); // Sign it @@ -393,8 +389,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - address(0), - "" + IPaymentsFunctions.ChainedCallDetails(address(0), "") ); // Sign it @@ -434,8 +429,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - address(payments), // Chained call to payments will fail - chainedCallData + IPaymentsFunctions.ChainedCallDetails(address(payments), chainedCallData) ); // Mint required tokens @@ -464,15 +458,21 @@ contract PaymentsTest is Test, IPaymentsSignals { (tokenAddr, tokenId, amount) = _validTokenParams(tokenType, tokenId, amount); bytes memory callData = abi.encodeWithSelector(IGenericToken.mint.selector, recipient, tokenId, amount); + IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = IPaymentsFunctions.ChainedCallDetails(tokenAddr, callData); + + // Sign it + bytes32 messageHash = payments.hashChainedCallDetails(chainedCallDetails); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); // Send it vm.prank(signer); - payments.performChainedCall(tokenAddr, callData); + payments.performChainedCall(chainedCallDetails, sig); assertEq(IGenericToken(tokenAddr).balanceOf(recipient, tokenId), amount); } - function testPerformChainedCallInvalidCaller(address caller, uint8 tokenTypeInt, uint256 tokenId, uint256 amount, address recipient) + function testPerformChainedCallInvalidSignature(address caller, uint8 tokenTypeInt, uint256 tokenId, uint256 amount, address recipient, bytes calldata sig) public safeAddress(recipient) { @@ -483,24 +483,30 @@ contract PaymentsTest is Test, IPaymentsSignals { (tokenAddr, tokenId, amount) = _validTokenParams(tokenType, tokenId, amount); bytes memory callData = abi.encodeWithSelector(IGenericToken.mint.selector, recipient, tokenId, amount); + IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = IPaymentsFunctions.ChainedCallDetails(tokenAddr, callData); // Send it - vm.expectRevert(InvalidSender.selector); + vm.expectRevert(InvalidSignature.selector); vm.prank(caller); - payments.performChainedCall(tokenAddr, callData); + payments.performChainedCall(chainedCallDetails, sig); } - function testPerformChainedCallInvalidCall(bytes memory chainedCallData) + function testPerformChainedCallInvalidCall(bytes calldata chainedCallData) public { + IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = IPaymentsFunctions.ChainedCallDetails(address(this), chainedCallData); // Check the call will fail - (bool success,) = address(payments).call(chainedCallData); + (bool success,) = chainedCallDetails.chainedCallAddress.call(chainedCallDetails.chainedCallData); vm.assume(!success); + // Sign it + bytes32 messageHash = payments.hashChainedCallDetails(chainedCallDetails); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); + bytes memory sig = abi.encodePacked(r, s, v); + vm.expectRevert(ChainedCallFailed.selector); vm.prank(signer); - // Chained call to payments will fail - payments.performChainedCall(address(payments), chainedCallData); + payments.performChainedCall(chainedCallDetails, sig); } // Update signer From 9e209cb40b621f81a1d0842c944ad8a054e320dd Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Wed, 17 Jul 2024 08:01:42 +1200 Subject: [PATCH 15/17] Format --- src/payments/IPayments.sol | 5 ++- src/payments/Payments.sol | 13 ++++---- test/payments/Payments.t.sol | 61 +++++++++++++++++++++--------------- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index edfdebb..4e54039 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -80,7 +80,10 @@ interface IPaymentsFunctions { * @param chainedCallDetails The chained call details. * @return callHash The hash of the chained call for signing. */ - function hashChainedCallDetails(ChainedCallDetails calldata chainedCallDetails) external view returns (bytes32 callHash); + function hashChainedCallDetails(ChainedCallDetails calldata chainedCallDetails) + external + view + returns (bytes32 callHash); /** * Complete a chained call. diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index 3ec47c3..29bbe51 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -103,7 +103,10 @@ contract Payments is Ownable, IPayments, IERC165 { /// @inheritdoc IPaymentsFunctions /// @dev As the signer can validate any payment (including zero) this function does not increase the security surface. - function performChainedCall(ChainedCallDetails calldata chainedCallDetails, bytes calldata signature) external override { + function performChainedCall(ChainedCallDetails calldata chainedCallDetails, bytes calldata signature) + external + override + { if (!isValidChainedCallSignature(chainedCallDetails, signature)) { revert InvalidSignature(); } @@ -125,11 +128,7 @@ contract Payments is Ownable, IPayments, IERC165 { /// @dev This hash includes the chain ID. function hashChainedCallDetails(ChainedCallDetails calldata chainedCallDetails) public view returns (bytes32) { return keccak256( - abi.encode( - block.chainid, - chainedCallDetails.chainedCallAddress, - chainedCallDetails.chainedCallData - ) + abi.encode(block.chainid, chainedCallDetails.chainedCallAddress, chainedCallDetails.chainedCallData) ); } @@ -137,7 +136,7 @@ contract Payments is Ownable, IPayments, IERC165 { * Perform a chained call and revert on error. */ function _performChainedCall(ChainedCallDetails calldata chainedCallDetails) internal { - (bool success, ) = chainedCallDetails.chainedCallAddress.call{value: 0}(chainedCallDetails.chainedCallData); + (bool success,) = chainedCallDetails.chainedCallAddress.call{value: 0}(chainedCallDetails.chainedCallData); if (!success) { revert ChainedCallFailed(); } diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index 33bf872..88c7824 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -75,7 +75,8 @@ contract PaymentsTest is Test, IPaymentsSignals { { uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = + _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); paymentRecipients[0] = input.paymentRecipient; paymentRecipients[0].amount = amount; @@ -89,10 +90,7 @@ contract PaymentsTest is Test, IPaymentsSignals { paymentRecipients, expiration, input.productId, - IPaymentsFunctions.ChainedCallDetails( - address(0), - "" - ) + IPaymentsFunctions.ChainedCallDetails(address(0), "") ); // Mint required tokens @@ -126,7 +124,8 @@ contract PaymentsTest is Test, IPaymentsSignals { { uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = + _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); paymentRecipients[0] = input.paymentRecipient; paymentRecipients[0].amount = amount; @@ -140,8 +139,10 @@ contract PaymentsTest is Test, IPaymentsSignals { } else { chainedTokenType = IPaymentsFunctions.TokenType.ERC20; } - (address chainedTokenAddr, uint256 chainedTokenId, uint256 chainedAmount) = _validTokenParams(chainedTokenType, input.tokenId, input.paymentRecipient.amount); - bytes memory chainedData = abi.encodeWithSelector(IGenericToken.mint.selector, input.productRecipient, chainedTokenId, chainedAmount); + (address chainedTokenAddr, uint256 chainedTokenId, uint256 chainedAmount) = + _validTokenParams(chainedTokenType, input.tokenId, input.paymentRecipient.amount); + bytes memory chainedData = + abi.encodeWithSelector(IGenericToken.mint.selector, input.productRecipient, chainedTokenId, chainedAmount); IPaymentsFunctions.PaymentDetails memory details = IPaymentsFunctions.PaymentDetails( input.purchaseId, @@ -187,7 +188,8 @@ contract PaymentsTest is Test, IPaymentsSignals { uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = + _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](2); paymentRecipients[0] = input.paymentRecipient; paymentRecipients[0].amount = amount; @@ -229,7 +231,8 @@ contract PaymentsTest is Test, IPaymentsSignals { { uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = + _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); paymentRecipients[0] = input.paymentRecipient; paymentRecipients[0].amount = amount; @@ -261,7 +264,8 @@ contract PaymentsTest is Test, IPaymentsSignals { vm.warp(blockTimestamp); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = + _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); paymentRecipients[0] = input.paymentRecipient; paymentRecipients[0].amount = amount; @@ -300,7 +304,8 @@ contract PaymentsTest is Test, IPaymentsSignals { { uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = + _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); paymentRecipients[0] = input.paymentRecipient; paymentRecipients[0].amount = amount; @@ -372,10 +377,10 @@ contract PaymentsTest is Test, IPaymentsSignals { safeAddress(caller) safeAddress(input.paymentRecipient.recipient) { - uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); IPaymentsFunctions.TokenType tokenType = IPaymentsFunctions.TokenType.ERC721; - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = + _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); paymentRecipients[0] = input.paymentRecipient; paymentRecipients[0].amount = _bound(amount, 2, type(uint256).max); // Invalid amount @@ -415,7 +420,8 @@ contract PaymentsTest is Test, IPaymentsSignals { uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); - (address tokenAddr, uint256 tokenId, uint256 amount) = _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); + (address tokenAddr, uint256 tokenId, uint256 amount) = + _validTokenParams(tokenType, input.tokenId, input.paymentRecipient.amount); IPaymentsFunctions.PaymentRecipient[] memory paymentRecipients = new IPaymentsFunctions.PaymentRecipient[](1); paymentRecipients[0] = input.paymentRecipient; paymentRecipients[0].amount = amount; @@ -458,7 +464,8 @@ contract PaymentsTest is Test, IPaymentsSignals { (tokenAddr, tokenId, amount) = _validTokenParams(tokenType, tokenId, amount); bytes memory callData = abi.encodeWithSelector(IGenericToken.mint.selector, recipient, tokenId, amount); - IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = IPaymentsFunctions.ChainedCallDetails(tokenAddr, callData); + IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = + IPaymentsFunctions.ChainedCallDetails(tokenAddr, callData); // Sign it bytes32 messageHash = payments.hashChainedCallDetails(chainedCallDetails); @@ -472,10 +479,14 @@ contract PaymentsTest is Test, IPaymentsSignals { assertEq(IGenericToken(tokenAddr).balanceOf(recipient, tokenId), amount); } - function testPerformChainedCallInvalidSignature(address caller, uint8 tokenTypeInt, uint256 tokenId, uint256 amount, address recipient, bytes calldata sig) - public - safeAddress(recipient) - { + function testPerformChainedCallInvalidSignature( + address caller, + uint8 tokenTypeInt, + uint256 tokenId, + uint256 amount, + address recipient, + bytes calldata sig + ) public safeAddress(recipient) { vm.assume(caller != signer); IPaymentsFunctions.TokenType tokenType = _toTokenType(tokenTypeInt); @@ -483,7 +494,8 @@ contract PaymentsTest is Test, IPaymentsSignals { (tokenAddr, tokenId, amount) = _validTokenParams(tokenType, tokenId, amount); bytes memory callData = abi.encodeWithSelector(IGenericToken.mint.selector, recipient, tokenId, amount); - IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = IPaymentsFunctions.ChainedCallDetails(tokenAddr, callData); + IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = + IPaymentsFunctions.ChainedCallDetails(tokenAddr, callData); // Send it vm.expectRevert(InvalidSignature.selector); @@ -491,10 +503,9 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.performChainedCall(chainedCallDetails, sig); } - function testPerformChainedCallInvalidCall(bytes calldata chainedCallData) - public - { - IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = IPaymentsFunctions.ChainedCallDetails(address(this), chainedCallData); + function testPerformChainedCallInvalidCall(bytes calldata chainedCallData) public { + IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = + IPaymentsFunctions.ChainedCallDetails(address(this), chainedCallData); // Check the call will fail (bool success,) = chainedCallDetails.chainedCallAddress.call(chainedCallDetails.chainedCallData); vm.assume(!success); From f1d9d2e4fd2de747e80b53aef49e96db7d823037 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 23 Jul 2024 09:42:06 +1200 Subject: [PATCH 16/17] Support contract signers for Payments --- .gas-snapshot | 30 +++--- src/payments/Payments.sol | 9 +- src/utils/SignatureValidator.sol | 44 +++++++++ test/_mocks/ERC1271Mock.sol | 35 +++++++ test/payments/Payments.t.sol | 151 ++++++++++++++++++------------- 5 files changed, 187 insertions(+), 82 deletions(-) create mode 100644 src/utils/SignatureValidator.sol create mode 100644 test/_mocks/ERC1271Mock.sol diff --git a/.gas-snapshot b/.gas-snapshot index 9de8b22..da2a0ca 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -187,21 +187,21 @@ PaymentCombinerTest:testListReleaseERC20(uint256,address[],address[],uint256[],u PaymentCombinerTest:testListReleaseNative(uint256,address[],address[],uint256[],uint256[]) (runs: 1024, μ: 1773950, ~: 1790406) PaymentCombinerTest:testRepeatedDeploysFail(address[],uint256[]) (runs: 1024, μ: 1040384595, ~: 1040390920) PaymentCombinerTest:testSupportsInterface() (gas: 10684) -PaymentsTest:testMakePaymentExpired(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),uint64) (runs: 1024, μ: 132853, ~: 141494) -PaymentsTest:testMakePaymentFailedChainedCall(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bytes) (runs: 1024, μ: 161245, ~: 179033) -PaymentsTest:testMakePaymentInvalidPayment(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 71571, ~: 71420) -PaymentsTest:testMakePaymentInvalidSignature(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bytes) (runs: 1024, μ: 26209, ~: 26182) -PaymentsTest:testMakePaymentInvalidTokenSettingsERC20(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),uint256) (runs: 1024, μ: 65392, ~: 65456) -PaymentsTest:testMakePaymentInvalidTokenSettingsERC721(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 65166, ~: 65224) -PaymentsTest:testMakePaymentSuccess(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 164567, ~: 180604) -PaymentsTest:testMakePaymentSuccessChainedCall(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string)) (runs: 1024, μ: 238662, ~: 237362) -PaymentsTest:testMakePaymentSuccessMultiplePaymentRecips(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),address) (runs: 1024, μ: 189205, ~: 172813) -PaymentsTest:testPerformChainedCallInvalidCall(bytes) (runs: 1024, μ: 14950, ~: 14906) -PaymentsTest:testPerformChainedCallInvalidCaller(address,uint8,uint256,uint256,address) (runs: 1024, μ: 22959, ~: 22922) -PaymentsTest:testPerformChainedCallSuccess(uint8,uint256,uint256,address) (runs: 1024, μ: 88806, ~: 98550) -PaymentsTest:testSupportsInterface() (gas: 10757) -PaymentsTest:testUpdateSignerInvalidSender(address,address) (runs: 1024, μ: 13776, ~: 13776) -PaymentsTest:testUpdateSignerSuccess(address) (runs: 1024, μ: 19182, ~: 19229) +PaymentsTest:testMakePaymentExpired(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),uint64,bool) (runs: 1024, μ: 150078, ~: 152514) +PaymentsTest:testMakePaymentFailedChainedCall(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bytes,bool) (runs: 1024, μ: 179235, ~: 180946) +PaymentsTest:testMakePaymentInvalidPayment(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bool) (runs: 1024, μ: 88211, ~: 73578) +PaymentsTest:testMakePaymentInvalidSignature(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bool) (runs: 1024, μ: 40236, ~: 45824) +PaymentsTest:testMakePaymentInvalidTokenSettingsERC20(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),uint256,bool) (runs: 1024, μ: 81223, ~: 66226) +PaymentsTest:testMakePaymentInvalidTokenSettingsERC721(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bool) (runs: 1024, μ: 81311, ~: 65948) +PaymentsTest:testMakePaymentSuccess(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bool) (runs: 1024, μ: 180481, ~: 182624) +PaymentsTest:testMakePaymentSuccessChainedCall(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),bool) (runs: 1024, μ: 254654, ~: 252842) +PaymentsTest:testMakePaymentSuccessMultiplePaymentRecips(address,(uint256,address,uint8,address,uint256,(address,uint256),uint64,string),address,bool) (runs: 1024, μ: 205142, ~: 205897) +PaymentsTest:testPerformChainedCallInvalidCall(bytes,bool) (runs: 1024, μ: 42194, ~: 27302) +PaymentsTest:testPerformChainedCallInvalidSignature(address,uint8,uint256,uint256,address,bool) (runs: 1024, μ: 39924, ~: 45709) +PaymentsTest:testPerformChainedCallSuccess(uint8,uint256,uint256,address,bool) (runs: 1024, μ: 113539, ~: 112428) +PaymentsTest:testSupportsInterface() (gas: 10790) +PaymentsTest:testUpdateSignerInvalidSender(address,address) (runs: 1024, μ: 13790, ~: 13790) +PaymentsTest:testUpdateSignerSuccess(address) (runs: 1024, μ: 19254, ~: 19287) SequenceProxyFactoryTest:testAddressCompute() (gas: 1021270) SequenceProxyFactoryTest:testDeployProxy() (gas: 1021715) SequenceProxyFactoryTest:testDeployProxyAfterUpgrade() (gas: 1033634) diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index 29bbe51..4a27664 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -6,14 +6,13 @@ import {IPayments, IPaymentsFunctions} from "./IPayments.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol"; +import {SignatureValidator} from "../utils/SignatureValidator.sol"; import {IERC721Transfer} from "../tokens/common/IERC721Transfer.sol"; import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol"; - import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; -import {ECDSA} from "solady/utils/ECDSA.sol"; contract Payments is Ownable, IPayments, IERC165 { - using ECDSA for bytes32; + using SignatureValidator for bytes32; address public signer; @@ -78,7 +77,7 @@ contract Payments is Ownable, IPayments, IERC165 { returns (bool) { bytes32 messageHash = hashPaymentDetails(paymentDetails); - address sigSigner = messageHash.recoverCalldata(signature); + address sigSigner = messageHash.recoverSigner(signature); return sigSigner == signer; } @@ -120,7 +119,7 @@ contract Payments is Ownable, IPayments, IERC165 { returns (bool) { bytes32 messageHash = hashChainedCallDetails(chainedCallDetails); - address sigSigner = messageHash.recoverCalldata(signature); + address sigSigner = messageHash.recoverSigner(signature); return sigSigner == signer; } diff --git a/src/utils/SignatureValidator.sol b/src/utils/SignatureValidator.sol new file mode 100644 index 0000000..94e9e4b --- /dev/null +++ b/src/utils/SignatureValidator.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.19; + +import {ECDSA} from "solady/utils/ECDSA.sol"; +import {IERC1271Wallet} from "@0xsequence/erc-1155/contracts/interfaces/IERC1271Wallet.sol"; + +library SignatureValidator { + using ECDSA for bytes32; + + uint8 private constant SIG_TYPE_ERC712 = 1; + uint8 private constant SIG_TYPE_ERC1271 = 2; + + bytes4 internal constant ERC1271_MAGICVALUE = 0x1626ba7e; + + /** + * Check if a signature is valid. + * @param digest The digest to check. + * @param signature The signature to check. + * @return signer The detected signer address if valid, otherwise address(0). + * @dev An ERC721 signature is formatted `0x01`. + * @dev An ERC1271 signature is formatted `0x02`. + */ + function recoverSigner(bytes32 digest, bytes calldata signature) internal view returns (address signer) { + // Check first byte of signature for signature type + uint8 sigType = uint8(signature[0]); + if (sigType == SIG_TYPE_ERC712) { + // ERC712 + signer = digest.recoverCalldata(signature[1:]); + } else if (sigType == SIG_TYPE_ERC1271 && signature.length >= 21) { + // ERC1271 + assembly { + let word := calldataload(add(1, signature.offset)) + signer := shr(96, word) + } + try IERC1271Wallet(signer).isValidSignature(digest, signature[21:]) returns (bytes4 magicValue) { + if (magicValue != ERC1271_MAGICVALUE) { + signer = address(0); + } + } catch { + signer = address(0); + } + } + } +} diff --git a/test/_mocks/ERC1271Mock.sol b/test/_mocks/ERC1271Mock.sol new file mode 100644 index 0000000..ca9d410 --- /dev/null +++ b/test/_mocks/ERC1271Mock.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.19; + +import {IERC1271Wallet} from "@0xsequence/erc-1155/contracts/interfaces/IERC1271Wallet.sol"; + +contract ERC1271Mock is IERC1271Wallet { + mapping(bytes32 => bool) private _validSignatures; + + function setValidSignature(bytes32 signature) public { + _validSignatures[signature] = true; + } + + function isValidSignature(bytes calldata, bytes calldata signature) + external + view + override + returns (bytes4 magicValue) + { + bytes32 sigBytes32 = abi.decode(signature, (bytes32)); + if (_validSignatures[sigBytes32]) { + return 0x20c13b0b; + } else { + return 0x0; + } + } + + function isValidSignature(bytes32, bytes calldata signature) external view override returns (bytes4 magicValue) { + bytes32 sigBytes32 = abi.decode(signature, (bytes32)); + if (_validSignatures[sigBytes32]) { + return 0x1626ba7e; + } else { + return 0x0; + } + } +} diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index 88c7824..7fe97c9 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -10,12 +10,14 @@ import {ERC1155Mock} from "test/_mocks/ERC1155Mock.sol"; import {ERC20Mock} from "test/_mocks/ERC20Mock.sol"; import {ERC721Mock} from "test/_mocks/ERC721Mock.sol"; import {IGenericToken} from "test/_mocks/IGenericToken.sol"; +import {ERC1271Mock} from "test/_mocks/ERC1271Mock.sol"; contract PaymentsTest is Test, IPaymentsSignals { Payments public payments; address public owner; address public signer; uint256 public signerPk; + ERC1271Mock public signer1271; ERC20Mock public erc20; ERC721Mock public erc721; @@ -29,6 +31,8 @@ contract PaymentsTest is Test, IPaymentsSignals { erc20 = new ERC20Mock(address(this)); erc721 = new ERC721Mock(address(this), "baseURI"); erc1155 = new ERC1155Mock(address(this), "baseURI"); + + signer1271 = new ERC1271Mock(); } struct DetailsInput { @@ -68,7 +72,42 @@ contract PaymentsTest is Test, IPaymentsSignals { return (address(erc1155), tokenId, bound(amount, 1, type(uint128).max / 10)); } - function testMakePaymentSuccess(address caller, DetailsInput calldata input) + function _signPayment(IPaymentsFunctions.PaymentDetails memory details, bool isERC1271, bool isValid) + internal + returns (bytes memory signature) + { + bytes32 digest = payments.hashPaymentDetails(details); + return _signDigest(digest, isERC1271, isValid); + } + + function _signChainedCall(IPaymentsFunctions.ChainedCallDetails memory details, bool isERC1271, bool isValid) + internal + returns (bytes memory signature) + { + bytes32 digest = payments.hashChainedCallDetails(details); + return _signDigest(digest, isERC1271, isValid); + } + + function _signDigest(bytes32 digest, bool isERC1271, bool isValid) internal returns (bytes memory signature) { + if (isERC1271) { + vm.prank(owner); + payments.updateSigner(address(signer1271)); + + // Pretend digest is the signature + if (isValid) { + signer1271.setValidSignature(digest); + } + return abi.encodePacked(uint8(2), address(signer1271), digest); + } else { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest); + if (!isValid) { + v--; // Invalidate sig + } + return abi.encodePacked(uint8(1), r, s, v); + } + } + + function testMakePaymentSuccess(address caller, DetailsInput calldata input, bool isERC1271) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) @@ -98,9 +137,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); // Sign it - bytes32 messageHash = payments.hashPaymentDetails(details); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signPayment(details, isERC1271, true); // Send it vm.expectEmit(true, true, true, true, address(payments)); @@ -116,7 +153,7 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } - function testMakePaymentSuccessChainedCall(address caller, DetailsInput calldata input) + function testMakePaymentSuccessChainedCall(address caller, DetailsInput calldata input, bool isERC1271) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) @@ -161,9 +198,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); // Sign it - bytes32 messageHash = payments.hashPaymentDetails(details); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signPayment(details, isERC1271, true); // Send it vm.expectEmit(true, true, true, true, address(payments)); @@ -176,12 +211,12 @@ contract PaymentsTest is Test, IPaymentsSignals { assertEq(IGenericToken(chainedTokenAddr).balanceOf(input.productRecipient, chainedTokenId), chainedAmount); } - function testMakePaymentSuccessMultiplePaymentRecips(address caller, DetailsInput calldata input, address recip2) - public - safeAddress(caller) - safeAddress(input.paymentRecipient.recipient) - safeAddress(recip2) - { + function testMakePaymentSuccessMultiplePaymentRecips( + address caller, + DetailsInput calldata input, + address recip2, + bool isERC1271 + ) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) safeAddress(recip2) { vm.assume(input.paymentRecipient.recipient != recip2); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); vm.assume(tokenType != IPaymentsFunctions.TokenType.ERC721); // ERC-721 not supported for multi payments @@ -212,9 +247,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount * 2); // Sign it - bytes32 messageHash = payments.hashPaymentDetails(details); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signPayment(details, isERC1271, true); // Send it vm.expectEmit(true, true, true, true, address(payments)); @@ -226,9 +259,7 @@ contract PaymentsTest is Test, IPaymentsSignals { assertEq(IGenericToken(tokenAddr).balanceOf(recip2, tokenId), amount); } - function testMakePaymentInvalidSignature(address caller, DetailsInput calldata input, bytes memory signature) - public - { + function testMakePaymentInvalidSignature(address caller, DetailsInput calldata input, bool isERC1271) public { uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); IPaymentsFunctions.TokenType tokenType = _toTokenType(input.tokenType); (address tokenAddr, uint256 tokenId, uint256 amount) = @@ -249,13 +280,16 @@ contract PaymentsTest is Test, IPaymentsSignals { IPaymentsFunctions.ChainedCallDetails(address(0), "") ); + // Invalid sign it + bytes memory sig = _signPayment(details, isERC1271, false); + // Send it vm.expectRevert(InvalidSignature.selector); vm.prank(caller); - payments.makePayment(details, signature); + payments.makePayment(details, sig); } - function testMakePaymentExpired(address caller, DetailsInput calldata input, uint64 blockTimestamp) + function testMakePaymentExpired(address caller, DetailsInput calldata input, uint64 blockTimestamp, bool isERC1271) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) @@ -287,9 +321,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); // Sign it - bytes32 messageHash = payments.hashPaymentDetails(details); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signPayment(details, isERC1271, true); // Send it vm.expectRevert(PaymentExpired.selector); @@ -297,7 +329,7 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } - function testMakePaymentInvalidPayment(address caller, DetailsInput calldata input) + function testMakePaymentInvalidPayment(address caller, DetailsInput calldata input, bool isERC1271) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) @@ -325,9 +357,7 @@ contract PaymentsTest is Test, IPaymentsSignals { // Do not mint required tokens // Sign it - bytes32 messageHash = payments.hashPaymentDetails(details); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signPayment(details, isERC1271, true); // Send it vm.expectRevert(); @@ -335,11 +365,12 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } - function testMakePaymentInvalidTokenSettingsERC20(address caller, DetailsInput calldata input, uint256 tokenId) - public - safeAddress(caller) - safeAddress(input.paymentRecipient.recipient) - { + function testMakePaymentInvalidTokenSettingsERC20( + address caller, + DetailsInput calldata input, + uint256 tokenId, + bool isERC1271 + ) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) { tokenId = _bound(tokenId, 1, type(uint256).max); // Non-zero uint64 expiration = uint64(_bound(input.expiration, block.timestamp, type(uint64).max)); @@ -362,9 +393,7 @@ contract PaymentsTest is Test, IPaymentsSignals { ); // Sign it - bytes32 messageHash = payments.hashPaymentDetails(details); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signPayment(details, isERC1271, true); // Send it vm.expectRevert(InvalidTokenTransfer.selector); @@ -372,7 +401,7 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } - function testMakePaymentInvalidTokenSettingsERC721(address caller, DetailsInput calldata input) + function testMakePaymentInvalidTokenSettingsERC721(address caller, DetailsInput calldata input, bool isERC1271) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) @@ -398,9 +427,7 @@ contract PaymentsTest is Test, IPaymentsSignals { ); // Sign it - bytes32 messageHash = payments.hashPaymentDetails(details); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signPayment(details, isERC1271, true); // Send it vm.expectRevert(InvalidTokenTransfer.selector); @@ -408,12 +435,12 @@ contract PaymentsTest is Test, IPaymentsSignals { payments.makePayment(details, sig); } - function testMakePaymentFailedChainedCall(address caller, DetailsInput calldata input, bytes memory chainedCallData) - public - safeAddress(caller) - safeAddress(input.paymentRecipient.recipient) - safeAddress(input.productRecipient) - { + function testMakePaymentFailedChainedCall( + address caller, + DetailsInput calldata input, + bytes memory chainedCallData, + bool isERC1271 + ) public safeAddress(caller) safeAddress(input.paymentRecipient.recipient) safeAddress(input.productRecipient) { // Check the call will fail (bool success,) = address(payments).call(chainedCallData); vm.assume(!success); @@ -443,9 +470,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IGenericToken(tokenAddr).approve(caller, address(payments), tokenId, amount); // Sign it - bytes32 messageHash = payments.hashPaymentDetails(details); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signPayment(details, isERC1271, true); // Send it vm.expectRevert(ChainedCallFailed.selector); @@ -455,10 +480,13 @@ contract PaymentsTest is Test, IPaymentsSignals { // Chained call - function testPerformChainedCallSuccess(uint8 tokenTypeInt, uint256 tokenId, uint256 amount, address recipient) - public - safeAddress(recipient) - { + function testPerformChainedCallSuccess( + uint8 tokenTypeInt, + uint256 tokenId, + uint256 amount, + address recipient, + bool isERC1271 + ) public safeAddress(recipient) { IPaymentsFunctions.TokenType tokenType = _toTokenType(tokenTypeInt); address tokenAddr; (tokenAddr, tokenId, amount) = _validTokenParams(tokenType, tokenId, amount); @@ -468,9 +496,7 @@ contract PaymentsTest is Test, IPaymentsSignals { IPaymentsFunctions.ChainedCallDetails(tokenAddr, callData); // Sign it - bytes32 messageHash = payments.hashChainedCallDetails(chainedCallDetails); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signChainedCall(chainedCallDetails, isERC1271, true); // Send it vm.prank(signer); @@ -485,7 +511,7 @@ contract PaymentsTest is Test, IPaymentsSignals { uint256 tokenId, uint256 amount, address recipient, - bytes calldata sig + bool isERC1271 ) public safeAddress(recipient) { vm.assume(caller != signer); @@ -497,13 +523,16 @@ contract PaymentsTest is Test, IPaymentsSignals { IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = IPaymentsFunctions.ChainedCallDetails(tokenAddr, callData); + // Fake sign it + bytes memory sig = _signChainedCall(chainedCallDetails, isERC1271, false); + // Send it vm.expectRevert(InvalidSignature.selector); vm.prank(caller); payments.performChainedCall(chainedCallDetails, sig); } - function testPerformChainedCallInvalidCall(bytes calldata chainedCallData) public { + function testPerformChainedCallInvalidCall(bytes calldata chainedCallData, bool isERC1271) public { IPaymentsFunctions.ChainedCallDetails memory chainedCallDetails = IPaymentsFunctions.ChainedCallDetails(address(this), chainedCallData); // Check the call will fail @@ -511,9 +540,7 @@ contract PaymentsTest is Test, IPaymentsSignals { vm.assume(!success); // Sign it - bytes32 messageHash = payments.hashChainedCallDetails(chainedCallDetails); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, messageHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = _signChainedCall(chainedCallDetails, isERC1271, true); vm.expectRevert(ChainedCallFailed.selector); vm.prank(signer); From d2febc98850026ecf1f35f4ad34ef2bf335adf98 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Thu, 8 Aug 2024 11:03:16 +1200 Subject: [PATCH 17/17] Add Factory for payments --- scripts/build.ts | 4 +-- scripts/constants.ts | 5 ++-- scripts/outputSelectors.ts | 4 +-- src/payments/IPayments.sol | 3 +++ src/payments/IPaymentsFactory.sol | 36 +++++++++++++++++++++++++ src/payments/Payments.sol | 16 ++++++++++- src/payments/PaymentsFactory.sol | 44 +++++++++++++++++++++++++++++++ test/payments/Payments.t.sol | 29 +++++++++++++++++--- 8 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 src/payments/IPaymentsFactory.sol create mode 100644 src/payments/PaymentsFactory.sol diff --git a/scripts/build.ts b/scripts/build.ts index fb8aa76..f62bf5d 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -5,7 +5,7 @@ import util from 'util' import { BUILD_DIR, DEPLOYABLE_CONTRACT_NAMES, - TOKEN_CONTRACT_NAMES, + PROXIED_TOKEN_CONTRACT_NAMES, } from './constants' const exec = util.promisify(execNonPromise) @@ -27,7 +27,7 @@ const main = async () => { // Create the compiler input files for (const solFile of [ ...DEPLOYABLE_CONTRACT_NAMES, - ...TOKEN_CONTRACT_NAMES, + ...PROXIED_TOKEN_CONTRACT_NAMES, 'TransparentUpgradeableBeaconProxy', 'UpgradeableBeacon', ]) { diff --git a/scripts/constants.ts b/scripts/constants.ts index 7ba954a..a47921c 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -8,11 +8,11 @@ export const DEPLOYABLE_CONTRACT_NAMES = [ 'ERC1155SaleFactory', 'ERC1155SoulboundFactory', 'PaymentCombiner', - 'Payments', + 'PaymentsFactory', 'Clawback', 'ClawbackMetadata', ] -export const TOKEN_CONTRACT_NAMES = [ +export const PROXIED_TOKEN_CONTRACT_NAMES = [ 'ERC20Items', 'ERC721Items', 'ERC721Sale', @@ -20,4 +20,5 @@ export const TOKEN_CONTRACT_NAMES = [ 'ERC1155Items', 'ERC1155Sale', 'ERC1155Soulbound', + 'Payments', ] diff --git a/scripts/outputSelectors.ts b/scripts/outputSelectors.ts index 6f37953..ddf7f93 100644 --- a/scripts/outputSelectors.ts +++ b/scripts/outputSelectors.ts @@ -1,4 +1,4 @@ -import { TOKEN_CONTRACT_NAMES } from './constants' +import { PROXIED_TOKEN_CONTRACT_NAMES } from './constants' const { spawn } = require('child_process') @@ -31,4 +31,4 @@ const outputSelectors = (contractName: string) => { }) } -TOKEN_CONTRACT_NAMES.forEach(outputSelectors) +PROXIED_TOKEN_CONTRACT_NAMES.forEach(outputSelectors) diff --git a/src/payments/IPayments.sol b/src/payments/IPayments.sol index 4e54039..54523c5 100644 --- a/src/payments/IPayments.sol +++ b/src/payments/IPayments.sol @@ -112,6 +112,9 @@ interface IPaymentsFunctions { } interface IPaymentsSignals { + /// @notice Emitted when contract is already initialized. + error InvalidInitialization(); + /// @notice Emitted when a payment is already accepted. This prevents double spending. error PaymentAlreadyAccepted(); diff --git a/src/payments/IPaymentsFactory.sol b/src/payments/IPaymentsFactory.sol new file mode 100644 index 0000000..118950a --- /dev/null +++ b/src/payments/IPaymentsFactory.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.19; + +interface IPaymentsFactoryFunctions { + /** + * Creates a Payments proxy contract + * @param proxyOwner The owner of the Payments proxy + * @param paymentsOwner The owner of the Payments implementation + * @param paymentsSigner The signer of the Payments implementation + * @return proxyAddr The address of the Payments proxy + */ + function deploy(address proxyOwner, address paymentsOwner, address paymentsSigner) + external + returns (address proxyAddr); + + /** + * Computes the address of a proxy instance. + * @param proxyOwner The owner of the Payments proxy + * @param paymentsOwner The owner of the Payments implementation + * @param paymentsSigner The signer of the Payments implementation + * @return proxyAddr The address of the Payments proxy + */ + function determineAddress(address proxyOwner, address paymentsOwner, address paymentsSigner) + external + returns (address proxyAddr); +} + +interface IPaymentsFactorySignals { + /** + * Event emitted when a new Payments proxy contract is deployed. + * @param proxyAddr The address of the deployed proxy. + */ + event PaymentsDeployed(address proxyAddr); +} + +interface IPaymentsFactory is IPaymentsFactoryFunctions, IPaymentsFactorySignals {} diff --git a/src/payments/Payments.sol b/src/payments/Payments.sol index 4a27664..48cca6b 100644 --- a/src/payments/Payments.sol +++ b/src/payments/Payments.sol @@ -14,14 +14,28 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; contract Payments is Ownable, IPayments, IERC165 { using SignatureValidator for bytes32; + bool private _initialized; + address public signer; // Payment accepted. Works as a nonce. mapping(uint256 => bool) public paymentAccepted; - constructor(address _owner, address _signer) { + /** + * Initialize the contract. + * @param _owner Owner address + * @param _signer Signer address + * @dev This should be called immediately after deployment. + */ + function initialize(address _owner, address _signer) public virtual { + if (_initialized) { + revert InvalidInitialization(); + } + Ownable._transferOwnership(_owner); signer = _signer; + + _initialized = true; } /** diff --git a/src/payments/PaymentsFactory.sol b/src/payments/PaymentsFactory.sol new file mode 100644 index 0000000..565de35 --- /dev/null +++ b/src/payments/PaymentsFactory.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.19; + +import {Payments} from "@0xsequence/contracts-library/payments/Payments.sol"; +import { + IPaymentsFactory, IPaymentsFactoryFunctions +} from "@0xsequence/contracts-library/payments/IPaymentsFactory.sol"; +import {SequenceProxyFactory} from "@0xsequence/contracts-library/proxies/SequenceProxyFactory.sol"; + +/** + * Deployer of Payments proxies. + */ +contract PaymentsFactory is IPaymentsFactory, SequenceProxyFactory { + /** + * Creates an Payments Factory. + * @param factoryOwner The owner of the Payments Factory + */ + constructor(address factoryOwner) { + Payments impl = new Payments(); + SequenceProxyFactory._initialize(address(impl), factoryOwner); + } + + /// @inheritdoc IPaymentsFactoryFunctions + function deploy(address proxyOwner, address paymentsOwner, address paymentsSigner) + external + returns (address proxyAddr) + { + bytes32 salt = keccak256(abi.encode(paymentsOwner, paymentsSigner)); + proxyAddr = _createProxy(salt, proxyOwner, ""); + Payments(proxyAddr).initialize(paymentsOwner, paymentsSigner); + emit PaymentsDeployed(proxyAddr); + return proxyAddr; + } + + /// @inheritdoc IPaymentsFactoryFunctions + function determineAddress(address proxyOwner, address paymentsOwner, address paymentsSigner) + external + view + returns (address proxyAddr) + { + bytes32 salt = keccak256(abi.encode(paymentsOwner, paymentsSigner)); + return _computeProxyAddress(salt, proxyOwner, ""); + } +} diff --git a/test/payments/Payments.t.sol b/test/payments/Payments.t.sol index 7fe97c9..f57983f 100644 --- a/test/payments/Payments.t.sol +++ b/test/payments/Payments.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.19; -import "forge-std/Test.sol"; +import {TestHelper} from "../TestHelper.sol"; +import {PaymentsFactory} from "src/payments/PaymentsFactory.sol"; import {Payments, IERC165} from "src/payments/Payments.sol"; import {IPayments, IPaymentsFunctions, IPaymentsSignals} from "src/payments/IPayments.sol"; @@ -12,7 +13,7 @@ import {ERC721Mock} from "test/_mocks/ERC721Mock.sol"; import {IGenericToken} from "test/_mocks/IGenericToken.sol"; import {ERC1271Mock} from "test/_mocks/ERC1271Mock.sol"; -contract PaymentsTest is Test, IPaymentsSignals { +contract PaymentsTest is TestHelper, IPaymentsSignals { Payments public payments; address public owner; address public signer; @@ -26,7 +27,8 @@ contract PaymentsTest is Test, IPaymentsSignals { function setUp() public { owner = makeAddr("owner"); (signer, signerPk) = makeAddrAndKey("signer"); - payments = new Payments(owner, signer); + PaymentsFactory factory = new PaymentsFactory(owner); + payments = Payments(factory.deploy(owner, owner, signer)); erc20 = new ERC20Mock(address(this)); erc721 = new ERC721Mock(address(this), "baseURI"); @@ -107,6 +109,27 @@ contract PaymentsTest is Test, IPaymentsSignals { } } + /** + * Test all public selectors for collisions against the proxy admin functions. + * @dev yarn ts-node scripts/outputSelectors.ts + */ + function testSelectorCollision() public pure { + checkSelectorCollision(0x0e6fe11f); // hashChainedCallDetails((address,bytes)) + checkSelectorCollision(0x98c3065f); // hashPaymentDetails((uint256,address,uint8,address,uint256,(address,uint256)[],uint64,string,(address,bytes))) + checkSelectorCollision(0x485cc955); // initialize(address,address) + checkSelectorCollision(0x579a97e6); // isValidChainedCallSignature((address,bytes),bytes) + checkSelectorCollision(0x7b8bdc8e); // isValidPaymentSignature((uint256,address,uint8,address,uint256,(address,uint256)[],uint64,string,(address,bytes)),bytes) + checkSelectorCollision(0xdecfb3b2); // makePayment((uint256,address,uint8,address,uint256,(address,uint256)[],uint64,string,(address,bytes)),bytes) + checkSelectorCollision(0x8da5cb5b); // owner() + checkSelectorCollision(0x3a63b803); // paymentAccepted(uint256) + checkSelectorCollision(0xb2238700); // performChainedCall((address,bytes),bytes) + checkSelectorCollision(0x715018a6); // renounceOwnership() + checkSelectorCollision(0x238ac933); // signer() + checkSelectorCollision(0x01ffc9a7); // supportsInterface(bytes4) + checkSelectorCollision(0xf2fde38b); // transferOwnership(address) + checkSelectorCollision(0xa7ecd37e); // updateSigner(address) + } + function testMakePaymentSuccess(address caller, DetailsInput calldata input, bool isERC1271) public safeAddress(caller)