Cross-VM Bridge
Flow provides the [Cross-VM Bridge] which allows the movement of fungible and non-fungible tokens between Flow-Cadence & Flow-EVM. The Cross-VM Bridge is a contract-based protocol which allows the automated and atomic bridging of tokens from Cadence into EVM with their corresponding ERC-20 and ERC-721 token types.
In the opposite direction, it supports bridging of arbitrary ERC-20 and ERC-721 tokens from EVM to Cadence as their corresponding FT or NFT token types.
By default, when a user onboards a new token to the bridge, the bridge will deploy a standard token contract in the other VM that only the core bridge protocol contracts retain limited control over. This bridge-deployed contract handles basic minting and metadata operations that are required for usage in the needed environment.
If a developer wants to define and connect the NFT contracts on both sides of the bridge, they can have each contract point to each other to indicate that they are associated and then register that association with the bridge so the token moves between VMs as either definition.
The Cross-VM Bridge internalizes the capabilities to deploy new token contracts in either VM state as needed, resolves access to and maintains links between associated contracts. It also automates account and contract calls to enforce source VM asset burn or lock, and target VM token mint or unlock.
Developers who want to use the Cross-VM Bridge must use a Cadence transaction. Cross-VM bridging functionality is not currently available natively in Flow EVM. By extension, this means that the EVM account bridging from EVM to Cadence must be a [CadenceOwnedAccount (COA)] as this is the only EVM account type that can be controlled from the Cadence runtime.
This [FLIP-233] outlines the architecture and implementation of the VM bridge. An additional [FLIP-318] describes how developers can create custom associations between NFTs they define and control in each VM.
This document will focus on how to use the Cross-VM Bridge and considerations for fungible and non-fungible token projects that deploy to either Cadence or EVM.
Deployments
You can find the core bridge contracts at the following addresses:
| Contracts | Testnet | Mainnet |
|---|---|---|
| All Cadence Bridge contracts | [0xdfc20aee650fcbdf] | [0x1e4aa0b87d10b141] |
FlowEVMBridgeFactory.sol | [0xf8146b4aef631853f0eb98dbe28706d029e52c52] | [0x1c6dea788ee774cf15bcd3d7a07ede892ef0be40] |
FlowEVMBridgeDeploymentRegistry.sol | [0x8781d15904d7e161f421400571dea24cc0db6938] | [0x8fdec2058535a2cb25c2f8cec65e8e0d0691f7b0] |
FlowEVMBridgedERC20Deployer.sol | [0x4d45CaD104A71D19991DE3489ddC5C7B284cf263] | [0x49631Eac7e67c417D036a4d114AD9359c93491e7] |
FlowEVMBridgedERC721Deployer.sol | [0x1B852d242F9c4C4E9Bb91115276f659D1D1f7c56] | [0xe7c2B80a9de81340AE375B3a53940E9aeEAd79Df] |
And below are the bridge escrow's EVM addresses. These addresses are COAs and are stored stored in the same Flow account as you'll find the Cadence contracts (see above).
| Network | Address |
|---|---|
| Testnet | [0x0000000000000000000000023f946ffbc8829bfd] |
| Mainnet | [0x00000000000000000000000249250a5c27ecab3b] |
Interact With the Bridge
All bridging activity in either direction is orchestrated via Cadence on COA EVM accounts. This means that all bridging activity must be initiated via a Cadence transaction, not an EVM transaction, regardless of the directionality of the bridge request. For more information on the interplay between Cadence and EVM, see [How Flow EVM Works].
Overview
The Flow EVM bridge allows both fungible and non-fungible tokens to move atomically between Cadence and EVM. In EVM, fungible tokens are defined as ERC20 tokens, and non-fungible tokens as ERC721 tokens. In Cadence, fungible tokens are defined by contracts that implement [the FungibleToken interface] and non-fungible tokens implement [the NonFungibleToken interface]. You can find full guides to create these projects [here].
Like all operations on Flow, there are native fees associated with both computation and storage. To prevent spam and sustain the bridge account's storage consumption, fees are charged to both onboard assets and bridge assets. In the case where storage consumption is expected, fees are charged based on the storage consumed at the current network storage rate.
Onboard your token to the bridge
For the purpose of this guide, we assume that the developer already deployed a token smart contract to their preferred VM (Flow-Cadence or Flow-EVM) and wants to bridge it to the other (target) VM.
For the developer to use their token in the target VM, there must be a contract that defines the asset and how it behaves in the target VM that also allows the bridge to fulfill the asset from Cadence to EVM and vice versa. This contract is separate from the contract in the native VM, but they are "associated" with each other by the mechanisms of the Flow VM bridge.
To create this association, the asset must be "onboarded" to the bridge before bridging operations can be fulfilled. This can happen in two ways:
Option 1: automatic onboarding
Any user registers the native token contract with the bridge and the bridge deploys a basic templated version of the contract in the target VM. This basic contract is automatically associated with the native contract and is used for bridging. The developer has no direct control over this bridge-deployed contract because the bridge controls it.
This method is covered in the [Automatic Onboarding Section].
Option 2: custom association onboarding
With this option (available for only for NFTs), developers can deploy their own contract to the target VM and declare a custom association between it and the native contract. This allows them to have more control over both contracts, which lets them to include more sophisticated features and mechanisms in their bridged token contracts such as ERC-721C, unique metadata views, and more that aren't included in the default bridged template versions.
This method is covered in the [Custom Association Section].
Before you continue to onboard your token, review the [Prep Your Assets for Bridging] section of this document. This describes some steps you should follow to make sure that your native asset or bridged asset are properly set up for you to register them with the bridge.
Automatic onboarding
To move from a Cadence-native asset to EVM, automatic onboarding can occur on the fly, where a template contract deploys in the same transaction as the asset is bridged to EVM if the transaction so specifies.
To move from EVM to Cadence, however, requires that onboarding occur in a separate transaction due to the fact that a Cadence contract is initialized at the end of a transaction and isn't available in the runtime until after the transaction has executed.
Below are transactions relevant to automatically onboarding assets native to either VM:
Automatically Onboard a Cadence-native asset:
onboard_by_type.cdc
Automatically Onboard an EVM-native asset:
onboard_by_evm_address.cdc
Custom association onboarding
With [Custom Associations], developers can deploy NFT contracts in both VMs and associate them with each other, which allows them to retain control of the contracts in both VMs as well as implement custom use-case specific functionality.
To do this, each contract must implement a special interface that tells the bridge what the associated contract is in the other VM. The fact that both point to each other validates the intended association, which prevents spoofing. If the contracts do not point to each other this way, you can't register them as a custom association.
Review the [Preparing Custom Associations] section to learn how to set up each of your contracts for a custom association.
Below is the transaction for onboarding NFTs for a custom association. Remember that both the Cadence and the Solidity contract need to be deployed and include the special interface conformances to point to each other before registration!
Onboard an NFT Custom Association:
register_cross_vm_nft.cdc
Bridging
After an asset gets onboarded, either by automatic or custom association, it can be bridged in either direction, referred to by its Cadence type. For Cadence-native assets, this is simply its native type.
For EVM-native assets, this is in most cases a templated Cadence contract deployed to the bridge account, the name of which is derived from the EVM contract address. For instance, an ERC721 contract at address 0x1234 would be onboarded to the bridge as
EVMVMBridgedNFT_0x1234, making its type identifier A.<BRIDGE_ADDRESS>.EVMVMBridgedNFT_0x1234.NFT.
To get the type identifier for a given NFT, you can use the following code:
_10// Where `nft` is either a @{NonFungibleToken.NFT} or &{NonFungibleToken.NFT}_10nft.getType().identifier
You may also retrieve the type associated with a given EVM contract address with the following script:
get_associated_type.cdc
Alternatively, given some onboarded Cadence type, you can retrieve the associated EVM address with the following script:
get_associated_address.cdc
NFTs
Any Cadence NFTs that bridge to EVM are escrowed in the bridge account and either minted in a bridge-deployed ERC721 contract or transferred from escrow to the calling COA in EVM. On the return trip, NFTs are escrowed in EVM - owned by the bridge's COA - and either unlocked from escrow if locked or minted from a bridge-owned NFT contract.
Below are transactions relevant to bridging NFTs:
bridge_nft_to_evm.cdc
bridge_nft_from_evm.cdc
Fungible tokens
Any Cadence fungible tokens that bridge to EVM are escrowed in the bridge account only if they are Cadence-native. If the bridge defines the tokens, they are burned. On the return trip the pattern is similar, as the bridge burns bridge-defined tokens or escrows them if they are EVM-native. In all cases, if the bridge has authority to mint on one side, it must escrow on the other as the native VM contract is owned by an external party.
With fungible tokens in particular, there may be some cases where the Cadence contract is not deployed to the bridge account, but the bridge still follows a mint/burn pattern in Cadence. These cases are handled via [TokenHandler] implementations. Also know that moving $FLOW to EVM is built into the EVMAddress object so any requests to bridge $FLOW to EVM will simply leverage this interface; however, you must move $FLOW from EVM to Cadence through the COA resource.
Below are transactions relevant to bridging fungible tokens:
bridge_tokens_to_evm.cdc
bridge_tokens_from_evm.cdc
Prep your assets for bridging
Context
To maximize utility to the ecosystem, this bridge is permissionless and open to any fungible or non-fungible token as defined by the respective Cadence standards and limited to ERC20 and ERC721 Solidity standards. Ultimately, a project does not have to do anything for users to bridge their assets between VMs. However, there are some considerations developers may take to enhance the representation of their assets in non-native VMs. These largely relate to asset metadata and ensuring that bridging does not compromise critical user assumptions about asset ownership.
EVMBridgedMetadata
Proposed in [@onflow/flow-nft/pull/203], the EVMBridgedMetadata view presents a mechanism to both represent metadata from bridged EVM assets as well as allow Cadence-native projects to specify the representation of their assets in EVM. It isn't required to implement this view to bridge asets, but the bridge does default to it when available as a way to provide projects greater control over their EVM asset definitions within the scope of ERC20 and ERC721 standards.
The interface for this view is as follows:
_20access(all) struct URI: MetadataViews.File {_20 /// The base URI prefix, if any. Not needed for all URIs, but helpful_20 /// for some use cases For example, updating a whole NFT collection's_20 /// image host easily_20 access(all) let baseURI: String?_20 /// The URI string value_20 /// NOTE: this is set on init as a concatenation of the baseURI and the_20 /// value if baseURI != nil_20 access(self) let value: String_20_20 access(all) view fun uri(): String_20_20}_20_20access(all) struct EVMBridgedMetadata {_20 access(all) let name: String_20 access(all) let symbol: String_20_20 access(all) let uri: {MetadataViews.File}_20}
This uri value could be a pointer to some offchain metadata if you expect your metadata to be static. Or you could couple the uri() method with the utility contract below to serialize the onchain metadata on the fly. Alternatively, you may choose to host a metadata proxy which serves the requested token URI content.
SerializeMetadata
The key consideration with respect to metadata is the distinct metadata storage patterns between ecosystem. It's critical for NFT utility that the metadata be bridged in addition to the representation of the NFTs ownership. However, it's commonplace for Cadence NFTs to store metadata onchain while EVM NFTs often store an onchain pointer to metadata stored offchain.
For Cadence NFTs to be properly represented in EVM platforms, the metadata must be bridged in a format expected by those platforms and be done in a manner that also preserves the atomicity of bridge requests. The path forward on this was decided to be a commitment of serialized Cadence NFT metadata into formats popular in the EVM ecosystem.
For assets that do not implement EVMBridgedMetadata, the bridge will attempt to serialize the metadata of the asset as a JSON data URL string. This is done via the [SerializeMetadata contract] which serializes metadata values into a JSON blob compatible with the OpenSea metadata standard. The serialized metadata is then committed as the ERC721 tokenURI upon bridging Cadence-native NFTs to EVM. Since Cadence NFTs can easily update onchain metadata either by field or by the ownership of sub-NFTs, this serialization pattern allows token URI updates on subsequent bridge requests.
Prepar custom associations
If you are a developer who wants to deploy and manage NFT contracts in both VMs and have tokens from each be exchangable for each other, you'll have to add some code to your contracts which indicate that they each represent the same token in their respective VMs so they point to each other.
For the purposes of these instructions, an NFT is native to a VM if that VM is the main source of truth for the contracts and where they are originally minted.
This feature is not available for Fungible Tokens at the moment, but may be in the future.
The bridge only supports a single custom association declaration. This means that once you register an association between your Cadence NFT & EVM contract, the association cannot be updated. If you wish to retain some upgradeability to your registered implementations, we recommend that you both retain keys on your Cadence NFT contract account **and ** implement an upgradeable Solidity pattern when deploying your ERC721, then register the association between your Cadence NFT Type & ERC721 proxy (not the implementation address).
Cadence
All Cadence NFT contracts implement [Metadata Views] that return metadata about their NFTs in standard ways via the {Contract}.resolveContractView() and {NFT}.resolveView() methods.
The following new view (CrossVMMetadataViews.EVMPointer) must be resolved at the contract level (ViewResolver.resolveContractView()) for a given Type and at the NFT level (ViewResolver.Resolver.resolveView())
_11/// View resolved at contract & resource level pointing to the associated EVM implementation_11access(all) struct EVMPointer {_11 /// The associated Cadence Type_11 access(all) let cadenceType: Type_11 /// The defining Cadence contract address_11 access(all) let cadenceContractAddress: Address_11 /// The associated EVM contract address_11 access(all) let evmContractAddress: EVM.EVMAddress_11 /// Whether the asset is Cadence- or EVM-native_11 access(all) let isCadenceNative: Bool_11}
This view allows a Cadence contract to specify which Solidity contract it is associated with.
You can see an example of how this view is implemented in [the ExampleNFT contract] in the Flow Non-Fungible Token repo.
If your EVM contract expects metadata to be passed from Cadence at the time of bridging, you must implement the CrossVMMetadataViews.EVMBytesMetadata view. You'll find this useful for Cadence-native NFTs with dynamic metadata. This view will be resolved by the bridge and passed to your EVM contract when the fulfillToEVM method is called.
How you handle the bridged bytes in your ERC721 implementation will be a matter of overriding the _beforeFulfillment and/or _afterFulfillment hooks included in the CrossVMBridgeERC721Fulfillment base contract.
Flow EVM-Native NFTs
If the NFT being onboarded to the bridge is native to Flow-EVM, then the associated contract's minter resource must implement the FlowEVMBridgeCustomAssociationTypes.NFTFulfillmentMinter interface:
_29/// Resource interface used by EVM-native NFT collections allowing for the fulfillment of NFTs from EVM into Cadence_29 ///_29 access(all) resource interface NFTFulfillmentMinter {_29 /// Getter for the type of NFT that's fulfilled by this implementation_29 ///_29 access(all) view fun getFulfilledType(): Type_29_29 /// Called by the VM bridge when moving NFTs from EVM into Cadence if the NFT is not in escrow. Since such NFTs_29 /// are EVM-native, they are distributed in EVM. On the Cadence side, those NFTs are handled by a mint & escrow_29 /// pattern. On moving to EVM, the NFTs are minted if not in escrow at the time of bridging._29 ///_29 /// @param id: The id of the token being fulfilled from EVM_29 ///_29 /// @return The NFT fulfilled from EVM as its Cadence implementation_29 ///_29 access(FulfillFromEVM)_29 fun fulfillFromEVM(id: UInt256): @{NonFungibleToken.NFT} {_29 pre {_29 id <= UInt256(UInt64.max):_29 "The requested ID \(id.toString()) exceeds the maximum assignable Cadence NFT ID \(UInt64.max.toString())"_29 }_29 post {_29 UInt256(result.id) == id:_29 "Resulting NFT ID \(result.id.toString()) does not match requested ID \(id.toString())"_29 result.getType() == self.getFulfilledType():_29 "Expected \(self.getFulfilledType().identifier) but fulfilled \(result.getType().identifier)"_29 }_29 }_29 }
You can see an example of an implementation of this interface in the [Flow EVM bridge repo ExampleNFT contract].
A Capability with the FulfillFromEVM entitlement is required at the time of registration so the bridge can fulfill NFTs bridged from EVM for the first time.
Solidity
For custom associations, the following interface must be implemented in the IERC721-conforming Solidity contract.
This provides functionality to point to the address and type of the associated Cadence NFT.
_10interface ICrossVM {_10 /**_10 * Returns the Cadence address defining the associated type_10 */_10 function getCadenceAddress() external view returns (string memory);_10 /**_10 * Returns the Cadence Type identifier associated with the EVM contract_10 */_10 function getCadenceIdentifier() external view returns (string memory);_10}
As an example, [ICrossVM is already implemented] and in use in the bridged [ERC721] and [ERC20] templates.
If you want to register a custom association for an NFT that is native to Cadence, which means that your project distributes NFTs to users on the Cadence side, then your ERC721 contract must implement the CrossVMBridgeERC721Fulfillment contract. This is a required conformance that does three primary things:
- Implements the mint/escrow pattern expected by the VM bridge.
- Allows for the passing of arbitrary abi-encodable metadata from the Cadence NFT at the time of bridging.
- Exposes two optional hooks, which allows you to update the fulfilled token's URI with the provided metadata at the time of bridging.
Here is the Solidity contract to implement:
_100abstract contract CrossVMBridgeERC721Fulfillment is ICrossVMBridgeERC721Fulfillment, CrossVMBridgeCallable, ERC721 {_100_100 /**_100 * Initializes the bridge EVM address such that only the bridge COA can call privileged methods_100 */_100 constructor(address _vmBridgeAddress) CrossVMBridgeCallable(_vmBridgeAddress) {}_100_100 /**_100 * @dev Fulfills the bridge request, minting (if non-existent) or transferring (if escrowed) the_100 * token with the given ID to the provided address. For dynamic metadata handling between_100 * Cadence & EVM, implementations should override and assign metadata as encoded from Cadence_100 * side. If overriding, be sure to preserve the mint/escrow pattern as shown in the default_100 * implementation. See `_beforeFulfillment` and `_afterFulfillment` hooks to enable pre-and/or_100 * post-processing without the need to override this function._100 *_100 * @param _to address of the token recipient_100 * @param _id the id of the token being moved into EVM from Cadence_100 * @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of_100 * bridging into EVM_100 */_100 function fulfillToEVM(address _to, uint256 _id, bytes memory _data) external onlyVMBridge {_100 _beforeFulfillment(_to, _id, _data); // hook allowing implementation to perform pre-fulfillment validation_100 if (_ownerOf(_id) == address(0)) {_100 _mint(_to, _id); // Doesn't exist, mint the token_100 } else {_100 // Should be escrowed under vm bridge - transfer from escrow to recipient_100 _requireEscrowed(_id);_100 safeTransferFrom(vmBridgeAddress(), _to, _id);_100 }_100 _afterFulfillment(_to, _id, _data); // hook allowing implementation to perform post-fulfillment processing_100 emit FulfilledToEVM(_to, _id);_100 }_100_100 /**_100 * @dev Returns whether the token is currently escrowed under custody of the designated VM bridge_100 *_100 * @param _id the ID of the token in question_100 */_100 function isEscrowed(uint256 _id) public view returns (bool) {_100 return _ownerOf(_id) == vmBridgeAddress();_100 }_100_100 /**_100 * @dev Returns whether the token is exists or not defined positively by whether the owner of_100 * the token is 0x0._100 *_100 * @param _id the ID of the token in question_100 */_100 function exists(uint256 _id) public view returns (bool) {_100 return _ownerOf(_id) != address(0);_100 }_100_100 /**_100 * @dev Allows a caller to determine the contract conforms to implemented interfaces_100 */_100 function supportsInterface(bytes4 interfaceId) public view virtual override(CrossVMBridgeCallable, ERC721, IERC165) returns (bool) {_100 return interfaceId == type(ICrossVMBridgeERC721Fulfillment).interfaceId_100 || interfaceId == type(ICrossVMBridgeCallable).interfaceId_100 || super.supportsInterface(interfaceId);_100 }_100_100 /**_100 * @dev Internal method that reverts with FulfillmentFailedTokenNotEscrowed if the provided_100 * token is not escrowed with the assigned vm bridge address as owner._100 *_100 * @param _id the token id that must be escrowed_100 */_100 function _requireEscrowed(uint256 _id) internal view {_100 if (!isEscrowed(_id)) {_100 revert FulfillmentFailedTokenNotEscrowed(_id, vmBridgeAddress());_100 }_100 }_100_100 /**_100 * @dev This internal method is included as a step implementations can override and have_100 * executed in the default fullfillToEVM call._100 *_100 * @param _to address of the pending token recipient_100 * @param _id the id of the token to be moved into EVM from Cadence_100 * @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of_100 * bridging into EVM_100 */_100 function _beforeFulfillment(address _to, uint256 _id, bytes memory _data) internal virtual {_100 // No-op by default, meant to be overridden by implementations_100 }_100_100 /**_100 * @dev This internal method is included as a step implementations can override and have_100 * executed in the default fullfillToEVM call._100 *_100 * @param _to address of the pending token recipient_100 * @param _id the id of the token to be moved into EVM from Cadence_100 * @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of_100 * bridging into EVM_100 */_100 function _afterFulfillment(address _to, uint256 _id, bytes memory _data) internal virtual {_100 // No-op by default, meant to be overridden by implementations for things like processing_100 // and setting metadata_100 }_100}
The _beforeFulfillment() and _afterFulfillment() hooks are virtual, which allows implementations to optionally override the methods and handle the provided metadata passed from your NFT if EVMBytesMetadata is resolved at the time of bridging. Also, notice that the fulfillToEVM method is onlyVMBridge, which allows the VM bridge to call the method either minting the NFT if it does not exist or transfer the NFT from escrow in a manner consistent with the bridge's mint/escrow pattern.
Opt Out
It's also recognized that the logic of some use cases may actually be compromised by the act of bridging, particularly in such a unique partitioned runtime environment. Such cases might include those that do not maintain ownership assumptions implicit to ecosystem standards.
For instance, an ERC721 implementation may reclaim a user's assets after a month of inactivity. In such a case, bridging that ERC721 to Cadence would decouple the representation of ownership of the bridged NFT from the actual ownership in the defining ERC721 contract after the token had been reclaimed - there would be no NFT in escrow for the bridge to transfer on fulfillment of the NFT back to EVM. In such cases, projects may choose to opt-out of bridging, but importantly must do so before the asset has been onboarded to the bridge.
For Solidity contracts, opting out is as simple as extending the [BridgePermissions.sol abstract contract] which defaults allowsBridging() to false. The bridge explicitly checks for the implementation of IBridgePermissions and the value of allowsBridging() to validate that the contract has not opted out of bridging.
Similarly, Cadence contracts can implement the [IBridgePermissions.cdc contract interface]. This contract has a single method allowsBridging() with a default implementation returning false. Again, the bridge explicitly checks for the implementation of IBridgePermissions and the value of allowsBridging() to validate that the contract has not opted out of bridging. Should you later choose to turn on bridging, you can simply override the default implementation and return true.
In both cases, allowsBridging() gates onboarding to the bridge. After the onboard occurs - a permissionless operation anyone can execute - the value of allowsBridging() is irrelevant and assets can move between VMs permissionlessly.
Under the hood
For an in-depth look at the high-level architecture of the bridge, see [FLIP #237]
Additional resources
For the current state of Flow EVM across various task paths, see the following resources:
- Flow EVM Equivalence forum post
- EVM Integration FLIP #223
- [Gateway & JSON RPC FLIP #235]
[Gateway & JSON RPC FLIP #235]: https://github.com/onflow/flips/pull/235)
[FLIP #237]: https://github.com/onflow/flips/blob/main/application/20231222-evm-vm-bridge.md
[IBridgePermissions.cdc contract interface]: https://github.com/onflow/flow-evm-bridge/blob/main/cadence/contracts/bridge/interfaces/IBridgePermissions.cdc
[BridgePermissions.sol abstract contract]: https://github.com/onflow/flow-evm-bridge/blob/main/solidity/src/interfaces/BridgePermissions.sol
[ICrossVM is already implemented]: https://github.com/onflow/flow-evm-bridge/blob/main/solidity/src/interfaces/ICrossVM.sol
[ERC721]: https://github.com/onflow/flow-evm-bridge/blob/flip-318/solidity/src/templates/FlowEVMBridgedERC721.sol#L37-L43
[ERC20]: https://github.com/onflow/flow-evm-bridge/blob/flip-318/solidity/src/templates/FlowEVMBridgedERC20.sol#L13-L40
[Flow EVM bridge repo ExampleNFT contract]: https://github.com/onflow/flow-evm-bridge/blob/flip-318/cadence/contracts/example-assets/cross-vm-nfts/ExampleEVMNativeNFT.cdc#L352-L377
[the ExampleNFT contract]: https://github.com/onflow/flow-nft/blob/master/contracts/ExampleNFT.cdc#L173-L195
[Metadata Views]: ../../build/cadence/advanced-concepts/metadata-views.md
[SerializeMetadata contract]: https://github.com/onflow/flow-evm-bridge/blob/main/cadence/contracts/utils/SerializeMetadata.cdc
[@onflow/flow-nft/pull/203]: https://github.com/onflow/flow-nft/pull/203
[TokenHandler]: https://github.com/onflow/flow-evm-bridge/blob/main/cadence/contracts/bridge/interfaces/FlowEVMBridgeHandlerInterfaces.cdc
[Preparing Custom Associations]: #preparing-custom-associations
[Custom Associations]: https://github.com/onflow/flips/blob/main/application/20250131-cross-vm-nft-support.md
[Automatic Onboarding Section]: #automatic-onboarding
[Custom Association Section]: #custom-association-onboarding
[Prep Your Assets for Bridging]: #prep-your-assets-for-bridging
[How Flow EVM Works]: ../../build/evm/how-it-works.md
[the FungibleToken interface]: https://github.com/onflow/flow-ft/blob/master/contracts/FungibleToken.cdc
[the NonFungibleToken interface]: https://github.com/onflow/flow-nft/blob/master/contracts/NonFungibleToken.cdc)
[here]: ../tokens/nft-cadence.md)
[0xdfc20aee650fcbdf]: https://contractbrowser.com/account/0xdfc20aee650fcbdf/contracts
[0x1e4aa0b87d10b141]: https://contractbrowser.com/account/0x1e4aa0b87d10b141/contracts)
[0xf8146b4aef631853f0eb98dbe28706d029e52c52]: https://evm-testnet.flowscan.io/address/0xF8146B4aEF631853F0eB98DBE28706d029e52c52)
[0x1c6dea788ee774cf15bcd3d7a07ede892ef0be40]: https://evm.flowscan.io/address/0x1C6dEa788Ee774CF15bCd3d7A07ede892ef0bE40)
[0x8781d15904d7e161f421400571dea24cc0db6938]: https://evm-testnet.flowscan.io/address0x8781d15904d7e161f421400571dea24cc0db6938
[0x8fdec2058535a2cb25c2f8cec65e8e0d0691f7b0]: https://evm.flowscan.io/address/0x8FDEc2058535A2Cb25C2f8ceC65e8e0D0691f7B0)
[0x4d45CaD104A71D19991DE3489ddC5C7B284cf263]: https://evm-testnet.flowscan.io/address/0x4d45CaD104A71D19991DE3489ddC5C7B284cf263
[0x49631Eac7e67c417D036a4d114AD9359c93491e7]: https://evm.flowscan.io/address/0x49631Eac7e67c417D036a4d114AD9359c93491e7
[0x1B852d242F9c4C4E9Bb91115276f659D1D1f7c56]: https://evm-testnet.flowscan.io/address/0x1B852d242F9c4C4E9Bb91115276f659D1D1f7c56
[0xe7c2B80a9de81340AE375B3a53940E9aeEAd79Df]: https://evm.flowscan.io/address/0xe7c2B80a9de81340AE375B3a53940E9aeEAd79Df
[0x0000000000000000000000023f946ffbc8829bfd]: https://evm-testnet.flowscan.io/address/0x0000000000000000000000023f946FFbc8829BFD
[0x00000000000000000000000249250a5c27ecab3b]: https://evm.flowscan.io/address/0x00000000000000000000000249250a5C27Ecab3B
[CadenceOwnedAccount (COA)]: interacting-with-coa.md
[FLIP-233]: https://github.com/onflow/flips/pull/233
[FLIP-318]: https://github.com/onflow/flips/blob/main/application/20250131-cross-vm-nft-support.md
[Cross-VM Bridge]: https://www.github.com/onflow/flow-evm-bridge