ERC-6982: Default Lockable Tokens
ERC-6982 standardises a lightweight lock state for NFTs: a tokenId can be locked (transfers blocked) or unlocked (transfers allowed), without committing to a specific lock-controller pattern. The collection-wide default lock state is set at deploy; per-token overrides flip individual tokens. Used for:
- Staking — locked while staked, unlocked when withdrawn.
- Escrow — locked during a deal, unlocked on settlement.
- Soulbinding-by-default — collections that mint locked, unlock on redemption (lighter than ERC-5192 / ERC-5484 since the lock can be flipped both ways).
- Lending collateral — locked while collateralising a loan, unlocked on repayment.
ERC-6982 is intentionally narrower than ERC-5192 (binary soulbound) and ERC-6147 (guard delegate). It just exposes the lock-state predicate; the who controls the lock is an implementation choice.
Required Interface
interface IERC6982 {
event DefaultLocked(bool locked);
event Locked(uint256 tokenId, bool locked);
function defaultLocked() external view returns (bool);
function locked(uint256 tokenId) external view returns (bool);
}The contract emits DefaultLocked(true) once at construction (or at init for proxies) so off-chain indexers can pre-compute "all unminted tokens are locked". Per-token Locked(tokenId, false) events flip individual entries.
Neo Equivalent: NEP-11 + Lock-State Storage
The Neo port adds a per-token lock-state storage prefix and a contract- level default-lock flag, plus a Transfer override that enforces the lock predicate. Storage layout:
Prefix_DefaultLock(single byte) — collection-wide default.Prefix_Lock+tokenId— per-token override (presence = override applies; value = locked/unlocked bit).
public static bool Locked(ByteString tokenId)
{
var override_ = Storage.Get(Storage.CurrentContext, LockKey(tokenId));
return override_ is null
? DefaultLocked()
: ((BigInteger)override_) == 1;
}| ERC-6982 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
defaultLocked() view | DefaultLocked() view returning the single-byte storage value | Set once at deploy |
locked(tokenId) view | Locked(tokenId) view honouring per-token override + default | Direct port |
DefaultLocked(bool) event | OnDefaultLocked(bool) notification, fired once at deploy | |
Locked(tokenId, bool) event | OnLocked(tokenId, bool) notification per flip | |
| Transfer-blocking enforcement | Transfer override checks Locked(tokenId) and reverts if locked | NEP-11 base hook |
Lock-Controller Pattern
ERC-6982 doesn't standardise the controller; common patterns:
- Owner-controlled — token holder can lock/unlock their own NFT.
- Issuer-controlled — collection issuer locks/unlocks (escrow, KYC freeze).
- External-contract-controlled — a staking contract calls
Lock(tokenId)when receiving deposits;Unlock(tokenId)on withdrawal. - Multi-controller — multiple addresses approved to flip locks (e.g. lending protocol + insurance contract).
The contract author picks the model and exposes it via a SetLock(tokenId, bool) method gated on whichever witness check is appropriate. ERC-6982's interface only covers the read path; the write path is application-specific.
Composition
- ERC-5192 — minimal soulbound. Locked = soulbound; ERC-6982 is a strict superset that allows unlocking.
- ERC-5484 — consensual soulbound (with
BurnAuth). Use 6982 for collections where holders should be able to opt back out of soulbinding. - ERC-6147 — NFT guard delegate. Heavier than 6982; use when third-party guard authorisation is needed.
- ERC-4907 — rental NFT. Locks tend to apply during active rentals —
Lockedreturns true iff the user role is set and not expired.
Migration Notes
For Solidity collections using ERC-6982:
- Direct port: per-token lock storage + override predicate; trivial in NEP-11 base.
- Combined with rental (ERC-4907): make
Locked()resolvetruewhenever the user role is active. - Combined with staking external contract: expose
LockBy(tokenId, controller)so the staking contract can claim a lock without becoming the NFT owner; release onUnlockBy(tokenId, controller).
