From 3358152658231016f9a08b64bb996cad5faad0d0 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 13 Jan 2023 12:06:47 -0700 Subject: [PATCH 1/3] 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)); + } +} From f6b73f7569e89f0ae83eec702d4cc07be82119b1 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 13 Jan 2023 14:39:34 -0700 Subject: [PATCH 2/3] erc1155 changes --- src/ERC1155/AllowanceTransferERC1155.sol | 105 ++++++++++++++---- src/ERC1155/SignatureTransferERC1155.sol | 13 ++- .../interfaces/IAllowanceTransferERC1155.sol | 80 +++++++++++-- .../interfaces/ISignatureTransferERC1155.sol | 2 + src/ERC1155/libraries/AllowanceERC1155.sol | 2 +- src/ERC1155/libraries/PermitHashERC1155.sol | 8 +- 6 files changed, 169 insertions(+), 41 deletions(-) diff --git a/src/ERC1155/AllowanceTransferERC1155.sol b/src/ERC1155/AllowanceTransferERC1155.sol index fa501c03..631e740a 100644 --- a/src/ERC1155/AllowanceTransferERC1155.sol +++ b/src/ERC1155/AllowanceTransferERC1155.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.17; import {PermitHashERC1155} from "./libraries/PermitHashERC1155.sol"; +import {ERC1155} from "solmate/src/tokens/ERC1155.sol"; import {SignatureVerification} from "../shared/SignatureVerification.sol"; import {EIP712ForERC1155} from "./EIP712ForERC1155.sol"; import {IAllowanceTransferERC1155} from "./interfaces/IAllowanceTransferERC1155.sol"; @@ -15,15 +16,26 @@ contract AllowanceTransferERC1155 is IAllowanceTransferERC1155, EIP712ForERC1155 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; + /// @dev Indexed in the order of token owner address, token address, spender address, tokenId + /// @dev The stored word saves the allowed amount of the tokenId, expiration on the allowance, and nonce + mapping(address => mapping(address => mapping(address => mapping(uint256 => PackedAllowance)))) public allowance; + + /// @notice Maps users to tokens to spender and sets whether or not the spender has operator status on an entire token collection. + /// @dev Indexed in the order of token owner address, token address, then spender address. + /// @dev Sets a timestamp at which the spender no longer has operator status. Max expiration is type(uint48).max + mapping(address => mapping(address => mapping(address => PackedOperatorAllowance))) public operators; /// @inheritdoc IAllowanceTransferERC1155 - function approve(address token, address spender, uint160 amount, uint48 expiration) external { - PackedAllowance storage allowed = allowance[msg.sender][token][spender]; + function approve(address token, address spender, uint160 amount, uint256 tokenId, uint48 expiration) external { + PackedAllowance storage allowed = allowance[msg.sender][token][spender][tokenId]; allowed.updateAmountAndExpiration(amount, expiration); - emit Approval(msg.sender, token, spender, amount, expiration); + emit Approval(msg.sender, token, spender, tokenId, amount, expiration); + } + + /// @inheritdoc IAllowanceTransferERC1155 + function setApprovalForAll(address token, address spender, uint48 expiration) external { + operators[msg.sender][token][spender].expiration = expiration; + emit ApprovalForAll(msg.sender, token, spender, expiration); } /// @inheritdoc IAllowanceTransferERC1155 @@ -53,8 +65,8 @@ contract AllowanceTransferERC1155 is IAllowanceTransferERC1155, EIP712ForERC1155 } /// @inheritdoc IAllowanceTransferERC1155 - function transferFrom(address from, address to, uint160 amount, address token) external { - _transfer(from, to, amount, token); + function transferFrom(address from, address to, uint256 tokenId, uint160 amount, address token) external { + _transfer(from, to, tokenId, amount, token); } /// @inheritdoc IAllowanceTransferERC1155 @@ -63,22 +75,37 @@ contract AllowanceTransferERC1155 is IAllowanceTransferERC1155, EIP712ForERC1155 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); + _transfer( + transferDetail.from, + transferDetail.to, + transferDetail.tokenId, + 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]; + function _transfer(address from, address to, uint256 tokenId, uint160 amount, address token) private { + PackedAllowance storage allowed = allowance[from][token][msg.sender][tokenId]; + + PackedOperatorAllowance storage operator = operators[from][token][msg.sender]; + bool operatorExpired = block.timestamp > operator.expiration; - if (block.timestamp > allowed.expiration) revert AllowanceExpired(allowed.expiration); + // At least one of the approval methods must not be expired. + if (block.timestamp > allowed.expiration && operatorExpired) { + revert AllowanceExpired(allowed.expiration, operator.expiration); + } uint256 maxAmount = allowed.amount; if (maxAmount != type(uint160).max) { if (amount > maxAmount) { - revert InsufficientAllowance(maxAmount); + // There is not a valid approval on the allowance mapping. + // However, only revert if there is also not a valid approval on the operator mapping. + // Otherwise, the spender is an operator & can transfer any amount of any tokenId in the collection. + if (operatorExpired) revert InsufficientAllowance(maxAmount); } else { unchecked { allowed.amount = uint160(maxAmount) - amount; @@ -87,28 +114,58 @@ contract AllowanceTransferERC1155 is IAllowanceTransferERC1155, EIP712ForERC1155 } // Transfer the tokens from the from address to the recipient. - ERC20(token).safeTransferFrom(from, to, amount); + ERC1155(token).safeTransferFrom(from, to, tokenId, amount, ""); } /// @inheritdoc IAllowanceTransferERC1155 - function lockdown(TokenSpenderPair[] calldata approvals) external { + function lockdown(TokenSpenderPair[] calldata operatorApprovals, TokenSpenderTokenId[] calldata tokenIdApprovals) + external + { address owner = msg.sender; - // Revoke allowances for each pair of spenders and tokens. + unchecked { - uint256 length = approvals.length; + // Revoke operator allowances for each pair of spenders and tokens. + uint256 length = operatorApprovals.length; for (uint256 i = 0; i < length; ++i) { - address token = approvals[i].token; - address spender = approvals[i].spender; + address token = operatorApprovals[i].token; + address spender = operatorApprovals[i].spender; - allowance[owner][token][spender].amount = 0; + operators[owner][token][spender].expiration = 0; emit Lockdown(owner, token, spender); } } + + unchecked { + // Revoke tokenId allowances for each tuple of token, spender, and tokenId. + uint256 length = tokenIdApprovals.length; + for (uint256 i = 0; i < length; i++) { + address token = tokenIdApprovals[i].token; + address spender = tokenIdApprovals[i].spender; + uint256 tokenId = tokenIdApprovals[i].tokenId; + allowance[owner][token][spender][tokenId].amount = 0; + } + } + } + + /// @inheritdoc IAllowanceTransferERC1155 + function invalidateNonces(address token, address spender, uint256 tokenId, uint48 newNonce) external { + uint48 oldNonce = allowance[msg.sender][token][spender][tokenId].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][tokenId].nonce = newNonce; + emit NonceInvalidation(msg.sender, token, spender, tokenId, newNonce, oldNonce); } /// @inheritdoc IAllowanceTransferERC1155 function invalidateNonces(address token, address spender, uint48 newNonce) external { - uint48 oldNonce = allowance[msg.sender][token][spender].nonce; + uint48 oldNonce = operators[msg.sender][token][spender].nonce; if (newNonce <= oldNonce) revert InvalidNonce(); @@ -118,7 +175,7 @@ contract AllowanceTransferERC1155 is IAllowanceTransferERC1155, EIP712ForERC1155 if (delta > type(uint16).max) revert ExcessiveInvalidation(); } - allowance[msg.sender][token][spender].nonce = newNonce; + operators[msg.sender][token][spender].nonce = newNonce; emit NonceInvalidation(msg.sender, token, spender, newNonce, oldNonce); } @@ -129,8 +186,10 @@ contract AllowanceTransferERC1155 is IAllowanceTransferERC1155, EIP712ForERC1155 uint48 nonce = details.nonce; address token = details.token; uint160 amount = details.amount; + uint256 tokenId = details.tokenId; uint48 expiration = details.expiration; - PackedAllowance storage allowed = allowance[owner][token][spender]; + + PackedAllowance storage allowed = allowance[owner][token][spender][tokenId]; if (allowed.nonce != nonce) revert InvalidNonce(); diff --git a/src/ERC1155/SignatureTransferERC1155.sol b/src/ERC1155/SignatureTransferERC1155.sol index 46512ee7..f5fa00d5 100644 --- a/src/ERC1155/SignatureTransferERC1155.sol +++ b/src/ERC1155/SignatureTransferERC1155.sol @@ -1,18 +1,19 @@ // 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"; +import {ISignatureTransferERC1155} from "./interfaces/ISignatureTransferERC1155.sol"; +import {ERC1155} from "solmate/src/tokens/ERC1155.sol"; contract SignatureTransferERC1155 is ISignatureTransferERC1155, EIP712ForERC1155 { using SignatureVerification for bytes; using PermitHashERC1155 for PermitTransferFrom; using PermitHashERC1155 for PermitBatchTransferFrom; - /// @inheritdoc ISignatureTransferERC115 + /// @inheritdoc ISignatureTransferERC1155 mapping(address => mapping(uint256 => uint256)) public nonceBitmap; /// @inheritdoc ISignatureTransferERC1155 @@ -61,7 +62,9 @@ contract SignatureTransferERC1155 is ISignatureTransferERC1155, EIP712ForERC1155 signature.verify(_hashTypedData(dataHash), owner); - ERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount); + ERC1155(permit.permitted.token).safeTransferFrom( + owner, transferDetails.to, permit.permitted.tokenId, requestedAmount, "" + ); } /// @inheritdoc ISignatureTransferERC1155 @@ -117,7 +120,9 @@ contract SignatureTransferERC1155 is ISignatureTransferERC1155, EIP712ForERC1155 if (requestedAmount != 0) { // allow spender to specify which of the permitted tokens should be transferred - ERC20(permitted.token).safeTransferFrom(owner, transferDetails[i].to, requestedAmount); + ERC1155(permitted.token).safeTransferFrom( + owner, transferDetails[i].to, permitted.tokenId, requestedAmount, "" + ); } } } diff --git a/src/ERC1155/interfaces/IAllowanceTransferERC1155.sol b/src/ERC1155/interfaces/IAllowanceTransferERC1155.sol index d3752809..7a9402ac 100644 --- a/src/ERC1155/interfaces/IAllowanceTransferERC1155.sol +++ b/src/ERC1155/interfaces/IAllowanceTransferERC1155.sol @@ -6,8 +6,9 @@ pragma solidity ^0.8.17; /// @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); + /// @param allowanceDeadline The timestamp at which the permissions on the token for a specific tokenId are no longer valid + /// @param operatorDeadline The timestamp at which the permissions given to an operator of an entire collection are no longer valid. + error AllowanceExpired(uint256 allowanceDeadline, uint256 operatorDeadline); /// @notice Thrown when an allowance on a token has been depleted. /// @param amount The maximum amount allowed @@ -16,16 +17,34 @@ interface IAllowanceTransferERC1155 { /// @notice Thrown when too many nonces are invalidated. error ExcessiveInvalidation(); - /// @notice Emits an event when the owner successfully invalidates an ordered nonce. + /// @notice Emits an event when the owner successfully invalidates an ordered nonce for the allowance mapping. + event NonceInvalidation( + address indexed owner, + address indexed token, + address indexed spender, + uint256 tokenId, + uint48 newNonce, + uint48 oldNonce + ); + + /// @notice Emits an event when the owner successfully invalidates an ordered nonce for the operator mapping. 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 + address indexed owner, + address indexed token, + address indexed spender, + uint256 tokenId, + uint160 amount, + uint48 expiration ); + /// @notice Emits an event when the owner successfully gives a spender operator permissions on a token. + event ApprovalForAll(address indexed owner, address indexed token, address indexed spender, 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, @@ -41,8 +60,10 @@ interface IAllowanceTransferERC1155 { /// @notice The permit data for a token struct PermitDetails { - // ERC20 token address + // ERC1155 token address address token; + // tokenId + uint256 tokenId; // the maximum amount allowed to spend uint160 amount; // timestamp at which a spender's token allowances become invalid @@ -83,6 +104,13 @@ interface IAllowanceTransferERC1155 { uint48 nonce; } + /// @notice The saved expiration on the operator. + /// @dev Holds a nonce value to prevent replay protection. + struct PackedOperatorAllowance { + uint48 expiration; + uint48 nonce; + } + /// @notice A token spender pair. struct TokenSpenderPair { // the token the spender is approved @@ -91,6 +119,16 @@ interface IAllowanceTransferERC1155 { address spender; } + /// @notice A token spender pair. + struct TokenSpenderTokenId { + // the token the spender is approved + address token; + // the spender address + address spender; + // the tokenId approved + uint256 tokenId; + } + /// @notice Details for a token transfer. struct AllowanceTransferDetails { // the owner of the token @@ -101,12 +139,14 @@ interface IAllowanceTransferERC1155 { uint160 amount; // the token to be transferred address token; + // the tokenId of the token + uint256 tokenId; } /// @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); + function allowance(address, address, address, uint256) 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 @@ -115,7 +155,15 @@ interface IAllowanceTransferERC1155 { /// @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; + function approve(address token, address spender, uint160 amount, uint256 tokenId, uint48 expiration) external; + + /// @notice Approves the spender to be an operator of the specified token up until the expiration + /// @param token The token to approve + /// @param spender The spender address to approve + /// @param expiration The timestamp at which the operator approval is no longer valid + /// @dev The packed allowance also holds a nonce, which will stay unchanged in approve + /// @dev Passing in expiration as 0 DOES NOT set the expiration to the block.timestamp unlike `approve`. + function setApprovalForAll(address token, address spender, 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 @@ -138,7 +186,7 @@ interface IAllowanceTransferERC1155 { /// @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; + function transferFrom(address from, address to, uint256 tokenId, uint160 amount, address token) external; /// @notice Transfer approved tokens in a batch /// @param transferDetails Array of owners, recipients, amounts, and tokens for the transfers @@ -148,10 +196,20 @@ interface IAllowanceTransferERC1155 { /// @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; + /// @param operatorApprovals Array of approvals to revoke on the operator mapping. Removes operator permissions. + /// @param tokenIdApprovals Array of approvals to revoke on the allowance mapping. Removes spender permissions on certain tokenIds. + function lockdown(TokenSpenderPair[] calldata operatorApprovals, TokenSpenderTokenId[] calldata tokenIdApprovals) + external; + + /// @notice Invalidate nonces for a given (token, spender, tokenId) tuple on the allowance mapping. + /// @param token The token to invalidate nonces for + /// @param spender The spender to invalidate nonces for + /// @param tokenId The tokenId to invalidate the 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, uint256 tokenId, uint48 newNonce) external; - /// @notice Invalidate nonces for a given (token, spender) pair + /// @notice Invalidate nonces for a given (token, spender) pair on the operator mapping. /// @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. diff --git a/src/ERC1155/interfaces/ISignatureTransferERC1155.sol b/src/ERC1155/interfaces/ISignatureTransferERC1155.sol index 6a527c4b..64f1e57c 100644 --- a/src/ERC1155/interfaces/ISignatureTransferERC1155.sol +++ b/src/ERC1155/interfaces/ISignatureTransferERC1155.sol @@ -22,6 +22,8 @@ interface ISignatureTransferERC1155 { address token; // the maximum amount that can be spent uint256 amount; + // the tokenId that can be spent + uint256 tokenId; } /// @notice The signed permit message for a single token transfer diff --git a/src/ERC1155/libraries/AllowanceERC1155.sol b/src/ERC1155/libraries/AllowanceERC1155.sol index 8552aea7..34e5203a 100644 --- a/src/ERC1155/libraries/AllowanceERC1155.sol +++ b/src/ERC1155/libraries/AllowanceERC1155.sol @@ -37,7 +37,7 @@ library AllowanceERC1155 { 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.expiration = expiration == BLOCK_TIMESTAMP_EXPIRATION ? uint48(block.timestamp) : expiration; allowed.amount = amount; } diff --git a/src/ERC1155/libraries/PermitHashERC1155.sol b/src/ERC1155/libraries/PermitHashERC1155.sol index d0d9cff2..852515fd 100644 --- a/src/ERC1155/libraries/PermitHashERC1155.sol +++ b/src/ERC1155/libraries/PermitHashERC1155.sol @@ -6,7 +6,7 @@ import {ISignatureTransferERC1155} from "../interfaces/ISignatureTransferERC1155 library PermitHashERC1155 { bytes32 public constant _PERMIT_DETAILS_TYPEHASH = - keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"); + keccak256("PermitDetails(address token,uint256 tokenId,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)" @@ -120,7 +120,11 @@ library PermitHashERC1155 { ); } - function _hashPermitDetails(IAllowanceTransferERC1155.PermitDetails memory details) private pure returns (bytes32) { + function _hashPermitDetails(IAllowanceTransferERC1155.PermitDetails memory details) + private + pure + returns (bytes32) + { return keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, details)); } From 5cd3a7cc6649402e9a2fdaad9e5cfe53cedb0a48 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 13 Jan 2023 15:11:33 -0700 Subject: [PATCH 3/3] import matching --- src/ERC20/AllowanceTransfer.sol | 4 ++-- src/ERC20/SignatureTransfer.sol | 4 ++-- src/{ERC20/interfaces => shared}/IERC1271.sol | 0 src/{ERC20 => shared}/PermitErrors.sol | 0 src/{ERC20/libraries => shared}/SignatureVerification.sol | 2 +- test/AllowanceTransferInvariants.t.sol | 2 +- test/AllowanceTransferTest.t.sol | 4 ++-- test/NonceBitmap.t.sol | 2 +- test/Permit2Lib.t.sol | 2 +- test/SignatureTransfer.t.sol | 6 +++--- test/TypehashGeneration.t.sol | 2 +- test/integration/Argent.t.sol | 2 +- test/integration/GnosisSafe.t.sol | 2 +- test/mocks/MockSignatureVerification.sol | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) rename src/{ERC20/interfaces => shared}/IERC1271.sol (100%) rename src/{ERC20 => shared}/PermitErrors.sol (100%) rename src/{ERC20/libraries => shared}/SignatureVerification.sol (97%) diff --git a/src/ERC20/AllowanceTransfer.sol b/src/ERC20/AllowanceTransfer.sol index 56c4cce0..c1339957 100644 --- a/src/ERC20/AllowanceTransfer.sol +++ b/src/ERC20/AllowanceTransfer.sol @@ -4,10 +4,10 @@ pragma solidity 0.8.17; import {ERC20} from "solmate/src/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; import {PermitHash} from "./libraries/PermitHash.sol"; -import {SignatureVerification} from "./libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../shared/SignatureVerification.sol"; import {EIP712} from "./EIP712.sol"; import {IAllowanceTransfer} from "./interfaces/IAllowanceTransfer.sol"; -import {SignatureExpired, InvalidNonce} from "./PermitErrors.sol"; +import {SignatureExpired, InvalidNonce} from "../shared/PermitErrors.sol"; import {Allowance} from "./libraries/Allowance.sol"; contract AllowanceTransfer is IAllowanceTransfer, EIP712 { diff --git a/src/ERC20/SignatureTransfer.sol b/src/ERC20/SignatureTransfer.sol index c026553a..14d69e86 100644 --- a/src/ERC20/SignatureTransfer.sol +++ b/src/ERC20/SignatureTransfer.sol @@ -2,10 +2,10 @@ pragma solidity 0.8.17; import {ISignatureTransfer} from "./interfaces/ISignatureTransfer.sol"; -import {SignatureExpired, InvalidNonce} from "./PermitErrors.sol"; +import {SignatureExpired, InvalidNonce} from "../shared/PermitErrors.sol"; import {ERC20} from "solmate/src/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; -import {SignatureVerification} from "./libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../shared/SignatureVerification.sol"; import {PermitHash} from "./libraries/PermitHash.sol"; import {EIP712} from "./EIP712.sol"; diff --git a/src/ERC20/interfaces/IERC1271.sol b/src/shared/IERC1271.sol similarity index 100% rename from src/ERC20/interfaces/IERC1271.sol rename to src/shared/IERC1271.sol diff --git a/src/ERC20/PermitErrors.sol b/src/shared/PermitErrors.sol similarity index 100% rename from src/ERC20/PermitErrors.sol rename to src/shared/PermitErrors.sol diff --git a/src/ERC20/libraries/SignatureVerification.sol b/src/shared/SignatureVerification.sol similarity index 97% rename from src/ERC20/libraries/SignatureVerification.sol rename to src/shared/SignatureVerification.sol index 904dfcd2..12d0b542 100644 --- a/src/ERC20/libraries/SignatureVerification.sol +++ b/src/shared/SignatureVerification.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import {IERC1271} from "../interfaces/IERC1271.sol"; +import {IERC1271} from "./IERC1271.sol"; library SignatureVerification { /// @notice Thrown when the passed in signature is not a valid length diff --git a/test/AllowanceTransferInvariants.t.sol b/test/AllowanceTransferInvariants.t.sol index dd6d111e..80224bb8 100644 --- a/test/AllowanceTransferInvariants.t.sol +++ b/test/AllowanceTransferInvariants.t.sol @@ -4,7 +4,7 @@ import "forge-std/Test.sol"; import {TokenProvider} from "./utils/TokenProvider.sol"; import {Permit2} from "../src/ERC20/Permit2.sol"; import {IAllowanceTransfer} from "../src/ERC20/interfaces/IAllowanceTransfer.sol"; -import {SignatureVerification} from "../src/ERC20/libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../src/shared/SignatureVerification.sol"; import {PermitSignature} from "./utils/PermitSignature.sol"; import {InvariantTest} from "./utils/InvariantTest.sol"; import {MockERC20} from "./mocks/MockERC20.sol"; diff --git a/test/AllowanceTransferTest.t.sol b/test/AllowanceTransferTest.t.sol index df827197..aa79a2a0 100644 --- a/test/AllowanceTransferTest.t.sol +++ b/test/AllowanceTransferTest.t.sol @@ -5,12 +5,12 @@ import "forge-std/Test.sol"; import {TokenProvider} from "./utils/TokenProvider.sol"; import {Permit2} from "../src/ERC20/Permit2.sol"; import {PermitSignature} from "./utils/PermitSignature.sol"; -import {SignatureVerification} from "../src/ERC20/libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../src/shared/SignatureVerification.sol"; import {AddressBuilder} from "./utils/AddressBuilder.sol"; import {StructBuilder} from "./utils/StructBuilder.sol"; import {AmountBuilder} from "./utils/AmountBuilder.sol"; import {AllowanceTransfer} from "../src/ERC20/AllowanceTransfer.sol"; -import {SignatureExpired, InvalidNonce} from "../src/ERC20/PermitErrors.sol"; +import {SignatureExpired, InvalidNonce} from "../src/shared/PermitErrors.sol"; import {IAllowanceTransfer} from "../src/ERC20/interfaces/IAllowanceTransfer.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; diff --git a/test/NonceBitmap.t.sol b/test/NonceBitmap.t.sol index 2f37f7f7..6984e9f6 100644 --- a/test/NonceBitmap.t.sol +++ b/test/NonceBitmap.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import {SafeERC20, IERC20, IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {MockPermit2} from "./mocks/MockPermit2.sol"; -import {InvalidNonce} from "../src/ERC20/PermitErrors.sol"; +import {InvalidNonce} from "../src/shared/PermitErrors.sol"; contract NonceBitmapTest is Test { MockPermit2 permit2; diff --git a/test/Permit2Lib.t.sol b/test/Permit2Lib.t.sol index a74143ce..48cb6173 100644 --- a/test/Permit2Lib.t.sol +++ b/test/Permit2Lib.t.sol @@ -16,7 +16,7 @@ import {MockPermit2Lib} from "./mocks/MockPermit2Lib.sol"; import {SafeCast160} from "../src/ERC20/libraries/SafeCast160.sol"; import {MockPermitWithSmallDS, MockPermitWithLargerDS} from "./mocks/MockPermitWithDS.sol"; import {MockNonPermitNonERC20WithDS} from "./mocks/MockNonPermitNonERC20WithDS.sol"; -import {SignatureVerification} from "../src/ERC20/libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../src/shared/SignatureVerification.sol"; import {MockFallbackERC20} from "./mocks/MockFallbackERC20.sol"; contract Permit2LibTest is Test, PermitSignature, GasSnapshot { diff --git a/test/SignatureTransfer.t.sol b/test/SignatureTransfer.t.sol index 8952a1c2..55e6bd53 100644 --- a/test/SignatureTransfer.t.sol +++ b/test/SignatureTransfer.t.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import {SafeERC20, IERC20, IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SignatureVerification} from "../src/ERC20/libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../src/shared/SignatureVerification.sol"; import {TokenProvider} from "./utils/TokenProvider.sol"; -import {SignatureVerification} from "../src/ERC20/libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../src/shared/SignatureVerification.sol"; import {PermitSignature} from "./utils/PermitSignature.sol"; import {AddressBuilder} from "./utils/AddressBuilder.sol"; import {AmountBuilder} from "./utils/AmountBuilder.sol"; @@ -14,7 +14,7 @@ import {Permit2} from "../src/ERC20/Permit2.sol"; import {SignatureTransfer} from "../src/ERC20/SignatureTransfer.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {ISignatureTransfer} from "../src/ERC20/interfaces/ISignatureTransfer.sol"; -import {InvalidNonce, SignatureExpired} from "../src/ERC20/PermitErrors.sol"; +import {InvalidNonce, SignatureExpired} from "../src/shared/PermitErrors.sol"; contract SignatureTransferTest is Test, PermitSignature, TokenProvider, GasSnapshot { using AddressBuilder for address[]; diff --git a/test/TypehashGeneration.t.sol b/test/TypehashGeneration.t.sol index 724f7345..a02b049b 100644 --- a/test/TypehashGeneration.t.sol +++ b/test/TypehashGeneration.t.sol @@ -9,7 +9,7 @@ import {ISignatureTransfer} from "../src/ERC20/interfaces/ISignatureTransfer.sol import {MockSignatureVerification} from "./mocks/MockSignatureVerification.sol"; import {MockHash} from "./mocks/MockHash.sol"; import {AddressBuilder} from "./utils/AddressBuilder.sol"; -import {SignatureVerification} from "../src/ERC20/libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../src/shared/SignatureVerification.sol"; contract TypehashGeneration is Test, PermitSignature { using PermitHash for *; diff --git a/test/integration/Argent.t.sol b/test/integration/Argent.t.sol index 49890196..77e38d69 100644 --- a/test/integration/Argent.t.sol +++ b/test/integration/Argent.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import {IERC1271} from "../../src/ERC20/interfaces/IERC1271.sol"; +import {IERC1271} from "../../src/shared/IERC1271.sol"; interface WalletFactory { function owner() external returns (address); diff --git a/test/integration/GnosisSafe.t.sol b/test/integration/GnosisSafe.t.sol index 4075bbfb..636b9cea 100644 --- a/test/integration/GnosisSafe.t.sol +++ b/test/integration/GnosisSafe.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import {IERC1271} from "../../src/ERC20/interfaces/IERC1271.sol"; +import {IERC1271} from "../../src/shared/IERC1271.sol"; interface GnosisSafeProxy is IERC1271 { function setup( diff --git a/test/mocks/MockSignatureVerification.sol b/test/mocks/MockSignatureVerification.sol index 718eff5f..a27c75ad 100644 --- a/test/mocks/MockSignatureVerification.sol +++ b/test/mocks/MockSignatureVerification.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import {SignatureVerification} from "../../src/ERC20/libraries/SignatureVerification.sol"; +import {SignatureVerification} from "../../src/shared/SignatureVerification.sol"; contract MockSignatureVerification { function verify(bytes calldata sig, bytes32 hashed, address signer) public view {