Skip to content

Instantly share code, notes, and snippets.

@Philogy
Created January 6, 2024 11:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Philogy/f31c97f21ff9668a711b061bb30c174b to your computer and use it in GitHub Desktop.
Save Philogy/f31c97f21ff9668a711b061bb30c174b to your computer and use it in GitHub Desktop.
EIP6909 Implementation optimized for IDs with a totalSuppy of 1 each (collection of purely non-fungible tokens
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/// @notice Simple EIP-6909 for IDs that are solely non-fungible.
/// @author Forked from Solady (https://github.com/vectorized/solady/blob/main/src/tokens/ERC6909.sol)
/// @author philogy <https://github.com/philogy>
///
/// @dev Note:
/// The ERC6909 standard allows minting and transferring to and from the zero address,
/// minting and transferring zero tokens, as well as self-approvals.
/// For performance, this implementation WILL NOT revert for such actions.
/// Please add any checks with overrides if desired.
///
/// If you are overriding:
/// - Make sure all variables written to storage are properly cleaned
// (e.g. the bool value for `isOperator` MUST be either 1 or 0 under the hood).
/// - Check that the overridden function is actually used in the function you want to
/// change the behavior of. Much of the code has been manually inlined for performance.
abstract contract ERC6909 {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Insufficient balance.
error InsufficientBalance();
/// @dev Insufficient permission to perform the action.
error InsufficientPermission();
/// @dev The balance has overflowed.
error BalanceOverflow();
/// @dev Minting Preexisting Token.
error MintingPreexisting();
/// @dev Burning Nonexistant Token.
error BurningNonexistant();
/// @dev Attempting to transfer token from non-owner
error NotOwner();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EVENTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Emitted when `by` transfers `amount` of token `id` from `from` to `to`.
event Transfer(address by, address indexed from, address indexed to, uint256 indexed id, uint256 amount);
/// @dev Emitted when `owner` enables or disables `operator` to manage all of their tokens.
event OperatorSet(address indexed owner, address indexed operator, bool approved);
/// @dev Emitted when `owner` approves `spender` to use `amount` of `id` token.
event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount);
/// @dev `keccak256(bytes("Transfer(address,address,address,uint256,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0x1b3d7edb2e9c0b0e7c525b20aaaef0f5940d2ed71663c7d39266ecafac728859;
/// @dev `keccak256(bytes("OperatorSet(address,address,bool)"))`.
uint256 private constant _OPERATOR_SET_EVENT_SIGNATURE =
0xceb576d9f15e4e200fdb5096d64d5dfd667e16def20c1eefd14256d8e3faa267;
/// @dev `keccak256(bytes("Approval(address,address,uint256,uint256)"))`.
uint256 private constant _APPROVAL_EVENT_SIGNATURE =
0xb3fd5071835887567a0671151121894ddccc2842f1d10bedad13e0d17cace9a7;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The owner slot for `id` is given by
/// ```
/// mstore(0x04, masterSlot)
/// mstore(0x00, id)
/// let ownerSlot := keccak256(0x00, 0x24)
/// ```
///
/// The `ownerSlotSeed` is given by.
/// ```
/// let ownerSlotSeed := or(_NFT_ERC6909_MASTER_SLOT_SEED, shl(96, owner))
/// ```
///
/// The operator approval slot of `owner` is given by.
/// ```
/// mstore(0x20, ownerSlotSeed)
/// mstore(0x00, operator)
/// let operatorApprovalSlot := keccak256(0x0c, 0x34)
/// ```
///
/// The allowance slot of (`owner`, `spender`, `id`) is given by:
/// ```
/// mstore(0x34, ownerSlotSeed)
/// mstore(0x14, spender)
/// mstore(0x00, id)
/// let allowanceSlot := keccak256(0x00, 0x54)
/// ```
///
/// The master slot constant is derived by keccak256("nft-erc6909.master-slot")[24:32]
uint256 private constant _NFT_ERC6909_MASTER_SLOT_SEED = 0xab9ca135e8ac258f;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERC6909 METADATA */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns the name for token `id`.
function name(uint256 id) public view virtual returns (string memory);
/// @dev Returns the symbol for token `id`.
function symbol(uint256 id) public view virtual returns (string memory);
/// @dev Returns the number of decimals for token `id`.
/// Returns 18 by default.
/// Please override this function if you need to return a custom value.
function decimals(uint256) public pure returns (uint8) {
return 0;
}
/// @dev Returns the Uniform Resource Identifier (URI) for token `id`.
function tokenURI(uint256 id) public view virtual returns (string memory);
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERC6909 */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns the amount of token `id` owned by `owner`.
function balanceOf(address owner, uint256 id) public view virtual returns (uint256 amount) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x04, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x00, id)
let tokenOwner := sload(keccak256(0x00, 0x24))
// 1 if owner == tokenOwner && owner != address(0) else 0
amount := gt(eq(tokenOwner, owner), iszero(owner))
}
}
function ownerOf(uint256 id) public view virtual returns (address owner) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x04, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x00, id)
owner := sload(keccak256(0x00, 0x24))
}
}
/// @dev Returns the amount of token `id` that `spender` can spend on behalf of `owner`.
function allowance(address owner, address spender, uint256 id) public view virtual returns (uint256 amount) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x34, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x28, owner)
mstore(0x14, spender)
mstore(0x00, id)
amount := sload(keccak256(0x00, 0x54))
// Restore the part of the free memory pointer that has been overwritten.
mstore(0x34, 0x00)
}
}
/// @dev Checks if a `spender` is approved by `owner` to manage all of their tokens.
function isOperator(address owner, address spender) public view virtual returns (bool status) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x20, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x14, owner)
mstore(0x00, spender)
status := sload(keccak256(0x0c, 0x34))
}
}
/// @dev Transfers `amount` of token `id` from the caller to `to`.
///
/// Requirements:
/// - caller must at least have `amount`.
///
/// Emits a {Transfer} event.
function transfer(address to, uint256 id, uint256 amount) public payable virtual returns (bool) {
_beforeTokenTransfer(msg.sender, to, id);
/// @solidity memory-safe-assembly
assembly {
// Retrieve the owner.
mstore(0x04, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x00, id)
let ownerSlot := keccak256(0x00, 0x24)
let owner := sload(ownerSlot)
// Revert if not owner or amount too high.
let fromBalance := eq(caller(), owner)
if gt(amount, fromBalance) {
mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`.
revert(0x1c, 0x04)
}
if amount {
// Update the owner.
sstore(ownerSlot, to)
}
// Emit the {Transfer} event.
mstore(0x00, caller())
mstore(0x20, amount)
log4(0x00, 0x40, _TRANSFER_EVENT_SIGNATURE, caller(), shr(96, shl(96, to)), id)
}
_afterTokenTransfer(msg.sender, to, id);
return true;
}
/// @dev Transfers `amount` of token `id` from `from` to `to`.
///
/// Note: Does not update the allowance if it is the maximum uint256 value.
///
/// Requirements:
/// - `from` must at least have `amount` of token `id`.
/// - The caller must have at least `amount` of allowance to transfer the
/// tokens of `from` or approved as an operator.
///
/// Emits a {Transfer} event.
function transferFrom(address from, address to, uint256 id, uint256 amount) public payable virtual returns (bool) {
_beforeTokenTransfer(from, to, id);
/// @solidity memory-safe-assembly
assembly {
// Compute the operator slot and load its value.
mstore(0x34, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x28, from)
mstore(0x14, caller())
// Check if the caller is an operator.
if iszero(sload(keccak256(0x20, 0x34))) {
// Compute the allowance slot and load its value.
mstore(0x00, id)
let allowanceSlot := keccak256(0x00, 0x54)
let allowance_ := sload(allowanceSlot)
// If the allowance is not above the maximum uint248 value.
if iszero(byte(0, allowance_)) {
// Revert if the amount to be transferred exceeds the allowance.
if gt(amount, allowance_) {
mstore(0x00, 0xdeda9030) // `InsufficientPermission()`.
revert(0x1c, 0x04)
}
// Subtract and store the updated allowance.
sstore(allowanceSlot, sub(allowance_, amount))
}
}
// Retrieve the owner.
mstore(0x04, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x00, id)
let ownerSlot := keccak256(0x00, 0x24)
let owner := sload(ownerSlot)
// Revert if not owner or amount too high.
let cleanFrom := shr(96, shl(96, from))
// 1 if from is owner, 0 otherwise.
let fromBalance := eq(cleanFrom, owner)
if gt(amount, fromBalance) {
mstore(0x00, 0xf4d678b8 /* InsufficientBalance() */ )
revert(0x1c, 0x04)
}
let cleanTo := shr(96, shl(96, to))
if amount {
// Update the owner.
sstore(ownerSlot, cleanTo)
}
// Emit the {Transfer} event.
mstore(0x00, caller())
mstore(0x20, amount)
// forgefmt: disable-next-line
log4(0x00, 0x40, _TRANSFER_EVENT_SIGNATURE, cleanFrom, cleanTo, id)
// Restore the part of the free memory pointer that has been overwritten.
mstore(0x34, 0x00)
}
_afterTokenTransfer(from, to, id);
return true;
}
/// @dev Sets `amount` as the allowance of `spender` for the caller for token `id`.
///
/// Emits a {Approval} event.
function approve(address spender, uint256 id, uint256 amount) public payable virtual returns (bool) {
/// @solidity memory-safe-assembly
assembly {
// Compute the allowance slot and store the amount.
mstore(0x34, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x28, caller())
mstore(0x14, spender)
mstore(0x00, id)
sstore(keccak256(0x00, 0x54), amount)
// Emit the {Approval} event.
mstore(0x00, amount)
log4(0x00, 0x20, _APPROVAL_EVENT_SIGNATURE, caller(), shr(96, mload(0x20)), id)
// Restore the part of the free memory pointer that has been overwritten.
mstore(0x34, 0x00)
}
return true;
}
/// @dev Sets whether `operator` is approved to manage the tokens of the caller.
///
/// Emits {OperatorSet} event.
function setOperator(address operator, bool approved) public payable virtual returns (bool) {
/// @solidity memory-safe-assembly
assembly {
// Convert `approved` to `0` or `1`.
let approvedCleaned := iszero(iszero(approved))
// Compute the operator slot and store the approved.
mstore(0x20, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x14, caller())
mstore(0x00, operator)
sstore(keccak256(0x0c, 0x34), approvedCleaned)
// Emit the {OperatorSet} event.
mstore(0x20, approvedCleaned)
log3(0x20, 0x20, _OPERATOR_SET_EVENT_SIGNATURE, caller(), shr(96, mload(0x0c)))
}
return true;
}
/// @dev Returns true if this contract implements the interface defined by `interfaceId`.
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool result) {
/// @solidity memory-safe-assembly
assembly {
let s := shr(224, interfaceId)
// ERC165: 0x01ffc9a7, ERC6909: 0x0f632fb3.
result := or(eq(s, 0x01ffc9a7), eq(s, 0x0f632fb3))
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Mints `amount` of token `id` to `to`.
///
/// Emits a {Transfer} event.
function _mint(address to, uint256 id) internal virtual {
_beforeTokenTransfer(address(0), to, id);
/// @solidity memory-safe-assembly
assembly {
// Compute owner slot.
mstore(0x04, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x00, id)
let ownerSlot := keccak256(0x00, 0x24)
// Check for existing owner.
if sload(ownerSlot) {
mstore(0x00, 0xd991bf83 /* MintingPreexisting() */ )
revert(0x1c, 0x04)
}
let cleanTo := shr(96, shl(96, to))
sstore(ownerSlot, cleanTo)
// Emit the {Transfer} event.
mstore(0x00, caller())
mstore(0x20, 1)
log4(0x00, 0x40, _TRANSFER_EVENT_SIGNATURE, 0, cleanTo, id)
}
_afterTokenTransfer(address(0), to, id);
}
/// @dev Burns `amount` token `id` from `from`.
///
/// Emits a {Transfer} event.
function _burn(address from, uint256 id) internal virtual {
_beforeTokenTransfer(from, address(0), id);
/// @solidity memory-safe-assembly
assembly {
// Compute owner slot.
mstore(0x04, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x00, id)
let ownerSlot := keccak256(0x00, 0x24)
// Check if token exists.
if iszero(sload(ownerSlot)) {
mstore(0x00, 0xc3e5fe70 /* BurningNonexistant() */ )
revert(0x1c, 0x04)
}
let cleanFrom := shr(96, shl(96, from))
sstore(ownerSlot, cleanFrom)
// Emit the {Transfer} event.
mstore(0x00, caller())
mstore(0x20, 1)
log4(0x00, 0x40, _TRANSFER_EVENT_SIGNATURE, cleanFrom, 0, id)
}
_afterTokenTransfer(from, address(0), id);
}
/// @dev Transfers `amount` of token `id` from `from` to `to`.
///
/// Note: Does not update the allowance if it is the maximum uint256 value.
///
/// Requirements:
/// - `from` must at least have `amount` of token `id`.
/// - If `by` is not the zero address,
/// it must have at least `amount` of allowance to transfer the
/// tokens of `from` or approved as an operator.
///
/// Emits a {Transfer} event.
function _transfer(address by, address from, address to, uint256 id) internal virtual {
_beforeTokenTransfer(from, to, id);
/// @solidity memory-safe-assembly
assembly {
let bitmaskAddress := 0xffffffffffffffffffffffffffffffffffffffff
// Compute the operator slot and load its value.
mstore(0x34, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x28, from)
// If `by` is not the zero address.
let cleanBy := and(bitmaskAddress, by)
if cleanBy {
mstore(0x14, by)
// Check if the `by` is an operator.
if iszero(sload(keccak256(0x20, 0x34))) {
// Compute the allowance slot and load its value.
mstore(0x00, id)
let allowanceSlot := keccak256(0x00, 0x54)
let allowance_ := sload(allowanceSlot)
// If the allowance is not above the maximum uint248 value.
if iszero(byte(0, allowance_)) {
// Revert if the amount to be transferred exceeds the allowance.
if iszero(allowance_) {
mstore(0x00, 0xdeda9030) // `InsufficientPermission()`.
revert(0x1c, 0x04)
}
// Subtract and store the updated allowance.
sstore(allowanceSlot, sub(allowance_, 1))
}
}
}
// Retrieve the owner.
mstore(0x04, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x00, id)
let ownerSlot := keccak256(0x00, 0x24)
let owner := sload(ownerSlot)
// Revert if not owner or amount too high.
let cleanFrom := and(bitmaskAddress, from)
if iszero(eq(cleanFrom, owner)) {
mstore(0x00, 0x30cd7471 /* NotOwner() */ )
revert(0x1c, 0x04)
}
// Update the owner.
let cleanTo := and(to, bitmaskAddress)
sstore(ownerSlot, to)
// Emit the {Transfer} event.
mstore(0x00, cleanBy)
mstore(0x20, 1)
// forgefmt: disable-next-line
log4(0x00, 0x40, _TRANSFER_EVENT_SIGNATURE, cleanFrom, cleanTo, id)
// Restore the part of the free memory pointer that has been overwritten.
mstore(0x34, 0x00)
}
_afterTokenTransfer(from, to, id);
}
/// @dev Sets `amount` as the allowance of `spender` for `owner` for token `id`.
///
/// Emits a {Approval} event.
function _approve(address owner, address spender, uint256 id, uint256 amount) internal virtual {
/// @solidity memory-safe-assembly
assembly {
// Compute the allowance slot and store the amount.
mstore(0x34, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x28, owner)
mstore(0x14, spender)
mstore(0x00, id)
sstore(keccak256(0x00, 0x54), amount)
// Emit the {Approval} event.
mstore(0x00, amount)
// forgefmt: disable-next-line
log4(0x00, 0x20, _APPROVAL_EVENT_SIGNATURE, shr(96, mload(0x34)), shr(96, mload(0x20)), id)
// Restore the part of the free memory pointer that has been overwritten.
mstore(0x34, 0x00)
}
}
/// @dev Sets whether `operator` is approved to manage the tokens of `owner`.
///
/// Emits {OperatorSet} event.
function _setOperator(address owner, address operator, bool approved) internal virtual {
/// @solidity memory-safe-assembly
assembly {
// Convert `approved` to `0` or `1`.
let approvedCleaned := iszero(iszero(approved))
// Compute the operator slot and store the approved.
mstore(0x20, _NFT_ERC6909_MASTER_SLOT_SEED)
mstore(0x14, owner)
mstore(0x00, operator)
sstore(keccak256(0x0c, 0x34), approvedCleaned)
// Emit the {OperatorSet} event.
mstore(0x20, approvedCleaned)
// forgefmt: disable-next-line
log3(0x20, 0x20, _OPERATOR_SET_EVENT_SIGNATURE, shr(96, shl(96, owner)), shr(96, mload(0x0c)))
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* HOOKS TO OVERRIDE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Hook that is called before any transfer of tokens.
/// This includes minting and burning.
function _beforeTokenTransfer(address from, address to, uint256 id) internal virtual {}
/// @dev Hook that is called after any transfer of tokens.
/// This includes minting and burning.
function _afterTokenTransfer(address from, address to, uint256 id) internal virtual {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment