Shadow Delegation
Implement non-custodial NFT delegation using Delegate Registry v2 and the shadow delegation pattern, enforced via Yuga Labs’ ExclusiveDelegateResolver.
In Production: How Yuga Uses Shadow Delegation
Yuga Labs uses shadow delegation to let holders of Ape NFTs on Ethereum delegate access to a wallet on ApeChain. This allows users to verify ownership and participate in ApeChain experiences without bridging or moving their Apes.
By combining Delegate Registry v2
and ExclusiveDelegateResolver
, Yuga enables onchain delegation that’s:
Non-custodial: the NFT stays in the original wallet
Cross-chain: access is granted to a different wallet on another chain
Exclusive: only one delegate per token is valid at a time
Infinite Use Cases
Delegate Registry v2
provides the primitives for onchain NFT access control, forming the base layer upon which shadow delegation builds customizable, scoped, and enforceable delegation logic.
Use cases include:
Assign agents to act on behalf of wallets across chains
Enable wearables, emotes, or other metadata-bound behaviors
Grant permissions in identity systems, social graphs, or gaming avatars
Build composable avatars and modular inventories
Enforce execution rights on specific chains or applications
Core Concepts
1) Delegate Registry v2
Delegate Registry v2 is an onchain registry that allows users to grant permission to other addresses to act on their behalf. It serves as a data layer, returning a boolean for whether a delegation exists.
Delegations can be scoped:
to specific tokens via
checkDelegateForERC721
to particular contracts
checkDelegateForContract
across all tokens and contracts
checkDelegateForAll
The registry does not enforce rules like exclusivity, expiration, or precedence. It simply stores data.
ExclusiveDelegateResolver
, developed by Yuga Labs, is responsible for interpreting that data and enforcing logic.
2) Resolver Contracts (ExclusiveDelegateResolver)
Resolver contracts apply custom logic to interpret delegation records by answering:
"Who currently has the right to act on this token, under these rules?"
Yuga Labs’ ExclusiveDelegateResolver
is used in production (e.g. Otherside, ApeChain) to enforce exclusive, scoped, onchain delegation, letting one wallet act on behalf of another without moving the NFT. It scans delegations scoped to a rightsNamespace
and applies specificity rules (token > contract > all-assets) to return the active delegate.
3) The Shadow Delegation Pattern
Shadow delegation is a design pattern that combines Delegate Registry v2
with a resolver like ExclusiveDelegateResolver
to enable non-custodial, exclusive delegation, scoped by use case and enforced onchain.
In this pattern:
Only one delegation per token (or namespace) should be valid at a time
Previous delegates must be explicitly revoked before a new one is assigned
Asset ownership remains with the original holder; no escrow or transfers
This pattern works by combining scoped delegation rights with onchain resolution logic, illustrated below.
How to Implement It
Shadow delegation follows a simple 3-step flow:

Delegate access to a token using
delegateERC721()
Resolve the active delegate using
ExclusiveDelegateResolver
Enforce the delegation with an onchain check (e.g.
require(msg.sender == delegate)
)
Step 1: Delegate an NFT via Delegate Registry v2
The owner calls delegateERC721()
on the registry to assign a delegate for a specific NFT:
delegateERC721(
address delegate,
address contract_,
uint256 tokenId,
bytes32 rights,
bool value
);
delegate
: The address receiving the delegation.contract_
: The NFT contract address.tokenId
: The specific token ID to delegate.rights
: Abytes32
value representing delegation scope.value
: Set totrue
to enable, orfalse
to revoke.
In the shadow delegation pattern, you must scope delegation to a unique chain or context by computing a rights
value. Use the following utility:
import { encodeAbiParameters, keccak256 } from 'viem';
function computeShadowRights(shadowChainId?: number): string {
if (!isShadowDelegation) return '';
if (!shadowChainId) return '0x000000000000000000000000000000000000000000000000000000ffffffffff';
const encodedData = encodeAbiParameters([{ type: 'uint' }], [BigInt(shadowChainId)]);
const hash = keccak256(encodedData);
const namespace = hash.slice(0, 50);
const padding = '000000';
const maxRights = (BigInt(2) ** BigInt(40) - BigInt(1)).toString(16).padStart(10, '0');
const rights = namespace + padding + maxRights;
return rights;
}
⚠️ Revoke Before Reassigning
If you're delegating the same token (and rights) to a new address, you must first revoke the previous delegate using delegateERC721(..., false)
. Otherwise, the storage slot is overwritten, and the resolver may return an outdated result.
💡 Use multicall()
to Minimize UX & Gas
Apps can batch the revocation and reassignment into a single transaction using the registry’s multicall()
method to reduce friction and cost.
🛠️ See It Live Yuga Labs uses this pattern in production. See how their Shadow Beacon contract performs explicit revocation before reassignment (lines 933–991).
How Rights are Constructed and Resolved
The rights
field is a 32-byte (bytes32
) value that encodes the scope and expiry of a delegation. This is how the resolver knows what context the delegation applies to, how specific it is, and whether it’s still valid.
0–23
rightsNamespace
Used to scope delegations (e.g., by chain or feature)
24–28
Reserved padding (000000
)
Required by resolver formatting
29–33
uint40 expiry
Unix timestamp after which delegation is invalid
Resolver Behavior:
Chain-specific scoping: Namespaces are typically derived from
keccak256(chainId)
so you can delegate the same token to different wallets on different chains. These are resolved independently.Global fallback delegation: If the first 24 bytes are zero, i.e.,
0x000...000
, the delegation is considered global, valid across all chains or apps. These are ranked lowest in specificity and only take effect if no scoped delegation exists.Automatic expiry: The final 5 bytes of
rights
encode an expiry timestamp (uint40
). When the current block time exceeds this value, the delegation is considered invalid. If you don't want an expiry, use the max value:0xffffffffff
.
This layered resolution system allows you to combine scoped, exclusive, expiring, and cross-chain delegation behavior.
Step 2: Use the ExclusiveDelegateResolver
Once delegation is recorded, you can resolve it using ExclusiveDelegateResolver
, which applies namespace-based resolution logic (token > contract > all-assets) and scopes based on the first 24 bytes of the rights
field. This means a token-level delegation takes priority over a contract-level one under the same namespace.
Resolver interface:
function exclusiveOwnerByRights(
address nftContract,
uint256 tokenId,
bytes24 rightsNamespace
) external view returns (address);
Example usage:
bytes24 rightsNamespace = bytes24(keccak256(abi.encode(shadowChainId)));
address delegate = IExclusiveDelegateResolver(resolverAddress).exclusiveOwnerByRights(
nftContractAddress,
tokenId,
rightsNamespace
);
This reinforces the resolver concept from Core Concept #2. The registry stores raw data and the resolver interprets it according to a rule set (in this case, exclusivity via specificity).
Step 3: Enforce Delegation in Your App or Smart Contract
Whenever your application or NFT/game logic needs to check control, replace ownerOf(tokenId)
or raw ownership checks with delegation resolution:
require(msg.sender == delegate, "Not authorized delegate");
This enforces non-custodial, exclusive access via the resolver logic, honoring only the delegate for the scoped rights.
Resources
Delegate Registry v2: https://github.com/delegatexyz/delegate-registry-v2
ApeChain Shadow Delegations: https://docs.apechain.com/start-building/NFT-Shadows
Yuga’s Exclusive Resolver: https://github.com/yuga-labs/ExclusiveDelegateResolver
Summary
Shadow delegation
with Delegate Registry v2
and ExclusiveDelegateResolver
enables:
Fully onchain delegation logic
Exclusive, non-custodial access
Use case-specific scoping via
rightsNamespace
Developers can build on this pattern by customizing how delegations are scoped and managed, without needing to deploy their own resolver contract.
Last updated