Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/CampaignInfo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@ contract CampaignInfo is
bufferTime = uint256(valueBytes);
}

/**
* @inheritdoc ICampaignInfo
*/
function getPermit2Address() external view override returns (address) {
return _getGlobalParams().getPermit2Address();
}

/**
* @inheritdoc ICampaignInfo
*/
Expand Down
10 changes: 10 additions & 0 deletions src/GlobalParams.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
*/
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/ICampaignInfo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 11 additions & 4 deletions src/interfaces/ICampaignPaymentTreasury.sol
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -137,7 +143,8 @@ interface ICampaignPaymentTreasury {
address paymentToken,
uint256 amount,
LineItem[] calldata lineItems,
ExternalFees[] calldata externalFees
ExternalFees[] calldata externalFees,
PermitData calldata permitData
) external;

/**
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/IGlobalParams.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
119 changes: 119 additions & 0 deletions src/interfaces/IPermit2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

// ============================================================================
// 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 ISignatureTransfer so that existing import paths work unchanged.
* @dev The canonical Permit2 deployment address is
* 0x000000000022D473030F116dDEE9F6B43aC78BA3 across all supported EVM chains.
*/
interface IPermit2 is ISignatureTransfer {}

/**
* @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.
*/
struct PermitData {
uint256 nonce;
uint256 deadline;
bytes signature;
}
98 changes: 85 additions & 13 deletions src/treasuries/AllOrNothing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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, ISignatureTransfer, PermitData} from "../interfaces/IPermit2.sol";

/**
* @title AllOrNothing
Expand All @@ -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.
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -400,12 +434,16 @@ 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)) {
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
Expand All @@ -424,7 +462,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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Check Permit2 code exists before processing pledges

_pledge unconditionally calls permitWitnessTransferFrom on INFO.getPermit2Address() without verifying that address has deployed code. On local/dev/unsupported chains where the canonical Permit2 address is empty, this external call can return success as a no-op, so execution continues and the contract mints a pledge NFT and updates raised totals without actually transferring tokens. Please gate this path with a code.length (or equivalent deployment) check before relying on Permit2 transfers.

Useful? React with 👍 / 👎.

ISignatureTransfer.PermitTransferFrom({
permitted: ISignatureTransfer.TokenPermissions({token: pledgeToken, amount: totalAmount}),
nonce: permitData.nonce,
deadline: permitData.deadline
}),
ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: totalAmount}),
backer,
witness,
witnessTypeString,
permitData.signature
);

uint256 tokenId = INFO.mintNFTForPledge(
backer, reward, pledgeToken, pledgeAmountInTokenDecimals, shippingFeeInTokenDecimals, 0
Expand Down
Loading
Loading