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)); }