From 733fb34b056a053a0ac21ef02c5b949c0c1b883f Mon Sep 17 00:00:00 2001 From: adnanhq Date: Sat, 14 Mar 2026 22:31:21 +0600 Subject: [PATCH 1/5] Add off-chain approval and args integrity validation for payment flows using Permit2 --- src/CampaignInfo.sol | 7 + src/GlobalParams.sol | 10 + src/interfaces/ICampaignInfo.sol | 6 + src/interfaces/ICampaignPaymentTreasury.sol | 15 +- src/interfaces/IGlobalParams.sol | 6 + src/interfaces/IPermit2.sol | 72 ++++++ src/treasuries/AllOrNothing.sol | 95 ++++++-- src/treasuries/KeepWhatsRaised.sol | 148 ++++++++++--- src/utils/BasePaymentTreasury.sol | 64 +++++- test/foundry/Base.t.sol | 19 ++ .../AllOrNothing/AllOrNothing.t.sol | 16 +- .../AllOrNothing/AllOrNothingFunction.t.sol | 60 +++-- .../KeepWhatsRaised/KeepWhatsRaised.t.sol | 16 +- .../KeepWhatsRaisedFunction.t.sol | 11 +- .../PaymentTreasury/PaymentTreasury.t.sol | 20 +- ...meConstrainedPaymentTreasuryFunction.t.sol | 52 +++-- test/foundry/unit/KeepWhatsRaised.t.sol | 208 +++++++++++------- test/foundry/unit/PaymentTreasury.t.sol | 7 +- .../unit/TimeConstrainedPaymentTreasury.t.sol | 74 ++++--- test/mocks/MockPermit2.sol | 30 +++ 20 files changed, 714 insertions(+), 222 deletions(-) create mode 100644 src/interfaces/IPermit2.sol create mode 100644 test/mocks/MockPermit2.sol diff --git a/src/CampaignInfo.sol b/src/CampaignInfo.sol index cd650eb..74c2f23 100644 --- a/src/CampaignInfo.sol +++ b/src/CampaignInfo.sol @@ -448,6 +448,13 @@ contract CampaignInfo is bufferTime = uint256(valueBytes); } + /** + * @inheritdoc ICampaignInfo + */ + function getPermit2Address() external view override returns (address) { + return _getGlobalParams().getPermit2Address(); + } + /** * @inheritdoc ICampaignInfo */ diff --git a/src/GlobalParams.sol b/src/GlobalParams.sol index 8c6078a..eab0806 100644 --- a/src/GlobalParams.sol +++ b/src/GlobalParams.sol @@ -19,6 +19,9 @@ contract GlobalParams is Initializable, IGlobalParams, OwnableUpgradeable, UUPSU bytes32 private constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000; + /// @dev The canonical Permit2 deployment address (same on all EVM chains). + address private constant PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + /** * @dev Emitted when a platform is enlisted. * @param platformHash The identifier of the enlisted platform. @@ -303,6 +306,13 @@ contract GlobalParams is Initializable, IGlobalParams, OwnableUpgradeable, UUPSU value = $.dataRegistry[key]; } + /** + * @inheritdoc IGlobalParams + */ + function getPermit2Address() external pure returns (address) { + return PERMIT2_ADDRESS; + } + /** * @inheritdoc IGlobalParams */ diff --git a/src/interfaces/ICampaignInfo.sol b/src/interfaces/ICampaignInfo.sol index 9fc9854..8b3e579 100644 --- a/src/interfaces/ICampaignInfo.sol +++ b/src/interfaces/ICampaignInfo.sol @@ -218,6 +218,12 @@ interface ICampaignInfo is IERC721 { */ function getBufferTime() external view returns (uint256 bufferTime); + /** + * @notice Returns the canonical Permit2 contract address from GlobalParams. + * @return The Permit2 contract address. + */ + function getPermit2Address() external view returns (address); + /** * @notice Retrieves a platform-specific line item type configuration from GlobalParams. * @param platformHash The identifier of the platform. diff --git a/src/interfaces/ICampaignPaymentTreasury.sol b/src/interfaces/ICampaignPaymentTreasury.sol index 48bbde4..d4dd9d1 100644 --- a/src/interfaces/ICampaignPaymentTreasury.sol +++ b/src/interfaces/ICampaignPaymentTreasury.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; +import {PermitData} from "./IPermit2.sol"; + /** * @title ICampaignPaymentTreasury * @notice An interface for managing campaign payment treasury contracts. @@ -121,14 +123,18 @@ interface ICampaignPaymentTreasury { /** * @notice Allows a buyer to make a direct crypto payment for an item. - * @dev This function transfers tokens directly from the buyer's wallet and confirms the payment immediately. + * @dev Tokens are transferred from `buyerAddress` via Permit2 `permitWitnessTransferFrom`. + * The permit's witness commits to `paymentId`, `itemId`, `buyerAddress`, `amount`, and + * a hash of `lineItems`, ensuring the caller cannot tamper with any of these values + * after the buyer has signed the permit. * @param paymentId The unique identifier of the payment. * @param itemId The identifier of the item being purchased. - * @param buyerAddress The address of the buyer making the payment. + * @param buyerAddress The address of the buyer making the payment (must be the permit signer). * @param paymentToken The token to use for the payment. - * @param amount The amount to be paid for the item. + * @param amount The amount to be associated with the NFT (in token's native decimals). * @param lineItems Array of line items associated with this payment. * @param externalFees Array of external fee metadata captured for this payment (informational only). + * @param permitData Permit2 permit data (nonce, deadline, signature) signed by `buyerAddress`. */ function processCryptoPayment( bytes32 paymentId, @@ -137,7 +143,8 @@ interface ICampaignPaymentTreasury { address paymentToken, uint256 amount, LineItem[] calldata lineItems, - ExternalFees[] calldata externalFees + ExternalFees[] calldata externalFees, + PermitData calldata permitData ) external; /** diff --git a/src/interfaces/IGlobalParams.sol b/src/interfaces/IGlobalParams.sol index 0f155eb..49eb0b1 100644 --- a/src/interfaces/IGlobalParams.sol +++ b/src/interfaces/IGlobalParams.sol @@ -135,6 +135,12 @@ interface IGlobalParams { */ function getFromRegistry(bytes32 key) external view returns (bytes32 value); + /** + * @notice Returns the canonical Permit2 contract address. + * @return The Permit2 contract address. + */ + function getPermit2Address() external pure returns (address); + /** * @notice Sets or updates a platform-specific line item type configuration. * @param platformHash The identifier of the platform. diff --git a/src/interfaces/IPermit2.sol b/src/interfaces/IPermit2.sol new file mode 100644 index 0000000..1c85fb5 --- /dev/null +++ b/src/interfaces/IPermit2.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +/** + * @title IPermit2 + * @notice Minimal interface for Uniswap's Permit2 contract, used for + * signature-based token approvals and transfers. + * @dev Only includes types and functions required for `permitWitnessTransferFrom`. + * The canonical Permit2 deployment address is 0x000000000022D473030F116dDEE9F6B43aC78BA3 + * across all supported EVM chains. + */ +interface IPermit2 { + /// @notice Token and maximum amount authorised by the permit signer. + struct TokenPermissions { + address token; + uint256 amount; + } + + /// @notice The permit message signed for a single token transfer. + struct PermitTransferFrom { + TokenPermissions permitted; + uint256 nonce; + uint256 deadline; + } + + /// @notice Recipient address and requested amount for a single transfer. + struct SignatureTransferDetails { + address to; + uint256 requestedAmount; + } + + /** + * @notice Transfers a token using a signed permit that includes a witness. + * @dev The witness hash is mixed into the EIP-712 digest so that all + * caller-supplied parameters (amounts, IDs, line items, etc.) are + * cryptographically bound to the owner's signature. Any attempt by a + * third party to modify those parameters will invalidate the signature. + * + * @param permit The permit data signed by the token owner. + * @param transferDetails Specifies the recipient and the exact amount to move. + * @param owner The token owner and permit signer. + * @param witness EIP-712 hash of the application-specific witness struct. + * @param witnessTypeString EIP-712 type string for the witness, appended to the + * Permit2 type hash stub. Must follow the form: + * " witness)(fields...) + * TokenPermissions(address token,uint256 amount)" + * @param signature The owner's EIP-712 signature over the combined digest. + */ + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Returns the EIP-712 domain separator used by this Permit2 deployment. + function DOMAIN_SEPARATOR() external view returns (bytes32); +} + +/** + * @notice Data required for a Permit2 signature-based token transfer. + * @param nonce Unique nonce preventing signature replay (managed by Permit2). + * @param deadline Unix timestamp after which the permit is no longer valid. + * @param signature EIP-712 signature produced by the token owner. + */ +struct PermitData { + uint256 nonce; + uint256 deadline; + bytes signature; +} diff --git a/src/treasuries/AllOrNothing.sol b/src/treasuries/AllOrNothing.sol index fb9391d..080dd09 100644 --- a/src/treasuries/AllOrNothing.sol +++ b/src/treasuries/AllOrNothing.sol @@ -10,6 +10,7 @@ import {ICampaignTreasury} from "../interfaces/ICampaignTreasury.sol"; import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; import {BaseTreasury} from "../utils/BaseTreasury.sol"; import {IReward} from "../interfaces/IReward.sol"; +import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; /** * @title AllOrNothing @@ -31,6 +32,23 @@ contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuar // Counter for reward tiers Counters.Counter private s_rewardCounter; + // --------------------------------------------------------------------------- + // Permit2 witness types for pledge functions + // --------------------------------------------------------------------------- + // pledgeForAReward witness – binds backer, reward array, and shipping fee + bytes32 internal constant AON_PLEDGE_FOR_REWARD_WITNESS_TYPEHASH = keccak256( + "PledgeForRewardWitness(address backer,bytes32 rewardsHash,uint256 shippingFee)" + ); + string internal constant AON_PLEDGE_FOR_REWARD_WITNESS_TYPE_STRING = + "PledgeForRewardWitness witness)PledgeForRewardWitness(address backer,bytes32 rewardsHash,uint256 shippingFee)TokenPermissions(address token,uint256 amount)"; + + // pledgeWithoutAReward witness – binds backer and pledge amount + bytes32 internal constant AON_PLEDGE_WITHOUT_REWARD_WITNESS_TYPEHASH = + keccak256("PledgeWithoutRewardWitness(address backer,uint256 pledgeAmount)"); + string internal constant AON_PLEDGE_WITHOUT_REWARD_WITNESS_TYPE_STRING = + "PledgeWithoutRewardWitness witness)PledgeWithoutRewardWitness(address backer,uint256 pledgeAmount)TokenPermissions(address token,uint256 amount)"; + + /** * @dev Emitted when a backer makes a pledge. * @param backer The address of the backer making the pledge. @@ -263,15 +281,23 @@ contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuar } /** - * @notice Allows a backer to pledge for a reward. - * @dev The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. - * The non-reward tiers cannot be pledged for without a reward. - * @param backer The address of the backer making the pledge. + * @notice Allows a backer to pledge for a reward using a Permit2 signature. + * @dev Tokens are transferred from `backer` via Permit2 `permitWitnessTransferFrom`. + * The permit's witness commits to `backer`, the reward array hash, and `shippingFee`, + * so the caller cannot change those values after the backer has signed. + * @param backer The address of the backer making the pledge (must be the permit signer). * @param pledgeToken The token address to use for the pledge. * @param shippingFee The shipping fee amount. * @param reward An array of reward names. + * @param permitData Permit2 permit data (nonce, deadline, signature) signed by `backer`. */ - function pledgeForAReward(address backer, address pledgeToken, uint256 shippingFee, bytes32[] calldata reward) + function pledgeForAReward( + address backer, + address pledgeToken, + uint256 shippingFee, + bytes32[] calldata reward, + PermitData calldata permitData + ) external nonReentrant currentTimeIsWithinRange(INFO.getLaunchTime(), INFO.getDeadline()) @@ -299,16 +325,24 @@ contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuar } pledgeAmount += tempReward.rewardValue; } - _pledge(backer, pledgeToken, reward[0], pledgeAmount, shippingFee, reward); + _pledge(backer, pledgeToken, reward[0], pledgeAmount, shippingFee, reward, permitData); } /** - * @notice Allows a backer to pledge without selecting a reward. - * @param backer The address of the backer making the pledge. + * @notice Allows a backer to pledge without selecting a reward using a Permit2 signature. + * @dev Tokens are transferred from `backer` via Permit2 `permitWitnessTransferFrom`. + * The permit's witness commits to `backer` and `pledgeAmount`. + * @param backer The address of the backer making the pledge (must be the permit signer). * @param pledgeToken The token address to use for the pledge. - * @param pledgeAmount The amount of the pledge. + * @param pledgeAmount The amount of the pledge (in token's native decimals). + * @param permitData Permit2 permit data (nonce, deadline, signature) signed by `backer`. */ - function pledgeWithoutAReward(address backer, address pledgeToken, uint256 pledgeAmount) + function pledgeWithoutAReward( + address backer, + address pledgeToken, + uint256 pledgeAmount, + PermitData calldata permitData + ) external nonReentrant currentTimeIsWithinRange(INFO.getLaunchTime(), INFO.getDeadline()) @@ -319,7 +353,7 @@ contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuar { bytes32[] memory emptyByteArray = new bytes32[](0); - _pledge(backer, pledgeToken, ZERO_BYTES, pledgeAmount, 0, emptyByteArray); + _pledge(backer, pledgeToken, ZERO_BYTES, pledgeAmount, 0, emptyByteArray, permitData); } /** @@ -400,7 +434,8 @@ contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuar bytes32 reward, uint256 pledgeAmount, uint256 shippingFee, - bytes32[] memory rewards + bytes32[] memory rewards, + PermitData calldata permitData ) private { // Validate token is accepted if (!INFO.isTokenAccepted(pledgeToken)) { @@ -424,7 +459,41 @@ contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuar uint256 totalAmount = pledgeAmountInTokenDecimals + shippingFeeInTokenDecimals; - IERC20(pledgeToken).safeTransferFrom(backer, address(this), totalAmount); + // Build the Permit2 witness that binds all pledge parameters to the + // backer's signature. Any third party attempting to: + // - redirect tokens from a different backer address, + // - swap reward tiers to avoid a pledge-with-reward check, or + // - alter the shipping fee + // will produce a signature mismatch, preventing exploitation. + bytes32 witness; + string memory witnessTypeString; + + if (reward != ZERO_BYTES) { + // For reward pledges, bind backer, the full rewards array hash, and shippingFee + bytes32 rewardsHash = keccak256(abi.encodePacked(rewards)); + witness = keccak256( + abi.encode(AON_PLEDGE_FOR_REWARD_WITNESS_TYPEHASH, backer, rewardsHash, shippingFee) + ); + witnessTypeString = AON_PLEDGE_FOR_REWARD_WITNESS_TYPE_STRING; + } else { + // For no-reward pledges, bind backer and pledgeAmount + witness = + keccak256(abi.encode(AON_PLEDGE_WITHOUT_REWARD_WITNESS_TYPEHASH, backer, pledgeAmountInTokenDecimals)); + witnessTypeString = AON_PLEDGE_WITHOUT_REWARD_WITNESS_TYPE_STRING; + } + + IPermit2(INFO.getPermit2Address()).permitWitnessTransferFrom( + IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: pledgeToken, amount: totalAmount}), + nonce: permitData.nonce, + deadline: permitData.deadline + }), + IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), + backer, + witness, + witnessTypeString, + permitData.signature + ); uint256 tokenId = INFO.mintNFTForPledge( backer, reward, pledgeToken, pledgeAmountInTokenDecimals, shippingFeeInTokenDecimals, 0 diff --git a/src/treasuries/KeepWhatsRaised.sol b/src/treasuries/KeepWhatsRaised.sol index 673e039..7a5a03f 100644 --- a/src/treasuries/KeepWhatsRaised.sol +++ b/src/treasuries/KeepWhatsRaised.sol @@ -12,6 +12,7 @@ import {ICampaignTreasury} from "../interfaces/ICampaignTreasury.sol"; import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; import {IReward} from "../interfaces/IReward.sol"; import {ICampaignData} from "../interfaces/ICampaignData.sol"; +import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; /** * @title KeepWhatsRaised @@ -97,6 +98,25 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa Config private s_config; CampaignData private s_campaignData; + // --------------------------------------------------------------------------- + // Permit2 witness types for direct user pledge functions + // (setFeeAndPledge is admin-only and uses standard ERC20 transferFrom) + // --------------------------------------------------------------------------- + // pledgeForAReward witness – binds pledgeId, backer, reward array, and tip + bytes32 internal constant KWR_PLEDGE_FOR_REWARD_WITNESS_TYPEHASH = keccak256( + "KWRPledgeForRewardWitness(bytes32 pledgeId,address backer,bytes32 rewardsHash,uint256 tip)" + ); + string internal constant KWR_PLEDGE_FOR_REWARD_WITNESS_TYPE_STRING = + "KWRPledgeForRewardWitness witness)KWRPledgeForRewardWitness(bytes32 pledgeId,address backer,bytes32 rewardsHash,uint256 tip)TokenPermissions(address token,uint256 amount)"; + + // pledgeWithoutAReward witness – binds pledgeId, backer, pledgeAmount, and tip + bytes32 internal constant KWR_PLEDGE_WITHOUT_REWARD_WITNESS_TYPEHASH = keccak256( + "KWRPledgeWithoutRewardWitness(bytes32 pledgeId,address backer,uint256 pledgeAmount,uint256 tip)" + ); + string internal constant KWR_PLEDGE_WITHOUT_REWARD_WITNESS_TYPE_STRING = + "KWRPledgeWithoutRewardWitness witness)KWRPledgeWithoutRewardWitness(bytes32 pledgeId,address backer,uint256 pledgeAmount,uint256 tip)TokenPermissions(address token,uint256 amount)"; + + /** * @dev Emitted when a backer makes a pledge. * @param backer The address of the backer making the pledge. @@ -683,29 +703,37 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa //Set Payment Gateway Fee setPaymentGatewayFee(pledgeId, fee); + // Admin flow: empty permitData signals the ERC20 transferFrom path in _pledge. + // Tokens are pulled from _msgSender() (the platform admin) who must have + // pre-approved this treasury contract via ERC20.approve(). + PermitData memory emptyPermit; + if (isPledgeForAReward) { - _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, _msgSender()); // Pass admin as token source + _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, _msgSender(), emptyPermit); } else { - _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, _msgSender()); // Pass admin as token source + _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, _msgSender(), emptyPermit); } } /** - * @notice Allows a backer to pledge for a reward. - * @dev The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. - * The non-reward tiers cannot be pledged for without a reward. + * @notice Allows a backer to pledge for a reward using a Permit2 signature. + * @dev Tokens are transferred from `backer` via Permit2 `permitWitnessTransferFrom`. + * The permit's witness commits to `pledgeId`, `backer`, the reward array hash, and + * `tip`, so the caller cannot tamper with those parameters after the backer has signed. * @param pledgeId The unique identifier of the pledge. - * @param backer The address of the backer making the pledge. + * @param backer The address of the backer making the pledge (must be the permit signer). * @param pledgeToken The token to use for the pledge. * @param tip An optional tip can be added during the process. * @param reward An array of reward names. + * @param permitData Permit2 permit data (nonce, deadline, signature) signed by `backer`. */ function pledgeForAReward( bytes32 pledgeId, address backer, address pledgeToken, uint256 tip, - bytes32[] calldata reward + bytes32[] calldata reward, + PermitData calldata permitData ) public nonReentrant @@ -715,29 +743,30 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa whenCampaignNotCancelled whenNotCancelled { - _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, backer); // Pass backer as token source for direct calls + _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, backer, permitData); } /** - * @notice Internal function that allows a backer to pledge for a reward with tokens transferred from a specified source. - * @dev The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. - * The non-reward tiers cannot be pledged for without a reward. - * This function is called internally by both public pledgeForAReward (with backer as token source) and - * setFeeAndPledge (with admin as token source). + * @notice Internal function that allows a backer to pledge for a reward. + * @dev Called by both the public `pledgeForAReward` (Permit2 transfer) and + * `setFeeAndPledge` (admin ERC20 transfer). The `permitData` is forwarded + * to `_pledge` which selects the appropriate transfer method. * @param pledgeId The unique identifier of the pledge. * @param backer The address of the backer making the pledge (receives the NFT). * @param pledgeToken The token to use for the pledge. * @param tip An optional tip can be added during the process. * @param reward An array of reward names. - * @param tokenSource The address from which tokens will be transferred (either backer for direct calls or admin for setFeeAndPledge calls). + * @param tokenSource Token source address for the admin (ERC20) path; ignored on Permit2 path. + * @param permitData Permit2 data; empty (zero-length signature) triggers the admin ERC20 path. */ function _pledgeForAReward( bytes32 pledgeId, address backer, address pledgeToken, uint256 tip, - bytes32[] calldata reward, - address tokenSource + bytes32[] memory reward, + address tokenSource, + PermitData memory permitData ) internal { bytes32 internalPledgeId = keccak256(abi.encodePacked(pledgeId, _msgSender())); @@ -765,23 +794,27 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa } pledgeAmount += tempReward.rewardValue; } - _pledge(pledgeId, backer, pledgeToken, reward[0], pledgeAmount, tip, reward, tokenSource); + _pledge(pledgeId, backer, pledgeToken, reward[0], pledgeAmount, tip, reward, tokenSource, permitData); } /** - * @notice Allows a backer to pledge without selecting a reward. + * @notice Allows a backer to pledge without selecting a reward using a Permit2 signature. + * @dev Tokens are transferred from `backer` via Permit2 `permitWitnessTransferFrom`. + * The permit's witness commits to `pledgeId`, `backer`, `pledgeAmount`, and `tip`. * @param pledgeId The unique identifier of the pledge. - * @param backer The address of the backer making the pledge. + * @param backer The address of the backer making the pledge (must be the permit signer). * @param pledgeToken The token to use for the pledge. - * @param pledgeAmount The amount of the pledge. - * @param tip An optional tip can be added during the process. + * @param pledgeAmount The amount of the pledge (in token's native decimals). + * @param tip An optional tip (in token's native decimals). + * @param permitData Permit2 permit data (nonce, deadline, signature) signed by `backer`. */ function pledgeWithoutAReward( bytes32 pledgeId, address backer, address pledgeToken, uint256 pledgeAmount, - uint256 tip + uint256 tip, + PermitData calldata permitData ) public nonReentrant @@ -791,19 +824,20 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa whenCampaignNotCancelled whenNotCancelled { - _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, backer); // Pass backer as token source for direct calls + _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, backer, permitData); } /** - * @notice Internal function that allows a backer to pledge without selecting a reward with tokens transferred from a specified source. - * @dev This function is called internally by both public pledgeWithoutAReward (with backer as token source) and - * setFeeAndPledge (with admin as token source). + * @notice Internal function that allows a backer to pledge without a reward. + * @dev Called by both the public `pledgeWithoutAReward` (Permit2 transfer) and + * `setFeeAndPledge` (admin ERC20 transfer). * @param pledgeId The unique identifier of the pledge. * @param backer The address of the backer making the pledge (receives the NFT). * @param pledgeToken The token to use for the pledge. * @param pledgeAmount The amount of the pledge. - * @param tip An optional tip can be added during the process. - * @param tokenSource The address from which tokens will be transferred (either backer for direct calls or admin for setFeeAndPledge calls). + * @param tip An optional tip. + * @param tokenSource Token source address for the admin (ERC20) path; ignored on Permit2 path. + * @param permitData Permit2 data; empty (zero-length signature) triggers the admin ERC20 path. */ function _pledgeWithoutAReward( bytes32 pledgeId, @@ -811,7 +845,8 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa address pledgeToken, uint256 pledgeAmount, uint256 tip, - address tokenSource + address tokenSource, + PermitData memory permitData ) internal { bytes32 internalPledgeId = keccak256(abi.encodePacked(pledgeId, _msgSender())); @@ -822,7 +857,7 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa bytes32[] memory emptyByteArray = new bytes32[](0); - _pledge(pledgeId, backer, pledgeToken, ZERO_BYTES, pledgeAmount, tip, emptyByteArray, tokenSource); + _pledge(pledgeId, backer, pledgeToken, ZERO_BYTES, pledgeAmount, tip, emptyByteArray, tokenSource, permitData); } /** @@ -1115,7 +1150,8 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa uint256 pledgeAmount, uint256 tip, bytes32[] memory rewards, - address tokenSource + address tokenSource, + PermitData memory permitData ) private { // Validate token is accepted if (!INFO.isTokenAccepted(pledgeToken)) { @@ -1136,7 +1172,55 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa uint256 totalAmount = pledgeAmountInTokenDecimals + tip; - IERC20(pledgeToken).safeTransferFrom(tokenSource, address(this), totalAmount); + if (permitData.signature.length > 0) { + // ------------------------------------------------------------------- + // User (direct pledge) path: transfer tokens via Permit2. + // The witness commits to all pledge parameters so any tampering + // (e.g. swapping reward tiers, inflating tips) invalidates the sig. + // ------------------------------------------------------------------- + bytes32 witness; + string memory witnessTypeString; + + if (reward != ZERO_BYTES) { + bytes32 rewardsHash = keccak256(abi.encodePacked(rewards)); + witness = keccak256( + abi.encode(KWR_PLEDGE_FOR_REWARD_WITNESS_TYPEHASH, pledgeId, backer, rewardsHash, tip) + ); + witnessTypeString = KWR_PLEDGE_FOR_REWARD_WITNESS_TYPE_STRING; + } else { + witness = keccak256( + abi.encode( + KWR_PLEDGE_WITHOUT_REWARD_WITNESS_TYPEHASH, + pledgeId, + backer, + pledgeAmountInTokenDecimals, + tip + ) + ); + witnessTypeString = KWR_PLEDGE_WITHOUT_REWARD_WITNESS_TYPE_STRING; + } + + IPermit2(INFO.getPermit2Address()).permitWitnessTransferFrom( + IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: pledgeToken, amount: totalAmount}), + nonce: permitData.nonce, + deadline: permitData.deadline + }), + IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), + backer, + witness, + witnessTypeString, + permitData.signature + ); + } else { + // ------------------------------------------------------------------- + // Admin (setFeeAndPledge) path: standard ERC20 transfer from the + // platform admin who owns the tokens and has pre-approved this + // treasury. This path is only reachable via the onlyPlatformAdmin- + // guarded setFeeAndPledge function. + // ------------------------------------------------------------------- + IERC20(pledgeToken).safeTransferFrom(tokenSource, address(this), totalAmount); + } uint256 tokenId = INFO.mintNFTForPledge(backer, reward, pledgeToken, pledgeAmountInTokenDecimals, 0, tip); diff --git a/src/utils/BasePaymentTreasury.sol b/src/utils/BasePaymentTreasury.sol index f7e45f4..24f6099 100644 --- a/src/utils/BasePaymentTreasury.sol +++ b/src/utils/BasePaymentTreasury.sol @@ -7,6 +7,7 @@ import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.s import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; +import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; import {CampaignAccessChecker} from "./CampaignAccessChecker.sol"; import {PausableCancellable} from "./PausableCancellable.sol"; import {DataRegistryKeys} from "../constants/DataRegistryKeys.sol"; @@ -30,6 +31,24 @@ abstract contract BasePaymentTreasury is uint256 internal constant STANDARD_DECIMALS = 18; address internal constant ZERO_ADDRESS = address(0); + // --------------------------------------------------------------------------- + // Permit2 witness type for processCryptoPayment + // --------------------------------------------------------------------------- + // Struct fields (in declaration order): + // bytes32 paymentId – which payment this authorises + // bytes32 itemId – item the NFT represents + // address buyerAddress – NFT recipient / token source + // uint256 amount – NFT-associated amount (not total transfer amount) + // bytes32 lineItemsHash – keccak256(abi.encode(lineItems)) + bytes32 internal constant CRYPTO_PAYMENT_WITNESS_TYPEHASH = keccak256( + "CryptoPaymentWitness(bytes32 paymentId,bytes32 itemId,address buyerAddress,uint256 amount,bytes32 lineItemsHash)" + ); + + // Appended to Permit2's _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB: + // "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline," + string internal constant CRYPTO_PAYMENT_WITNESS_TYPE_STRING = + "CryptoPaymentWitness witness)CryptoPaymentWitness(bytes32 paymentId,bytes32 itemId,address buyerAddress,uint256 amount,bytes32 lineItemsHash)TokenPermissions(address token,uint256 amount)"; + bytes32 internal PLATFORM_HASH; uint256 internal PLATFORM_FEE_PERCENT; @@ -278,11 +297,14 @@ abstract contract BasePaymentTreasury is /** * @dev Scopes a payment ID for on-chain crypto payments (processCryptoPayment). + * @dev Scoped by the buyer address (the Permit2 signer) rather than the tx sender, + * so the payment can be looked up by anyone using the stored creator address. * @param paymentId The external payment ID. + * @param owner The buyer/signer address. * @return The scoped internal payment ID. */ - function _scopePaymentIdForOnChain(bytes32 paymentId) internal view returns (bytes32) { - return keccak256(abi.encodePacked(paymentId, _msgSender())); + function _scopePaymentIdForOnChain(bytes32 paymentId, address owner) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(paymentId, owner)); } /** @@ -787,7 +809,8 @@ abstract contract BasePaymentTreasury is address paymentToken, uint256 amount, ICampaignPaymentTreasury.LineItem[] calldata lineItems, - ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees, + PermitData calldata permitData ) public virtual override nonReentrant whenCampaignNotPaused whenCampaignNotCancelled { if ( buyerAddress == address(0) || amount == 0 || paymentId == ZERO_BYTES || itemId == ZERO_BYTES @@ -817,8 +840,8 @@ abstract contract BasePaymentTreasury is revert PaymentTreasuryPaymentAlreadyExist(existingPaymentId); } - // Check if an on-chain payment with the same paymentId already exists for this caller - bytes32 internalPaymentId = _scopePaymentIdForOnChain(paymentId); + // Scope by buyerAddress so any relayer can call on behalf of the same buyer + bytes32 internalPaymentId = _scopePaymentIdForOnChain(paymentId, buyerAddress); if ( s_payment[internalPaymentId].buyerAddress != address(0) || s_payment[internalPaymentId].buyerId != ZERO_BYTES @@ -826,6 +849,9 @@ abstract contract BasePaymentTreasury is revert PaymentTreasuryPaymentAlreadyExist(internalPaymentId); } + // Compute lineItemsHash to bind line items in the Permit2 witness + bytes32 lineItemsHash = keccak256(abi.encode(lineItems)); + // Validate, calculate total, store, and process line items uint256 totalAmount = amount; address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); @@ -912,7 +938,31 @@ abstract contract BasePaymentTreasury is } } - IERC20(paymentToken).safeTransferFrom(buyerAddress, address(this), totalAmount); + // Build the witness hash that commits to all critical payment parameters. + // This ensures the buyer's signature authorises exactly this payment: + // - which payment (paymentId), which item (itemId), who receives the NFT + // (buyerAddress), the NFT-associated amount (amount), and the exact set of + // line items (lineItemsHash). Any attempt to change these values will + // invalidate the signature, preventing both unauthorised execution and + // parameter tampering. + bytes32 witness = keccak256( + abi.encode(CRYPTO_PAYMENT_WITNESS_TYPEHASH, paymentId, itemId, buyerAddress, amount, lineItemsHash) + ); + + // Transfer tokens from the buyer via Permit2. The permit authorises the + // exact totalAmount and the witness binds all payment parameters. + IPermit2(INFO.getPermit2Address()).permitWitnessTransferFrom( + IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: paymentToken, amount: totalAmount}), + nonce: permitData.nonce, + deadline: permitData.deadline + }), + IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), + buyerAddress, + witness, + CRYPTO_PAYMENT_WITNESS_TYPE_STRING, + permitData.signature + ); s_payment[internalPaymentId] = PaymentInfo({ buyerId: ZERO_BYTES, @@ -926,7 +976,7 @@ abstract contract BasePaymentTreasury is }); s_paymentIdToToken[internalPaymentId] = paymentToken; - s_paymentIdToCreator[paymentId] = _msgSender(); // Store creator address for getPaymentData lookup + s_paymentIdToCreator[paymentId] = buyerAddress; // Scoped by buyer for getPaymentData lookup s_confirmedPaymentPerToken[paymentToken] += amount; s_lifetimeConfirmedPaymentPerToken[paymentToken] += amount; s_availableConfirmedPerToken[paymentToken] += amount; diff --git a/test/foundry/Base.t.sol b/test/foundry/Base.t.sol index 55b7911..ed24b6a 100644 --- a/test/foundry/Base.t.sol +++ b/test/foundry/Base.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import {Users} from "./utils/Types.sol"; import {Defaults} from "./utils/Defaults.sol"; import {TestToken} from "../mocks/TestToken.sol"; +import {MockPermit2} from "../mocks/MockPermit2.sol"; import {GlobalParams} from "src/GlobalParams.sol"; import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; @@ -12,6 +13,7 @@ import {TreasuryFactory} from "src/TreasuryFactory.sol"; import {AllOrNothing} from "src/treasuries/AllOrNothing.sol"; import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; +import {IPermit2, PermitData} from "src/interfaces/IPermit2.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; @@ -35,6 +37,17 @@ abstract contract Base_Test is Test, Defaults { KeepWhatsRaised internal keepWhatsRaisedImplementation; CampaignInfo internal campaignInfo; + /// @dev MockPermit2 deployed at the canonical Permit2 address for all tests. + MockPermit2 internal mockPermit2; + /// @dev Canonical Permit2 address used by the contracts under test. + address internal constant CANONICAL_PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + /// @dev Helper to build a no-op PermitData that satisfies the signature length check. + /// The MockPermit2 ignores signature content so any non-empty bytes work. + function _buildPermitData(uint256 nonce, uint256 deadline) internal pure returns (PermitData memory) { + return PermitData({nonce: nonce, deadline: deadline, signature: abi.encodePacked(bytes1(0x01))}); + } + function setUp() public virtual { // Create users for testing. users = Users({ @@ -50,6 +63,12 @@ abstract contract Base_Test is Test, Defaults { vm.startPrank(users.contractOwner); + // Deploy MockPermit2 at the canonical Permit2 address so that + // treasury contracts (which hardcode that address) use our mock. + MockPermit2 permit2Impl = new MockPermit2(); + vm.etch(CANONICAL_PERMIT2_ADDRESS, address(permit2Impl).code); + mockPermit2 = MockPermit2(CANONICAL_PERMIT2_ADDRESS); + // Deploy multiple test tokens with different decimals usdtToken = new TestToken("Tether USD", "USDT", 6); usdcToken = new TestToken("USD Coin", "USDC", 6); diff --git a/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol b/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol index e1e7a90..763979a 100644 --- a/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol +++ b/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol @@ -9,6 +9,8 @@ import {AllOrNothing} from "src/treasuries/AllOrNothing.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {IReward} from "src/interfaces/IReward.sol"; import {LogDecoder} from "../../utils/LogDecoder.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; /// @notice Common testing logic needed by all AllOrNothing integration tests. abstract contract AllOrNothing_Integration_Shared_Test is IReward, LogDecoder, Base_Test { @@ -157,13 +159,16 @@ abstract contract AllOrNothing_Integration_Shared_Test is IReward, LogDecoder, B vm.startPrank(caller); vm.recordLogs(); - testToken.approve(allOrNothingAddress, pledgeAmount + shippingFee); + // Approve MockPermit2 (at canonical address) instead of the treasury directly. + IERC20(token).approve(CANONICAL_PERMIT2_ADDRESS, type(uint256).max); vm.warp(launchTime); bytes32[] memory reward = new bytes32[](1); reward[0] = rewardName; - AllOrNothing(allOrNothingAddress).pledgeForAReward(caller, address(token), shippingFee, reward); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + + AllOrNothing(allOrNothingAddress).pledgeForAReward(caller, address(token), shippingFee, reward, permitData); logs = vm.getRecordedLogs(); @@ -191,10 +196,13 @@ abstract contract AllOrNothing_Integration_Shared_Test is IReward, LogDecoder, B vm.startPrank(caller); vm.recordLogs(); - testToken.approve(allOrNothingAddress, pledgeAmount); + // Approve MockPermit2 (at canonical address) instead of the treasury directly. + IERC20(token).approve(CANONICAL_PERMIT2_ADDRESS, type(uint256).max); vm.warp(launchTime); - AllOrNothing(allOrNothingAddress).pledgeWithoutAReward(caller, address(token), pledgeAmount); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + + AllOrNothing(allOrNothingAddress).pledgeWithoutAReward(caller, address(token), pledgeAmount, permitData); logs = vm.getRecordedLogs(); diff --git a/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol b/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol index 6037e12..ed9d54c 100644 --- a/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol +++ b/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol @@ -156,18 +156,20 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio uint256 usdcShippingFee = getTokenAmount(address(usdcToken), SHIPPING_FEE); vm.startPrank(users.backer1Address); - usdcToken.approve(address(allOrNothing), usdcPledgeAmount + usdcShippingFee); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcPledgeAmount + usdcShippingFee); vm.warp(LAUNCH_TIME); bytes32[] memory reward1 = new bytes32[](1); reward1[0] = REWARD_NAME_1_HASH; - allOrNothing.pledgeForAReward(users.backer1Address, address(usdcToken), usdcShippingFee, reward1); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeForAReward(users.backer1Address, address(usdcToken), usdcShippingFee, reward1, permitData1); vm.stopPrank(); // Pledge with cUSD (18 decimals) - no conversion needed vm.startPrank(users.backer2Address); - cUSDToken.approve(address(allOrNothing), PLEDGE_AMOUNT); - allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT, permitData2); vm.stopPrank(); // Verify balances @@ -188,9 +190,10 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio // USDC pledge (6 decimals) uint256 usdcAmount = baseAmount / 1e12; vm.startPrank(users.backer1Address); - usdcToken.approve(address(allOrNothing), usdcAmount); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); vm.warp(LAUNCH_TIME); - allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount); + PermitData memory permitData3 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount, permitData3); vm.stopPrank(); uint256 raisedAfterUSDC = allOrNothing.getRaisedAmount(); @@ -198,8 +201,9 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio // cUSD pledge (18 decimals) vm.startPrank(users.backer2Address); - cUSDToken.approve(address(allOrNothing), baseAmount); - allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), baseAmount); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, baseAmount); + PermitData memory permitData4 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), baseAmount, permitData4); vm.stopPrank(); uint256 raisedAfterCUSD = allOrNothing.getRaisedAmount(); @@ -212,15 +216,17 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio // Pledge with USDC uint256 usdcAmount = getTokenAmount(address(usdcToken), PLEDGE_AMOUNT); vm.startPrank(users.backer1Address); - usdcToken.approve(address(allOrNothing), usdcAmount); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); vm.warp(LAUNCH_TIME); - allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount); + PermitData memory permitData5 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount, permitData5); vm.stopPrank(); // Pledge with cUSD to meet goal vm.startPrank(users.backer2Address); - cUSDToken.approve(address(allOrNothing), GOAL_AMOUNT); - allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), GOAL_AMOUNT); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, GOAL_AMOUNT); + PermitData memory permitData6 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), GOAL_AMOUNT, permitData6); vm.stopPrank(); uint256 protocolBalanceUSDCBefore = usdcToken.balanceOf(users.protocolAdminAddress); @@ -271,20 +277,23 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio uint256 usdtAmount = getTokenAmount(address(usdtToken), PLEDGE_AMOUNT); vm.startPrank(users.backer1Address); - usdcToken.approve(address(allOrNothing), usdcAmount); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); vm.warp(LAUNCH_TIME); - allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount); + PermitData memory permitData7 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount, permitData7); vm.stopPrank(); vm.startPrank(users.backer2Address); - usdtToken.approve(address(allOrNothing), usdtAmount); - allOrNothing.pledgeWithoutAReward(users.backer2Address, address(usdtToken), usdtAmount); + usdtToken.approve(CANONICAL_PERMIT2_ADDRESS, usdtAmount); + PermitData memory permitData8 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(usdtToken), usdtAmount, permitData8); vm.stopPrank(); // Need cUSD pledge to meet goal vm.startPrank(users.backer1Address); - cUSDToken.approve(address(allOrNothing), GOAL_AMOUNT); - allOrNothing.pledgeWithoutAReward(users.backer1Address, address(cUSDToken), GOAL_AMOUNT); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, GOAL_AMOUNT); + PermitData memory permitData9 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(cUSDToken), GOAL_AMOUNT, permitData9); vm.stopPrank(); // Disburse fees and withdraw @@ -314,16 +323,18 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio // Backer1 pledges with USDC uint256 usdcAmount = getTokenAmount(address(usdcToken), PLEDGE_AMOUNT); vm.startPrank(users.backer1Address); - usdcToken.approve(address(allOrNothing), usdcAmount); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); vm.warp(LAUNCH_TIME); - allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount); + PermitData memory permitData10 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount, permitData10); uint256 usdcTokenId = 1; // First pledge vm.stopPrank(); // Backer2 pledges with cUSD vm.startPrank(users.backer2Address); - cUSDToken.approve(address(allOrNothing), PLEDGE_AMOUNT); - allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); + PermitData memory permitData11 = _buildPermitData(0, block.timestamp + 1 hours); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT, permitData11); uint256 cUSDTokenId = 2; // Second pledge vm.stopPrank(); @@ -363,13 +374,14 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); vm.startPrank(users.backer1Address); - unacceptedToken.approve(address(allOrNothing), PLEDGE_AMOUNT); + unacceptedToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); vm.warp(LAUNCH_TIME); + PermitData memory emptyPermit; vm.expectRevert( abi.encodeWithSelector(AllOrNothing.AllOrNothingTokenNotAccepted.selector, address(unacceptedToken)) ); - allOrNothing.pledgeWithoutAReward(users.backer1Address, address(unacceptedToken), PLEDGE_AMOUNT); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(unacceptedToken), PLEDGE_AMOUNT, emptyPermit); vm.stopPrank(); } } diff --git a/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol index 02bdcd9..6bcc9c4 100644 --- a/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol +++ b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol @@ -9,6 +9,8 @@ import {IReward} from "src/interfaces/IReward.sol"; import {ICampaignData} from "src/interfaces/ICampaignData.sol"; import {LogDecoder} from "../../utils/LogDecoder.sol"; import {Base_Test} from "../../Base.t.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; /// @notice Common testing logic needed by all KeepWhatsRaised integration tests. abstract contract KeepWhatsRaised_Integration_Shared_Test is IReward, LogDecoder, Base_Test { @@ -308,13 +310,16 @@ abstract contract KeepWhatsRaised_Integration_Shared_Test is IReward, LogDecoder vm.startPrank(caller); vm.recordLogs(); - testToken.approve(keepWhatsRaisedAddress, pledgeAmount + tip); + // Approve MockPermit2 (at canonical address) instead of the treasury directly. + IERC20(token).approve(CANONICAL_PERMIT2_ADDRESS, type(uint256).max); vm.warp(launchTime); bytes32[] memory reward = new bytes32[](1); reward[0] = rewardName; - KeepWhatsRaised(keepWhatsRaisedAddress).pledgeForAReward(pledgeId, caller, token, tip, reward); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + + KeepWhatsRaised(keepWhatsRaisedAddress).pledgeForAReward(pledgeId, caller, token, tip, reward, permitData); logs = vm.getRecordedLogs(); @@ -344,10 +349,13 @@ abstract contract KeepWhatsRaised_Integration_Shared_Test is IReward, LogDecoder vm.startPrank(caller); vm.recordLogs(); - testToken.approve(keepWhatsRaisedAddress, pledgeAmount + tip); + // Approve MockPermit2 (at canonical address) instead of the treasury directly. + IERC20(token).approve(CANONICAL_PERMIT2_ADDRESS, type(uint256).max); vm.warp(launchTime); - KeepWhatsRaised(keepWhatsRaisedAddress).pledgeWithoutAReward(pledgeId, caller, token, pledgeAmount, tip); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + + KeepWhatsRaised(keepWhatsRaisedAddress).pledgeWithoutAReward(pledgeId, caller, token, pledgeAmount, tip, permitData); logs = vm.getRecordedLogs(); diff --git a/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaisedFunction.t.sol b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaisedFunction.t.sol index 35cd7b7..0ddf8d5 100644 --- a/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaisedFunction.t.sol +++ b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaisedFunction.t.sol @@ -10,6 +10,7 @@ import {Users} from "../../utils/Types.sol"; import {IReward} from "src/interfaces/IReward.sol"; import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; contract KeepWhatsRaisedFunction_Integration_Shared_Test is KeepWhatsRaised_Integration_Shared_Test { function setUp() public virtual override { @@ -587,10 +588,11 @@ contract KeepWhatsRaisedFunction_Integration_Shared_Test is KeepWhatsRaised_Inte // Verify campaign is cancelled vm.startPrank(users.backer2Address); - testToken.approve(address(keepWhatsRaised), PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); + PermitData memory emptyPermit1; vm.expectRevert(); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID_2, users.backer2Address, address(testToken), PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID_2, users.backer2Address, address(testToken), PLEDGE_AMOUNT, 0, emptyPermit1 ); vm.stopPrank(); } @@ -621,10 +623,11 @@ contract KeepWhatsRaisedFunction_Integration_Shared_Test is KeepWhatsRaised_Inte // Verify campaign is cancelled vm.startPrank(users.backer2Address); - testToken.approve(address(keepWhatsRaised), PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); + PermitData memory emptyPermit2; vm.expectRevert(); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID_2, users.backer2Address, address(testToken), PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID_2, users.backer2Address, address(testToken), PLEDGE_AMOUNT, 0, emptyPermit2 ); vm.stopPrank(); } diff --git a/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol b/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol index 71004aa..1141ae2 100644 --- a/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol +++ b/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol @@ -9,6 +9,8 @@ import {CampaignInfo} from "src/CampaignInfo.sol"; import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury.sol"; import {LogDecoder} from "../../utils/LogDecoder.sol"; import {TestToken} from "../../../mocks/TestToken.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; /// @notice Common testing logic needed by all PaymentTreasury integration tests. abstract contract PaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Test { @@ -161,6 +163,8 @@ abstract contract PaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Te /** * @notice Processes a crypto payment + * @dev `caller` must have pre-approved MockPermit2 for the token. + * The helper sets up the approval and builds a dummy PermitData automatically. */ function processCryptoPayment( address caller, @@ -172,10 +176,22 @@ abstract contract PaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Te ICampaignPaymentTreasury.LineItem[] memory lineItems, ICampaignPaymentTreasury.ExternalFees[] memory externalFees ) internal { - vm.prank(caller); + // Compute total transfer amount to approve + uint256 totalAmount = amount; + for (uint256 i = 0; i < lineItems.length; i++) { + totalAmount += lineItems[i].amount; + } + + vm.startPrank(caller); + // Approve MockPermit2 (at canonical address) for the token. + IERC20(paymentToken).approve(CANONICAL_PERMIT2_ADDRESS, totalAmount); + + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + paymentTreasury.processCryptoPayment( - paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees + paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees, permitData ); + vm.stopPrank(); } /** diff --git a/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol index f832b03..a743ebf 100644 --- a/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol +++ b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol @@ -11,6 +11,8 @@ import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury. import {TimeConstrainedPaymentTreasury} from "src/treasuries/TimeConstrainedPaymentTreasury.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {TestToken} from "../../../mocks/TestToken.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is TimeConstrainedPaymentTreasury_Integration_Shared_Test @@ -92,11 +94,12 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is function test_processCryptoPayment() external { advanceToWithinRange(); - // Approve tokens for the treasury + // Approve MockPermit2 (at canonical address) for the token. vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -105,7 +108,8 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Payment processed successfully @@ -146,9 +150,10 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -157,7 +162,8 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Payment created and confirmed successfully by processCryptoPayment @@ -174,9 +180,10 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is // Use processCryptoPayment for both payments which creates and confirms them vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId1, @@ -185,13 +192,15 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData1 ); vm.prank(users.backer2Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_2); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_2); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems2 = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId2, @@ -200,7 +209,8 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is address(testToken), PAYMENT_AMOUNT_2, emptyLineItems2, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData2 ); // Payments created and confirmed successfully by processCryptoPayment @@ -217,9 +227,10 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -228,7 +239,8 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Advance to after launch to be able to claim refund @@ -256,9 +268,10 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -267,7 +280,8 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Advance to after launch to be able to disburse fees @@ -289,9 +303,10 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -300,7 +315,8 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Advance to after launch to be able to withdraw @@ -491,9 +507,10 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -502,7 +519,8 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Advance to after launch time diff --git a/test/foundry/unit/KeepWhatsRaised.t.sol b/test/foundry/unit/KeepWhatsRaised.t.sol index d31a64d..27ed065 100644 --- a/test/foundry/unit/KeepWhatsRaised.t.sol +++ b/test/foundry/unit/KeepWhatsRaised.t.sol @@ -124,12 +124,13 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(keepWhatsRaised.getLaunchTime()); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; - keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection, permitData); vm.stopPrank(); // Available amount should not include Colombian tax deduction at pledge time @@ -445,13 +446,14 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // Pledge vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeForAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection, permitData ); vm.stopPrank(); @@ -468,20 +470,22 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT * 2); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT * 2); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; // First pledge - keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection, permitData1); // Try to pledge with same ID bytes32 internalPledgeId = keccak256(abi.encodePacked(TEST_PLEDGE_ID, users.backer1Address)); vm.expectRevert( abi.encodeWithSelector(KeepWhatsRaised.KeepWhatsRaisedPledgeAlreadyProcessed.selector, internalPledgeId) ); - keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection); + PermitData memory emptyPermit1; + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection, emptyPermit1); vm.stopPrank(); } @@ -499,13 +503,14 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // Try to pledge vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; + PermitData memory emptyPermit; vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); - keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection); + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection, emptyPermit); vm.stopPrank(); } @@ -518,9 +523,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // Pledge vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), pledgeAmount + TEST_TIP_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, pledgeAmount + TEST_TIP_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), pledgeAmount, TEST_TIP_AMOUNT + TEST_PLEDGE_ID, users.backer1Address, address(testToken), pledgeAmount, TEST_TIP_AMOUNT, permitData ); vm.stopPrank(); @@ -536,11 +542,12 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT * 2); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT * 2); // First pledge + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData1 ); // Try to pledge with same ID - internal pledge ID includes caller @@ -548,8 +555,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.expectRevert( abi.encodeWithSelector(KeepWhatsRaised.KeepWhatsRaisedPledgeAlreadyProcessed.selector, internalPledgeId) ); + PermitData memory emptyPermit1; keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, emptyPermit1 ); vm.stopPrank(); } @@ -559,16 +567,18 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME - 1); vm.expectRevert(); vm.prank(users.backer1Address); + PermitData memory emptyPermit1; keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, emptyPermit1 ); // After deadline vm.warp(DEADLINE + 1); vm.expectRevert(); vm.prank(users.backer1Address); + PermitData memory emptyPermit2; keepWhatsRaised.pledgeWithoutAReward( - keccak256("newPledge"), users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + keccak256("newPledge"), users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, emptyPermit2 ); } @@ -581,14 +591,15 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // Try to pledge vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; + PermitData memory emptyPermit; vm.expectRevert(); keepWhatsRaised.pledgeForAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection, emptyPermit ); vm.stopPrank(); } @@ -744,8 +755,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), largePledge); - keepWhatsRaised.pledgeWithoutAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), largePledge, 0); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, largePledge); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), largePledge, 0, permitData); vm.stopPrank(); uint256 availableAfterPledge = keepWhatsRaised.getAvailableRaisedAmount(); @@ -786,9 +798,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); vm.stopPrank(); @@ -829,9 +842,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); uint256 tokenId = 1; // First token ID after pledge vm.stopPrank(); @@ -866,9 +880,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); uint256 tokenId = 1; // First token ID after pledge vm.stopPrank(); @@ -886,9 +901,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); uint256 tokenId = 1; // First token ID after pledge vm.stopPrank(); @@ -925,9 +941,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); uint256 tokenId = 0; vm.stopPrank(); @@ -947,9 +964,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); uint256 tokenId = 0; vm.stopPrank(); @@ -1146,8 +1164,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.expectRevert(); vm.prank(users.backer1Address); + PermitData memory emptyPermit1; keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, emptyPermit1 ); } @@ -1162,8 +1181,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.expectRevert(); vm.prank(users.backer1Address); + PermitData memory emptyPermit2; keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, emptyPermit2 ); } @@ -1238,8 +1258,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), smallPledge); - keepWhatsRaised.pledgeWithoutAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), smallPledge, 0); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, smallPledge); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), smallPledge, 0, permitData); vm.stopPrank(); vm.prank(users.platform2AdminAddress); @@ -1260,9 +1281,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); vm.stopPrank(); @@ -1275,9 +1297,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); vm.stopPrank(); @@ -1329,11 +1352,12 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te ); vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeForAReward( - keccak256("pledge1"), users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection + keccak256("pledge1"), users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection, permitData1 ); vm.stopPrank(); @@ -1343,8 +1367,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te users.platform2AdminAddress, address(keepWhatsRaised), keccak256("pledge2"), differentGatewayFee ); vm.startPrank(users.backer2Address); - testToken.approve(address(keepWhatsRaised), 2000e18); - keepWhatsRaised.pledgeWithoutAReward(keccak256("pledge2"), users.backer2Address, address(testToken), 2000e18, 0); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, 2000e18); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(keccak256("pledge2"), users.backer2Address, address(testToken), 2000e18, 0, permitData2); vm.stopPrank(); // Verify total raised and available amounts @@ -1382,9 +1407,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), smallAmount); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, smallAmount); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("small"), users.backer1Address, address(testToken), smallAmount, 0 + keccak256("small"), users.backer1Address, address(testToken), smallAmount, 0, permitData ); vm.stopPrank(); @@ -1474,19 +1500,21 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // Backer 1 pledge with reward vm.startPrank(users.backer1Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; + PermitData memory permitDataBacker1 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeForAReward( - keccak256("pledge1"), users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection + keccak256("pledge1"), users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection, permitDataBacker1 ); vm.stopPrank(); // Backer 2 pledge without reward vm.startPrank(users.backer2Address); - testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + PermitData memory permitDataBacker2 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("pledge2"), users.backer2Address, address(testToken), TEST_PLEDGE_AMOUNT, TEST_TIP_AMOUNT + keccak256("pledge2"), users.backer2Address, address(testToken), TEST_PLEDGE_AMOUNT, TEST_TIP_AMOUNT, permitDataBacker2 ); vm.stopPrank(); } @@ -1521,9 +1549,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - usdcToken.approve(address(keepWhatsRaised), usdcAmount); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("usdc_pledge"), users.backer1Address, address(usdcToken), usdcAmount, 0 + keccak256("usdc_pledge"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1 ); vm.stopPrank(); @@ -1531,9 +1560,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cusd_pledge"), 0); vm.startPrank(users.backer2Address); - cUSDToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("cusd_pledge"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, 0 + keccak256("cusd_pledge"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, 0, permitData2 ); vm.stopPrank(); @@ -1556,8 +1586,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); deal(address(usdcToken), users.backer1Address, usdcAmount); // Ensure enough tokens - usdcToken.approve(address(keepWhatsRaised), usdcAmount); - keepWhatsRaised.pledgeWithoutAReward(keccak256("usdc"), users.backer1Address, address(usdcToken), usdcAmount, 0); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(keccak256("usdc"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1); vm.stopPrank(); // Pledge with cUSD @@ -1565,8 +1596,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.startPrank(users.backer2Address); deal(address(cUSDToken), users.backer2Address, cUSDAmount); // Ensure enough tokens - cUSDToken.approve(address(keepWhatsRaised), cUSDAmount); - keepWhatsRaised.pledgeWithoutAReward(keccak256("cusd"), users.backer2Address, address(cUSDToken), cUSDAmount, 0); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, cUSDAmount); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(keccak256("cusd"), users.backer2Address, address(cUSDToken), cUSDAmount, 0, permitData2); vm.stopPrank(); // Approve withdrawal @@ -1606,21 +1638,24 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDC pledge vm.startPrank(users.backer1Address); - usdcToken.approve(address(keepWhatsRaised), usdcAmount); - keepWhatsRaised.pledgeWithoutAReward(keccak256("usdc"), users.backer1Address, address(usdcToken), usdcAmount, 0); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(keccak256("usdc"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1); vm.stopPrank(); // USDT pledge vm.startPrank(users.backer2Address); - usdtToken.approve(address(keepWhatsRaised), usdtAmount); - keepWhatsRaised.pledgeWithoutAReward(keccak256("usdt"), users.backer2Address, address(usdtToken), usdtAmount, 0); + usdtToken.approve(CANONICAL_PERMIT2_ADDRESS, usdtAmount); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(keccak256("usdt"), users.backer2Address, address(usdtToken), usdtAmount, 0, permitData2); vm.stopPrank(); // cUSD pledge vm.startPrank(users.backer1Address); - cUSDToken.approve(address(keepWhatsRaised), PLEDGE_AMOUNT); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); + PermitData memory permitData3 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("cusd"), users.backer1Address, address(cUSDToken), PLEDGE_AMOUNT, 0 + keccak256("cusd"), users.backer1Address, address(cUSDToken), PLEDGE_AMOUNT, 0, permitData3 ); vm.stopPrank(); @@ -1675,9 +1710,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - usdcToken.approve(address(keepWhatsRaised), usdcAmount); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("usdc_pledge"), users.backer1Address, address(usdcToken), usdcAmount, 0 + keccak256("usdc_pledge"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1 ); uint256 usdcTokenId = 1; // First pledge vm.stopPrank(); @@ -1686,9 +1722,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cusd_pledge"), 0); vm.startPrank(users.backer2Address); - cUSDToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("cusd_pledge"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, 0 + keccak256("cusd_pledge"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, 0, permitData2 ); uint256 cUSDTokenId = 2; // Second pledge vm.stopPrank(); @@ -1729,9 +1766,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); - usdcToken.approve(address(keepWhatsRaised), usdcPledge + tipAmountUSDC); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcPledge + tipAmountUSDC); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("usdc"), users.backer1Address, address(usdcToken), usdcPledge, tipAmountUSDC + keccak256("usdc"), users.backer1Address, address(usdcToken), usdcPledge, tipAmountUSDC, permitData1 ); vm.stopPrank(); @@ -1739,9 +1777,10 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cusd"), 0); vm.startPrank(users.backer2Address); - cUSDToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + tipAmountCUSD); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + tipAmountCUSD); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("cusd"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, tipAmountCUSD + keccak256("cusd"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, tipAmountCUSD, permitData2 ); vm.stopPrank(); @@ -1783,8 +1822,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDC pledge vm.startPrank(users.backer1Address); - usdcToken.approve(address(keepWhatsRaised), usdcAmount); - keepWhatsRaised.pledgeWithoutAReward(keccak256("p1"), users.backer1Address, address(usdcToken), usdcAmount, 0); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(keccak256("p1"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1); vm.stopPrank(); uint256 raisedAfterUSDC = keepWhatsRaised.getRaisedAmount(); @@ -1792,8 +1832,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDT pledge vm.startPrank(users.backer2Address); - usdtToken.approve(address(keepWhatsRaised), usdtAmount); - keepWhatsRaised.pledgeWithoutAReward(keccak256("p2"), users.backer2Address, address(usdtToken), usdtAmount, 0); + usdtToken.approve(CANONICAL_PERMIT2_ADDRESS, usdtAmount); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(keccak256("p2"), users.backer2Address, address(usdtToken), usdtAmount, 0, permitData2); vm.stopPrank(); uint256 raisedAfterUSDT = keepWhatsRaised.getRaisedAmount(); @@ -1801,8 +1842,9 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // cUSD pledge vm.startPrank(users.backer1Address); - cUSDToken.approve(address(keepWhatsRaised), cUSDAmount); - keepWhatsRaised.pledgeWithoutAReward(keccak256("p3"), users.backer1Address, address(cUSDToken), cUSDAmount, 0); + cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, cUSDAmount); + PermitData memory permitData3 = _buildPermitData(0, block.timestamp + 1 hours); + keepWhatsRaised.pledgeWithoutAReward(keccak256("p3"), users.backer1Address, address(cUSDToken), cUSDAmount, 0, permitData3); vm.stopPrank(); uint256 finalRaised = keepWhatsRaised.getRaisedAmount(); @@ -1828,17 +1870,19 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDT pledge vm.startPrank(users.backer1Address); - usdtToken.approve(address(keepWhatsRaised), usdtAmount); + usdtToken.approve(CANONICAL_PERMIT2_ADDRESS, usdtAmount); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("usdt_pledge"), users.backer1Address, address(usdtToken), usdtAmount, 0 + keccak256("usdt_pledge"), users.backer1Address, address(usdtToken), usdtAmount, 0, permitData1 ); vm.stopPrank(); // USDC pledge vm.startPrank(users.backer2Address); - usdcToken.approve(address(keepWhatsRaised), usdcAmount); + usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( - keccak256("usdc_pledge"), users.backer2Address, address(usdcToken), usdcAmount, 0 + keccak256("usdc_pledge"), users.backer2Address, address(usdcToken), usdcAmount, 0, permitData2 ); vm.stopPrank(); diff --git a/test/foundry/unit/PaymentTreasury.t.sol b/test/foundry/unit/PaymentTreasury.t.sol index a07907c..3bda5c0 100644 --- a/test/foundry/unit/PaymentTreasury.t.sol +++ b/test/foundry/unit/PaymentTreasury.t.sol @@ -7,6 +7,7 @@ import {PaymentTreasury} from "src/treasuries/PaymentTreasury.sol"; import {BasePaymentTreasury} from "src/utils/BasePaymentTreasury.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {TestToken} from "../../mocks/TestToken.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Test { // Helper function to create payment tokens array with same token for all payments @@ -467,8 +468,9 @@ contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Te deal(address(testToken), users.backer1Address, totalAmount); vm.prank(users.backer1Address); - testToken.approve(treasuryAddress, totalAmount); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, totalAmount); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); vm.prank(users.backer1Address); paymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -477,7 +479,8 @@ contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Te address(testToken), PAYMENT_AMOUNT_1, lineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); uint256 claimableAt = CampaignInfo(campaignAddress).getDeadline() + claimDelay; diff --git a/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol b/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol index 845d244..ddb8eb0 100644 --- a/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol +++ b/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol @@ -7,6 +7,8 @@ import {TimeConstrainedPaymentTreasury} from "src/treasuries/TimeConstrainedPaym import {CampaignInfo} from "src/CampaignInfo.sol"; import {TestToken} from "../../mocks/TestToken.sol"; import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; contract TimeConstrainedPaymentTreasury_UnitTest is Test, @@ -219,12 +221,13 @@ contract TimeConstrainedPaymentTreasury_UnitTest is function testProcessCryptoPaymentWithinTimeRange() public { advanceToWithinRange(); - // Approve tokens for the treasury + // Approve MockPermit2 (at canonical address) for the token. vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); - vm.prank(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, @@ -232,7 +235,8 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Payment processed successfully @@ -242,9 +246,10 @@ contract TimeConstrainedPaymentTreasury_UnitTest is function testProcessCryptoPaymentRevertWhenBeforeLaunchTime() public { advanceToBeforeLaunch(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory emptyPermit; vm.expectRevert(); vm.prank(users.platform1AdminAddress); - ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, @@ -252,7 +257,8 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + emptyPermit ); } @@ -294,10 +300,11 @@ contract TimeConstrainedPaymentTreasury_UnitTest is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); - vm.prank(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, @@ -305,7 +312,8 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Payment created and confirmed successfully by processCryptoPayment @@ -325,10 +333,11 @@ contract TimeConstrainedPaymentTreasury_UnitTest is // Use processCryptoPayment for both payments which creates and confirms them vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); - vm.prank(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, @@ -336,14 +345,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData1 ); vm.prank(users.backer2Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_2); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_2); - vm.prank(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems2 = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_2, ITEM_ID_2, @@ -351,7 +362,8 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_2, emptyLineItems2, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData2 ); // Payments created and confirmed successfully by processCryptoPayment @@ -379,10 +391,11 @@ contract TimeConstrainedPaymentTreasury_UnitTest is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); - vm.prank(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, @@ -390,7 +403,8 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Advance to after launch to be able to claim refund @@ -422,10 +436,11 @@ contract TimeConstrainedPaymentTreasury_UnitTest is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); - vm.prank(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, @@ -433,7 +448,8 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Advance to after launch time @@ -460,10 +476,11 @@ contract TimeConstrainedPaymentTreasury_UnitTest is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); - vm.prank(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, @@ -471,7 +488,8 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Advance to after launch time @@ -612,10 +630,11 @@ contract TimeConstrainedPaymentTreasury_UnitTest is // Use processCryptoPayment which creates and confirms payment in one step vm.prank(users.backer1Address); - testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); - vm.prank(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, @@ -623,7 +642,8 @@ contract TimeConstrainedPaymentTreasury_UnitTest is address(testToken), PAYMENT_AMOUNT_1, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Advance to after launch time diff --git a/test/mocks/MockPermit2.sol b/test/mocks/MockPermit2.sol new file mode 100644 index 0000000..5a25c45 --- /dev/null +++ b/test/mocks/MockPermit2.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IPermit2} from "src/interfaces/IPermit2.sol"; + +/** + * @title MockPermit2 + * @notice A test-only mock that mimics the Permit2 `permitWitnessTransferFrom` interface. + * @dev Signature verification is intentionally skipped so tests can exercise the contract + * logic without needing real ECDSA signatures. DO NOT deploy to production. + */ +contract MockPermit2 { + function permitWitnessTransferFrom( + IPermit2.PermitTransferFrom memory permit, + IPermit2.SignatureTransferDetails calldata transferDetails, + address owner, + bytes32, /* witness */ + string calldata, /* witnessTypeString */ + bytes calldata /* signature */ + ) external { + // Transfer the requested amount from `owner` to `to`. + // The owner must have approved this MockPermit2 address for the token. + IERC20(permit.permitted.token).transferFrom(owner, transferDetails.to, transferDetails.requestedAmount); + } + + function DOMAIN_SEPARATOR() external pure returns (bytes32) { + return bytes32(0); + } +} From aefbc16a1a50d90c9eec9722658ee8d5f6979e3f Mon Sep 17 00:00:00 2001 From: adnanhq Date: Mon, 16 Mar 2026 07:57:13 +0600 Subject: [PATCH 2/5] Fix empty permit fallback in KWR, add Uniswap deps and update tests --- .gitmodules | 3 + foundry.toml | 6 +- src/interfaces/IPermit2.sol | 64 ++------- src/treasuries/AllOrNothing.sol | 10 +- src/treasuries/KeepWhatsRaised.sol | 84 ++++++++---- src/treasuries/PaymentTreasury.sol | 15 ++- .../TimeConstrainedPaymentTreasury.sol | 15 ++- src/utils/BasePaymentTreasury.sol | 9 +- test/foundry/Base.t.sol | 51 +++++-- .../AllOrNothing/AllOrNothing.t.sol | 78 ++++++++++- .../AllOrNothing/AllOrNothingFunction.t.sol | 22 +-- .../KeepWhatsRaised/KeepWhatsRaised.t.sol | 79 ++++++++++- .../PaymentTreasury/PaymentTreasury.t.sol | 32 ++++- .../PaymentTreasuryFunction.t.sol | 18 ++- .../PaymentTreasuryLineItems.t.sol | 25 ++-- .../TimeConstrainedPaymentTreasury.t.sol | 31 +++++ ...meConstrainedPaymentTreasuryFunction.t.sol | 88 ++++++++++-- test/foundry/unit/KeepWhatsRaised.t.sol | 125 +++++++++++------- test/foundry/unit/PaymentTreasury.t.sol | 77 +++++++++-- .../unit/TimeConstrainedPaymentTreasury.t.sol | 88 ++++++++++-- test/mocks/MockPermit2.sol | 30 ----- test/mocks/Permit2Deployer.sol | 6 + 22 files changed, 717 insertions(+), 239 deletions(-) delete mode 100644 test/mocks/MockPermit2.sol create mode 100644 test/mocks/Permit2Deployer.sol diff --git a/.gitmodules b/.gitmodules index 9296efd..cc0351b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/permit2"] + path = lib/permit2 + url = https://github.com/Uniswap/permit2 diff --git a/foundry.toml b/foundry.toml index 4ba7667..c37842e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,11 +5,13 @@ libs = ["lib"] via_ir = true optimizer = true optimizer_runs = 200 -solc_version = "0.8.22" +auto_detect_solc = true remappings = [ "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", - "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/" + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "permit2/=lib/permit2/", + "solmate/=lib/permit2/lib/solmate/" ] [rpc_endpoints] diff --git a/src/interfaces/IPermit2.sol b/src/interfaces/IPermit2.sol index 1c85fb5..c15cf1e 100644 --- a/src/interfaces/IPermit2.sol +++ b/src/interfaces/IPermit2.sol @@ -1,66 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; + /** * @title IPermit2 - * @notice Minimal interface for Uniswap's Permit2 contract, used for - * signature-based token approvals and transfers. - * @dev Only includes types and functions required for `permitWitnessTransferFrom`. - * The canonical Permit2 deployment address is 0x000000000022D473030F116dDEE9F6B43aC78BA3 - * across all supported EVM chains. + * @notice Re-exports Uniswap's canonical ISignatureTransfer interface so that + * existing import paths continue to work unchanged. + * @dev The canonical Permit2 deployment address is + * 0x000000000022D473030F116dDEE9F6B43aC78BA3 across all supported EVM chains. */ -interface IPermit2 { - /// @notice Token and maximum amount authorised by the permit signer. - struct TokenPermissions { - address token; - uint256 amount; - } - - /// @notice The permit message signed for a single token transfer. - struct PermitTransferFrom { - TokenPermissions permitted; - uint256 nonce; - uint256 deadline; - } - - /// @notice Recipient address and requested amount for a single transfer. - struct SignatureTransferDetails { - address to; - uint256 requestedAmount; - } - - /** - * @notice Transfers a token using a signed permit that includes a witness. - * @dev The witness hash is mixed into the EIP-712 digest so that all - * caller-supplied parameters (amounts, IDs, line items, etc.) are - * cryptographically bound to the owner's signature. Any attempt by a - * third party to modify those parameters will invalidate the signature. - * - * @param permit The permit data signed by the token owner. - * @param transferDetails Specifies the recipient and the exact amount to move. - * @param owner The token owner and permit signer. - * @param witness EIP-712 hash of the application-specific witness struct. - * @param witnessTypeString EIP-712 type string for the witness, appended to the - * Permit2 type hash stub. Must follow the form: - * " witness)(fields...) - * TokenPermissions(address token,uint256 amount)" - * @param signature The owner's EIP-712 signature over the combined digest. - */ - function permitWitnessTransferFrom( - PermitTransferFrom memory permit, - SignatureTransferDetails calldata transferDetails, - address owner, - bytes32 witness, - string calldata witnessTypeString, - bytes calldata signature - ) external; - - /// @notice Returns the EIP-712 domain separator used by this Permit2 deployment. - function DOMAIN_SEPARATOR() external view returns (bytes32); -} +interface IPermit2 is ISignatureTransfer {} /** - * @notice Data required for a Permit2 signature-based token transfer. + * @notice Application-specific struct bundling the Permit2 fields a caller must + * supply alongside each signature-based token transfer. * @param nonce Unique nonce preventing signature replay (managed by Permit2). * @param deadline Unix timestamp after which the permit is no longer valid. * @param signature EIP-712 signature produced by the token owner. diff --git a/src/treasuries/AllOrNothing.sol b/src/treasuries/AllOrNothing.sol index 080dd09..1e3e65b 100644 --- a/src/treasuries/AllOrNothing.sol +++ b/src/treasuries/AllOrNothing.sol @@ -11,6 +11,7 @@ import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; import {BaseTreasury} from "../utils/BaseTreasury.sol"; import {IReward} from "../interfaces/IReward.sol"; import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; /** * @title AllOrNothing @@ -441,6 +442,9 @@ contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuar if (!INFO.isTokenAccepted(pledgeToken)) { revert AllOrNothingTokenNotAccepted(pledgeToken); } + if (permitData.signature.length == 0) { + revert AllOrNothingInvalidInput(); + } // If this is for a reward, pledgeAmount and shippingFee are in 18 decimals // If not for a reward, amounts are already in token decimals @@ -483,12 +487,12 @@ contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuar } IPermit2(INFO.getPermit2Address()).permitWitnessTransferFrom( - IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({token: pledgeToken, amount: totalAmount}), + ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: pledgeToken, amount: totalAmount}), nonce: permitData.nonce, deadline: permitData.deadline }), - IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), + ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), backer, witness, witnessTypeString, diff --git a/src/treasuries/KeepWhatsRaised.sol b/src/treasuries/KeepWhatsRaised.sol index 7a5a03f..c2dda37 100644 --- a/src/treasuries/KeepWhatsRaised.sol +++ b/src/treasuries/KeepWhatsRaised.sol @@ -13,6 +13,7 @@ import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; import {IReward} from "../interfaces/IReward.sol"; import {ICampaignData} from "../interfaces/ICampaignData.sol"; import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; /** * @title KeepWhatsRaised @@ -30,7 +31,7 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa mapping(uint256 => uint256) private s_tokenToPaymentFee; // Mapping to store reward details by name mapping(bytes32 => Reward) private s_reward; - /// Tracks whether a pledge with a specific ID has already been processed + /// Tracks whether an external pledge ID has already been processed. mapping(bytes32 => bool) public s_processedPledges; /// Mapping to store payment gateway fees by unique pledge ID mapping(bytes32 => uint256) public s_paymentGatewayFees; @@ -703,15 +704,12 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa //Set Payment Gateway Fee setPaymentGatewayFee(pledgeId, fee); - // Admin flow: empty permitData signals the ERC20 transferFrom path in _pledge. - // Tokens are pulled from _msgSender() (the platform admin) who must have - // pre-approved this treasury contract via ERC20.approve(). - PermitData memory emptyPermit; + PermitData memory emptyPermitData = PermitData({nonce: 0, deadline: 0, signature: ""}); if (isPledgeForAReward) { - _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, _msgSender(), emptyPermit); + _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, _msgSender(), false, emptyPermitData); } else { - _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, _msgSender(), emptyPermit); + _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, _msgSender(), false, emptyPermitData); } } @@ -743,21 +741,25 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa whenCampaignNotCancelled whenNotCancelled { - _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, backer, permitData); + if (permitData.signature.length == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + + _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, address(0), true, permitData); } /** * @notice Internal function that allows a backer to pledge for a reward. * @dev Called by both the public `pledgeForAReward` (Permit2 transfer) and - * `setFeeAndPledge` (admin ERC20 transfer). The `permitData` is forwarded - * to `_pledge` which selects the appropriate transfer method. + * `setFeeAndPledge` (admin ERC20 transfer). * @param pledgeId The unique identifier of the pledge. * @param backer The address of the backer making the pledge (receives the NFT). * @param pledgeToken The token to use for the pledge. * @param tip An optional tip can be added during the process. * @param reward An array of reward names. - * @param tokenSource Token source address for the admin (ERC20) path; ignored on Permit2 path. - * @param permitData Permit2 data; empty (zero-length signature) triggers the admin ERC20 path. + * @param tokenSource Token source address for the admin (ERC20) path. + * @param usePermit2 Whether to transfer tokens via Permit2 or direct ERC20 transfer. + * @param permitData Permit2 data for the direct user path. */ function _pledgeForAReward( bytes32 pledgeId, @@ -766,9 +768,10 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa uint256 tip, bytes32[] memory reward, address tokenSource, + bool usePermit2, PermitData memory permitData ) internal { - bytes32 internalPledgeId = keccak256(abi.encodePacked(pledgeId, _msgSender())); + bytes32 internalPledgeId = pledgeId; if (s_processedPledges[internalPledgeId]) { revert KeepWhatsRaisedPledgeAlreadyProcessed(internalPledgeId); @@ -794,7 +797,18 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa } pledgeAmount += tempReward.rewardValue; } - _pledge(pledgeId, backer, pledgeToken, reward[0], pledgeAmount, tip, reward, tokenSource, permitData); + _pledge( + pledgeId, + backer, + pledgeToken, + reward[0], + pledgeAmount, + tip, + reward, + tokenSource, + usePermit2, + permitData + ); } /** @@ -824,7 +838,11 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa whenCampaignNotCancelled whenNotCancelled { - _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, backer, permitData); + if (permitData.signature.length == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + + _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, address(0), true, permitData); } /** @@ -836,8 +854,9 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa * @param pledgeToken The token to use for the pledge. * @param pledgeAmount The amount of the pledge. * @param tip An optional tip. - * @param tokenSource Token source address for the admin (ERC20) path; ignored on Permit2 path. - * @param permitData Permit2 data; empty (zero-length signature) triggers the admin ERC20 path. + * @param tokenSource Token source address for the admin (ERC20) path. + * @param usePermit2 Whether to transfer tokens via Permit2 or direct ERC20 transfer. + * @param permitData Permit2 data for the direct user path. */ function _pledgeWithoutAReward( bytes32 pledgeId, @@ -846,9 +865,10 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa uint256 pledgeAmount, uint256 tip, address tokenSource, + bool usePermit2, PermitData memory permitData ) internal { - bytes32 internalPledgeId = keccak256(abi.encodePacked(pledgeId, _msgSender())); + bytes32 internalPledgeId = pledgeId; if (s_processedPledges[internalPledgeId]) { revert KeepWhatsRaisedPledgeAlreadyProcessed(internalPledgeId); @@ -857,7 +877,18 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa bytes32[] memory emptyByteArray = new bytes32[](0); - _pledge(pledgeId, backer, pledgeToken, ZERO_BYTES, pledgeAmount, tip, emptyByteArray, tokenSource, permitData); + _pledge( + pledgeId, + backer, + pledgeToken, + ZERO_BYTES, + pledgeAmount, + tip, + emptyByteArray, + tokenSource, + usePermit2, + permitData + ); } /** @@ -1151,12 +1182,19 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa uint256 tip, bytes32[] memory rewards, address tokenSource, + bool usePermit2, PermitData memory permitData ) private { // Validate token is accepted if (!INFO.isTokenAccepted(pledgeToken)) { revert KeepWhatsRaisedTokenNotAccepted(pledgeToken); } + if (usePermit2 && permitData.signature.length == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + if (!usePermit2 && tokenSource == address(0)) { + revert KeepWhatsRaisedInvalidInput(); + } // If this is for a reward, pledgeAmount is in 18 decimals and needs to be denormalized // If not for a reward (pledgeWithoutAReward), pledgeAmount is already in token decimals @@ -1172,7 +1210,7 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa uint256 totalAmount = pledgeAmountInTokenDecimals + tip; - if (permitData.signature.length > 0) { + if (usePermit2) { // ------------------------------------------------------------------- // User (direct pledge) path: transfer tokens via Permit2. // The witness commits to all pledge parameters so any tampering @@ -1201,12 +1239,12 @@ contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignDa } IPermit2(INFO.getPermit2Address()).permitWitnessTransferFrom( - IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({token: pledgeToken, amount: totalAmount}), + ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: pledgeToken, amount: totalAmount}), nonce: permitData.nonce, deadline: permitData.deadline }), - IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), + ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), backer, witness, witnessTypeString, diff --git a/src/treasuries/PaymentTreasury.sol b/src/treasuries/PaymentTreasury.sol index 4063fce..68bc221 100644 --- a/src/treasuries/PaymentTreasury.sol +++ b/src/treasuries/PaymentTreasury.sol @@ -5,6 +5,7 @@ import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeE import {BasePaymentTreasury} from "../utils/BasePaymentTreasury.sol"; import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; +import {PermitData} from "../interfaces/IPermit2.sol"; contract PaymentTreasury is BasePaymentTreasury { using SafeERC20 for IERC20; @@ -67,9 +68,19 @@ contract PaymentTreasury is BasePaymentTreasury { address paymentToken, uint256 amount, ICampaignPaymentTreasury.LineItem[] calldata lineItems, - ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees, + PermitData calldata permitData ) public override whenNotPaused whenNotCancelled { - super.processCryptoPayment(paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees); + super.processCryptoPayment( + paymentId, + itemId, + buyerAddress, + paymentToken, + amount, + lineItems, + externalFees, + permitData + ); } /** diff --git a/src/treasuries/TimeConstrainedPaymentTreasury.sol b/src/treasuries/TimeConstrainedPaymentTreasury.sol index cf2f182..f200080 100644 --- a/src/treasuries/TimeConstrainedPaymentTreasury.sol +++ b/src/treasuries/TimeConstrainedPaymentTreasury.sol @@ -5,6 +5,7 @@ import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeE import {BasePaymentTreasury} from "../utils/BasePaymentTreasury.sol"; import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; +import {PermitData} from "../interfaces/IPermit2.sol"; import {TimestampChecker} from "../utils/TimestampChecker.sol"; contract TimeConstrainedPaymentTreasury is BasePaymentTreasury, TimestampChecker { @@ -88,10 +89,20 @@ contract TimeConstrainedPaymentTreasury is BasePaymentTreasury, TimestampChecker address paymentToken, uint256 amount, ICampaignPaymentTreasury.LineItem[] calldata lineItems, - ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees, + PermitData calldata permitData ) public override whenNotPaused whenNotCancelled { _checkTimeWithinRange(); - super.processCryptoPayment(paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees); + super.processCryptoPayment( + paymentId, + itemId, + buyerAddress, + paymentToken, + amount, + lineItems, + externalFees, + permitData + ); } /** diff --git a/src/utils/BasePaymentTreasury.sol b/src/utils/BasePaymentTreasury.sol index 24f6099..da38d2e 100644 --- a/src/utils/BasePaymentTreasury.sol +++ b/src/utils/BasePaymentTreasury.sol @@ -8,6 +8,7 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; import {CampaignAccessChecker} from "./CampaignAccessChecker.sol"; import {PausableCancellable} from "./PausableCancellable.sol"; import {DataRegistryKeys} from "../constants/DataRegistryKeys.sol"; @@ -814,7 +815,7 @@ abstract contract BasePaymentTreasury is ) public virtual override nonReentrant whenCampaignNotPaused whenCampaignNotCancelled { if ( buyerAddress == address(0) || amount == 0 || paymentId == ZERO_BYTES || itemId == ZERO_BYTES - || paymentToken == address(0) + || paymentToken == address(0) || permitData.signature.length == 0 ) { revert PaymentTreasuryInvalidInput(); } @@ -952,12 +953,12 @@ abstract contract BasePaymentTreasury is // Transfer tokens from the buyer via Permit2. The permit authorises the // exact totalAmount and the witness binds all payment parameters. IPermit2(INFO.getPermit2Address()).permitWitnessTransferFrom( - IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({token: paymentToken, amount: totalAmount}), + ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: paymentToken, amount: totalAmount}), nonce: permitData.nonce, deadline: permitData.deadline }), - IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), + ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}), buyerAddress, witness, CRYPTO_PAYMENT_WITNESS_TYPE_STRING, diff --git a/test/foundry/Base.t.sol b/test/foundry/Base.t.sol index ed24b6a..a988db3 100644 --- a/test/foundry/Base.t.sol +++ b/test/foundry/Base.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import {Users} from "./utils/Types.sol"; import {Defaults} from "./utils/Defaults.sol"; import {TestToken} from "../mocks/TestToken.sol"; -import {MockPermit2} from "../mocks/MockPermit2.sol"; import {GlobalParams} from "src/GlobalParams.sol"; import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; @@ -19,8 +18,14 @@ import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; /// @notice Base test contract with common logic needed by all tests. abstract contract Base_Test is Test, Defaults { + bytes32 internal constant PERMIT2_TOKEN_PERMISSIONS_TYPEHASH = + keccak256("TokenPermissions(address token,uint256 amount)"); + string internal constant PERMIT2_WITNESS_TYPE_STRING_STUB = + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; + //Variables Users internal users; + mapping(address => uint256) internal userPrivateKeys; //Test Contracts - Multiple tokens for multi-token testing TestToken internal usdtToken; // 6 decimals - Tether @@ -37,15 +42,34 @@ abstract contract Base_Test is Test, Defaults { KeepWhatsRaised internal keepWhatsRaisedImplementation; CampaignInfo internal campaignInfo; - /// @dev MockPermit2 deployed at the canonical Permit2 address for all tests. - MockPermit2 internal mockPermit2; /// @dev Canonical Permit2 address used by the contracts under test. address internal constant CANONICAL_PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; - /// @dev Helper to build a no-op PermitData that satisfies the signature length check. - /// The MockPermit2 ignores signature content so any non-empty bytes work. - function _buildPermitData(uint256 nonce, uint256 deadline) internal pure returns (PermitData memory) { - return PermitData({nonce: nonce, deadline: deadline, signature: abi.encodePacked(bytes1(0x01))}); + function _buildSignedPermitData( + address owner, + address spender, + address token, + uint256 amount, + bytes32 witness, + string memory witnessTypeString, + uint256 nonce, + uint256 deadline + ) internal returns (PermitData memory) { + uint256 ownerPrivateKey = userPrivateKeys[owner]; + require(ownerPrivateKey != 0, "missing test private key"); + + bytes32 witnessTypeHash = + keccak256(bytes(string(abi.encodePacked(PERMIT2_WITNESS_TYPE_STRING_STUB, witnessTypeString)))); + bytes32 tokenPermissions = + keccak256(abi.encode(PERMIT2_TOKEN_PERMISSIONS_TYPEHASH, token, amount)); + bytes32 structHash = keccak256(abi.encode(witnessTypeHash, tokenPermissions, spender, nonce, deadline, witness)); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", IPermit2(CANONICAL_PERMIT2_ADDRESS).DOMAIN_SEPARATOR(), structHash) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + return PermitData({nonce: nonce, deadline: deadline, signature: abi.encodePacked(r, s, v)}); } function setUp() public virtual { @@ -63,11 +87,9 @@ abstract contract Base_Test is Test, Defaults { vm.startPrank(users.contractOwner); - // Deploy MockPermit2 at the canonical Permit2 address so that - // treasury contracts (which hardcode that address) use our mock. - MockPermit2 permit2Impl = new MockPermit2(); - vm.etch(CANONICAL_PERMIT2_ADDRESS, address(permit2Impl).code); - mockPermit2 = MockPermit2(CANONICAL_PERMIT2_ADDRESS); + // Deploy the real Uniswap Permit2 contract at the canonical address + // so that treasury contracts (which hardcode that address) use it. + deployCodeTo("Permit2.sol:Permit2", CANONICAL_PERMIT2_ADDRESS); // Deploy multiple test tokens with different decimals usdtToken = new TestToken("Tether USD", "USDT", 6); @@ -176,7 +198,10 @@ abstract contract Base_Test is Test, Defaults { /// @dev Generates a user, labels its address, and funds it with test assets. function createUser(string memory name) internal returns (address payable) { - address payable user = payable(makeAddr(name)); + uint256 privateKey = uint256(keccak256(abi.encodePacked("oak-network-test-user", name))); + address payable user = payable(vm.addr(privateKey)); + userPrivateKeys[user] = privateKey; + vm.label(user, name); vm.deal({account: user, newBalance: 100 ether}); return user; } diff --git a/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol b/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol index 763979a..367471c 100644 --- a/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol +++ b/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol @@ -10,13 +10,25 @@ import {CampaignInfo} from "src/CampaignInfo.sol"; import {IReward} from "src/interfaces/IReward.sol"; import {LogDecoder} from "../../utils/LogDecoder.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {PermitData} from "src/interfaces/IPermit2.sol"; /// @notice Common testing logic needed by all AllOrNothing integration tests. abstract contract AllOrNothing_Integration_Shared_Test is IReward, LogDecoder, Base_Test { + bytes32 internal constant AON_PLEDGE_FOR_REWARD_WITNESS_TYPEHASH = keccak256( + "PledgeForRewardWitness(address backer,bytes32 rewardsHash,uint256 shippingFee)" + ); + string internal constant AON_PLEDGE_FOR_REWARD_WITNESS_TYPE_STRING = + "PledgeForRewardWitness witness)PledgeForRewardWitness(address backer,bytes32 rewardsHash,uint256 shippingFee)TokenPermissions(address token,uint256 amount)"; + bytes32 internal constant AON_PLEDGE_WITHOUT_REWARD_WITNESS_TYPEHASH = + keccak256("PledgeWithoutRewardWitness(address backer,uint256 pledgeAmount)"); + string internal constant AON_PLEDGE_WITHOUT_REWARD_WITNESS_TYPE_STRING = + "PledgeWithoutRewardWitness witness)PledgeWithoutRewardWitness(address backer,uint256 pledgeAmount)TokenPermissions(address token,uint256 amount)"; + address campaignAddress; address treasuryAddress; AllOrNothing internal allOrNothing; + mapping(address => uint256) internal aonNonceCounter; uint256 pledgeForARewardTokenId; @@ -166,7 +178,8 @@ abstract contract AllOrNothing_Integration_Shared_Test is IReward, LogDecoder, B bytes32[] memory reward = new bytes32[](1); reward[0] = rewardName; - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + uint256 nonce = aonNonceCounter[caller]++; + PermitData memory permitData = _buildSignedAllOrNothingRewardPermitData(caller, address(token), shippingFee, reward, nonce, block.timestamp + 1 hours); AllOrNothing(allOrNothingAddress).pledgeForAReward(caller, address(token), shippingFee, reward, permitData); @@ -200,7 +213,8 @@ abstract contract AllOrNothing_Integration_Shared_Test is IReward, LogDecoder, B IERC20(token).approve(CANONICAL_PERMIT2_ADDRESS, type(uint256).max); vm.warp(launchTime); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + uint256 nonce = aonNonceCounter[caller]++; + PermitData memory permitData = _buildSignedAllOrNothingNoRewardPermitData(caller, address(token), pledgeAmount, nonce, block.timestamp + 1 hours); AllOrNothing(allOrNothingAddress).pledgeWithoutAReward(caller, address(token), pledgeAmount, permitData); @@ -291,4 +305,64 @@ abstract contract AllOrNothing_Integration_Shared_Test is IReward, LogDecoder, B return (logs, to, amount); } + + function _buildSignedAllOrNothingRewardPermitData( + address backer, + address token, + uint256 shippingFee, + bytes32[] memory rewardSelection, + uint256 nonce, + uint256 deadline + ) internal returns (PermitData memory) { + uint256 pledgeAmount; + for (uint256 i = 0; i < rewardSelection.length; i++) { + pledgeAmount += allOrNothing.getReward(rewardSelection[i]).rewardValue; + } + + uint256 totalAmount = _denormalizeForToken(token, pledgeAmount) + _denormalizeForToken(token, shippingFee); + bytes32 rewardsHash = keccak256(abi.encodePacked(rewardSelection)); + bytes32 witness = keccak256( + abi.encode(AON_PLEDGE_FOR_REWARD_WITNESS_TYPEHASH, backer, rewardsHash, shippingFee) + ); + + return _buildSignedPermitData( + backer, treasuryAddress, token, totalAmount, witness, AON_PLEDGE_FOR_REWARD_WITNESS_TYPE_STRING, nonce, deadline + ); + } + + function _buildSignedAllOrNothingNoRewardPermitData( + address backer, + address token, + uint256 pledgeAmount, + uint256 nonce, + uint256 deadline + ) internal returns (PermitData memory) { + bytes32 witness = + keccak256(abi.encode(AON_PLEDGE_WITHOUT_REWARD_WITNESS_TYPEHASH, backer, pledgeAmount)); + + return _buildSignedPermitData( + backer, + treasuryAddress, + token, + pledgeAmount, + witness, + AON_PLEDGE_WITHOUT_REWARD_WITNESS_TYPE_STRING, + nonce, + deadline + ); + } + + function _denormalizeForToken(address token, uint256 amount) internal view returns (uint256) { + uint8 decimals = IERC20Metadata(token).decimals(); + + if (decimals == 18) { + return amount; + } + + if (decimals < 18) { + return amount / (10 ** (18 - decimals)); + } + + return amount * (10 ** (decimals - 18)); + } } diff --git a/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol b/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol index ed9d54c..2234d16 100644 --- a/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol +++ b/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol @@ -161,14 +161,14 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio bytes32[] memory reward1 = new bytes32[](1); reward1[0] = REWARD_NAME_1_HASH; - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedAllOrNothingRewardPermitData(users.backer1Address, address(usdcToken), usdcShippingFee, reward1, 0, block.timestamp + 1 hours); allOrNothing.pledgeForAReward(users.backer1Address, address(usdcToken), usdcShippingFee, reward1, permitData1); vm.stopPrank(); // Pledge with cUSD (18 decimals) - no conversion needed vm.startPrank(users.backer2Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedAllOrNothingNoRewardPermitData(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT, permitData2); vm.stopPrank(); @@ -192,7 +192,7 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); vm.warp(LAUNCH_TIME); - PermitData memory permitData3 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData3 = _buildSignedAllOrNothingNoRewardPermitData(users.backer1Address, address(usdcToken), usdcAmount, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount, permitData3); vm.stopPrank(); @@ -202,7 +202,7 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio // cUSD pledge (18 decimals) vm.startPrank(users.backer2Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, baseAmount); - PermitData memory permitData4 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData4 = _buildSignedAllOrNothingNoRewardPermitData(users.backer2Address, address(cUSDToken), baseAmount, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), baseAmount, permitData4); vm.stopPrank(); @@ -218,14 +218,14 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); vm.warp(LAUNCH_TIME); - PermitData memory permitData5 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData5 = _buildSignedAllOrNothingNoRewardPermitData(users.backer1Address, address(usdcToken), usdcAmount, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount, permitData5); vm.stopPrank(); // Pledge with cUSD to meet goal vm.startPrank(users.backer2Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, GOAL_AMOUNT); - PermitData memory permitData6 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData6 = _buildSignedAllOrNothingNoRewardPermitData(users.backer2Address, address(cUSDToken), GOAL_AMOUNT, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), GOAL_AMOUNT, permitData6); vm.stopPrank(); @@ -279,20 +279,20 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); vm.warp(LAUNCH_TIME); - PermitData memory permitData7 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData7 = _buildSignedAllOrNothingNoRewardPermitData(users.backer1Address, address(usdcToken), usdcAmount, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount, permitData7); vm.stopPrank(); vm.startPrank(users.backer2Address); usdtToken.approve(CANONICAL_PERMIT2_ADDRESS, usdtAmount); - PermitData memory permitData8 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData8 = _buildSignedAllOrNothingNoRewardPermitData(users.backer2Address, address(usdtToken), usdtAmount, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer2Address, address(usdtToken), usdtAmount, permitData8); vm.stopPrank(); // Need cUSD pledge to meet goal vm.startPrank(users.backer1Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, GOAL_AMOUNT); - PermitData memory permitData9 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData9 = _buildSignedAllOrNothingNoRewardPermitData(users.backer1Address, address(cUSDToken), GOAL_AMOUNT, 1, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer1Address, address(cUSDToken), GOAL_AMOUNT, permitData9); vm.stopPrank(); @@ -325,7 +325,7 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); vm.warp(LAUNCH_TIME); - PermitData memory permitData10 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData10 = _buildSignedAllOrNothingNoRewardPermitData(users.backer1Address, address(usdcToken), usdcAmount, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount, permitData10); uint256 usdcTokenId = 1; // First pledge vm.stopPrank(); @@ -333,7 +333,7 @@ contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integratio // Backer2 pledges with cUSD vm.startPrank(users.backer2Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); - PermitData memory permitData11 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData11 = _buildSignedAllOrNothingNoRewardPermitData(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT, 0, block.timestamp + 1 hours); allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT, permitData11); uint256 cUSDTokenId = 2; // Second pledge vm.stopPrank(); diff --git a/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol index 6bcc9c4..4def927 100644 --- a/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol +++ b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol @@ -10,10 +10,22 @@ import {ICampaignData} from "src/interfaces/ICampaignData.sol"; import {LogDecoder} from "../../utils/LogDecoder.sol"; import {Base_Test} from "../../Base.t.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {PermitData} from "src/interfaces/IPermit2.sol"; /// @notice Common testing logic needed by all KeepWhatsRaised integration tests. abstract contract KeepWhatsRaised_Integration_Shared_Test is IReward, LogDecoder, Base_Test { + bytes32 internal constant KWR_PLEDGE_FOR_REWARD_WITNESS_TYPEHASH = keccak256( + "KWRPledgeForRewardWitness(bytes32 pledgeId,address backer,bytes32 rewardsHash,uint256 tip)" + ); + string internal constant KWR_PLEDGE_FOR_REWARD_WITNESS_TYPE_STRING = + "KWRPledgeForRewardWitness witness)KWRPledgeForRewardWitness(bytes32 pledgeId,address backer,bytes32 rewardsHash,uint256 tip)TokenPermissions(address token,uint256 amount)"; + bytes32 internal constant KWR_PLEDGE_WITHOUT_REWARD_WITNESS_TYPEHASH = keccak256( + "KWRPledgeWithoutRewardWitness(bytes32 pledgeId,address backer,uint256 pledgeAmount,uint256 tip)" + ); + string internal constant KWR_PLEDGE_WITHOUT_REWARD_WITNESS_TYPE_STRING = + "KWRPledgeWithoutRewardWitness witness)KWRPledgeWithoutRewardWitness(bytes32 pledgeId,address backer,uint256 pledgeAmount,uint256 tip)TokenPermissions(address token,uint256 amount)"; + address campaignAddress; address treasuryAddress; KeepWhatsRaised internal keepWhatsRaised; @@ -317,7 +329,7 @@ abstract contract KeepWhatsRaised_Integration_Shared_Test is IReward, LogDecoder bytes32[] memory reward = new bytes32[](1); reward[0] = rewardName; - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedRewardPermitData(caller, address(token), pledgeId, tip, reward, 0, block.timestamp + 1 hours); KeepWhatsRaised(keepWhatsRaisedAddress).pledgeForAReward(pledgeId, caller, token, tip, reward, permitData); @@ -353,7 +365,7 @@ abstract contract KeepWhatsRaised_Integration_Shared_Test is IReward, LogDecoder IERC20(token).approve(CANONICAL_PERMIT2_ADDRESS, type(uint256).max); vm.warp(launchTime); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(caller, address(token), pledgeId, pledgeAmount, tip, 0, block.timestamp + 1 hours); KeepWhatsRaised(keepWhatsRaisedAddress).pledgeWithoutAReward(pledgeId, caller, token, pledgeAmount, tip, permitData); @@ -501,4 +513,67 @@ abstract contract KeepWhatsRaised_Integration_Shared_Test is IReward, LogDecoder KeepWhatsRaised(treasury).cancelTreasury(message); vm.stopPrank(); } + + function _buildSignedKeepWhatsRaisedRewardPermitData( + address backer, + address token, + bytes32 pledgeId, + uint256 tip, + bytes32[] memory rewardSelection, + uint256 nonce, + uint256 deadline + ) internal returns (PermitData memory) { + uint256 pledgeAmount; + for (uint256 i = 0; i < rewardSelection.length; i++) { + pledgeAmount += keepWhatsRaised.getReward(rewardSelection[i]).rewardValue; + } + + uint256 totalAmount = _denormalizeForToken(token, pledgeAmount) + tip; + bytes32 rewardsHash = keccak256(abi.encodePacked(rewardSelection)); + bytes32 witness = keccak256( + abi.encode(KWR_PLEDGE_FOR_REWARD_WITNESS_TYPEHASH, pledgeId, backer, rewardsHash, tip) + ); + + return _buildSignedPermitData( + backer, treasuryAddress, token, totalAmount, witness, KWR_PLEDGE_FOR_REWARD_WITNESS_TYPE_STRING, nonce, deadline + ); + } + + function _buildSignedKeepWhatsRaisedNoRewardPermitData( + address backer, + address token, + bytes32 pledgeId, + uint256 pledgeAmount, + uint256 tip, + uint256 nonce, + uint256 deadline + ) internal returns (PermitData memory) { + bytes32 witness = + keccak256(abi.encode(KWR_PLEDGE_WITHOUT_REWARD_WITNESS_TYPEHASH, pledgeId, backer, pledgeAmount, tip)); + + return _buildSignedPermitData( + backer, + treasuryAddress, + token, + pledgeAmount + tip, + witness, + KWR_PLEDGE_WITHOUT_REWARD_WITNESS_TYPE_STRING, + nonce, + deadline + ); + } + + function _denormalizeForToken(address token, uint256 amount) internal view returns (uint256) { + uint8 decimals = IERC20Metadata(token).decimals(); + + if (decimals == 18) { + return amount; + } + + if (decimals < 18) { + return amount / (10 ** (18 - decimals)); + } + + return amount * (10 ** (decimals - 18)); + } } diff --git a/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol b/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol index 1141ae2..c262413 100644 --- a/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol +++ b/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol @@ -14,6 +14,12 @@ import {PermitData} from "src/interfaces/IPermit2.sol"; /// @notice Common testing logic needed by all PaymentTreasury integration tests. abstract contract PaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Test { + bytes32 internal constant CRYPTO_PAYMENT_WITNESS_TYPEHASH = keccak256( + "CryptoPaymentWitness(bytes32 paymentId,bytes32 itemId,address buyerAddress,uint256 amount,bytes32 lineItemsHash)" + ); + string internal constant CRYPTO_PAYMENT_WITNESS_TYPE_STRING = + "CryptoPaymentWitness witness)CryptoPaymentWitness(bytes32 paymentId,bytes32 itemId,address buyerAddress,uint256 amount,bytes32 lineItemsHash)TokenPermissions(address token,uint256 amount)"; + address campaignAddress; address treasuryAddress; PaymentTreasury internal paymentTreasury; @@ -186,7 +192,7 @@ abstract contract PaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Te // Approve MockPermit2 (at canonical address) for the token. IERC20(paymentToken).approve(CANONICAL_PERMIT2_ADDRESS, totalAmount); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData(buyerAddress, paymentToken, paymentId, itemId, amount, lineItems, 0, block.timestamp + 1 hours); paymentTreasury.processCryptoPayment( paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees, permitData @@ -194,6 +200,30 @@ abstract contract PaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Te vm.stopPrank(); } + function _buildSignedCryptoPaymentPermitData( + address buyer, + address paymentToken, + bytes32 paymentId, + bytes32 itemId, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] memory lineItems, + uint256 nonce, + uint256 deadline + ) internal returns (PermitData memory) { + uint256 totalAmount = amount; + for (uint256 i = 0; i < lineItems.length; i++) { + totalAmount += lineItems[i].amount; + } + + bytes32 lineItemsHash = keccak256(abi.encode(lineItems)); + bytes32 witness = + keccak256(abi.encode(CRYPTO_PAYMENT_WITNESS_TYPEHASH, paymentId, itemId, buyer, amount, lineItemsHash)); + + return _buildSignedPermitData( + buyer, treasuryAddress, paymentToken, totalAmount, witness, CRYPTO_PAYMENT_WITNESS_TYPE_STRING, nonce, deadline + ); + } + /** * @notice Cancels a payment */ diff --git a/test/foundry/integration/PaymentTreasury/PaymentTreasuryFunction.t.sol b/test/foundry/integration/PaymentTreasury/PaymentTreasuryFunction.t.sol index 7a72f3a..d2b0d83 100644 --- a/test/foundry/integration/PaymentTreasury/PaymentTreasuryFunction.t.sol +++ b/test/foundry/integration/PaymentTreasury/PaymentTreasuryFunction.t.sol @@ -8,6 +8,7 @@ import "forge-std/Test.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {PaymentTreasury} from "src/treasuries/PaymentTreasury.sol"; import {TestToken} from "../../../mocks/TestToken.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; /** * @title PaymentTreasuryFunction_Integration_Test @@ -512,22 +513,25 @@ contract PaymentTreasuryFunction_Integration_Test is PaymentTreasury_Integration uint256 amount = 1000e18; rejectedToken.mint(users.backer1Address, amount); - vm.prank(users.backer1Address); - rejectedToken.approve(treasuryAddress, amount); + vm.startPrank(users.backer1Address); + rejectedToken.approve(CANONICAL_PERMIT2_ADDRESS, amount); - // Try to process crypto payment with unaccepted token - vm.expectRevert(); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - processCryptoPayment( - users.backer1Address, + PermitData memory emptyPermit; + + // Place vm.expectRevert right before the external call + vm.expectRevert(); + paymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, users.backer1Address, address(rejectedToken), amount, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + emptyPermit ); + vm.stopPrank(); } function test_balanceTrackingAcrossMultipleTokens() public { diff --git a/test/foundry/integration/PaymentTreasury/PaymentTreasuryLineItems.t.sol b/test/foundry/integration/PaymentTreasury/PaymentTreasuryLineItems.t.sol index dc7dfcb..883811a 100644 --- a/test/foundry/integration/PaymentTreasury/PaymentTreasuryLineItems.t.sol +++ b/test/foundry/integration/PaymentTreasury/PaymentTreasuryLineItems.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.22; import "./PaymentTreasury.t.sol"; import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; /// @notice Tests for PaymentTreasury with line items and expiration contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Test { @@ -228,11 +229,12 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes deal(address(testToken), users.backer1Address, totalAmount); vm.prank(users.backer1Address); - testToken.approve(treasuryAddress, totalAmount); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, totalAmount); ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: SHIPPING_FEE_TYPE_ID, amount: shippingFeeAmount}); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData(users.backer1Address, address(testToken), PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, lineItems, 0, block.timestamp + 1 hours); vm.prank(users.backer1Address); paymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -241,7 +243,8 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes address(testToken), PAYMENT_AMOUNT_1, lineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Payment should be confirmed immediately for crypto payments @@ -255,13 +258,14 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes deal(address(testToken), users.backer1Address, totalAmount); vm.prank(users.backer1Address); - testToken.approve(treasuryAddress, totalAmount); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, totalAmount); uint256 platformAdminBalanceBefore = testToken.balanceOf(users.platform1AdminAddress); ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: TIP_TYPE_ID, amount: tipAmount}); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData(users.backer1Address, address(testToken), PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, lineItems, 0, block.timestamp + 1 hours); vm.prank(users.backer1Address); paymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -270,7 +274,8 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes address(testToken), PAYMENT_AMOUNT_1, lineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Tip doesn't count toward goal, but payment amount does @@ -289,7 +294,7 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes deal(address(testToken), users.backer1Address, totalAmount); vm.prank(users.backer1Address); - testToken.approve(treasuryAddress, totalAmount); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, totalAmount); uint256 platformAdminBalanceBefore = testToken.balanceOf(users.platform1AdminAddress); uint256 tipNetAmount = tipAmount; // No protocol fee on tip @@ -299,6 +304,7 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes lineItems[1] = ICampaignPaymentTreasury.LineItem({typeId: TIP_TYPE_ID, amount: tipAmount}); lineItems[2] = ICampaignPaymentTreasury.LineItem({typeId: INTEREST_TYPE_ID, amount: interestAmount}); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData(users.backer1Address, address(testToken), PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, lineItems, 0, block.timestamp + 1 hours); vm.prank(users.backer1Address); paymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -307,7 +313,8 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes address(testToken), PAYMENT_AMOUNT_1, lineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); // Only payment amount + shipping fee count toward goal @@ -520,7 +527,7 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes deal(address(testToken), users.backer1Address, totalAmount); vm.prank(users.backer1Address); - testToken.approve(treasuryAddress, totalAmount); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, totalAmount); ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); lineItems[0] = @@ -528,6 +535,7 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes bytes32 paymentId = keccak256("refundableFeePayment"); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData(users.backer1Address, address(testToken), paymentId, ITEM_ID_1, baseAmount, lineItems, 0, block.timestamp + 1 hours); vm.prank(users.backer1Address); paymentTreasury.processCryptoPayment( paymentId, @@ -536,7 +544,8 @@ contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Tes address(testToken), baseAmount, lineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); uint256 buyerBalanceAfterPayment = testToken.balanceOf(users.backer1Address); diff --git a/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasury.t.sol b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasury.t.sol index b25a5ad..7b2ede4 100644 --- a/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasury.t.sol +++ b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasury.t.sol @@ -6,12 +6,19 @@ import "forge-std/console.sol"; import {TimeConstrainedPaymentTreasury} from "src/treasuries/TimeConstrainedPaymentTreasury.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury.sol"; +import {PermitData} from "src/interfaces/IPermit2.sol"; import {LogDecoder} from "../../utils/LogDecoder.sol"; import {Base_Test} from "../../Base.t.sol"; import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; /// @notice Common testing logic needed by all TimeConstrainedPaymentTreasury integration tests. abstract contract TimeConstrainedPaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Test { + bytes32 internal constant CRYPTO_PAYMENT_WITNESS_TYPEHASH = keccak256( + "CryptoPaymentWitness(bytes32 paymentId,bytes32 itemId,address buyerAddress,uint256 amount,bytes32 lineItemsHash)" + ); + string internal constant CRYPTO_PAYMENT_WITNESS_TYPE_STRING = + "CryptoPaymentWitness witness)CryptoPaymentWitness(bytes32 paymentId,bytes32 itemId,address buyerAddress,uint256 amount,bytes32 lineItemsHash)TokenPermissions(address token,uint256 amount)"; + address campaignAddress; address treasuryAddress; TimeConstrainedPaymentTreasury internal timeConstrainedPaymentTreasury; @@ -180,4 +187,28 @@ abstract contract TimeConstrainedPaymentTreasury_Integration_Shared_Test is LogD function advanceToAfterLaunch() internal { vm.warp(campaignLaunchTime + 1); } + + function _buildSignedCryptoPaymentPermitData( + address buyer, + address paymentToken, + bytes32 paymentId, + bytes32 itemId, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] memory lineItems, + uint256 nonce, + uint256 deadline + ) internal returns (PermitData memory) { + uint256 totalAmount = amount; + for (uint256 i = 0; i < lineItems.length; i++) { + totalAmount += lineItems[i].amount; + } + + bytes32 lineItemsHash = keccak256(abi.encode(lineItems)); + bytes32 witness = + keccak256(abi.encode(CRYPTO_PAYMENT_WITNESS_TYPEHASH, paymentId, itemId, buyer, amount, lineItemsHash)); + + return _buildSignedPermitData( + buyer, treasuryAddress, paymentToken, totalAmount, witness, CRYPTO_PAYMENT_WITNESS_TYPE_STRING, nonce, deadline + ); + } } diff --git a/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol index a743ebf..3fbddb0 100644 --- a/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol +++ b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol @@ -99,7 +99,16 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -153,7 +162,16 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + uniquePaymentId, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -183,7 +201,16 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + uniquePaymentId1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId1, @@ -200,7 +227,16 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_2); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems2 = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedCryptoPaymentPermitData( + users.backer2Address, + address(testToken), + uniquePaymentId2, + ITEM_ID_2, + PAYMENT_AMOUNT_2, + emptyLineItems2, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId2, @@ -230,7 +266,16 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + uniquePaymentId, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -271,7 +316,16 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + uniquePaymentId, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -306,7 +360,16 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + uniquePaymentId, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, @@ -510,7 +573,16 @@ contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + uniquePaymentId, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( uniquePaymentId, diff --git a/test/foundry/unit/KeepWhatsRaised.t.sol b/test/foundry/unit/KeepWhatsRaised.t.sol index 27ed065..2cbe23d 100644 --- a/test/foundry/unit/KeepWhatsRaised.t.sol +++ b/test/foundry/unit/KeepWhatsRaised.t.sol @@ -13,6 +13,7 @@ import {Defaults} from "../Base.t.sol"; import {IReward} from "src/interfaces/IReward.sol"; import {ICampaignData} from "src/interfaces/ICampaignData.sol"; import {TestToken} from "../../mocks/TestToken.sol"; +import {SignatureVerification} from "permit2/src/libraries/SignatureVerification.sol"; contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Test { // Test constants @@ -129,7 +130,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, 0, rewardSelection, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection, permitData); vm.stopPrank(); @@ -451,7 +452,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_TIP_AMOUNT, rewardSelection, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeForAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection, permitData ); @@ -476,16 +477,15 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te rewardSelection[0] = TEST_REWARD_NAME; // First pledge - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, 0, rewardSelection, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection, permitData1); // Try to pledge with same ID - bytes32 internalPledgeId = keccak256(abi.encodePacked(TEST_PLEDGE_ID, users.backer1Address)); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, 0, rewardSelection, 1, block.timestamp + 1 hours); vm.expectRevert( - abi.encodeWithSelector(KeepWhatsRaised.KeepWhatsRaisedPledgeAlreadyProcessed.selector, internalPledgeId) + abi.encodeWithSelector(KeepWhatsRaised.KeepWhatsRaisedPledgeAlreadyProcessed.selector, TEST_PLEDGE_ID) ); - PermitData memory emptyPermit1; - keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection, emptyPermit1); + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection, permitData2); vm.stopPrank(); } @@ -524,7 +524,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, pledgeAmount + TEST_TIP_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, pledgeAmount, TEST_TIP_AMOUNT, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), pledgeAmount, TEST_TIP_AMOUNT, permitData ); @@ -545,19 +545,54 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT * 2); // First pledge - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData1 ); - // Try to pledge with same ID - internal pledge ID includes caller - bytes32 internalPledgeId = keccak256(abi.encodePacked(TEST_PLEDGE_ID, users.backer1Address)); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 1, block.timestamp + 1 hours); vm.expectRevert( - abi.encodeWithSelector(KeepWhatsRaised.KeepWhatsRaisedPledgeAlreadyProcessed.selector, internalPledgeId) + abi.encodeWithSelector(KeepWhatsRaised.KeepWhatsRaisedPledgeAlreadyProcessed.selector, TEST_PLEDGE_ID) ); - PermitData memory emptyPermit1; keepWhatsRaised.pledgeWithoutAReward( - TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, emptyPermit1 + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData2 + ); + vm.stopPrank(); + } + + function testPledgeWithoutARewardRevertWhenPermitMissing() public { + vm.warp(LAUNCH_TIME); + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + vm.prank(users.backer1Address); + PermitData memory emptyPermitData = PermitData({nonce: 0, deadline: 0, signature: ""}); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, emptyPermitData + ); + } + + function testPledgeWithoutARewardRevertWhenSignedPledgeIdIsTampered() public { + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); + + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData( + users.backer1Address, + address(testToken), + TEST_PLEDGE_ID, + TEST_PLEDGE_AMOUNT, + 0, + 55, + block.timestamp + 1 hours + ); + + vm.expectRevert(SignatureVerification.InvalidSigner.selector); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("tamperedPledgeId"), + users.backer1Address, + address(testToken), + TEST_PLEDGE_AMOUNT, + 0, + permitData ); vm.stopPrank(); } @@ -756,7 +791,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, largePledge); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, largePledge, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), largePledge, 0, permitData); vm.stopPrank(); @@ -799,7 +834,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); @@ -843,7 +878,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); @@ -881,7 +916,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); @@ -902,7 +937,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); @@ -942,7 +977,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); @@ -965,7 +1000,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); @@ -1259,7 +1294,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, smallPledge); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, smallPledge, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), smallPledge, 0, permitData); vm.stopPrank(); @@ -1282,7 +1317,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); @@ -1298,7 +1333,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), TEST_PLEDGE_ID, TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0, permitData ); @@ -1355,7 +1390,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedRewardPermitData(users.backer1Address, address(testToken), keccak256("pledge1"), TEST_TIP_AMOUNT, rewardSelection, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeForAReward( keccak256("pledge1"), users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection, permitData1 ); @@ -1368,7 +1403,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te ); vm.startPrank(users.backer2Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, 2000e18); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(testToken), keccak256("pledge2"), 2000e18, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(keccak256("pledge2"), users.backer2Address, address(testToken), 2000e18, 0, permitData2); vm.stopPrank(); @@ -1408,7 +1443,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, smallAmount); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(testToken), keccak256("small"), smallAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("small"), users.backer1Address, address(testToken), smallAmount, 0, permitData ); @@ -1503,7 +1538,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); bytes32[] memory rewardSelection = new bytes32[](1); rewardSelection[0] = TEST_REWARD_NAME; - PermitData memory permitDataBacker1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitDataBacker1 = _buildSignedKeepWhatsRaisedRewardPermitData(users.backer1Address, address(testToken), keccak256("pledge1"), TEST_TIP_AMOUNT, rewardSelection, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeForAReward( keccak256("pledge1"), users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection, permitDataBacker1 ); @@ -1512,7 +1547,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // Backer 2 pledge without reward vm.startPrank(users.backer2Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); - PermitData memory permitDataBacker2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitDataBacker2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(testToken), keccak256("pledge2"), TEST_PLEDGE_AMOUNT, TEST_TIP_AMOUNT, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("pledge2"), users.backer2Address, address(testToken), TEST_PLEDGE_AMOUNT, TEST_TIP_AMOUNT, permitDataBacker2 ); @@ -1550,7 +1585,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(usdcToken), keccak256("usdc_pledge"), usdcAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("usdc_pledge"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1 ); @@ -1561,7 +1596,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.startPrank(users.backer2Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(cUSDToken), keccak256("cusd_pledge"), TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("cusd_pledge"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, 0, permitData2 ); @@ -1587,7 +1622,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.startPrank(users.backer1Address); deal(address(usdcToken), users.backer1Address, usdcAmount); // Ensure enough tokens usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(usdcToken), keccak256("usdc"), usdcAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(keccak256("usdc"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1); vm.stopPrank(); @@ -1597,7 +1632,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.startPrank(users.backer2Address); deal(address(cUSDToken), users.backer2Address, cUSDAmount); // Ensure enough tokens cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, cUSDAmount); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(cUSDToken), keccak256("cusd"), cUSDAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(keccak256("cusd"), users.backer2Address, address(cUSDToken), cUSDAmount, 0, permitData2); vm.stopPrank(); @@ -1639,21 +1674,21 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDC pledge vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(usdcToken), keccak256("usdc"), usdcAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(keccak256("usdc"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1); vm.stopPrank(); // USDT pledge vm.startPrank(users.backer2Address); usdtToken.approve(CANONICAL_PERMIT2_ADDRESS, usdtAmount); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(usdtToken), keccak256("usdt"), usdtAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(keccak256("usdt"), users.backer2Address, address(usdtToken), usdtAmount, 0, permitData2); vm.stopPrank(); // cUSD pledge vm.startPrank(users.backer1Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, PLEDGE_AMOUNT); - PermitData memory permitData3 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData3 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(cUSDToken), keccak256("cusd"), PLEDGE_AMOUNT, 0, 1, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("cusd"), users.backer1Address, address(cUSDToken), PLEDGE_AMOUNT, 0, permitData3 ); @@ -1711,7 +1746,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(usdcToken), keccak256("usdc_pledge"), usdcAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("usdc_pledge"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1 ); @@ -1723,7 +1758,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.startPrank(users.backer2Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(cUSDToken), keccak256("cusd_pledge"), TEST_PLEDGE_AMOUNT, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("cusd_pledge"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, 0, permitData2 ); @@ -1767,7 +1802,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.warp(LAUNCH_TIME); vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcPledge + tipAmountUSDC); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(usdcToken), keccak256("usdc"), usdcPledge, tipAmountUSDC, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("usdc"), users.backer1Address, address(usdcToken), usdcPledge, tipAmountUSDC, permitData1 ); @@ -1778,7 +1813,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te vm.startPrank(users.backer2Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, TEST_PLEDGE_AMOUNT + tipAmountCUSD); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(cUSDToken), keccak256("cusd"), TEST_PLEDGE_AMOUNT, tipAmountCUSD, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("cusd"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, tipAmountCUSD, permitData2 ); @@ -1823,7 +1858,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDC pledge vm.startPrank(users.backer1Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(usdcToken), keccak256("p1"), usdcAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(keccak256("p1"), users.backer1Address, address(usdcToken), usdcAmount, 0, permitData1); vm.stopPrank(); @@ -1833,7 +1868,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDT pledge vm.startPrank(users.backer2Address); usdtToken.approve(CANONICAL_PERMIT2_ADDRESS, usdtAmount); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(usdtToken), keccak256("p2"), usdtAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(keccak256("p2"), users.backer2Address, address(usdtToken), usdtAmount, 0, permitData2); vm.stopPrank(); @@ -1843,7 +1878,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // cUSD pledge vm.startPrank(users.backer1Address); cUSDToken.approve(CANONICAL_PERMIT2_ADDRESS, cUSDAmount); - PermitData memory permitData3 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData3 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(cUSDToken), keccak256("p3"), cUSDAmount, 0, 1, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward(keccak256("p3"), users.backer1Address, address(cUSDToken), cUSDAmount, 0, permitData3); vm.stopPrank(); @@ -1871,7 +1906,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDT pledge vm.startPrank(users.backer1Address); usdtToken.approve(CANONICAL_PERMIT2_ADDRESS, usdtAmount); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer1Address, address(usdtToken), keccak256("usdt_pledge"), usdtAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("usdt_pledge"), users.backer1Address, address(usdtToken), usdtAmount, 0, permitData1 ); @@ -1880,7 +1915,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te // USDC pledge vm.startPrank(users.backer2Address); usdcToken.approve(CANONICAL_PERMIT2_ADDRESS, usdcAmount); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedKeepWhatsRaisedNoRewardPermitData(users.backer2Address, address(usdcToken), keccak256("usdc_pledge"), usdcAmount, 0, 0, block.timestamp + 1 hours); keepWhatsRaised.pledgeWithoutAReward( keccak256("usdc_pledge"), users.backer2Address, address(usdcToken), usdcAmount, 0, permitData2 ); diff --git a/test/foundry/unit/PaymentTreasury.t.sol b/test/foundry/unit/PaymentTreasury.t.sol index 3bda5c0..48db9b7 100644 --- a/test/foundry/unit/PaymentTreasury.t.sol +++ b/test/foundry/unit/PaymentTreasury.t.sol @@ -8,6 +8,7 @@ import {BasePaymentTreasury} from "src/utils/BasePaymentTreasury.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {TestToken} from "../../mocks/TestToken.sol"; import {PermitData} from "src/interfaces/IPermit2.sol"; +import {SignatureVerification} from "permit2/src/libraries/SignatureVerification.sol"; contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Test { // Helper function to create payment tokens array with same token for all payments @@ -336,6 +337,32 @@ contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Te assertEq(testToken.balanceOf(treasuryAddress), amount); } + function testProcessCryptoPaymentRevertWhenSignedItemIdIsTampered() public { + uint256 amount = 1500e18; + deal(address(testToken), users.backer1Address, amount); + + vm.prank(users.backer1Address); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, amount); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, address(testToken), PAYMENT_ID_1, ITEM_ID_1, amount, emptyLineItems, 77, block.timestamp + 1 hours + ); + + vm.expectRevert(SignatureVerification.InvalidSigner.selector); + vm.prank(users.platform1AdminAddress); + paymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_2, + users.backer1Address, + address(testToken), + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData + ); + } + function testProcessCryptoPaymentStoresExternalFees() public { uint256 amount = 1000e18; deal(address(testToken), users.backer1Address, amount); @@ -371,32 +398,40 @@ contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Te } function testProcessCryptoPaymentRevertWhenZeroBuyerAddress() public { - vm.expectRevert(); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - processCryptoPayment( - users.platform1AdminAddress, + // Build dummy permit data (won't be reached due to input validation) + PermitData memory permitData = PermitData({nonce: 0, deadline: block.timestamp + 1 hours, signature: new bytes(65)}); + + vm.expectRevert(BasePaymentTreasury.PaymentTreasuryInvalidInput.selector); + vm.prank(users.platform1AdminAddress); + paymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, address(0), address(testToken), 1000e18, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); } function testProcessCryptoPaymentRevertWhenZeroAmount() public { - vm.expectRevert(); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - processCryptoPayment( - users.platform1AdminAddress, + // Build dummy permit data (won't be reached due to input validation) + PermitData memory permitData = PermitData({nonce: 0, deadline: block.timestamp + 1 hours, signature: new bytes(65)}); + + vm.expectRevert(BasePaymentTreasury.PaymentTreasuryInvalidInput.selector); + vm.prank(users.platform1AdminAddress); + paymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, users.backer1Address, address(testToken), 0, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData ); } @@ -405,7 +440,7 @@ contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Te deal(address(testToken), users.backer1Address, amount * 2); vm.prank(users.backer1Address); - testToken.approve(treasuryAddress, amount * 2); + testToken.approve(CANONICAL_PERMIT2_ADDRESS, amount * 2); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); processCryptoPayment( @@ -419,16 +454,23 @@ contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Te new ICampaignPaymentTreasury.ExternalFees[](0) ); + // Build permit data with a unique nonce so Permit2 doesn't reject the nonce + // before reaching the "payment exists" check in the contract + PermitData memory permitData2 = _buildSignedCryptoPaymentPermitData( + users.backer1Address, address(testToken), PAYMENT_ID_1, ITEM_ID_1, amount, emptyLineItems, 1, block.timestamp + 1 hours + ); + vm.expectRevert(); - processCryptoPayment( - users.backer1Address, + vm.prank(users.backer1Address); + paymentTreasury.processCryptoPayment( PAYMENT_ID_1, ITEM_ID_1, users.backer1Address, address(testToken), amount, emptyLineItems, - new ICampaignPaymentTreasury.ExternalFees[](0) + new ICampaignPaymentTreasury.ExternalFees[](0), + permitData2 ); } @@ -470,7 +512,16 @@ contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Te vm.prank(users.backer1Address); testToken.approve(CANONICAL_PERMIT2_ADDRESS, totalAmount); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + lineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.backer1Address); paymentTreasury.processCryptoPayment( PAYMENT_ID_1, diff --git a/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol b/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol index ddb8eb0..44fc876 100644 --- a/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol +++ b/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol @@ -226,7 +226,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -303,7 +312,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -336,7 +354,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData1 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData1 = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -353,7 +380,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_2); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems2 = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData2 = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData2 = _buildSignedCryptoPaymentPermitData( + users.backer2Address, + address(testToken), + PAYMENT_ID_2, + ITEM_ID_2, + PAYMENT_AMOUNT_2, + emptyLineItems2, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_2, @@ -394,7 +430,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -439,7 +484,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -479,7 +533,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, @@ -633,7 +696,16 @@ contract TimeConstrainedPaymentTreasury_UnitTest is testToken.approve(CANONICAL_PERMIT2_ADDRESS, PAYMENT_AMOUNT_1); ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); - PermitData memory permitData = _buildPermitData(0, block.timestamp + 1 hours); + PermitData memory permitData = _buildSignedCryptoPaymentPermitData( + users.backer1Address, + address(testToken), + PAYMENT_ID_1, + ITEM_ID_1, + PAYMENT_AMOUNT_1, + emptyLineItems, + 0, + block.timestamp + 1 hours + ); vm.prank(users.platform1AdminAddress); timeConstrainedPaymentTreasury.processCryptoPayment( PAYMENT_ID_1, diff --git a/test/mocks/MockPermit2.sol b/test/mocks/MockPermit2.sol deleted file mode 100644 index 5a25c45..0000000 --- a/test/mocks/MockPermit2.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IPermit2} from "src/interfaces/IPermit2.sol"; - -/** - * @title MockPermit2 - * @notice A test-only mock that mimics the Permit2 `permitWitnessTransferFrom` interface. - * @dev Signature verification is intentionally skipped so tests can exercise the contract - * logic without needing real ECDSA signatures. DO NOT deploy to production. - */ -contract MockPermit2 { - function permitWitnessTransferFrom( - IPermit2.PermitTransferFrom memory permit, - IPermit2.SignatureTransferDetails calldata transferDetails, - address owner, - bytes32, /* witness */ - string calldata, /* witnessTypeString */ - bytes calldata /* signature */ - ) external { - // Transfer the requested amount from `owner` to `to`. - // The owner must have approved this MockPermit2 address for the token. - IERC20(permit.permitted.token).transferFrom(owner, transferDetails.to, transferDetails.requestedAmount); - } - - function DOMAIN_SEPARATOR() external pure returns (bytes32) { - return bytes32(0); - } -} diff --git a/test/mocks/Permit2Deployer.sol b/test/mocks/Permit2Deployer.sol new file mode 100644 index 0000000..8971ae7 --- /dev/null +++ b/test/mocks/Permit2Deployer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +// Force compilation of the Permit2 contract so that `deployCodeTo("Permit2.sol:Permit2", ...)` +// can locate the artifact during tests. +import {Permit2} from "permit2/src/Permit2.sol"; From e77328d2732d2c871bc842a1156cb99ed448618d Mon Sep 17 00:00:00 2001 From: adnanhq Date: Mon, 16 Mar 2026 11:20:20 +0600 Subject: [PATCH 3/5] Add permit2 submodule gitlink to fix CI builds --- lib/permit2 | 1 + 1 file changed, 1 insertion(+) create mode 160000 lib/permit2 diff --git a/lib/permit2 b/lib/permit2 new file mode 160000 index 0000000..cc56ad0 --- /dev/null +++ b/lib/permit2 @@ -0,0 +1 @@ +Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 From 2a4f2a7751fce57d41f7768854676735773da925 Mon Sep 17 00:00:00 2001 From: adnanhq Date: Mon, 16 Mar 2026 17:44:44 +0600 Subject: [PATCH 4/5] Use Mock Permit2 in tests to avoid solc mismatch --- .gitmodules | 3 - foundry.toml | 6 +- lib/permit2 | 1 - src/interfaces/IPermit2.sol | 99 +++++++- src/treasuries/AllOrNothing.sol | 3 +- src/treasuries/KeepWhatsRaised.sol | 3 +- src/utils/BasePaymentTreasury.sol | 3 +- test/foundry/Base.t.sol | 7 +- test/foundry/unit/KeepWhatsRaised.t.sol | 4 +- test/foundry/unit/PaymentTreasury.t.sol | 4 +- test/mocks/MockPermit2.sol | 323 ++++++++++++++++++++++++ test/mocks/Permit2Deployer.sol | 6 - 12 files changed, 432 insertions(+), 30 deletions(-) delete mode 160000 lib/permit2 create mode 100644 test/mocks/MockPermit2.sol delete mode 100644 test/mocks/Permit2Deployer.sol diff --git a/.gitmodules b/.gitmodules index cc0351b..9296efd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,3 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable -[submodule "lib/permit2"] - path = lib/permit2 - url = https://github.com/Uniswap/permit2 diff --git a/foundry.toml b/foundry.toml index c37842e..4ba7667 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,13 +5,11 @@ libs = ["lib"] via_ir = true optimizer = true optimizer_runs = 200 -auto_detect_solc = true +solc_version = "0.8.22" remappings = [ "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", - "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", - "permit2/=lib/permit2/", - "solmate/=lib/permit2/lib/solmate/" + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/" ] [rpc_endpoints] diff --git a/lib/permit2 b/lib/permit2 deleted file mode 160000 index cc56ad0..0000000 --- a/lib/permit2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 diff --git a/src/interfaces/IPermit2.sol b/src/interfaces/IPermit2.sol index c15cf1e..5ec0735 100644 --- a/src/interfaces/IPermit2.sol +++ b/src/interfaces/IPermit2.sol @@ -1,12 +1,105 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; -import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +// ============================================================================ +// Inlined Permit2 interfaces (originally from Uniswap permit2 package) +// ============================================================================ + +/// @title IEIP712 +interface IEIP712 { + function DOMAIN_SEPARATOR() external view returns (bytes32); +} + +/// @title ISignatureTransfer +/// @notice Handles ERC20 token transfers through signature based actions +/// @dev Requires user's token approval on the Permit2 contract +interface ISignatureTransfer is IEIP712 { + /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount + /// @param maxAmount The maximum amount a spender can request to transfer + error InvalidAmount(uint256 maxAmount); + + /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred + error LengthMismatch(); + + /// @notice Emits an event when the owner successfully invalidates an unordered nonce. + event UnorderedNonceInvalidation(address indexed owner, uint256 word, uint256 mask); + + /// @notice The token and amount details for a transfer signed in the permit transfer signature + struct TokenPermissions { + address token; + uint256 amount; + } + + /// @notice The signed permit message for a single token transfer + struct PermitTransferFrom { + TokenPermissions permitted; + uint256 nonce; + uint256 deadline; + } + + /// @notice Specifies the recipient address and amount for batched transfers. + struct SignatureTransferDetails { + address to; + uint256 requestedAmount; + } + + /// @notice Used to reconstruct the signed permit message for multiple token transfers + struct PermitBatchTransferFrom { + TokenPermissions[] permitted; + uint256 nonce; + uint256 deadline; + } + + /// @notice A map from token owner address and a caller specified word index to a bitmap. + function nonceBitmap(address, uint256) external view returns (uint256); + + /// @notice Transfers a token using a signed permit message + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers a token using a signed permit message with extra witness data + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + function permitTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message with extra witness data + function permitWitnessTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Invalidates the bits specified in mask for the bitmap at the word position + function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; +} + +// ============================================================================ +// Application-level types +// ============================================================================ /** * @title IPermit2 - * @notice Re-exports Uniswap's canonical ISignatureTransfer interface so that - * existing import paths continue to work unchanged. + * @notice Re-exports ISignatureTransfer so that existing import paths work unchanged. * @dev The canonical Permit2 deployment address is * 0x000000000022D473030F116dDEE9F6B43aC78BA3 across all supported EVM chains. */ diff --git a/src/treasuries/AllOrNothing.sol b/src/treasuries/AllOrNothing.sol index 1e3e65b..afa494c 100644 --- a/src/treasuries/AllOrNothing.sol +++ b/src/treasuries/AllOrNothing.sol @@ -10,8 +10,7 @@ import {ICampaignTreasury} from "../interfaces/ICampaignTreasury.sol"; import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; import {BaseTreasury} from "../utils/BaseTreasury.sol"; import {IReward} from "../interfaces/IReward.sol"; -import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; -import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +import {IPermit2, ISignatureTransfer, PermitData} from "../interfaces/IPermit2.sol"; /** * @title AllOrNothing diff --git a/src/treasuries/KeepWhatsRaised.sol b/src/treasuries/KeepWhatsRaised.sol index c2dda37..54522fa 100644 --- a/src/treasuries/KeepWhatsRaised.sol +++ b/src/treasuries/KeepWhatsRaised.sol @@ -12,8 +12,7 @@ import {ICampaignTreasury} from "../interfaces/ICampaignTreasury.sol"; import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; import {IReward} from "../interfaces/IReward.sol"; import {ICampaignData} from "../interfaces/ICampaignData.sol"; -import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; -import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +import {IPermit2, ISignatureTransfer, PermitData} from "../interfaces/IPermit2.sol"; /** * @title KeepWhatsRaised diff --git a/src/utils/BasePaymentTreasury.sol b/src/utils/BasePaymentTreasury.sol index da38d2e..f8099bd 100644 --- a/src/utils/BasePaymentTreasury.sol +++ b/src/utils/BasePaymentTreasury.sol @@ -7,8 +7,7 @@ import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.s import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; -import {IPermit2, PermitData} from "../interfaces/IPermit2.sol"; -import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +import {IPermit2, ISignatureTransfer, PermitData} from "../interfaces/IPermit2.sol"; import {CampaignAccessChecker} from "./CampaignAccessChecker.sol"; import {PausableCancellable} from "./PausableCancellable.sol"; import {DataRegistryKeys} from "../constants/DataRegistryKeys.sol"; diff --git a/test/foundry/Base.t.sol b/test/foundry/Base.t.sol index a988db3..049c392 100644 --- a/test/foundry/Base.t.sol +++ b/test/foundry/Base.t.sol @@ -13,6 +13,7 @@ import {AllOrNothing} from "src/treasuries/AllOrNothing.sol"; import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; import {IPermit2, PermitData} from "src/interfaces/IPermit2.sol"; +import {MockPermit2} from "../mocks/MockPermit2.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; @@ -87,9 +88,9 @@ abstract contract Base_Test is Test, Defaults { vm.startPrank(users.contractOwner); - // Deploy the real Uniswap Permit2 contract at the canonical address - // so that treasury contracts (which hardcode that address) use it. - deployCodeTo("Permit2.sol:Permit2", CANONICAL_PERMIT2_ADDRESS); + // Deploy our MockPermit2 (solc 0.8.22 compatible) at the canonical address + // so that treasury contracts (which use that address via GlobalParams) work in tests. + deployCodeTo("MockPermit2.sol:MockPermit2", CANONICAL_PERMIT2_ADDRESS); // Deploy multiple test tokens with different decimals usdtToken = new TestToken("Tether USD", "USDT", 6); diff --git a/test/foundry/unit/KeepWhatsRaised.t.sol b/test/foundry/unit/KeepWhatsRaised.t.sol index 2cbe23d..c51b817 100644 --- a/test/foundry/unit/KeepWhatsRaised.t.sol +++ b/test/foundry/unit/KeepWhatsRaised.t.sol @@ -13,7 +13,7 @@ import {Defaults} from "../Base.t.sol"; import {IReward} from "src/interfaces/IReward.sol"; import {ICampaignData} from "src/interfaces/ICampaignData.sol"; import {TestToken} from "../../mocks/TestToken.sol"; -import {SignatureVerification} from "permit2/src/libraries/SignatureVerification.sol"; +import {MockPermit2} from "../../mocks/MockPermit2.sol"; contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Test { // Test constants @@ -585,7 +585,7 @@ contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Te block.timestamp + 1 hours ); - vm.expectRevert(SignatureVerification.InvalidSigner.selector); + vm.expectRevert(MockPermit2.InvalidSigner.selector); keepWhatsRaised.pledgeWithoutAReward( keccak256("tamperedPledgeId"), users.backer1Address, diff --git a/test/foundry/unit/PaymentTreasury.t.sol b/test/foundry/unit/PaymentTreasury.t.sol index 48db9b7..2e53d27 100644 --- a/test/foundry/unit/PaymentTreasury.t.sol +++ b/test/foundry/unit/PaymentTreasury.t.sol @@ -8,7 +8,7 @@ import {BasePaymentTreasury} from "src/utils/BasePaymentTreasury.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {TestToken} from "../../mocks/TestToken.sol"; import {PermitData} from "src/interfaces/IPermit2.sol"; -import {SignatureVerification} from "permit2/src/libraries/SignatureVerification.sol"; +import {MockPermit2} from "../../mocks/MockPermit2.sol"; contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Test { // Helper function to create payment tokens array with same token for all payments @@ -349,7 +349,7 @@ contract PaymentTreasury_UnitTest is Test, PaymentTreasury_Integration_Shared_Te users.backer1Address, address(testToken), PAYMENT_ID_1, ITEM_ID_1, amount, emptyLineItems, 77, block.timestamp + 1 hours ); - vm.expectRevert(SignatureVerification.InvalidSigner.selector); + vm.expectRevert(MockPermit2.InvalidSigner.selector); vm.prank(users.platform1AdminAddress); paymentTreasury.processCryptoPayment( PAYMENT_ID_1, diff --git a/test/mocks/MockPermit2.sol b/test/mocks/MockPermit2.sol new file mode 100644 index 0000000..5e0e66c --- /dev/null +++ b/test/mocks/MockPermit2.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ISignatureTransfer, IEIP712} from "../../src/interfaces/IPermit2.sol"; + +/** + * @title MockPermit2 + * @notice A solc 0.8.22-compatible re-implementation of Uniswap's Permit2 + * (SignatureTransfer only) for use in Foundry tests. + * @dev Faithfully reproduces the EIP-712 domain, struct hashing, nonce bitmap, + * signature verification, and token transfer logic of the canonical Permit2 + * so that tests exercise real cryptographic signing paths. + */ +contract MockPermit2 is ISignatureTransfer { + using SafeERC20 for IERC20; + + // ------------------------------------------------------------------------- + // EIP-712 domain + // ------------------------------------------------------------------------- + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; + uint256 private immutable _CACHED_CHAIN_ID; + + bytes32 private constant _HASHED_NAME = keccak256("Permit2"); + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + // ------------------------------------------------------------------------- + // PermitHash constants (mirrors permit2/src/libraries/PermitHash.sol) + // ------------------------------------------------------------------------- + bytes32 private constant _TOKEN_PERMISSIONS_TYPEHASH = + keccak256("TokenPermissions(address token,uint256 amount)"); + + bytes32 private constant _PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + bytes32 private constant _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + string private constant _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB = + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; + + string private constant _PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB = + "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,"; + + // ------------------------------------------------------------------------- + // Errors (mirrors permit2/src/PermitErrors.sol & SignatureVerification.sol) + // ------------------------------------------------------------------------- + error SignatureExpired(uint256 signatureDeadline); + error InvalidNonce(); + error InvalidSignatureLength(); + error InvalidSignature(); + error InvalidSigner(); + error InvalidContractSignature(); + + // ------------------------------------------------------------------------- + // Nonce bitmap + // ------------------------------------------------------------------------- + /// @inheritdoc ISignatureTransfer + mapping(address => mapping(uint256 => uint256)) public nonceBitmap; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + constructor() { + _CACHED_CHAIN_ID = block.chainid; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME); + } + + // ------------------------------------------------------------------------- + // IEIP712 + // ------------------------------------------------------------------------- + function DOMAIN_SEPARATOR() public view override returns (bytes32) { + return block.chainid == _CACHED_CHAIN_ID + ? _CACHED_DOMAIN_SEPARATOR + : _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME); + } + + // ------------------------------------------------------------------------- + // ISignatureTransfer — single-token functions + // ------------------------------------------------------------------------- + /// @inheritdoc ISignatureTransfer + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external { + _permitTransferFrom(permit, transferDetails, owner, _hash(permit), signature); + } + + /// @inheritdoc ISignatureTransfer + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external { + _permitTransferFrom( + permit, transferDetails, owner, _hashWithWitness(permit, witness, witnessTypeString), signature + ); + } + + // ------------------------------------------------------------------------- + // ISignatureTransfer — batch functions + // ------------------------------------------------------------------------- + /// @inheritdoc ISignatureTransfer + function permitTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes calldata signature + ) external { + _permitBatchTransferFrom(permit, transferDetails, owner, _hash(permit), signature); + } + + /// @inheritdoc ISignatureTransfer + function permitWitnessTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external { + _permitBatchTransferFrom( + permit, transferDetails, owner, _hashWithWitness(permit, witness, witnessTypeString), signature + ); + } + + /// @inheritdoc ISignatureTransfer + function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external { + nonceBitmap[msg.sender][wordPos] |= mask; + emit UnorderedNonceInvalidation(msg.sender, wordPos, mask); + } + + // ========================================================================= + // Internal helpers + // ========================================================================= + + // ---- single-token transfer core ---- + function _permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 dataHash, + bytes calldata signature + ) private { + uint256 requestedAmount = transferDetails.requestedAmount; + + if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline); + if (requestedAmount > permit.permitted.amount) revert InvalidAmount(permit.permitted.amount); + + _useUnorderedNonce(owner, permit.nonce); + _verify(signature, _hashTypedData(dataHash), owner); + + IERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount); + } + + // ---- batch-token transfer core ---- + function _permitBatchTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32 dataHash, + bytes calldata signature + ) private { + uint256 numPermitted = permit.permitted.length; + + if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline); + if (numPermitted != transferDetails.length) revert LengthMismatch(); + + _useUnorderedNonce(owner, permit.nonce); + _verify(signature, _hashTypedData(dataHash), owner); + + unchecked { + for (uint256 i = 0; i < numPermitted; ++i) { + TokenPermissions memory permitted = permit.permitted[i]; + uint256 requestedAmount = transferDetails[i].requestedAmount; + + if (requestedAmount > permitted.amount) revert InvalidAmount(permitted.amount); + + if (requestedAmount != 0) { + IERC20(permitted.token).safeTransferFrom(owner, transferDetails[i].to, requestedAmount); + } + } + } + } + + // ---- EIP-712 ---- + function _buildDomainSeparator(bytes32 typeHash, bytes32 nameHash) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, block.chainid, address(this))); + } + + function _hashTypedData(bytes32 dataHash) private view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), dataHash)); + } + + // ---- struct hashing (mirrors PermitHash library) ---- + function _hashTokenPermissions(TokenPermissions memory permitted) private pure returns (bytes32) { + return keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, permitted)); + } + + function _hash(PermitTransferFrom memory permit) private view returns (bytes32) { + bytes32 tokenPermissionsHash = _hashTokenPermissions(permit.permitted); + return keccak256( + abi.encode(_PERMIT_TRANSFER_FROM_TYPEHASH, tokenPermissionsHash, msg.sender, permit.nonce, permit.deadline) + ); + } + + function _hash(PermitBatchTransferFrom memory permit) private view returns (bytes32) { + uint256 numPermitted = permit.permitted.length; + bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted); + + for (uint256 i = 0; i < numPermitted; ++i) { + tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]); + } + + return keccak256( + abi.encode( + _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH, + keccak256(abi.encodePacked(tokenPermissionHashes)), + msg.sender, + permit.nonce, + permit.deadline + ) + ); + } + + function _hashWithWitness( + PermitTransferFrom memory permit, + bytes32 witness, + string calldata witnessTypeString + ) private view returns (bytes32) { + bytes32 typeHash = + keccak256(abi.encodePacked(_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, witnessTypeString)); + bytes32 tokenPermissionsHash = _hashTokenPermissions(permit.permitted); + return keccak256( + abi.encode(typeHash, tokenPermissionsHash, msg.sender, permit.nonce, permit.deadline, witness) + ); + } + + function _hashWithWitness( + PermitBatchTransferFrom memory permit, + bytes32 witness, + string calldata witnessTypeString + ) private view returns (bytes32) { + bytes32 typeHash = + keccak256(abi.encodePacked(_PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB, witnessTypeString)); + + uint256 numPermitted = permit.permitted.length; + bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted); + + for (uint256 i = 0; i < numPermitted; ++i) { + tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]); + } + + return keccak256( + abi.encode( + typeHash, + keccak256(abi.encodePacked(tokenPermissionHashes)), + msg.sender, + permit.nonce, + permit.deadline, + witness + ) + ); + } + + // ---- nonce management ---- + function _useUnorderedNonce(address from, uint256 nonce) private { + (uint256 wordPos, uint256 bitPos) = _bitmapPositions(nonce); + uint256 bit = 1 << bitPos; + uint256 flipped = nonceBitmap[from][wordPos] ^= bit; + + if (flipped & bit == 0) revert InvalidNonce(); + } + + function _bitmapPositions(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) { + wordPos = uint248(nonce >> 8); + bitPos = uint8(nonce); + } + + // ---- signature verification (mirrors SignatureVerification library) ---- + bytes32 private constant _UPPER_BIT_MASK = + 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + function _verify(bytes calldata signature, bytes32 hash, address claimedSigner) private view { + bytes32 r; + bytes32 s; + uint8 v; + + if (claimedSigner.code.length == 0) { + if (signature.length == 65) { + (r, s) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(signature[64]); + } else if (signature.length == 64) { + // EIP-2098 + bytes32 vs; + (r, vs) = abi.decode(signature, (bytes32, bytes32)); + s = vs & _UPPER_BIT_MASK; + v = uint8(uint256(vs >> 255)) + 27; + } else { + revert InvalidSignatureLength(); + } + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) revert InvalidSignature(); + if (signer != claimedSigner) revert InvalidSigner(); + } else { + (bool success, bytes memory result) = claimedSigner.staticcall( + abi.encodeWithSignature("isValidSignature(bytes32,bytes)", hash, signature) + ); + if (!success || result.length < 32 || abi.decode(result, (bytes4)) != bytes4(0x1626ba7e)) { + revert InvalidContractSignature(); + } + } + } +} diff --git a/test/mocks/Permit2Deployer.sol b/test/mocks/Permit2Deployer.sol deleted file mode 100644 index 8971ae7..0000000 --- a/test/mocks/Permit2Deployer.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.17; - -// Force compilation of the Permit2 contract so that `deployCodeTo("Permit2.sol:Permit2", ...)` -// can locate the artifact during tests. -import {Permit2} from "permit2/src/Permit2.sol"; From 85ac5dcbf625d58586f9515c0390b272f40ddd5e Mon Sep 17 00:00:00 2001 From: adnanhq Date: Mon, 16 Mar 2026 18:59:18 +0600 Subject: [PATCH 5/5] refactor: extract shared refund logic to helper function Reduces contract size across PaymentTreasury variants by removing duplicated refund logic. --- src/utils/BasePaymentTreasury.sol | 162 ++++++++---------------------- 1 file changed, 41 insertions(+), 121 deletions(-) diff --git a/src/utils/BasePaymentTreasury.sol b/src/utils/BasePaymentTreasury.sol index f8099bd..90d0189 100644 --- a/src/utils/BasePaymentTreasury.sol +++ b/src/utils/BasePaymentTreasury.sol @@ -1308,115 +1308,9 @@ abstract contract BasePaymentTreasury is revert PaymentTreasuryCryptoPayment(internalPaymentId); } - // Use snapshots of line item type configuration from payment creation time - // This prevents issues if line item type configuration changed after payment creation/confirmation - ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems = s_paymentLineItems[internalPaymentId]; - uint256 protocolFeePercent = INFO.getProtocolFeePercent(); - - // Calculate total line item refund amount using snapshots - uint256 totalGoalLineItemRefundAmount = 0; - uint256 totalNonGoalLineItemRefundAmount = 0; - - for (uint256 i = 0; i < lineItems.length; i++) { - ICampaignPaymentTreasury.PaymentLineItem memory item = lineItems[i]; - - // Use snapshot flags instead of current configuration - if (!item.canRefund) { - continue; // Skip non-refundable line items (based on snapshot at creation time) - } - - if (item.countsTowardGoal) { - // Goal line items: full amount is refundable from goal tracking - totalGoalLineItemRefundAmount += item.amount; - } else { - // Non-goal line items: handle fees and instant transfers - // For instant transfer items, the net amount was already sent to platform admin - don't refund - // For non-instant items, only refund the net amount (after fees), not the fees themselves - if (item.instantTransfer) { - // Skip instant transfer items - they were already sent to platform admin - continue; - } - - uint256 feeAmount = 0; - if (item.applyProtocolFee) { - feeAmount = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; - } - uint256 netAmount = item.amount - feeAmount; - - // Only refund the net amount (fees are not refundable) - totalNonGoalLineItemRefundAmount += netAmount; - } - } - - // Check that we have enough available balance for the total refund (BEFORE modifying state) - // Goal line items are in availableConfirmedPerToken, non-goal items need separate check - uint256 totalRefundAmount = amountToRefund + totalGoalLineItemRefundAmount + totalNonGoalLineItemRefundAmount; - - // For goal line items and base payment, check availableConfirmedPerToken - if (availablePaymentAmount < (amountToRefund + totalGoalLineItemRefundAmount)) { - revert PaymentTreasuryPaymentNotClaimable(paymentId); - } - - // For non-goal line items, check that we have enough claimable balance - // (only non-instant transfer items are refundable, and only their net amounts after fees) - if (totalNonGoalLineItemRefundAmount > 0) { - uint256 availableRefundable = s_refundableNonGoalLineItemPerToken[paymentToken]; - if (availableRefundable < totalNonGoalLineItemRefundAmount) { - revert PaymentTreasuryPaymentNotClaimable(paymentId); - } - } - - // Check that contract has enough actual balance to perform the transfer - uint256 contractBalance = IERC20(paymentToken).balanceOf(address(this)); - if (contractBalance < totalRefundAmount) { - revert PaymentTreasuryPaymentNotClaimable(paymentId); - } - - // Update state: remove tracking for refundable line items using snapshots - for (uint256 i = 0; i < lineItems.length; i++) { - ICampaignPaymentTreasury.PaymentLineItem memory item = lineItems[i]; - - // Use snapshot flags instead of current configuration - if (!item.canRefund) { - continue; // Skip non-refundable line items (based on snapshot at creation time) - } - - if (item.countsTowardGoal) { - // Goal line items: remove from goal tracking - s_confirmedPaymentPerToken[paymentToken] -= item.amount; - s_availableConfirmedPerToken[paymentToken] -= item.amount; - } else { - // Non-goal line items: remove from non-goal tracking - // Note: instantTransfer items are skipped in the refund calculation above - if (item.instantTransfer) { - // Instant transfer items were already sent to platform admin; nothing tracked - continue; - } - - // Calculate fees and net amount using snapshot - uint256 feeAmount = 0; - if (item.applyProtocolFee) { - feeAmount = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; - // Fees are NOT refunded - they remain in the protocol fee pool - } - - uint256 netAmount = item.amount - feeAmount; - - // Remove net amount from outstanding non-goal tracking - s_nonGoalLineItemConfirmedPerToken[paymentToken] -= netAmount; - - // Remove from refundable tracking (only net amount is refundable) - s_refundableNonGoalLineItemPerToken[paymentToken] -= netAmount; - } - } - - delete s_payment[internalPaymentId]; - delete s_paymentIdToToken[internalPaymentId]; - delete s_paymentLineItems[internalPaymentId]; - delete s_paymentExternalFeeMetadata[internalPaymentId]; - - s_confirmedPaymentPerToken[paymentToken] -= amountToRefund; - s_availableConfirmedPerToken[paymentToken] -= amountToRefund; + uint256 totalRefundAmount = _executeRefund( + internalPaymentId, paymentToken, amountToRefund, availablePaymentAmount, paymentId + ); IERC20(paymentToken).safeTransfer(refundAddress, totalRefundAmount); emit RefundClaimed(paymentId, totalRefundAmount, refundAddress); @@ -1453,6 +1347,39 @@ abstract contract BasePaymentTreasury is // Get NFT owner before burning address nftOwner = INFO.ownerOf(tokenId); + uint256 totalRefundAmount = _executeRefund( + internalPaymentId, paymentToken, amountToRefund, availablePaymentAmount, internalPaymentId + ); + + // Additional cleanup for NFT payments + delete s_paymentIdToTokenId[internalPaymentId]; + delete s_paymentIdToCreator[paymentId]; // Clean up creator mapping for on-chain payments + + // Burn NFT (requires treasury approval from owner) + INFO.burn(tokenId); + + IERC20(paymentToken).safeTransfer(nftOwner, totalRefundAmount); + emit RefundClaimed(paymentId, totalRefundAmount, nftOwner); + } + + /** + * @dev Shared refund logic for both claimRefund overloads. + * Calculates refund amounts from line item snapshots, validates balances, + * updates state, removes common storage entries, and returns the total refund amount. + * @param internalPaymentId The scoped internal payment ID. + * @param paymentToken The token used for the payment. + * @param amountToRefund The base payment amount to refund. + * @param availablePaymentAmount The available confirmed amount for this token. + * @param revertId The payment ID to use in revert messages (preserves original error context). + * @return totalRefundAmount The total amount to transfer to the refund recipient. + */ + function _executeRefund( + bytes32 internalPaymentId, + address paymentToken, + uint256 amountToRefund, + uint256 availablePaymentAmount, + bytes32 revertId + ) private returns (uint256 totalRefundAmount) { // Use snapshots of line item type configuration from payment creation time // This prevents issues if line item type configuration changed after payment creation/confirmation ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems = s_paymentLineItems[internalPaymentId]; @@ -1495,11 +1422,11 @@ abstract contract BasePaymentTreasury is // Check that we have enough available balance for the total refund (BEFORE modifying state) // Goal line items are in availableConfirmedPerToken, non-goal items need separate check - uint256 totalRefundAmount = amountToRefund + totalGoalLineItemRefundAmount + totalNonGoalLineItemRefundAmount; + totalRefundAmount = amountToRefund + totalGoalLineItemRefundAmount + totalNonGoalLineItemRefundAmount; // For goal line items and base payment, check availableConfirmedPerToken if (availablePaymentAmount < (amountToRefund + totalGoalLineItemRefundAmount)) { - revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + revert PaymentTreasuryPaymentNotClaimable(revertId); } // For non-goal line items, check that we have enough claimable balance @@ -1507,14 +1434,14 @@ abstract contract BasePaymentTreasury is if (totalNonGoalLineItemRefundAmount > 0) { uint256 availableRefundable = s_refundableNonGoalLineItemPerToken[paymentToken]; if (availableRefundable < totalNonGoalLineItemRefundAmount) { - revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + revert PaymentTreasuryPaymentNotClaimable(revertId); } } // Check that contract has enough actual balance to perform the transfer uint256 contractBalance = IERC20(paymentToken).balanceOf(address(this)); if (contractBalance < totalRefundAmount) { - revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + revert PaymentTreasuryPaymentNotClaimable(revertId); } // Update state: remove tracking for refundable line items using snapshots @@ -1555,21 +1482,14 @@ abstract contract BasePaymentTreasury is } } + // Clean up common storage entries delete s_payment[internalPaymentId]; delete s_paymentIdToToken[internalPaymentId]; delete s_paymentLineItems[internalPaymentId]; delete s_paymentExternalFeeMetadata[internalPaymentId]; - delete s_paymentIdToTokenId[internalPaymentId]; - delete s_paymentIdToCreator[paymentId]; // Clean up creator mapping for on-chain payments s_confirmedPaymentPerToken[paymentToken] -= amountToRefund; s_availableConfirmedPerToken[paymentToken] -= amountToRefund; - - // Burn NFT (requires treasury approval from owner) - INFO.burn(tokenId); - - IERC20(paymentToken).safeTransfer(nftOwner, totalRefundAmount); - emit RefundClaimed(paymentId, totalRefundAmount, nftOwner); } /**