Start Building
NFT Shadows

Building with NFT Shadows

NFT Shadows enable cross-chain NFT ownership through a novel mechanism that replicates ownership states across chains. This allows developers to build applications that interact with NFT ownership on any supported chain, whether it's the NFT's native chain or a shadow chain.

Shadow NFTs maintain ownership consistency across chains through the Beacon contract's cross-chain messaging system.

Core Components

    • Beacon.sol
    • ShadowNFT.sol
    • IShadowCallbackReceiver.sol
  • The Beacon

    The Beacon contract coordinates cross-chain messaging and ownership verification. It:

    • Manages LayerZero messaging configuration
    • Handles NFT locking/unlocking on native chains
    • Coordinates ownership reads across chains
    • Maintains shadow-to-base collection mappings

    Shadow NFTs

    Shadow NFTs are ERC721-compatible tokens that mirror ownership from their native chain. They have two states:

    • Locked: Can only be transferred by the Beacon contract, follows canonical ownership
    • Unlocked: Behaves like a standard ERC721, can be transferred freely

    Integration Patterns

    Native Chain Integration

    When building on the native chain where the original NFTs exist, you can leverage the delegation system to enable cross-chain ownership control. Here's an example rental system:

    contract NFTRental {
        IExclusiveDelegateResolver public resolver;
        IDelegateRegistry public registry;
     
        mapping(uint256 => uint40) public tokenIdToExpiration;
        
        function rentOut(
            address nft, 
            uint256 tokenId, 
            uint40 duration
        ) external {
            // Verify contract owns the NFT
            require(IERC721(nft).ownerOf(tokenId) == address(this), "Contract does not own NFT");
     
            // Verify NFT is not already rented out
            require(tokenIdToExpiration[tokenId] < block.timestamp, "NFT is already rented out");
            
            // Calculate expiration timestamp
            uint40 expiration = uint40(block.timestamp) + duration;
            
            // Generate delegation rights with expiration
            bytes32 rights = resolver.generateRightsWithExpiration(
                bytes24(0), // rights identifier
                expiration
            );
            
            // Issue delegation to renter
            registry.delegateERC721(
                renter,
                nft,
                tokenId,
                rights,
                true // enable delegation
            );
     
            tokenIdToExpiration[tokenId] = expiration;
        }
    }

    Shadow Chain Integration

    When building on chains where Shadow NFTs exist, you have two approaches for ownership verification:

    1. Optimistic Ownership Checks

    Best for non-critical operations where slight ownership lag is acceptable:

    contract OptimisticIntegration {
        INFTShadow public shadowNft;
        
        function doSomethingWithNFT(uint256 tokenId) external {
            // Simple ownership check
            require(shadowNft.ownerOf(tokenId) == msg.sender, "Not owner");
            
            // Proceed with operation...
        }
    }
    ⚠️

    Optimistic checks should only be used for low-risk operations as there may be a slight delay in ownership updates across chains.

    2. Verified Ownership with Callbacks

    For operations requiring guaranteed up-to-date ownership:

    contract VerifiedIntegration is IShadowCallbackReceiver {
        INFTShadow public shadowNft;
        IBeacon public beacon;
        
        struct PendingOperation {
            address caller;
            uint256 tokenId;
        }
        
        mapping(bytes32 => PendingOperation) public pendingOperations;
        
        function doSomethingWithVerifiedOwnership(uint256 tokenId) external payable {
            uint256[] memory tokenIds = new uint256[](1);
            tokenIds[0] = tokenId;
            
            uint32[] memory eids = new uint32[](1);
            eids[0] = sourceChainEid;
            
            uint256 fee = beacon.quoteRead(
                address(shadowNft),
                tokenIds,
                eids,
                60000 // callback gas limit
            );
            require(msg.value >= fee, "Insufficient fee");
            
            bytes32 guid = shadowNft.readWithCallback{value: fee}(
                tokenIds,
                eids,
                60000
            );
            
            pendingOperations[guid] = PendingOperation({
                caller: msg.sender,
                tokenId: tokenId
            });
        }
    }

    Error Handling and Edge Cases

    Read Request Failures

    • If a read request fails, the callback will not be executed
    • Operations should be designed to be idempotent
    • Consider allowing operations to be cancelled after a timeout period

    Ownership Transitions

    • Shadow ownership can change between read request and callback
    • Implement appropriate checks in callback handler
    • Consider if operation should still proceed if ownership changed

    Gas Considerations

    readWithCallback accepts a callbackGasLimit parameter that is used to estimate the gas limit for the callback. This is added as a buffer to the predicted lzReceive gas cost.

    // Calculate gas limit based on operation complexity
    uint128 baseGas = 60000; // example base gas limit that callback requires to execute
    uint128 perNftGas = 15000; // example gas for operations per NFT
    uint128 callbackGasLimit = baseGas + (perNftGas * tokenIds.length);

    Best Practices

    1. Ownership Verification

      • Use optimistic checks for low-risk operations
      • Implement callbacks for high-value transactions
      • Consider implementing timeout mechanisms
    2. Gas Management

      • Scale callback gas limits with operation complexity
      • Include buffer for unexpected gas costs
    3. State Management

      • Store minimal data in pending operation mappings
      • Clean up state after callback execution
      • Implement operation timeout cleanup

    Contracts:

    ContractNetworkAddress
    BAYC ShadowApeChain0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d (opens in a new tab)
    MAYC ShadowApeChain0x60e4d786628fea6478f785a6d7e704777c86a7c6 (opens in a new tab)
    BAKC ShadowApeChain0xba30e5f9bb24caa003e9f2f0497ad287fdf95623 (opens in a new tab)