ERC: Slot-Based Equipment for ERC-6551 Token Bound Accounts
been working on an equipment interface for 6551 accounts, not here to put labels on what it should or shouldn’t be used for but the core problem felt obvious enough to write a standard around it. nine functions, three events, one struct. no catalog infrastructure, no framework dependency, just the primitive.
PR | Reference Implementation | Live Frontend
Abstract
a standard interface (IERC6551Equipment) for managing slot-based equipment within ERC-6551 Token Bound Accounts. tokens (ERC-721 or ERC-1155) can be equipped into application-defined slots, queried as a structured loadout, and optionally permanently locked, i.e. enabling on-chain identity, provenance, and composable character systems.
ERC-165 interface ID: 0xd38f0891
Motivation
6551 gives NFTs wallets but says nothing about what goes inside them, and every project that wants characters with loadouts ends up writing their own slot logic from scratch, none of it compatible with anything else. been stress testing this framework for a while now through what felt like an infinite iteration process and it felt right to formalize a standard around it; the interoperability part only works if everyone’s equipment interface is compatible, that’s what this solves.
use cases this enables:
-
gaming: characters with equippable gear slots where identity traits are permanently locked at mint and fashion items are freely swappable
-
social/identity: profiles with locked verification credentials and swappable display badges
-
art: composable NFTs with locked base layers and swappable frames or accessories
-
marketplaces: universal loadout display via a single
getLoadout()call, distinguishing permanent traits from tradeable gear
Specification
Interface
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.24;
interface IERC6551Equipment {
struct SlotEntry {
bytes32 slotId;
address tokenContract;
uint256 tokenId;
uint256 amount;
bool locked;
}
event Equipped(bytes32 indexed slotId, address indexed tokenContract, uint256 indexed tokenId, uint256 amount);
event Unequipped(bytes32 indexed slotId, address indexed tokenContract, uint256 indexed tokenId, uint256 amount);
event SlotLocked(bytes32 indexed slotId, address indexed tokenContract, uint256 tokenId);
function equip(bytes32 slotId, address tokenContract, uint256 tokenId, uint256 amount) external;
function unequip(bytes32 slotId) external;
function lockSlot(bytes32 slotId) external;
function equipBatch(bytes32[] calldata slotIds, address[] calldata tokenContracts, uint256[] calldata tokenIds, uint256[] calldata amounts) external;
function lockSlots(bytes32[] calldata slotIds) external;
function getEquipped(bytes32 slotId) external view returns (address tokenContract, uint256 tokenId, uint256 amount);
function getLoadout() external view returns (SlotEntry[] memory entries);
function isSlotOccupied(bytes32 slotId) external view returns (bool);
function isSlotLocked(bytes32 slotId) external view returns (bool);
}
Slot Identifiers
slots are bytes32: applications define their own taxonomy via keccak256("slot.head"), keccak256("slot.weapon"), whatever makes sense for the context. no central registry, no opinion on what slots should exist; the standard defines the mechanism not the taxonomy.
applications sharing a TBA across multiple contexts SHOULD namespace slots to avoid collisions, e.g. keccak256("myapp.slot.head") vs keccak256("otherapp.slot.head").
Behavior
equip: MUST revert if caller is not a valid signer, slot is occupied, slot is locked, or amount is zero. MUST update state before external transfers (CEI). MUST transfer the token into the TBA via safeTransferFrom. MUST emit Equipped event.
unequip: MUST revert if caller is not a valid signer, slot is empty, or slot is locked. MUST delete state before external transfers (CEI). MUST transfer the token back to the caller. MUST emit Unequipped event.
lockSlot: MUST revert if caller is not a valid signer, slot is empty, or already locked. MUST set slot as permanently locked. this is irreversible, there is no unlockSlot. MUST emit SlotLocked event.
equipBatch: MUST revert if array lengths do not match or any individual equip would revert. all-or-nothing semantics. MUST emit Equipped for each slot.
lockSlots: MUST revert if any individual lock would revert. all-or-nothing semantics. MUST emit SlotLocked for each slot.
getEquipped: returns token contract, token ID, and amount for a given slot. returns (address(0), 0, 0) if empty.
getLoadout: returns an array of SlotEntry structs for every occupied slot. lock status included via bool locked. returns empty array if nothing equipped.
isSlotOccupied / isSlotLocked: boolean views. a locked slot is always occupied, i.e. you cannot lock an empty slot.
Ownership Transfer Semantics
when the parent NFT is transferred to a new owner:
-
all equipped items remain in the TBA
-
unlocked slots can be modified by the new owner (equip/unequip)
-
locked slots remain locked forever: the new owner inherits the locks
-
the new owner CAN equip new items to empty slots and lock additional unlocked slots
i.e. a character’s identity (locked traits) travels with it across sales, while the new owner can customize the character’s gear (unlocked slots).
Rationale
the design decisions that survived the iteration process and why they matter:
locking is permanent: this one i care about a lot. lockSlot() is irreversible, no unlock, no timelock, no admin override. if isSlotLocked() returns true it returns true forever, across ownership transfers, across time. the new owner inherits the lock. this is the whole point: provable on-chain immutable identity. you lock your character’s core traits at mint and they become the character forever. if you want something swappable, don’t lock it.
CEI is mandatory not recommended: equip() and unequip() trigger safeTransferFrom which invokes receiver callbacks; the spec requires state updates before external calls. went through the process of catching this in our own implementation and fixing it, felt important enough to make it a MUST in the spec rather than leaving it as a suggestion.
getLoadout() returns everything in one call: all occupied slots, lock status included in the struct via bool locked. frontends and marketplaces get the complete picture without N separate view calls. this was a deliberate struct change, adding locked to SlotEntry so one call gives you the full character.
batch operations exist because mint flows demand it: equipping 12 traits and locking 5 identity slots shouldn’t cost 17 transactions. equipBatch() + lockSlots() brings it down to 2. all-or-nothing semantics matching the pattern from 1155.
what intentionally doesn’t exist and why:
-
no
unlockSlot(): permanence is the feature not a limitation -
no
swap(): unequip then equip, multicall viaexecute()if you need atomicity -
no
getLockedSlots():getLoadout()covers it, events handle indexing -
no opinion on which slots should exist or what they mean
Prior Art
aware of ERC-6220 (composable NFTs utilizing equippable parts) from RMRK. different approach, different scope. 6220 extends ERC-721 directly, requires a Catalog system with pre-defined parts, equippable address whitelists, and 5+ interfaces across the RMRK suite; designed for visual composability, i.e. assembling rendered images from component parts. this ERC extends ERC-6551 specifically, requires zero extra infrastructure beyond the TBA itself, and focuses on ownership composability: tokens physically transfer into the character’s wallet. the key primitive that doesn’t exist in 6220 or anywhere else i’ve found is permanent slot locking.
the question of equipping on 6551 has come up before in the original ERC-6551 discussion thread (post #128) where 6220 was suggested as a solution. 6220 doesn’t extend 6551: it extends 721 directly and requires the RMRK catalog infrastructure. this ERC is the native answer to that question.
EIP-4973 (account-bound tokens) uses “equip/unequip” terminology but in a different context: showing or hiding a soulbound credential, not placing transferable tokens into named slots. 4973 makes the token immovable. this ERC makes the slot immovable: different primitive, different use case.
ERC-7635 (multi-fungible token) proposes a new token standard with built-in slots but replaces the existing token model entirely. this ERC works within the existing 721 + 1155 + 6551 ecosystem: no new token type, just an interface for what’s already deployed.
ERC-998 (composable top-down NFTs) predates 6551, handles parent-child nesting but doesn’t define named slots, lock semantics, or batch operations. ERC-4883 (composable SVG NFTs) is visual-only with no slot logic.
if i’m missing prior art that covers this exact surface, i.e. slot-based equipment with permanent locking for TBAs, genuinely want to know.
Backwards Compatibility
no backwards compatibility issues. extends ERC-6551 Token Bound Accounts with new functionality. existing TBAs are unaffected. accounts implementing this interface are fully compatible with ERC-721 and ERC-1155 token standards.
Reference Implementation
27 foundry tests covering core flow, locking, batch ops, ownership transfer, CEI verification, and ERC-165. deployed on Base Sepolia with a live frontend consuming the interface at phantoma.io: demonstrates getLoadout() rendering with lock status display, equipment management across both injected and embedded wallet providers, and supportsInterface detection.
Security Considerations
checks-effects-interactions: equip(), unequip(), and equipBatch() involve external calls via safeTransferFrom which invoke receiver callbacks. implementations MUST update slot state before performing any external token transfers.
irreversible locking: lockSlot() is permanent and cannot be undone. frontends SHOULD present a clear confirmation dialog. tokens locked in slots are permanently committed to the TBA.
batch atomicity: equipBatch() and lockSlots() use all-or-nothing semantics. implementations MUST validate all array lengths match before processing.
ownership transfer: when a parent NFT is transferred, the new owner gains control of all unlocked slots but inherits all locks. buyers SHOULD inspect the loadout before purchasing.
ERC-165 detection: marketplaces and contracts SHOULD call supportsInterface(0xd38f0891) to verify equipment support before making equipment-related calls.
Copyright
Copyright and related rights waived via CC0.