Abstract Token
Abstract
Abstract tokens provide a standard interface to:
- Mint tokens off-chain as messages
- Reify tokens on-chain via smart contract
- Dereify tokens back into messages
Abstract tokens can comply with existing standards like ERC-20, ERC-721, and ERC-1155. The standard allows wallets and other applications to better handle potential tokens before any consensus-dependent events occur on-chain.
Motivation
Abstract tokens enable zero-cost token minting, facilitating high-volume applications by allowing token holders to reify tokens (place the tokens on-chain) as desired. Example use cases:
- airdrops
- POAPs / receipts
- identity / access credentials
Merkle trees are often used for large token distributions to spread mint/claim costs to participants, but they require participants to provide a markle proof when claiming tokens. This standard aims to improve the claims proces for similar distributions:
- Generic: compatible with merkle trees, digital signatures, or other eligibility proofs
- Legible: users can query an abstract token contract to understand their potential tokens (e.g. token id, quantity, or uri)
- Contained: users do not need to understand the proof mechanism used by the particular token implementation contract
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Data Types
Token Messages
A token message defines one or more tokens along with the context needed to reify the token(s) using a smart contract.
chainId
& implementation
: set the domain of the token message to a specific chain and contract: this is where the token can be reified
owner
: the address that owns the tokens defined in the messages when reified
meta
: implementation-specific context necessary to reify the defined token(s), such as id, amount, or uri.
proof
: implementation-specific authorization to reify the defined token(s).
nonce
: counter that may be incremented when multiple otherwise-identical abstract token messages are needed
struct AbstractTokenMessage {
uint256 chainId;
address implementation;
address owner;
bytes meta;
uint256 nonce;
bytes proof;
}
Message Status
A message status may be defined for every (abstract token contract, abstract token message) pair.
invalid
: the contract cannot interact with the message
valid
: the contract can interact with the message
used
: the contract has already interacted with the message
enum AbstractTokenMessageStatus {
invalid,
valid,
used
}
Methods
reify
Moves token(s) from a message to a contract
function reify(AbstractTokenMessage calldata message) external;
The token contract MUST reify a valid token message.
Reification MUST be idempotent: a particular token message may be used to reify tokens at most once. Calling reify
with an already used token message MAY succeed or revert.
status
Returns the status of a particular message
function status(AbstractTokenMessage calldata message) external view returns (AbstractTokenMessageStatus status);
dereify
Moves token(s) from a contract to a message intended for another contract and/or chain.
function dereify(AbstractTokenMessage calldata message) external;
OPTIONAL - allows tokens to be moved between contracts and/or chains by dereifying them from one context and reifying them in another. Dereification MUST be idempotent: a particular token message must be used to dereify tokens at most once.
If implemented, dereification:
- MUST burn the exact tokens from the holder as defined in the token message
- MUST NOT dereify token messages scoped to the same contract and chain.
- MAY succeed or revert if the token message is already used.
- MUST emit the
Reify
event on only the firstreify
call with a specific token message
id
Return the id of token(s) defined in a token message.
function id(AbstractTokenMessage calldata message) external view returns (uint256);
OPTIONAL - abstract token contracts without a well-defined token ID (e.g. ERC-20) MAY return 0
or not implement this method.
amount
Return the amount of token(s) defined in a token message.
function amount(AbstractTokenMessage calldata message) external view returns (uint256);
OPTIONAL - abstract token contracts without a well-defined token amount (e.g. ERC-721) MAY return 0
or not implement this method.
uri
Return the amount of token(s) defined in a token message.
function uri(AbstractTokenMessage calldata message) external view returns (string memory);
OPTIONAL - abstract token contracts without a well-defined uri (e.g. ERC-20) MAY return ""
or not implement this method.
supportsInterface
All abstract token contracts must support ERC-165 and include the Abstract Token interface ID in their supported interfaces.
Events
Reify
The Reify event MUST be emitted when a token message is reified into tokens
event Reify(AbstractTokenMessage);
Dereify
The Dereify event MUST be emitted when tokens are dereified into a message
event Dereify(AbstractTokenMessage);
Application to existing token standards
Abstract tokens compatible with existing token standards MUST overload existing token transfer functions to allow transfers from abstract token messages.
Abstract ERC-20
interface IAbstractERC20 is IAbstractToken, IERC20, IERC165 {
// reify the message and then transfer tokens
function transfer(
address to,
uint256 amount,
AbstractTokenMessage calldata message
) external returns (bool);
// reify the message and then transferFrom tokens
function transferFrom(
address from,
address to,
uint256 amount,
AbstractTokenMessage calldata message
) external returns (bool);
}
Abstract ERC-721
interface IAbstractERC721 is IAbstractToken, IERC721 {
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata _data,
AbstractTokenMessage calldata message
) external;
function transferFrom(
address from,
address to,
uint256 tokenId,
AbstractTokenMessage calldata message
) external;
}
Abstract ERC-1155
interface IAbstractERC1155 is IAbstractToken, IERC1155 {
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes calldata data,
AbstractTokenMessage calldata message
) external;
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata amounts,
bytes calldata data,
AbstractTokenMessage[] calldata messages
) external;
}
Rationale
Meta format
The abstract token message meta
field is simply a byte array to preserve the widest possible accesibility.
- Applications handling abstract tokens can interact with the implementation contract for token metadata rather than parsing this field, so legibility is of secondary importance
- A byte array can be decoded as a struct and checked for errors within the implementation contract
- Future token standards will include unpredictable metadata
Proof format
Similar considerations went into defining the proof
field as a plain byte array:
- The contents of this field may vary, e.g. an array of
bytes32
merkle tree nodes or a 65 byte signature. - a byte array handles all potential use cases at the expense of increased message size.
Backwards Compatibility
No backward compatibility issues found.
Reference Implementation
See here.
Security Considerations
Several concerns are highlighted.
Message Loss
Because token messages are not held on-chain, loss of the message may result in loss of the token. Applications that issue abstract tokens to their users can store the messages themselves, but ideally users would be able to store and interact with abstract token messages within their crypto wallets.
Authorizing Reification
Token messages may only be reified if they include a validity proof. While the proof mechanism itself is out of scope for this standard, those designing proof mechanisms should consider:
- Does total supply need to audited on-chain and/or off-chain?
- Does the mechanism require ongoing access to a secret (e.g. digital signature) or is it immutable (e.g. merkle proof)?
- Is there any way for an attacker to prevent the reification of an otherwise valid token message?
Non-owner (De)Reification
Can non-owners (de)reify a token message on behalf of the owner?
Pro: supporting apps should be able to handle this because once a valid message exists, the owner could (de)reify the message at any time Con: if the token contract reverts upon (de)reification of a used message, an attacker could grief the owner by front-running the transaction
Abstract Token Bridge Double Spend
Abstract tokens could be used for a token-specific bridge:
- Dereify the token from chain A to with message M
- Reify the token on chain B with message M
Because the abstract token standard does not specify any cross-chain message passing, the abstract token contracts on chains A and B cannot know whether a (de)reification of message M has occurred on the other chain.
A naive bridge would be subject to double spend attacks:
- An attacker requests bridging tokens they hold on chain A to chain B
- A bridging mechanism creates an abstract token message M
- The attacker reifies message M on chain B but does not dereify message M on chain A
- The attacker continues to use tokens
Some oracle mechanism is necessary to prevent the reification of message M on chain B until the corresponding tokens on chain A are dereified.
Copyright
Copyright and related rights waived via CC0.