Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1155 cherrypick #175

Open
wants to merge 3 commits into
base: 1155-feature
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions src/ERC1155/AllowanceTransferERC1155.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// SPDX-License-Identifier: MIT
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";
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, 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, uint256 tokenId, uint48 expiration) external {
PackedAllowance storage allowed = allowance[msg.sender][token][spender][tokenId];
allowed.updateAmountAndExpiration(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
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, uint256 tokenId, uint160 amount, address token) external {
_transfer(from, to, tokenId, 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.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, 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;

// 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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Whats the reason for having this as a 256 for a while and then casting it back to a 160 later?

if (maxAmount != type(uint160).max) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

could save a level of nesting here i think by switching the ordering around:

if (maxAmount == type(uint160).max) {
   ... max stuff
} else if (amount > maxAmount) {
   revert
}

if (amount > 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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

wait isn't this backwards? like, if approval is MAX then we shouldn't update, but if it's not MAX then we should update?

}
}
}

// Transfer the tokens from the from address to the recipient.
ERC1155(token).safeTransferFrom(from, to, tokenId, amount, "");
}

/// @inheritdoc IAllowanceTransferERC1155
function lockdown(TokenSpenderPair[] calldata operatorApprovals, TokenSpenderTokenId[] calldata tokenIdApprovals)
external
{
address owner = msg.sender;

unchecked {
// Revoke operator allowances for each pair of spenders and tokens.
uint256 length = operatorApprovals.length;
for (uint256 i = 0; i < length; ++i) {
address token = operatorApprovals[i].token;
address spender = operatorApprovals[i].spender;

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure but maybe invalidateOperatorNonces better for clarity?

uint48 oldNonce = operators[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();
}

operators[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;
uint256 tokenId = details.tokenId;
uint48 expiration = details.expiration;

PackedAllowance storage allowed = allowance[owner][token][spender][tokenId];

if (allowed.nonce != nonce) revert InvalidNonce();

allowed.updateAll(amount, expiration, nonce);
emit Permit(owner, token, spender, amount, expiration, nonce);
}
}
39 changes: 39 additions & 0 deletions src/ERC1155/EIP712ForERC1155.sol
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

isnt this contract the same for all types minus the _HASHED_NAME? wonder if we could have it be shared and take the name as constructor arg

// 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));
}
}
11 changes: 11 additions & 0 deletions src/ERC1155/Permit2.sol
Original file line number Diff line number Diff line change
@@ -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.
}
Loading