Try   HackMD

Basic NFT Vault with Transfer Spec

tags: NFTs Aztec Connect Solidity

Spec

  1. What does this bridge do? Why did you build it?

This bridge allows users to deposit and withdraw NFTs to/from Aztec Connect as well as send their NFTs to other bridge contracts. A user may want to send their NFT to another bridge contract because it may have additional functionality (swapping, minting, etc).

  1. What protocol(s) does the bridge interact with?

ERC-721 contracts on Etheruem.

ERC-721 contracts are the most widely used NFT standard. A bridge that supports 721 contracts should be able to handle most NFTs.

  1. What is the flow of the bridge?

    • What actions does this bridge make possible?
      • Depositing NFTs to Aztec from an Ethereum account
      • Withdrawing NFTs from Aztec to an Ethereum account
      • Transfering NFTs around the Aztec network, anonymously
    • Are all bridge interactions synchronous, or is the a asynchronous flow?
      • all interactions are synchronous
    • For each interaction define:
      • All input + output tokens, including AztecType info
        • Deposit:
          • input: ETH
          • output: VIRTUAL
        • Withdrawal:
          • input: VIRTUAL
          • output: ETH (not used, == 0)
        • Transfer
          • input: VIRTUAL
          • output: VIRTUAL
      • All relevant bridge address ids (may fill in after deployment)
      • use of auxData
      • gas usage
        • TBD
      • cases that would make the interaction revert (low liquidity etc)
        • Eth address to withdraw to is not registered
    • Please include any diagrams that would be helpful to understand asset flow.
  2. Please list any edge cases that may restrict the usefulness of the bridge or that the bridge prevents explicit.

As written, this bridge has very little functionality. We will think about how to extend this functionality in future NFT specs.

Providing privacy for NFTs is also tricky because each NFT is unique. Aztec users typically get privacy by hiding a crowd of users, but with NFTs the size of the crowd is 1.

  1. How can the accounting of the bridge be impacted by interactions performed by other parties than the bridge? Example, if borrowing, how does it handle liquidations etc.

  2. What functions are available in /src/client? How should they be used?

  • getAuxData
    • should return the range of registered eth addresses for withdrawal
    • or
    • should return the range of whitelisted bridge contracts
  • getExpectedOutput
    • this should always return 1 output for deposits
    • should always return 0 outputs for withdrawals
    • should always return 1 output for transfers
  • getInteractionPresentValue
    • this should always return 1 for a VIRTUAL asset that is mapped to an NFT
  1. Is this contract upgradable? If so, what are the restrictions on upgradability?

No

  1. Does this bridge maintain state? If so, what is stored and why?

Yes. The bridge stores a few things in state

  • Nft Assets
    • These are structs that map an nft id (unique to the contract) to the corresponding NFT info (collection, tokenId, virtual asset id)
  • whitelist
  1. Any other relevant information

NftVault.sol

Structs

Nft data is stored in a NftAsset struct

struct NftAsset {
    address collection;
    uint256 tokenId;
}

Mappings

// virtual asset id => NftAsset
mapping(uint256 => NftAsset) public nftAssets;

Functions

convert

Cases:

  • deposit
    • Assets: (ETH in (1 wei), VIRTUAL out)
    • auxData: none
  • withdraw
    • Assets: (VIRTUAL in, ETH (not used) out)
    • auxData: withdraw address id, 64 bits?
  • transfer
    • assets:
      • VIRTUAL in
      • VIRUTAL out
    • auxData
      • id of the bridge contract to send the nft to
// DEPOSIT
// return virutal asset id, will not actually match to NFT until matchDeposit is called from ethereum
if(
   _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH &&
   _outputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL
) {
    require(_totalInputValue == 1, "send only 1 wei");
    tokens[_interactionNonce] = NftAsset({
        collection: address(0x0),
        id: 0
    });
    return (1, 0, false); 
}


// WITHDRAW
else if (
    _inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL &&
    _outputAssetA.assetType == AztecTypes.AztecAssetType.ETH
) {
    NftAsset token = tokens[inputAssetA.id];
    require(token.collection != address(0x0), "NFT doesn't exist");

    address _to = registry.addresses(_auxData);
    require(_to != address(0x0), "unregistered withdraw address");

    NFT(token.collection).transferFrom(this, _to, token.id);
    delete tokens[inputAssetA.id];
    return (0, 0, false); 
}

// TRANSFER TO OTHER BRIDGE CONTRACT
else if (
    _inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL &&
    _outputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL
) {
    NftAsset token = tokens[inputAssetA.id];
    require(token.collection != address(0x0), "NFT doesn't exist");
    
    address to = AddressRegistry.addresses(auxData);
    delete tokens[inputAssetA.id];
    
    Nft(token.collection).approve(to, token.id);
    NftVault(to).matchDeposit(_interactionNonce, token.collection, token.id);
    
    return (1, 0, false); 
}

Match Deposit

User Flow:

  1. Aztec account triggers a deposit
  2. Eth account approves this bridge to take their NFT
  3. Eth account calls matchDeposit, which takes the NFT and holds it, matching the owner to the virtual asset id
function matchDeposit(uint256 _virtualAssetId, address _collection, uint256 _tokenId) external {
    if (nftAssets[_virtualAssetId].collection != address(0x0)) {
        revert InvalidVirtualAssetId();
    }
    nftAssets[_virtualAssetId] = NftAsset({collection: _collection, tokenId: _tokenId});
    IERC721(_collection).transferFrom(msg.sender, address(this), _tokenId);
    emit NftDeposit(_virtualAssetId, _collection, _tokenId);
}