ERC-7066: Lockable Extension for ERC-721
ERC-7066 adds an approval-based lock mechanism to ERC-721. Where ERC-6982 gives each NFT a default-locked / per-token override flag controlled by the holder, ERC-7066 lets the holder delegate lock authority to an external contract or operator (typically a staking contract, escrow contract, or rental marketplace). The locker can lock and unlock the NFT; the owner cannot transfer while locked. On unlock or NFT transfer, the locker permission is preserved or cleared per the lock type.
Key distinction:
- ERC-6982 = "owner controls the lock state directly".
- ERC-7066 = "owner delegates lock authority to an approved locker" (e.g. a staking contract that locks the NFT for the staking duration).
Required Interface
interface IERC7066 {
enum LockType { OWNER_LOCK, APPROVED_LOCK }
event Locked(uint256 indexed tokenId, address indexed locker, LockType lockType);
event Unlocked(uint256 indexed tokenId);
function lock(uint256 tokenId) external; // OWNER_LOCK by holder
function lock(uint256 tokenId, address locker) external; // APPROVED_LOCK by holder, delegating
function unlock(uint256 tokenId) external; // by current locker
function locker(uint256 tokenId) external view returns (address);
function lockType(uint256 tokenId) external view returns (LockType);
function isLocked(uint256 tokenId) external view returns (bool);
}Neo Equivalent: NEP-11 + Approval-Based Lock Storage
The Neo port adds two storage slots per locked token:
Prefix_Locker+tokenId— the address allowed to unlock.Prefix_LockType+tokenId—OWNER_LOCKorAPPROVED_LOCK.
The base NEP-11 Transfer override checks IsLocked(tokenId) and reverts when locked. Lock owner can lock OR delegate to an external contract; the external contract is the only one that can unlock.
public static void Lock(ByteString tokenId, UInt160 locker)
{
var owner = OwnerOf(tokenId);
if (!Runtime.CheckWitness(owner)) throw new Exception("NEP11:NotOwner");
if (Locker(tokenId) is not null) throw new Exception("NEP11:AlreadyLocked");
Storage.Put(Storage.CurrentContext, LockerKey(tokenId), locker);
Storage.Put(Storage.CurrentContext, LockTypeKey(tokenId), locker.Equals(owner) ? 0 : 1);
OnLocked(tokenId, locker, locker.Equals(owner) ? 0 : 1);
}
public static void Unlock(ByteString tokenId)
{
var locker = Locker(tokenId);
if (locker is null) throw new Exception("NEP11:NotLocked");
if (!Runtime.CheckWitness(locker)) throw new Exception("NEP11:NotLocker");
Storage.Delete(Storage.CurrentContext, LockerKey(tokenId));
Storage.Delete(Storage.CurrentContext, LockTypeKey(tokenId));
OnUnlocked(tokenId);
}
// Nep11Token<T> has no virtual hook; redefine Transfer with `public new static`.
public new static bool Transfer(UInt160 to, ByteString tokenId, object data = null)
{
if (IsLocked(tokenId)) throw new Exception("NEP11:Locked");
return Nep11Token<TokenState>.Transfer(to, tokenId, data);
}| ERC-7066 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
lock(tokenId) | Lock(tokenId, owner) (OWNER_LOCK) | Holder locks for themselves |
lock(tokenId, locker) | Lock(tokenId, locker) (APPROVED_LOCK) | Holder delegates to a contract |
unlock(tokenId) | Unlock(tokenId) checked against current locker | Locker is sole un-locker |
locker(tokenId) | Locker(tokenId) view | Returns current locker or null |
isLocked(tokenId) | IsLocked(tokenId) view | Truthy iff locker is set |
| Transfer-block enforcement | OnBeforeTransfer override checks IsLocked | NEP-11 base hook |
Comparison With ERC-6982 + ERC-6147
- ERC-6982 (Default Lockable) — owner-controlled binary state with collection-wide default. Cheap, holder-only.
- ERC-7066 (Lockable Extension) — approval-based; locker can be external. Fits staking / escrow / rental patterns.
- ERC-6147 (NFT Guard) — generalised guard delegate with configurable allowed actions. Heavier than 7066.
For most "stake an NFT" / "escrow this NFT" use cases, ERC-7066 is the right shape. For "let an arbitrary delegate authorise multiple action types", reach for ERC-6147.
Composition
- ERC-6982 — default lockable. Use 6982 for collection-wide defaults, 7066 for per-token delegation.
- ERC-4907 — rental NFT. Pair: rental contract is the locker; lock on rent, unlock on rental expiry / return.
- ERC-2981 — royalties. Lock during a sale escrow; unlock and pay royalty on completion.
Migration Notes
For staking / escrow contracts that want to immobilise NFTs without taking custody:
- The contract calls
Lock(tokenId, address(this))to take lock authority. - The NFT stays in the holder's wallet (better UX, no custody transfer).
- On unstake / release, the contract calls
Unlock(tokenId).
This is strictly better than the alternative of transferring custody to the staking contract and back — no transfer fees, no risk of contract bugs trapping the NFT.
