From 4a5a145f59bc1fbd223c95218cbe4a6f69887c7d Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 13 Jan 2023 12:06:47 -0700 Subject: [PATCH] add 1155 directory and rename --- src/ERC1155/AllowanceTransferERC1155.sol | 140 +++++++++++++++ src/ERC1155/EIP712ForERC1155.sol | 39 +++++ src/ERC1155/Permit2.sol | 11 ++ src/ERC1155/SignatureTransferERC1155.sol | 154 +++++++++++++++++ .../interfaces/IAllowanceTransferERC1155.sol | 160 ++++++++++++++++++ .../interfaces/ISignatureTransferERC1155.sol | 132 +++++++++++++++ src/ERC1155/libraries/AllowanceERC1155.sol | 48 ++++++ src/ERC1155/libraries/PermitHashERC1155.sol | 134 +++++++++++++++ 8 files changed, 818 insertions(+) create mode 100644 src/ERC1155/AllowanceTransferERC1155.sol create mode 100644 src/ERC1155/EIP712ForERC1155.sol create mode 100644 src/ERC1155/Permit2.sol create mode 100644 src/ERC1155/SignatureTransferERC1155.sol create mode 100644 src/ERC1155/interfaces/IAllowanceTransferERC1155.sol create mode 100644 src/ERC1155/interfaces/ISignatureTransferERC1155.sol create mode 100644 src/ERC1155/libraries/AllowanceERC1155.sol create mode 100644 src/ERC1155/libraries/PermitHashERC1155.sol diff --git a/src/ERC1155/AllowanceTransferERC1155.sol b/src/ERC1155/AllowanceTransferERC1155.sol new file mode 100644 index 00000000..fa501c03 --- /dev/null +++ b/src/ERC1155/AllowanceTransferERC1155.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {PermitHashERC1155} from "./libraries/PermitHashERC1155.sol"; +import {SignatureVerification} from "../shared/SignatureVerification.sol"; +import {EIP712ForERC1155} from "./EIP712ForERC1155.sol"; +import {IAllowanceTransferERC1155} from "./interfaces/IAllowanceTransferERC1155.sol"; +import {SignatureExpired, InvalidNonce} from "../shared/PermitErrors.sol"; +import {AllowanceERC1155} from "./libraries/AllowanceERC1155.sol"; + +contract AllowanceTransferERC1155 is IAllowanceTransferERC1155, EIP712ForERC1155 { + using SignatureVerification for bytes; + using PermitHashERC1155 for PermitSingle; + using PermitHashERC1155 for PermitBatch; + using AllowanceERC1155 for PackedAllowance; + + /// @notice Maps users to tokens to spender addresses and information about the approval on the token + /// @dev Indexed in the order of token owner address, token address, spender address + /// @dev The stored word saves the allowed amount, expiration on the allowance, and nonce + mapping(address => mapping(address => mapping(address => PackedAllowance))) public allowance; + + /// @inheritdoc IAllowanceTransferERC1155 + function approve(address token, address spender, uint160 amount, uint48 expiration) external { + PackedAllowance storage allowed = allowance[msg.sender][token][spender]; + allowed.updateAmountAndExpiration(amount, expiration); + emit Approval(msg.sender, token, spender, amount, expiration); + } + + /// @inheritdoc IAllowanceTransferERC1155 + function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external { + if (block.timestamp > permitSingle.sigDeadline) revert SignatureExpired(permitSingle.sigDeadline); + + // Verify the signer address from the signature. + signature.verify(_hashTypedData(permitSingle.hash()), owner); + + _updateApproval(permitSingle.details, owner, permitSingle.spender); + } + + /// @inheritdoc IAllowanceTransferERC1155 + function permit(address owner, PermitBatch memory permitBatch, bytes calldata signature) external { + if (block.timestamp > permitBatch.sigDeadline) revert SignatureExpired(permitBatch.sigDeadline); + + // Verify the signer address from the signature. + signature.verify(_hashTypedData(permitBatch.hash()), owner); + + address spender = permitBatch.spender; + unchecked { + uint256 length = permitBatch.details.length; + for (uint256 i = 0; i < length; ++i) { + _updateApproval(permitBatch.details[i], owner, spender); + } + } + } + + /// @inheritdoc IAllowanceTransferERC1155 + function transferFrom(address from, address to, uint160 amount, address token) external { + _transfer(from, to, amount, token); + } + + /// @inheritdoc IAllowanceTransferERC1155 + function transferFrom(AllowanceTransferDetails[] calldata transferDetails) external { + unchecked { + uint256 length = transferDetails.length; + for (uint256 i = 0; i < length; ++i) { + AllowanceTransferDetails memory transferDetail = transferDetails[i]; + _transfer(transferDetail.from, transferDetail.to, transferDetail.amount, transferDetail.token); + } + } + } + + /// @notice Internal function for transferring tokens using stored allowances + /// @dev Will fail if the allowed timeframe has passed + function _transfer(address from, address to, uint160 amount, address token) private { + PackedAllowance storage allowed = allowance[from][token][msg.sender]; + + if (block.timestamp > allowed.expiration) revert AllowanceExpired(allowed.expiration); + + uint256 maxAmount = allowed.amount; + if (maxAmount != type(uint160).max) { + if (amount > maxAmount) { + revert InsufficientAllowance(maxAmount); + } else { + unchecked { + allowed.amount = uint160(maxAmount) - amount; + } + } + } + + // Transfer the tokens from the from address to the recipient. + ERC20(token).safeTransferFrom(from, to, amount); + } + + /// @inheritdoc IAllowanceTransferERC1155 + function lockdown(TokenSpenderPair[] calldata approvals) external { + address owner = msg.sender; + // Revoke allowances for each pair of spenders and tokens. + unchecked { + uint256 length = approvals.length; + for (uint256 i = 0; i < length; ++i) { + address token = approvals[i].token; + address spender = approvals[i].spender; + + allowance[owner][token][spender].amount = 0; + emit Lockdown(owner, token, spender); + } + } + } + + /// @inheritdoc IAllowanceTransferERC1155 + function invalidateNonces(address token, address spender, uint48 newNonce) external { + uint48 oldNonce = allowance[msg.sender][token][spender].nonce; + + if (newNonce <= oldNonce) revert InvalidNonce(); + + // Limit the amount of nonces that can be invalidated in one transaction. + unchecked { + uint48 delta = newNonce - oldNonce; + if (delta > type(uint16).max) revert ExcessiveInvalidation(); + } + + allowance[msg.sender][token][spender].nonce = newNonce; + emit NonceInvalidation(msg.sender, token, spender, newNonce, oldNonce); + } + + /// @notice Sets the new values for amount, expiration, and nonce. + /// @dev Will check that the signed nonce is equal to the current nonce and then incrememnt the nonce value by 1. + /// @dev Emits a Permit event. + function _updateApproval(PermitDetails memory details, address owner, address spender) private { + uint48 nonce = details.nonce; + address token = details.token; + uint160 amount = details.amount; + uint48 expiration = details.expiration; + PackedAllowance storage allowed = allowance[owner][token][spender]; + + if (allowed.nonce != nonce) revert InvalidNonce(); + + allowed.updateAll(amount, expiration, nonce); + emit Permit(owner, token, spender, amount, expiration, nonce); + } +} diff --git a/src/ERC1155/EIP712ForERC1155.sol b/src/ERC1155/EIP712ForERC1155.sol new file mode 100644 index 00000000..93c26f9d --- /dev/null +++ b/src/ERC1155/EIP712ForERC1155.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +/// @notice EIP712 helpers for Permit2 ERC1155s +/// @dev Maintains cross-chain replay protection in the event of a fork +/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol +contract EIP712ForERC1155 { + // Cache the domain separator as an immutable value, but also store the chain id that it + // corresponds to, in order to invalidate the cached domain separator if the chain id changes. + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; + uint256 private immutable _CACHED_CHAIN_ID; + + bytes32 private constant _HASHED_NAME = keccak256("Permit2ERC1155"); + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + constructor() { + _CACHED_CHAIN_ID = block.chainid; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME); + } + + /// @notice Returns the domain separator for the current chain. + /// @dev Uses cached version if chainid and address are unchanged from construction. + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return block.chainid == _CACHED_CHAIN_ID + ? _CACHED_DOMAIN_SEPARATOR + : _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME); + } + + /// @notice Builds a domain separator using the current chainId and contract address. + function _buildDomainSeparator(bytes32 typeHash, bytes32 nameHash) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, block.chainid, address(this))); + } + + /// @notice Creates an EIP-712 typed data hash + function _hashTypedData(bytes32 dataHash) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), dataHash)); + } +} diff --git a/src/ERC1155/Permit2.sol b/src/ERC1155/Permit2.sol new file mode 100644 index 00000000..b78bf6b7 --- /dev/null +++ b/src/ERC1155/Permit2.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {SignatureTransferERC1155} from "./SignatureTransferERC1155.sol"; +import {AllowanceTransferERC1155} from "./AllowanceTransferERC1155.sol"; + +/// @notice Permit2 handles signature-based transfers in SignatureTransfer and allowance-based transfers in AllowanceTransfer. +/// @dev Users must approve Permit2 before calling any of the transfer functions. +contract Permit2ERC1155 is SignatureTransferERC1155, AllowanceTransferERC1155 { +// Permit2 unifies the two contracts so users have maximal flexibility with their approval. +} diff --git a/src/ERC1155/SignatureTransferERC1155.sol b/src/ERC1155/SignatureTransferERC1155.sol new file mode 100644 index 00000000..46512ee7 --- /dev/null +++ b/src/ERC1155/SignatureTransferERC1155.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {ISignatureTransferERC1155} from "./interfaces/ISignatureTransferERC1155.sol"; +import {SignatureExpired, InvalidNonce} from "../shared/PermitErrors.sol"; +import {SignatureVerification} from "../shared/SignatureVerification.sol"; +import {PermitHashERC1155} from "./libraries/PermitHashERC1155.sol"; +import {EIP712ForERC1155} from "./EIP712ForERC1155.sol"; + +contract SignatureTransferERC1155 is ISignatureTransferERC1155, EIP712ForERC1155 { + using SignatureVerification for bytes; + using PermitHashERC1155 for PermitTransferFrom; + using PermitHashERC1155 for PermitBatchTransferFrom; + + /// @inheritdoc ISignatureTransferERC115 + mapping(address => mapping(uint256 => uint256)) public nonceBitmap; + + /// @inheritdoc ISignatureTransferERC1155 + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external { + _permitTransferFrom(permit, transferDetails, owner, permit.hash(), signature); + } + + /// @inheritdoc ISignatureTransferERC1155 + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external { + _permitTransferFrom( + permit, transferDetails, owner, permit.hashWithWitness(witness, witnessTypeString), signature + ); + } + + /// @notice Transfers a token using a signed permit message. + /// @param permit The permit data signed over by the owner + /// @param dataHash The EIP-712 hash of permit data to include when checking signature + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param signature The signature to verify + 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); + + signature.verify(_hashTypedData(dataHash), owner); + + ERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount); + } + + /// @inheritdoc ISignatureTransferERC1155 + function permitTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes calldata signature + ) external { + _permitTransferFrom(permit, transferDetails, owner, permit.hash(), signature); + } + + /// @inheritdoc ISignatureTransferERC1155 + function permitWitnessTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external { + _permitTransferFrom( + permit, transferDetails, owner, permit.hashWithWitness(witness, witnessTypeString), signature + ); + } + + /// @notice Transfers tokens using a signed permit messages + /// @param permit The permit data signed over by the owner + /// @param dataHash The EIP-712 hash of permit data to include when checking signature + /// @param owner The owner of the tokens to transfer + /// @param signature The signature to verify + function _permitTransferFrom( + 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); + signature.verify(_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) { + // allow spender to specify which of the permitted tokens should be transferred + ERC20(permitted.token).safeTransferFrom(owner, transferDetails[i].to, requestedAmount); + } + } + } + } + + /// @inheritdoc ISignatureTransferERC1155 + function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external { + nonceBitmap[msg.sender][wordPos] |= mask; + + emit UnorderedNonceInvalidation(msg.sender, wordPos, mask); + } + + /// @notice Returns the index of the bitmap and the bit position within the bitmap. Used for unordered nonces + /// @param nonce The nonce to get the associated word and bit positions + /// @return wordPos The word position or index into the nonceBitmap + /// @return bitPos The bit position + /// @dev The first 248 bits of the nonce value is the index of the desired bitmap + /// @dev The last 8 bits of the nonce value is the position of the bit in the bitmap + function bitmapPositions(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) { + wordPos = uint248(nonce >> 8); + bitPos = uint8(nonce); + } + + /// @notice Checks whether a nonce is taken and sets the bit at the bit position in the bitmap at the word position + /// @param from The address to use the nonce at + /// @param nonce The nonce to spend + function _useUnorderedNonce(address from, uint256 nonce) internal { + (uint256 wordPos, uint256 bitPos) = bitmapPositions(nonce); + uint256 bit = 1 << bitPos; + uint256 flipped = nonceBitmap[from][wordPos] ^= bit; + + if (flipped & bit == 0) revert InvalidNonce(); + } +} diff --git a/src/ERC1155/interfaces/IAllowanceTransferERC1155.sol b/src/ERC1155/interfaces/IAllowanceTransferERC1155.sol new file mode 100644 index 00000000..d3752809 --- /dev/null +++ b/src/ERC1155/interfaces/IAllowanceTransferERC1155.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @title AllowanceTransfer +/// @notice Handles ERC1155 token permissions through signature based allowance setting and ERC20 token transfers by checking allowed amounts +/// @dev Requires user's token approval on the Permit2 contract +interface IAllowanceTransferERC1155 { + /// @notice Thrown when an allowance on a token has expired. + /// @param deadline The timestamp at which the allowed amount is no longer valid + error AllowanceExpired(uint256 deadline); + + /// @notice Thrown when an allowance on a token has been depleted. + /// @param amount The maximum amount allowed + error InsufficientAllowance(uint256 amount); + + /// @notice Thrown when too many nonces are invalidated. + error ExcessiveInvalidation(); + + /// @notice Emits an event when the owner successfully invalidates an ordered nonce. + event NonceInvalidation( + address indexed owner, address indexed token, address indexed spender, uint48 newNonce, uint48 oldNonce + ); + + /// @notice Emits an event when the owner successfully sets permissions on a token for the spender. + event Approval( + address indexed owner, address indexed token, address indexed spender, uint160 amount, uint48 expiration + ); + + /// @notice Emits an event when the owner successfully sets permissions using a permit signature on a token for the spender. + event Permit( + address indexed owner, + address indexed token, + address indexed spender, + uint160 amount, + uint48 expiration, + uint48 nonce + ); + + /// @notice Emits an event when the owner sets the allowance back to 0 with the lockdown function. + event Lockdown(address indexed owner, address token, address spender); + + /// @notice The permit data for a token + struct PermitDetails { + // ERC20 token address + address token; + // the maximum amount allowed to spend + uint160 amount; + // timestamp at which a spender's token allowances become invalid + uint48 expiration; + // an incrementing value indexed per owner,token,and spender for each signature + uint48 nonce; + } + + /// @notice The permit message signed for a single token allownce + struct PermitSingle { + // the permit data for a single token alownce + PermitDetails details; + // address permissioned on the allowed tokens + address spender; + // deadline on the permit signature + uint256 sigDeadline; + } + + /// @notice The permit message signed for multiple token allowances + struct PermitBatch { + // the permit data for multiple token allowances + PermitDetails[] details; + // address permissioned on the allowed tokens + address spender; + // deadline on the permit signature + uint256 sigDeadline; + } + + /// @notice The saved permissions + /// @dev This info is saved per owner, per token, per spender and all signed over in the permit message + /// @dev Setting amount to type(uint160).max sets an unlimited approval + struct PackedAllowance { + // amount allowed + uint160 amount; + // permission expiry + uint48 expiration; + // an incrementing value indexed per owner,token,and spender for each signature + uint48 nonce; + } + + /// @notice A token spender pair. + struct TokenSpenderPair { + // the token the spender is approved + address token; + // the spender address + address spender; + } + + /// @notice Details for a token transfer. + struct AllowanceTransferDetails { + // the owner of the token + address from; + // the recipient of the token + address to; + // the amount of the token + uint160 amount; + // the token to be transferred + address token; + } + + /// @notice A mapping from owner address to token address to spender address to PackedAllowance struct, which contains details and conditions of the approval. + /// @notice The mapping is indexed in the above order see: allowance[ownerAddress][tokenAddress][spenderAddress] + /// @dev The packed slot holds the allowed amount, expiration at which the allowed amount is no longer valid, and current nonce thats updated on any signature based approvals. + function allowance(address, address, address) external view returns (uint160, uint48, uint48); + + /// @notice Approves the spender to use up to amount of the specified token up until the expiration + /// @param token The token to approve + /// @param spender The spender address to approve + /// @param amount The approved amount of the token + /// @param expiration The timestamp at which the approval is no longer valid + /// @dev The packed allowance also holds a nonce, which will stay unchanged in approve + /// @dev Setting amount to type(uint160).max sets an unlimited approval + function approve(address token, address spender, uint160 amount, uint48 expiration) external; + + /// @notice Permit a spender to a given amount of the owners token via the owner's EIP-712 signature + /// @dev May fail if the owner's nonce was invalidated in-flight by invalidateNonce + /// @param owner The owner of the tokens being approved + /// @param permitSingle Data signed over by the owner specifying the terms of approval + /// @param signature The owner's signature over the permit data + function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external; + + /// @notice Permit a spender to the signed amounts of the owners tokens via the owner's EIP-712 signature + /// @dev May fail if the owner's nonce was invalidated in-flight by invalidateNonce + /// @param owner The owner of the tokens being approved + /// @param permitBatch Data signed over by the owner specifying the terms of approval + /// @param signature The owner's signature over the permit data + function permit(address owner, PermitBatch memory permitBatch, bytes calldata signature) external; + + /// @notice Transfer approved tokens from one address to another + /// @param from The address to transfer from + /// @param to The address of the recipient + /// @param amount The amount of the token to transfer + /// @param token The token address to transfer + /// @dev Requires the from address to have approved at least the desired amount + /// of tokens to msg.sender. + function transferFrom(address from, address to, uint160 amount, address token) external; + + /// @notice Transfer approved tokens in a batch + /// @param transferDetails Array of owners, recipients, amounts, and tokens for the transfers + /// @dev Requires the from addresses to have approved at least the desired amount + /// of tokens to msg.sender. + function transferFrom(AllowanceTransferDetails[] calldata transferDetails) external; + + /// @notice Enables performing a "lockdown" of the sender's Permit2 identity + /// by batch revoking approvals + /// @param approvals Array of approvals to revoke. + function lockdown(TokenSpenderPair[] calldata approvals) external; + + /// @notice Invalidate nonces for a given (token, spender) pair + /// @param token The token to invalidate nonces for + /// @param spender The spender to invalidate nonces for + /// @param newNonce The new nonce to set. Invalidates all nonces less than it. + /// @dev Can't invalidate more than 2**16 nonces per transaction. + function invalidateNonces(address token, address spender, uint48 newNonce) external; +} diff --git a/src/ERC1155/interfaces/ISignatureTransferERC1155.sol b/src/ERC1155/interfaces/ISignatureTransferERC1155.sol new file mode 100644 index 00000000..6a527c4b --- /dev/null +++ b/src/ERC1155/interfaces/ISignatureTransferERC1155.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @title SignatureTransfer +/// @notice Handles ERC1155 token transfers through signature based actions +/// @dev Requires user's token approval on the Permit2 contract +interface ISignatureTransferERC1155 { + /// @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 + /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be 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 { + // ERC20 token address + address token; + // the maximum amount that can be spent + uint256 amount; + } + + /// @notice The signed permit message for a single token transfer + struct PermitTransferFrom { + TokenPermissions permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice Specifies the recipient address and amount for batched transfers. + /// @dev Recipients and amounts correspond to the index of the signed token permissions array. + /// @dev Reverts if the requested amount is greater than the permitted signed amount. + struct SignatureTransferDetails { + // recipient address + address to; + // spender requested amount + uint256 requestedAmount; + } + + /// @notice Used to reconstruct the signed permit message for multiple token transfers + /// @dev Do not need to pass in spender address as it is required that it is msg.sender + /// @dev Note that a user still signs over a spender address + struct PermitBatchTransferFrom { + // the tokens and corresponding amounts permitted for a transfer + TokenPermissions[] permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection + /// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order + /// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce + /// @dev It returns a uint256 bitmap + /// @dev The index, or wordPosition is capped at type(uint248).max + function nonceBitmap(address, uint256) external view returns (uint256); + + /// @notice Transfers a token using a signed permit message + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param signature The signature to verify + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers a token using a signed permit message + /// @notice Includes extra data provided by the caller to verify signature over + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + 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 + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param signature The signature to verify + function permitTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @notice Includes extra data provided by the caller to verify signature over + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + 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 + /// @dev The wordPos is maxed at type(uint248).max + /// @param wordPos A number to index the nonceBitmap at + /// @param mask A bitmap masked against msg.sender's current bitmap at the word position + function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; +} diff --git a/src/ERC1155/libraries/AllowanceERC1155.sol b/src/ERC1155/libraries/AllowanceERC1155.sol new file mode 100644 index 00000000..8552aea7 --- /dev/null +++ b/src/ERC1155/libraries/AllowanceERC1155.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IAllowanceTransferERC1155} from "../interfaces/IAllowanceTransferERC1155.sol"; + +library AllowanceERC1155 { + // note if the expiration passed is 0, then it the approval set to the block.timestamp + uint256 private constant BLOCK_TIMESTAMP_EXPIRATION = 0; + + /// @notice Sets the allowed amount, expiry, and nonce of the spender's permissions on owner's token. + /// @dev Nonce is incremented. + /// @dev If the inputted expiration is 0, the stored expiration is set to block.timestamp + function updateAll( + IAllowanceTransferERC1155.PackedAllowance storage allowed, + uint160 amount, + uint48 expiration, + uint48 nonce + ) internal { + uint48 storedNonce; + unchecked { + storedNonce = nonce + 1; + } + + uint48 storedExpiration = expiration == BLOCK_TIMESTAMP_EXPIRATION ? uint48(block.timestamp) : expiration; + + uint256 word = pack(amount, storedExpiration, storedNonce); + assembly { + sstore(allowed.slot, word) + } + } + + /// @notice Sets the allowed amount and expiry of the spender's permissions on owner's token. + /// @dev Nonce does not need to be incremented. + function updateAmountAndExpiration( + IAllowanceTransferERC1155.PackedAllowance storage allowed, + uint160 amount, + uint48 expiration + ) internal { + // If the inputted expiration is 0, the allowance only lasts the duration of the block. + allowed.expiration = expiration == 0 ? uint48(block.timestamp) : expiration; + allowed.amount = amount; + } + + /// @notice Computes the packed slot of the amount, expiration, and nonce that make up PackedAllowance + function pack(uint160 amount, uint48 expiration, uint48 nonce) internal pure returns (uint256 word) { + word = (uint256(nonce) << 208) | uint256(expiration) << 160 | amount; + } +} diff --git a/src/ERC1155/libraries/PermitHashERC1155.sol b/src/ERC1155/libraries/PermitHashERC1155.sol new file mode 100644 index 00000000..d0d9cff2 --- /dev/null +++ b/src/ERC1155/libraries/PermitHashERC1155.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IAllowanceTransferERC1155} from "../interfaces/IAllowanceTransferERC1155.sol"; +import {ISignatureTransferERC1155} from "../interfaces/ISignatureTransferERC1155.sol"; + +library PermitHashERC1155 { + bytes32 public constant _PERMIT_DETAILS_TYPEHASH = + keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"); + + bytes32 public constant _PERMIT_SINGLE_TYPEHASH = keccak256( + "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + + bytes32 public constant _PERMIT_BATCH_TYPEHASH = keccak256( + "PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + + bytes32 public constant _TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + + bytes32 public constant _PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + bytes32 public constant _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + string public constant _TOKEN_PERMISSIONS_TYPESTRING = "TokenPermissions(address token,uint256 amount)"; + + string public constant _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB = + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; + + string public constant _PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB = + "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,"; + + function hash(IAllowanceTransferERC1155.PermitSingle memory permitSingle) internal pure returns (bytes32) { + bytes32 permitHash = _hashPermitDetails(permitSingle.details); + return + keccak256(abi.encode(_PERMIT_SINGLE_TYPEHASH, permitHash, permitSingle.spender, permitSingle.sigDeadline)); + } + + function hash(IAllowanceTransferERC1155.PermitBatch memory permitBatch) internal pure returns (bytes32) { + uint256 numPermits = permitBatch.details.length; + bytes32[] memory permitHashes = new bytes32[](numPermits); + for (uint256 i = 0; i < numPermits; ++i) { + permitHashes[i] = _hashPermitDetails(permitBatch.details[i]); + } + return keccak256( + abi.encode( + _PERMIT_BATCH_TYPEHASH, + keccak256(abi.encodePacked(permitHashes)), + permitBatch.spender, + permitBatch.sigDeadline + ) + ); + } + + function hash(ISignatureTransferERC1155.PermitTransferFrom memory permit) internal 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(ISignatureTransferERC1155.PermitBatchTransferFrom memory permit) internal 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( + ISignatureTransferERC1155.PermitTransferFrom memory permit, + bytes32 witness, + string calldata witnessTypeString + ) internal 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( + ISignatureTransferERC1155.PermitBatchTransferFrom memory permit, + bytes32 witness, + string calldata witnessTypeString + ) internal 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 + ) + ); + } + + function _hashPermitDetails(IAllowanceTransferERC1155.PermitDetails memory details) private pure returns (bytes32) { + return keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, details)); + } + + function _hashTokenPermissions(ISignatureTransferERC1155.TokenPermissions memory permitted) + private + pure + returns (bytes32) + { + return keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, permitted)); + } +}