Abstract
This ERC extends ERC-7984 with a minimal interface for defining tokenized real world assets. It provides a pair of plaintext eligibility checks, a confidential validation function answering whether a specific transfer is permitted, a confidential figure for the portion of a balance that is currently spendable, and an access restricted forced transfer. Amounts remain confidential pointers throughout. The standard constrains the behavior of minting, burning, halting, and freezing without mandating interfaces for them, leaving issuance and restriction mechanics to implementations.
Motivation
Real world assets carry obligations that ordinary fungible tokens do not. Holders must be eligible to receive and transfer assets, transfers must satisfy rules that depend on the amount being moved, portions of a balance may be unspendable due to an issuer freeze or a vesting schedule, and an issuer may be required to move assets without a holder's consent.
ERC-3643 and ERC-7943 address these needs for tokens whose balances are public. Neither translates to a token whose balances and transfer amounts are confidential pointers.
Confidentiality complicates this. Determining if a transfer is compliant depends on confidential data and therefore must be confidential itself. Yet parties wish to have easy access to compliance information to enable smart-contract interactions and applications. A standard for confidential real world assets must serve both needs without letting either compromise the other.
This standard defines the minimum interface that does so, adapting prior compliance standards to a confidential token.
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.
Token
Compliant tokens MUST implement ERC-7984 and ERC-165. The supportsInterface function MUST return true when the interfaceID argument is 0x00000000.
All amounts are confidential pointers represented as bytes32 values, as defined by ERC-7984. The mechanism by which a pointer is resolved, and the mechanism by which an account is authorized to resolve one, are implementation specific.
Functions accepting a confidential pointer as input also take a bytes calldata data parameter. This parameter may carry cryptographic proofs, authorization grants, or other mechanism specific material used to properly process the pointer. Contracts MUST accept empty input in cases where the confidential pointer is sufficient on its own.
Interface
interface IERC8424 is IERC7984 {
event ConfidentialCanTransfer(address indexed operator, address indexed from, address indexed to, bytes32 result);
event ConfidentialAvailableBalanceOf(address indexed account, bytes32 indexed result);
event ConfidentialForcedTransfer(address indexed from, address indexed to, bytes32 indexed amount);
function canSend(address sender) external view returns (bool);
function canReceive(address receiver) external view returns (bool);
function confidentialCanTransfer(
address operator,
address from,
address to,
bytes32 amount,
bytes calldata data
) external returns (bytes32);
function confidentialAvailableBalanceOf(address account) external returns (bytes32);
function forceConfidentialTransferFrom(
address from,
address to,
bytes32 amount,
bytes calldata data
) external returns (bytes32);
}Methods
-
canSendReturns whether
senderis eligible to send the asset, ignoring any amount.- MUST NOT revert.
- MUST NOT encode quantitative rules. Amount based restrictions and limitation checks belong in
confidentialCanTransfer.
function canSend(address sender) external view returns (bool) -
canReceiveReturns whether
receiveris eligible to receive the asset, ignoring any amount.- MUST NOT revert.
- MUST NOT encode quantitative rules. Amount based restrictions and limitation checks belong in
confidentialCanTransfer.
function canReceive(address receiver) external view returns (bool) -
confidentialCanTransferReturns a pointer to a boolean indicating whether
operatormay moveamountfromfromtoto.- MUST return a pointer to false OR revert if
canSend(from)returns false, unlessfromis the zero address. - MUST return a pointer to false OR revert if
canReceive(to)returns false, unlesstois the zero address. - MUST return a pointer to false OR revert if any other rule would prevent the transfer (such as vesting, balance caps, etc).
- MUST return a pointer to false if
amountexceeds the value returned byconfidentialAvailableBalanceOf(from), unlessfromis the zero address. - MUST NOT return a pointer to false solely because
operatoris not an authorized operator forfrom. Operator authorization is enforced by ERC-7984. Theoperatorparameter exists so that rules constraining who may initiate a transfer can be expressed. - MUST emit
ConfidentialCanTransferwith the returned pointer.
function confidentialCanTransfer(address operator, address from, address to, bytes32 amount, bytes calldata data) external returns (bytes32) - MUST return a pointer to false OR revert if
-
confidentialAvailableBalanceOfReturns a pointer to the largest amount
accountcould transfer at the time of the call, disregarding rules that depend on the recipient.- MUST be less than or equal to
confidentialBalanceOf(account). - MUST account for every restriction the implementation applies to the account's own balance, including issuer freezes, lockups, vesting schedules, and pledged amounts.
- SHOULD NOT revert.
- MUST emit
ConfidentialAvailableBalanceOfwith the returned pointer.
function confidentialAvailableBalanceOf(address account) external returns (bytes32) - MUST be less than or equal to
-
forceConfidentialTransferFromMoves
amountfromfromtoto. Returns a pointer to the amount actually moved.- MUST be restricted in access.
- MUST move 0 tokens if
amountexceedsconfidentialBalanceOf(from). - MAY move 0 tokens if
amountexceedsconfidentialAvailableBalanceOf(from) - MUST revert if
canReceive(to)returns false. - MUST NOT call
canSend(from). - MUST NOT call
confidentialCanTransfer. - MUST emit
ConfidentialTransferas defined by ERC-7984, in addition toConfidentialForcedTransfer.
function forceConfidentialTransferFrom(address from, address to, bytes32 amount, bytes calldata data) external returns (bytes32)
Events
-
ConfidentialForcedTransferevent ConfidentialForcedTransfer(address indexed from, address indexed to, bytes32 indexed amount) -
ConfidentialCanTransferevent ConfidentialCanTransfer(address indexed operator, address indexed from, address indexed to, bytes32 result) -
ConfidentialAvailableBalanceOfevent ConfidentialAvailableBalanceOf(address indexed account, bytes32 indexed result)
Transfer Behavior
An implementation MUST NOT complete a transfer, including a mint or burn, of an amount for which confidentialCanTransfer would resolve to false, except through forceConfidentialTransferFrom.
Implementations SHOULD satisfy that requirement by transferring an amount of zero rather than reverting.
Rationale
Plaintext eligibility alongside a confidential predicate
This standard answers two distinct questions. The first is whether an address may send/receive an asset at all, which does not depend on any amount and is often derived from a non-confidential source such as an identity registry, allow-list, or block-list. The second is whether a particular transfer may proceed, which accounts for every rule, confidential and non-confidential alike, and whose result must therefore be confidential.
canSend and canReceive answer the first question in plaintext as view functions. Consumers must understand that the answer is not exhaustive: a transfer to or from an eligible address may still fail on a rule evaluated within confidentialCanTransfer. confidentialCanTransfer answers the second question as a confidential pointer, subsuming the first, and is consumed both by the token in the course of a transfer and by integrators informing a user whether a specific transfer would be permitted.
The two are not collapsible into a single function. Doing so would force an inherently public boolean to be delivered as a confidential pointer, which cannot drive control flow in an integrating contract and often cannot be read without sending a transaction. Conversely, it is often impossible or undesirable to return the result from confidentialCanTransfer as plaintext.
The available balance is not a view function
Deriving the spendable portion of a balance often requires operations on confidential values, and pointer mechanisms usually require writing on-chain to operate on existing values. A view function therefore cannot produce a resolvable answer.
Halting, freezing, and vesting are not in the interface
A halted token, a frozen balance, and an unfinished vesting schedule are all rules that determine whether a transfer may proceed. confidentialCanTransfer and confidentialAvailableBalanceOf already answer that question completely, so a separate accessor for each mechanism would add surface without adding information. Mandating one mechanism would also privilege it over the others an issuer may need. This core can be extended to support more specific use cases through additional standards or implementation extensions.
Security Considerations
Disclosure through reverts
A transfer that reverts when a compliance rule is not satisfied publicly discloses that a specific pair of addresses failed that rule, which may reveal details about the balance of the sender or recipient. Transferring zero avoids the disclosure but produces a transaction that appears successful while moving nothing. Integrating contracts must verify the returned amount rather than assuming a transfer of the requested size occurred.
Available balance accounting
Where the available balance is derived by naively subtracting a restricted amount from a balance, a forced transfer or a permissioned burn that moves more than the available balance will underflow that subtraction. Ensure saturating subtraction is used in this situation.
Copyright
Copyright and related rights waived via CC0.
