{"id":"ERC-6909","name":"Minimal Multi-Token Interface","status":"Final","chain":"both","category":{"id":"nft","name":"NFT Capabilities","description":"What NFTs can do beyond basic ownership"},"journeyStages":[{"id":"asset-display","name":"Asset Discovery & Display","description":"Seeing tokens, NFTs, and balances"},{"id":"approving","name":"Approving & Permissioning","description":"Granting permissions for actions"},{"id":"executing","name":"Executing Transactions","description":"Performing on-chain actions"}],"uxImpact":"Streamlined multi-token with granular per-token-type approvals and no mandatory callbacks — simpler than ERC-1155.","officialUrl":"https://ercs.ethereum.org/ERCS/erc-6909","lastReviewed":"2026-09-18","lastUpdated":"2026-09-18","kind":"proposal","hasDetailedContent":true,"content":{"id":"ERC-6909","sources":[{"label":"Official specification","url":"https://ercs.ethereum.org/ERCS/erc-6909","type":"official-spec"}],"lastReviewed":"2026-09-18","lastUpdated":"2026-09-18","specConstraints":[{"id":"minimal-multitoken-surface","constraint":"ERC-6909 is a reduced multi-token interface without ERC-1155 callbacks or batching, using hybrid allowance and operator permissions.","appliesTo":"application","normativity":"rationale","specQuote":"The following specifies a multi-token contract as a simplified alternative to the [ERC-1155](./eip-1155.md) Multi-Token Standard. In contrast to ERC-1155, callbacks and batching have been removed from the interface and the permission system is a hybrid operator-approval scheme for granular and scalable permissions.","specSection":"Abstract","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#abstract","designImplication":"Build per-token-type balances and approvals without assuming ERC-1155 batch or receiver-callback flows, and label the reduced surface as an intentional design choice."},{"id":"callback-removal-motivation","constraint":"The specification removes mandatory receiver callbacks so contract recipients do not incur unnecessary external calls and callback code size.","appliesTo":"application","normativity":"rationale","specQuote":"According to ERC-1155, callbacks are required for each transfer and batch transfer to contract accounts. This requires potentially unnecessary external calls to the recipient when the recipient account is a contract account.","specSection":"Motivation","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#motivation","designImplication":"Do not add an ERC-1155-style callback step to an ERC-6909 transfer flow; explain that recipient acceptance is outside this interface."},{"id":"batching-omission-motivation","constraint":"Batching is intentionally omitted so individual implementations can choose calldata and runtime tradeoffs.","appliesTo":"application","normativity":"rationale","specQuote":"Batching transfers, while useful, are excluded from this standard to allow for opinionated batch transfer operations on different implementations.","specSection":"Motivation","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#motivation","designImplication":"Keep any batch UI provider-specific and do not imply that ERC-6909 itself guarantees atomic multi-token batching."},{"id":"erc6909-interface","constraint":"Every ERC-6909 contract must implement ERC-165 in addition to the ERC-6909 interface.","appliesTo":"account-implementation","normativity":"normative","specQuote":"Every [ERC-6909](./eip-6909.md) compliant contract must implement the [ERC-165](./eip-165.md) interface in addition to the following interface.","specSection":"Specification","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#specification","designImplication":"Detect ERC-165 support before rendering ERC-6909 balances, allowances, or operator controls."},{"id":"transfer-balance","constraint":"transfer must revert when the caller lacks enough balance for the token ID.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST revert when the caller's balance for the token `id` is insufficient.","specSection":"Specification / Methods / transfer","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transfer","designImplication":"Show the selected token ID and available balance before submission, and keep insufficient-balance failures distinct from approval failures."},{"id":"transfer-event","constraint":"transfer must log the Transfer event.","appliesTo":"application","normativity":"normative","specQuote":"MUST log the `Transfer` event.","specSection":"Specification / Methods / transfer","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transfer","designImplication":"Use the emitted event to reconcile activity and balances rather than treating a returned boolean as the only receipt."},{"id":"transfer-return","constraint":"transfer must return True on success.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST return True.","specSection":"Specification / Methods / transfer","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transfer","designImplication":"Treat a successful return value and emitted transfer event as separate confirmation signals when building transaction status."},{"id":"transfer-from-authorization","constraint":"transferFrom must reject a caller without sender authority or sufficient allowance for the token ID.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST revert when the caller is neither the `sender` nor an operator for the `sender` and the caller's allowance for the token `id` for the `sender` is insufficient.","specSection":"Specification / Methods / transferFrom","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transferfrom","designImplication":"Show whether a transfer is authorized by the sender, an operator grant, or a per-token allowance before asking for confirmation."},{"id":"transfer-from-balance","constraint":"transferFrom must revert when the sender's balance for the token ID is insufficient.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST revert when the `sender`'s balance for the token id is insufficient.","specSection":"Specification / Methods / transferFrom","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transferfrom","designImplication":"Keep sender balance failure distinct from delegated-permission failure in transfer status and recovery guidance."},{"id":"transfer-from-event","constraint":"transferFrom must log the Transfer event.","appliesTo":"application","normativity":"normative","specQuote":"MUST log the `Transfer` event.","specSection":"Specification / Methods / transferFrom","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transferfrom","designImplication":"Reconcile delegated transfers from the event's sender and receiver fields rather than only the caller's view."},{"id":"allowance-decrement","constraint":"A finite non-operator allowance must decrease by the transferred amount when transferFrom uses it.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST decrease the caller's `allowance` by the same `amount` of the `sender`'s balance decrease if the caller is not an operator for the `sender` and the caller's `allowance` is not infinite.","specSection":"Specification / Methods / transferFrom","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transferfrom","designImplication":"Show the allowance remaining after a delegated transfer and distinguish finite per-token spending from unlimited operator access."},{"id":"infinite-allowance-preservation","constraint":"Implementations should not decrease an infinite allowance during transferFrom.","appliesTo":"account-implementation","normativity":"normative","specQuote":"SHOULD NOT decrease the caller's `allowance` for the token `id` for the `sender` if the `allowance` is infinite.","specSection":"Specification / Methods / transferFrom","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transferfrom","designImplication":"Label an infinite allowance clearly and do not present ordinary spending as reducing it when the implementation preserves it."},{"id":"operator-allowance-preservation","constraint":"Implementations should not decrease allowance when the caller is the operator or sender.","appliesTo":"account-implementation","normativity":"normative","specQuote":"SHOULD NOT decrease the caller's `allowance` for the token `id` for the `sender` if the caller is an operator or the `sender`.","specSection":"Specification / Methods / transferFrom","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transferfrom","designImplication":"Keep self-transfer and operator transfer history separate from finite allowance consumption in the approval UI."},{"id":"transfer-from-return","constraint":"transferFrom must return True on success.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST return True.","specSection":"Specification / Methods / transferFrom","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transferfrom","designImplication":"Treat the success return as part of delegated-transfer confirmation alongside the event and updated allowance."},{"id":"approve-set","constraint":"approve must set the caller's allowance for the spender and token ID to the requested amount.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST set the `allowance` of the `spender` of the token `id` for the caller to the `amount`.","specSection":"Specification / Methods / approve","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#approve","designImplication":"Show token ID, spender, amount, and the resulting allowance in the approval confirmation."},{"id":"approval-event","constraint":"approve must log the Approval event.","appliesTo":"application","normativity":"normative","specQuote":"MUST log the `Approval` event.","specSection":"Specification / Methods / approve","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#approve","designImplication":"Use Approval events to update per-token allowance rows and revoke controls."},{"id":"approve-return","constraint":"approve must return True on success.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST return True.","specSection":"Specification / Methods / approve","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#approve","designImplication":"Do not show a new allowance as active until the approval transaction succeeds and returns the expected result."},{"id":"operator-status","constraint":"setOperator must set unlimited operator status to the approved value for all token IDs.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST set the operator status to the `approved` value.","specSection":"Specification / Methods / setOperator","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#setoperator","designImplication":"Present operator approval as unlimited authority across token IDs and require explicit confirmation for grant or revoke."},{"id":"operator-event","constraint":"setOperator must log OperatorSet when operator status changes; logging an unchanged status is optional.","appliesTo":"application","normativity":"normative","specQuote":"MUST be logged when the operator status is set.\n\nMAY be logged when the operator status is set to the same status it was before the current call.","specSection":"Specification / Events / OperatorSet","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#operatorset","designImplication":"Use OperatorSet for permission history while allowing an unchanged-status transaction to produce an optional duplicate event."},{"id":"operator-return","constraint":"setOperator must return True on success.","appliesTo":"account-implementation","normativity":"normative","specQuote":"MUST return True.","specSection":"Specification / Methods / setOperator","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#setoperator","designImplication":"Confirm operator grant or revoke only after the call succeeds and its status can be refreshed."},{"id":"transfer-event-accounting","constraint":"Transfer must be logged for ordinary transfers, minting with a zero sender, and burning with a zero receiver.","appliesTo":"application","normativity":"normative","specQuote":"MUST be logged when an `amount` of a token `id` is transferred from one account to another.\n\nMUST be logged with the `sender` address as the zero address when an `amount` of a token `id` is minted.\n\nMUST be logged with the `receiver` address as the zero address when an `amount` of a token `id` is burned.","specSection":"Specification / Events / Transfer","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#transfer","designImplication":"Classify transfer history as transfer, mint, or burn from zero-address fields and avoid showing mint or burn as ordinary user sends."},{"id":"approval-event-accounting","constraint":"Approval must be logged when an owner sets an allowance.","appliesTo":"application","normativity":"normative","specQuote":"MUST be logged when the `allowance` is set by an `owner`.","specSection":"Specification / Events / Approval","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#approval","designImplication":"Keep approval activity attributable to the owner, spender, and token ID."},{"id":"token-uri-placeholder","constraint":"The client must replace `{id}` occurrences in tokenURI output with the token ID.","appliesTo":"application","normativity":"normative","specQuote":"MUST replace occurrences of `{id}` in the returned URI string by the client.","specSection":"Specification / Content URI Extension / Methods / tokenURI","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#tokenuri","designImplication":"Resolve the token ID placeholder before loading metadata and show unavailable metadata separately when the token does not exist."},{"id":"contract-metadata-name","constraint":"Contract metadata must require a name field.","appliesTo":"application","normativity":"normative","specQuote":"\"required\": [\"name\"]","specSection":"Specification / Content URI Extension / Metadata Structure / Contract URI","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#contract-uri","designImplication":"Treat missing contract name as invalid or incomplete metadata rather than inventing a collection label."},{"id":"token-metadata-required-fields","constraint":"Token metadata must require name, description, and image fields.","appliesTo":"application","normativity":"normative","specQuote":"\"required\": [\"name\", \"description\", \"image\"]","specSection":"Specification / Content URI Extension / Metadata Structure / Token URI","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#token-uri","designImplication":"Keep missing token metadata fields visible as an incomplete state and do not silently substitute unrelated media."},{"id":"granular-approval-model","constraint":"ERC-6909 combines ERC-20-style per-token allowances with ERC-1155-style unlimited operators, allowing scoped or all-token permissions.","appliesTo":"application","normativity":"rationale","specQuote":"This standard requires both be implemented, with the only modification being to the \"allowance model\" where the token id must be specified as well. This allows an account to grant specific approvals to specific token ids, infinite approvals to specific token ids, or infinite approvals to all token ids.","specSection":"Rationale / Granular Approvals","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#granular-approvals","designImplication":"Make token-specific allowances and all-token operator grants visibly different risk choices in approval UI."},{"id":"batching-rationale","constraint":"Batching is left to individual implementations so calldata layout and runtime tradeoffs can vary by environment.","appliesTo":"application","normativity":"rationale","specQuote":"While batching operations is useful, its place should not be in the standard itself, but rather on a case-by-case basis.","specSection":"Rationale / Removal of Batching","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#removal-of-batching","designImplication":"Label any adapter-provided batch action as implementation-specific and preserve per-transfer status when batching is available."},{"id":"callback-omission-rationale","constraint":"The rationale identifies callback omission as a way to reduce contract size, gas cost, and complexity.","appliesTo":"application","normativity":"rationale","specQuote":"Requiring callbacks unnecessarily encumbers implementors that either have no particular use case for callbacks or prefer a bespoke callback mechanism.","specSection":"Rationale / Removal of Required Callbacks","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#removal-of-required-callbacks","designImplication":"Do not promise ERC-1155 receiver acceptance semantics for a standard ERC-6909 transfer."},{"id":"erc1155-backwards-compatibility","constraint":"ERC-6909 is not backwards compatible with ERC-1155, although wrappers can support ERC-20, ERC-721, and ERC-1155 standards.","appliesTo":"application","normativity":"backwards-compatibility","specQuote":"This is not backwards compatible with ERC-1155 as some methods are removed. However, wrappers can be implemented for the ERC-20, ERC-721, and ERC-1155 standards.","specSection":"Backwards Compatibility","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#backwards-compatibility","designImplication":"Detect wrappers and capability gaps before reusing ERC-1155 batch or callback UI for an ERC-6909 contract."},{"id":"delegated-permission-risk","constraint":"An allowance holder can transfer the full allowance until revoked, and an operator can transfer any amount of any token ID until operator permission is revoked.","appliesTo":"wallet","normativity":"security-considerations","specQuote":"Any account with an allowance may transfer the full allowance for any reason at any time until the allowance is revoked. Any account with operator permissions may transfer any amount of any token id on behalf of the owner until the operator permission is revoked.","specSection":"Security Considerations / Approvals and Operators","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#approvals-and-operators","designImplication":"Show spender, token ID, amount, revocation state, and the broader impact of an operator grant before approval."},{"id":"operator-allowance-precedence","constraint":"When a caller is both an operator and has insufficient allowance, implementations may differ in whether allowance or operator checks happen first; the specification leaves this unconstrained.","appliesTo":"wallet","normativity":"security-considerations","specQuote":"If an account has both operator permissions and an insufficient allowance for a given transfer, performing the allowance check before the operator check would result in a revert while performing the operator check before the allowance check would not. The specification intentionally leaves this unconstrained for cases where implementors may track allowances despite the operator status.","specSection":"Security Considerations / Approvals and Operators","specUrl":"https://ercs.ethereum.org/ERCS/erc-6909#approvals-and-operators","designImplication":"Do not promise identical error behavior across implementations when operator and allowance state overlap; show the effective permission path and preserve a retry or revoke explanation."}],"summary":"Streamlined multi-token with granular per-token-type approvals and no mandatory callbacks — simpler than ERC-1155.","designerTakeaways":["You can show per-token-type allowance rows mirroring ERC-20 revoke.cash patterns.","Your approval modal can distinguish Approve 50 of this type from Allow app to manage all types.","You can skip ERC-1155 batch UI when the contract only needs simple per-id transfers."],"applicability":{"whenToUse":["One contract emits many fungible-like token types.","You need ERC-20-style allowances without ERC-1155 callbacks.","Gas-efficient multi-token vaults or game currencies."],"whenToAvoid":["True batch NFT transfers with mixed metadata are required.","Contract is ERC-1155-only with existing indexer support.","Users never need granular approvals — operator suffices."]},"prototypeFirst":[{"screen":"Multi-type balance sheet","why":"Users must see each token type balance distinctly.","covers":["Multiple ids","Zero balance types","Single type"],"include":["Type name","Balance","Token id disclosure in advanced"]},{"screen":"Approve specific amount","why":"ERC-20 mental model is the anchor.","covers":["Amount approval","Unlimited toggle","Revoke"],"include":["Amount input","Spender address","Current allowance row"]},{"screen":"Operator approval warning","why":"Full operator access is high risk.","covers":["Operator grant","Revoke operator"],"include":["Scary unlimited copy","List affected types","Confirm checkbox"]},{"screen":"Transfer type picker","why":"Sending requires choosing which type.","covers":["Single type send","Insufficient balance"],"include":["Type selector","Amount","Recipient","Preview"]}],"mentalModel":[{"label":"Token type ID","description":"Each id is a distinct balance lane inside one contract address."},{"label":"Amount approval","description":"Spender can move up to N of one type — like ERC-20 allowance."},{"label":"Operator approval","description":"Spender can move all types — like ERC-1155 setApprovalForAll."},{"label":"Single contract","description":"One address on chain; UI must label types clearly to avoid confusion."},{"label":"No callbacks","description":"Transfers won't auto-notify receivers — apps track balance via events."}],"statesToDesign":[{"state":"Sufficient balance — type selected","trigger":"User picks type and amount.","userNeed":"Confirm correct type before send.","designResponse":"Type name prominent in confirmation; id in advanced."},{"state":"Approval required","trigger":"Spender lacks allowance.","userNeed":"Grant minimum needed access.","designResponse":"Suggest exact amount approval, not operator by default."},{"state":"Operator fully approved","trigger":"User previously granted operator.","userNeed":"Audit and revoke if unintended.","designResponse":"Show in permissions dashboard with high-risk badge."},{"state":"Insufficient type balance","trigger":"Amount exceeds id balance.","userNeed":"Fix amount without guessing.","designResponse":"Inline max available for this type."},{"state":"Revoked approval","trigger":"User revokes spender.","userNeed":"Confirm app may break until re-approved.","designResponse":"Success toast with Re-approve if needed link."}],"designDecisions":[{"question":"Expose token type ids to users?","recommendation":"Show friendly names; tuck numeric id behind advanced.","rationale":"Ids are implementation detail unless debugging."},{"question":"Default approval style?","recommendation":"Amount approval per action; operator requires extra confirmation.","rationale":"Operator equals unlimited across types — high risk."},{"question":"Use batch transfer UI?","recommendation":"Skip unless contract exposes batch; prefer simple single-type flows.","rationale":"6909 value is simplicity over 1155 batch complexity."}],"problemsSolved":[{"problem":"ERC-1155 approval UX too coarse","oldWay":"setApprovalForAll for entire contract","newWay":"Per-type amount allowances like ERC-20","impact":"high"},{"problem":"Multiple contracts clutter wallet","oldWay":"One ERC-20 per game currency","newWay":"Single 6909 contract with typed balances","impact":"medium"},{"problem":"Callback gas and reentrancy concerns","oldWay":"ERC-1155 onReceived hooks","newWay":"No mandatory callbacks — simpler transfer path","impact":"medium"}],"uxPatterns":[{"name":"Per-Type Allowance Row","description":"ERC-20-style allowance display per token id.","components":["AllowanceRow","RevokeButton","AmountLabel"],"userFlow":["User opens permissions","Sees per-type allowances","Revokes or edits","Spender updated"]},{"name":"Type Picker Transfer","description":"Send flow with explicit token type selection.","components":["TypeSelector","AmountInput","ConfirmSheet"],"userFlow":["User taps Send","Picks type","Enters amount","Confirms"]}],"antiPatterns":[{"pattern":"Defaulting to operator approval","why":"Grants unlimited access across all types","instead":"Default amount approval with scary operator path","severity":"critical"},{"pattern":"Showing only contract address without type","why":"Users send wrong token type","instead":"Type name in every confirmation step","severity":"high"},{"pattern":"ERC-1155 batch UI on 6909 contract","why":"Unnecessary complexity","instead":"Simple single-type transfer and approve flows","severity":"medium"}],"vocabulary":[{"use":"Token type","avoid":"Token id 8472","why":"Friendly naming unless advanced view."},{"use":"Spending limit","avoid":"Allowance mapping","why":"ERC-20 familiar terms."},{"use":"Manage all types","avoid":"Operator set true","why":"Plain permission language."}],"technicalNotes":"ERC-6909 uses per-id allowances; treat operator approval as high-risk unlimited grant.","relatedStandards":[{"id":"ERC-1155","relationship":"Multi-token alternative with callbacks"},{"id":"ERC-20","relationship":"Approval mental model per token type"}]},"sources":[{"label":"Official specification","url":"https://ercs.ethereum.org/ERCS/erc-6909","type":"official-spec"},{"label":"Discussion search","url":"https://ethereum-magicians.org/search?q=ERC-6909","type":"discussion"}],"urls":{"canonical":"https://www.eipsfordesigners.com/standards/ERC-6909","markdown":"https://www.eipsfordesigners.com/standards/ERC-6909/content.md","agent":"https://www.eipsfordesigners.com/standards/ERC-6909/agent.md","api":"https://www.eipsfordesigners.com/api/standards/ERC-6909","official":"https://ercs.ethereum.org/ERCS/erc-6909","discussion":"https://ethereum-magicians.org/search?q=ERC-6909"},"freshness":{"lastReviewed":"2026-09-18","lastUpdated":"2026-09-18"}}