Cross-VM Bridge
Flow provides the Cross-VM Bridge which enables the movement of fungible and non-fungible tokens between Cadence & EVM. The Cross-VM Bridge is a contract-based protocol enabling 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.
The Cross-VM Bridge internalizes the capabilities to deploy new token contracts in either VM state as needed, resolving access to, and maintaining links between associated contracts. It additionally automates account and contract calls to enforce source VM asset burn or lock, and target VM token mint or unlock.
Developers wishing to use the Cross-VM Bridge will be required to use a Cadence transaction. Cross-VM bridging
functionality is not currently available natively in EVM on Flow. 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 outlines the architecture and implementation of the VM bridge. This document will focus on how to use the Cross-VM Bridge and considerations for fungible and non-fungible token projects deploying to either Cadence or EVM.
Deployments
The core bridge contracts can be found 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 |
Interacting 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 EVM on Flow Works.
Overview
The Flow EVM bridge allows both fungible and non-fungible tokens to move atomically between Cadence and EVM. In the context of EVM, fungible tokens are defined as ERC20 tokens, and non-fungible tokens as ERC721 tokens. In Cadence, fungible tokens are defined by contracts implementing FungibleToken and non-fungible tokens the NonFungibleToken standard contract interfaces.
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 for both onboarding assets and bridging assets. In the case where storage consumption is expected, fees are charged based on the storage consumed at the current network storage rate.
Onboarding
Since a contract must define the asset in the target VM, an asset must be "onboarded" to the bridge before requests can be fulfilled.
Moving from Cadence to EVM, onboarding can occur on the fly, deploying a template contract in the same transaction as the asset is bridged to EVM if the transaction so specifies.
Moving 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 onboarding assets:
onboard_by_type.cdc
_58// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/onboarding/onboard_by_type.cdc_58_58import "FungibleToken"_58import "FlowToken"_58_58import "ScopedFTProviders"_58_58import "EVM"_58_58import "FlowEVMBridge"_58import "FlowEVMBridgeConfig"_58_58/// This transaction onboards the asset type to the bridge, configuring the bridge to move assets between environments_58/// NOTE: This must be done before bridging a Cadence-native asset to EVM_58///_58/// @param type: The Cadence type of the bridgeable asset to onboard to the bridge_58///_58transaction(type: Type) {_58_58 let scopedProvider: @ScopedFTProviders.ScopedFTProvider_58 _58 prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {_58_58 /* --- Configure a ScopedFTProvider --- */_58 //_58 // Issue and store bridge-dedicated Provider Capability in storage if necessary_58 if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {_58 let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(_58 /storage/flowTokenVault_58 )_58 signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)_58 }_58 // Copy the stored Provider capability and create a ScopedFTProvider_58 let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(_58 from: FlowEVMBridgeConfig.providerCapabilityStoragePath_58 ) ?? panic("Invalid Provider Capability found in storage.")_58 let providerFilter = ScopedFTProviders.AllowanceFilter(FlowEVMBridgeConfig.onboardFee)_58 self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(_58 provider: providerCapCopy,_58 filters: [ providerFilter ],_58 expiration: getCurrentBlock().timestamp + 1.0_58 )_58 }_58_58 execute {_58 // Onboard the asset Type_58 FlowEVMBridge.onboardByType(_58 type,_58 feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}_58 )_58 destroy self.scopedProvider_58 }_58_58 post {_58 FlowEVMBridge.typeRequiresOnboarding(type) == false:_58 "Asset ".concat(type.identifier).concat(" was not onboarded to the bridge.")_58 }_58}
onboard_by_evm_address.cdc
_57// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/onboarding/onboard_by_evm_address.cdc_57_57import "FungibleToken"_57import "FlowToken"_57_57import "ScopedFTProviders"_57_57import "EVM"_57_57import "FlowEVMBridge"_57import "FlowEVMBridgeConfig"_57_57/// This transaction onboards the NFT type to the bridge, configuring the bridge to move NFTs between environments_57/// NOTE: This must be done before bridging a Cadence-native NFT to EVM_57///_57/// @param contractAddressHex: The EVM address of the contract defining the bridgeable asset to be onboarded_57///_57transaction(contractAddressHex: String) {_57_57 let contractAddress: EVM.EVMAddress_57 let scopedProvider: @ScopedFTProviders.ScopedFTProvider_57 _57 prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {_57 /* --- Construct EVMAddress from hex string (no leading `"0x"`) --- */_57 //_57 self.contractAddress = EVM.addressFromString(contractAddressHex)_57_57 /* --- Configure a ScopedFTProvider --- */_57 //_57 // Issue and store bridge-dedicated Provider Capability in storage if necessary_57 if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {_57 let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(_57 /storage/flowTokenVault_57 )_57 signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)_57 }_57 // Copy the stored Provider capability and create a ScopedFTProvider_57 let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(_57 from: FlowEVMBridgeConfig.providerCapabilityStoragePath_57 ) ?? panic("Invalid Provider Capability found in storage.")_57 let providerFilter = ScopedFTProviders.AllowanceFilter(FlowEVMBridgeConfig.onboardFee)_57 self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(_57 provider: providerCapCopy,_57 filters: [ providerFilter ],_57 expiration: getCurrentBlock().timestamp + 1.0_57 )_57 }_57_57 execute {_57 // Onboard the EVM contract_57 FlowEVMBridge.onboardByEVMAddress(_57 self.contractAddress,_57 feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}_57 )_57 destroy self.scopedProvider_57 }_57}
Bridging
Once an asset has been onboarded, either by its Cadence type or EVM contract address, 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 using the following script:
get_associated_type.cdc
_18// source: https://github.com/onflow/flow-evm-bridge/blob/main/cadence/scripts/bridge/get_associated_type.cdc_18_18import "EVM"_18_18import "FlowEVMBridgeConfig"_18_18/// Returns the Cadence Type associated with the given EVM address (as its hex String)_18///_18/// @param evmAddressHex: The hex-encoded address of the EVM contract as a String_18///_18/// @return The Cadence Type associated with the EVM address or nil if the address is not onboarded. `nil` may also be_18/// returned if the address is not a valid EVM address._18///_18access(all)_18fun main(addressHex: String): Type? {_18 let address = EVM.addressFromString(addressHex)_18 return FlowEVMBridgeConfig.getTypeAssociated(with: address)_18}
Alternatively, given some onboarded Cadence type, you can retrieve the associated EVM address using the following script:
get_associated_address.cdc
_21// source: https://github.com/onflow/flow-evm-bridge/blob/main/cadence/scripts/bridge/get_associated_evm_address.cdc_21_21import "EVM"_21_21import "FlowEVMBridgeConfig"_21_21/// Returns the EVM address associated with the given Cadence type (as its identifier String)_21///_21/// @param typeIdentifier: The Cadence type identifier String_21///_21/// @return The EVM address as a hex string if the type has an associated EVMAddress, otherwise nil_21///_21access(all)_21fun main(identifier: String): String? {_21 if let type = CompositeType(identifier) {_21 if let address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) {_21 return address.toString()_21 }_21 }_21 return nil_21}
NFTs
Any Cadence NFTs bridging 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
_120// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/nft/bridge_nft_to_evm.cdc_120_120import "FungibleToken"_120import "NonFungibleToken"_120import "ViewResolver"_120import "MetadataViews"_120import "FlowToken"_120_120import "ScopedFTProviders"_120_120import "EVM"_120_120import "FlowEVMBridge"_120import "FlowEVMBridgeConfig"_120import "FlowEVMBridgeUtils"_120_120/// Bridges an NFT from the signer's collection in Cadence to the signer's COA in FlowEVM_120///_120/// NOTE: This transaction also onboards the NFT to the bridge if necessary which may incur additional fees_120/// than bridging an asset that has already been onboarded._120///_120/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier_120/// @param id: The Cadence NFT.id of the NFT to bridge to EVM_120///_120transaction(nftIdentifier: String, id: UInt64) {_120 _120 let nft: @{NonFungibleToken.NFT}_120 let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount_120 let requiresOnboarding: Bool_120 let scopedProvider: @ScopedFTProviders.ScopedFTProvider_120 _120 prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {_120 /* --- Reference the signer's CadenceOwnedAccount --- */_120 //_120 // Borrow a reference to the signer's COA_120 self.coa = signer.storage.borrow<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)_120 ?? panic("Could not borrow COA from provided gateway address")_120 _120 /* --- Construct the NFT type --- */_120 //_120 // Construct the NFT type from the provided identifier_120 let nftType = CompositeType(nftIdentifier)_120 ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier))_120 // Parse the NFT identifier into its components_120 let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: nftType)_120 ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier))_120 let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: nftType)_120 ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier))_120_120 /* --- Retrieve the NFT --- */_120 //_120 // Borrow a reference to the NFT collection, configuring if necessary_120 let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName)_120 ?? panic("Could not borrow ViewResolver from NFT contract")_120 let collectionData = viewResolver.resolveContractView(_120 resourceType: nftType,_120 viewType: Type<MetadataViews.NFTCollectionData>()_120 ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view")_120 let collection = signer.storage.borrow<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>(_120 from: collectionData.storagePath_120 ) ?? panic("Could not access signer's NFT Collection")_120_120 // Withdraw the requested NFT & calculate the approximate bridge fee based on NFT storage usage_120 let currentStorageUsage = signer.storage.used_120 self.nft <- collection.withdraw(withdrawID: id)_120 let withdrawnStorageUsage = signer.storage.used_120 var approxFee = FlowEVMBridgeUtils.calculateBridgeFee(_120 bytes: currentStorageUsage - withdrawnStorageUsage_120 ) * 1.10_120 // Determine if the NFT requires onboarding - this impacts the fee required_120 self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.nft.getType())_120 ?? panic("Bridge does not support this asset type")_120 if self.requiresOnboarding {_120 approxFee = approxFee + FlowEVMBridgeConfig.onboardFee_120 }_120_120 /* --- Configure a ScopedFTProvider --- */_120 //_120 // Issue and store bridge-dedicated Provider Capability in storage if necessary_120 if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {_120 let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(_120 /storage/flowTokenVault_120 )_120 signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)_120 }_120 // Copy the stored Provider capability and create a ScopedFTProvider_120 let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(_120 from: FlowEVMBridgeConfig.providerCapabilityStoragePath_120 ) ?? panic("Invalid Provider Capability found in storage.")_120 let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)_120 self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(_120 provider: providerCapCopy,_120 filters: [ providerFilter ],_120 expiration: getCurrentBlock().timestamp + 1.0_120 )_120 }_120_120 pre {_120 self.nft.getType().identifier == nftIdentifier:_120 "Attempting to send invalid nft type - requested: ".concat(nftIdentifier)_120 .concat(", sending: ").concat(self.nft.getType().identifier)_120 }_120_120 execute {_120 if self.requiresOnboarding {_120 // Onboard the NFT to the bridge_120 FlowEVMBridge.onboardByType(_120 self.nft.getType(),_120 feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}_120 )_120 }_120 // Execute the bridge_120 self.coa.depositNFT(_120 nft: <-self.nft,_120 feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}_120 )_120 // Destroy the ScopedFTProvider_120 destroy self.scopedProvider_120 }_120}
bridge_nft_from_evm.cdc
_108// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/nft/bridge_nft_from_evm.cdc_108_108import "FungibleToken"_108import "NonFungibleToken"_108import "ViewResolver"_108import "MetadataViews"_108import "FlowToken"_108_108import "ScopedFTProviders"_108_108import "EVM"_108_108import "FlowEVMBridge"_108import "FlowEVMBridgeConfig"_108import "FlowEVMBridgeUtils"_108_108/// This transaction bridges an NFT from EVM to Cadence assuming it has already been onboarded to the FlowEVMBridge_108/// NOTE: The ERC721 must have first been onboarded to the bridge. This can be checked via the method_108/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress)_108///_108/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier_108/// @param id: The ERC721 id of the NFT to bridge to Cadence from EVM_108///_108transaction(nftIdentifier: String, id: UInt256) {_108_108 let nftType: Type_108 let collection: &{NonFungibleToken.Collection}_108 let scopedProvider: @ScopedFTProviders.ScopedFTProvider_108 let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount_108 _108 prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) {_108 /* --- Reference the signer's CadenceOwnedAccount --- */_108 //_108 // Borrow a reference to the signer's COA_108 self.coa = signer.storage.borrow<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)_108 ?? panic("Could not borrow COA from provided gateway address")_108_108 /* --- Construct the NFT type --- */_108 //_108 // Construct the NFT type from the provided identifier_108 self.nftType = CompositeType(nftIdentifier)_108 ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier))_108 // Parse the NFT identifier into its components_108 let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType)_108 ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier))_108 let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType)_108 ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier))_108_108 /* --- Reference the signer's NFT Collection --- */_108 //_108 // Borrow a reference to the NFT collection, configuring if necessary_108 let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName)_108 ?? panic("Could not borrow ViewResolver from NFT contract")_108 let collectionData = viewResolver.resolveContractView(_108 resourceType: self.nftType,_108 viewType: Type<MetadataViews.NFTCollectionData>()_108 ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view")_108 if signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) == nil {_108 signer.storage.save(<-collectionData.createEmptyCollection(), to: collectionData.storagePath)_108 signer.capabilities.unpublish(collectionData.publicPath)_108 let collectionCap = signer.capabilities.storage.issue<&{NonFungibleToken.Collection}>(collectionData.storagePath)_108 signer.capabilities.publish(collectionCap, at: collectionData.publicPath)_108 }_108 self.collection = signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath)_108 ?? panic("Could not borrow collection from storage path")_108_108 /* --- Configure a ScopedFTProvider --- */_108 //_108 // Calculate the bridge fee - bridging from EVM consumes no storage, so flat fee_108 let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)_108 // Issue and store bridge-dedicated Provider Capability in storage if necessary_108 if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {_108 let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(_108 /storage/flowTokenVault_108 )_108 signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)_108 }_108 // Copy the stored Provider capability and create a ScopedFTProvider_108 let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(_108 from: FlowEVMBridgeConfig.providerCapabilityStoragePath_108 ) ?? panic("Invalid Provider Capability found in storage.")_108 let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)_108 self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(_108 provider: providerCapCopy,_108 filters: [ providerFilter ],_108 expiration: getCurrentBlock().timestamp + 1.0_108 )_108 }_108_108 execute {_108 // Execute the bridge_108 let nft: @{NonFungibleToken.NFT} <- self.coa.withdrawNFT(_108 type: self.nftType,_108 id: id,_108 feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}_108 )_108 // Ensure the bridged nft is the correct type_108 assert(_108 nft.getType() == self.nftType,_108 message: "Bridged nft type mismatch - requeswted: ".concat(self.nftType.identifier)_108 .concat(", received: ").concat(nft.getType().identifier)_108 )_108 // Deposit the bridged NFT into the signer's collection_108 self.collection.deposit(token: <-nft)_108 // Destroy the ScopedFTProvider_108 destroy self.scopedProvider_108 }_108}
Fungible Tokens
Any Cadence fungible tokens bridging 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, with the bridge burning bridge-defined tokens or escrowing 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 bridging $FLOW
to EVM will simply leverage this interface; however, moving $FLOW from EVM to Cadence must be done through the COA
resource.
Below are transactions relevant to bridging fungible tokens:
bridge_tokens_to_evm.cdc
_121// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/tokens/bridge_tokens_to_evm.cdc_121_121import "FungibleToken"_121import "ViewResolver"_121import "FungibleTokenMetadataViews"_121import "FlowToken"_121_121import "ScopedFTProviders"_121_121import "EVM"_121_121import "FlowEVMBridge"_121import "FlowEVMBridgeConfig"_121import "FlowEVMBridgeUtils"_121_121/// Bridges a Vault from the signer's storage to the signer's COA in EVM.Account._121///_121/// NOTE: This transaction also onboards the Vault to the bridge if necessary which may incur additional fees_121/// than bridging an asset that has already been onboarded._121///_121/// @param vaultIdentifier: The Cadence type identifier of the FungibleToken Vault to bridge_121/// - e.g. vault.getType().identifier_121/// @param amount: The amount of tokens to bridge from EVM_121///_121transaction(vaultIdentifier: String, amount: UFix64) {_121_121 let sentVault: @{FungibleToken.Vault}_121 let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount_121 let requiresOnboarding: Bool_121 let scopedProvider: @ScopedFTProviders.ScopedFTProvider_121_121 prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {_121 /* --- Reference the signer's CadenceOwnedAccount --- */_121 //_121 // Borrow a reference to the signer's COA_121 self.coa = signer.storage.borrow<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)_121 ?? panic("Could not borrow COA from provided gateway address")_121_121 /* --- Construct the Vault type --- */_121 //_121 // Construct the Vault type from the provided identifier_121 let vaultType = CompositeType(vaultIdentifier)_121 ?? panic("Could not construct Vault type from identifier: ".concat(vaultIdentifier))_121 // Parse the Vault identifier into its components_121 let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: vaultType)_121 ?? panic("Could not get contract address from identifier: ".concat(vaultIdentifier))_121 let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: vaultType)_121 ?? panic("Could not get contract name from identifier: ".concat(vaultIdentifier))_121_121 /* --- Retrieve the funds --- */_121 //_121 // Borrow a reference to the FungibleToken Vault_121 let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName)_121 ?? panic("Could not borrow ViewResolver from FungibleToken contract")_121 let vaultData = viewResolver.resolveContractView(_121 resourceType: vaultType,_121 viewType: Type<FungibleTokenMetadataViews.FTVaultData>()_121 ) as! FungibleTokenMetadataViews.FTVaultData? ?? panic("Could not resolve FTVaultData view")_121 let vault = signer.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(_121 from: vaultData.storagePath_121 ) ?? panic("Could not access signer's FungibleToken Vault")_121_121 // Withdraw the requested balance & calculate the approximate bridge fee based on storage usage_121 let currentStorageUsage = signer.storage.used_121 self.sentVault <- vault.withdraw(amount: amount)_121 let withdrawnStorageUsage = signer.storage.used_121 // Approximate the bridge fee based on the difference in storage usage with some buffer_121 var approxFee = FlowEVMBridgeUtils.calculateBridgeFee(_121 bytes: currentStorageUsage - withdrawnStorageUsage_121 ) * 1.10_121 // Determine if the Vault requires onboarding - this impacts the fee required_121 self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.sentVault.getType())_121 ?? panic("Bridge does not support this asset type")_121 if self.requiresOnboarding {_121 approxFee = approxFee + FlowEVMBridgeConfig.onboardFee_121 }_121_121 /* --- Configure a ScopedFTProvider --- */_121 //_121 // Issue and store bridge-dedicated Provider Capability in storage if necessary_121 if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {_121 let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(_121 /storage/flowTokenVault_121 )_121 signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)_121 }_121 // Copy the stored Provider capability and create a ScopedFTProvider_121 let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(_121 from: FlowEVMBridgeConfig.providerCapabilityStoragePath_121 ) ?? panic("Invalid Provider Capability found in storage.")_121 let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)_121 self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(_121 provider: providerCapCopy,_121 filters: [ providerFilter ],_121 expiration: getCurrentBlock().timestamp + 1.0_121 )_121 }_121_121 pre {_121 self.sentVault.getType().identifier == vaultIdentifier:_121 "Attempting to send invalid vault type - requested: ".concat(vaultIdentifier)_121 .concat(", sending: ").concat(self.sentVault.getType().identifier)_121 }_121_121 execute {_121 if self.requiresOnboarding {_121 // Onboard the Vault to the bridge_121 FlowEVMBridge.onboardByType(_121 self.sentVault.getType(),_121 feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}_121 )_121 }_121 // Execute the bridge_121 self.coa.depositTokens(_121 vault: <-self.sentVault,_121 feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}_121 )_121 // Destroy the ScopedFTProvider_121 destroy self.scopedProvider_121 }_121}
bridge_tokens_from_evm.cdc
_114// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/tokens/bridge_tokens_from_evm.cdc_114_114import "FungibleToken"_114import "FungibleTokenMetadataViews"_114import "ViewResolver"_114import "MetadataViews"_114import "FlowToken"_114_114import "ScopedFTProviders"_114_114import "EVM"_114_114import "FlowEVMBridge"_114import "FlowEVMBridgeConfig"_114import "FlowEVMBridgeUtils"_114_114/// This transaction bridges fungible tokens from EVM to Cadence assuming it has already been onboarded to the_114/// FlowEVMBridge._114///_114/// NOTE: The ERC20 must have first been onboarded to the bridge. This can be checked via the method_114/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress)_114///_114/// @param vaultIdentifier: The Cadence type identifier of the FungibleToken Vault to bridge_114/// - e.g. vault.getType().identifier_114/// @param amount: The amount of tokens to bridge from EVM_114///_114transaction(vaultIdentifier: String, amount: UInt256) {_114_114 let vaultType: Type_114 let receiver: &{FungibleToken.Vault}_114 let scopedProvider: @ScopedFTProviders.ScopedFTProvider_114 let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount_114_114 prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) {_114 /* --- Reference the signer's CadenceOwnedAccount --- */_114 //_114 // Borrow a reference to the signer's COA_114 self.coa = signer.storage.borrow<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)_114 ?? panic("Could not borrow COA from provided gateway address")_114_114 /* --- Construct the Vault type --- */_114 //_114 // Construct the Vault type from the provided identifier_114 self.vaultType = CompositeType(vaultIdentifier)_114 ?? panic("Could not construct Vault type from identifier: ".concat(vaultIdentifier))_114 // Parse the Vault identifier into its components_114 let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.vaultType)_114 ?? panic("Could not get contract address from identifier: ".concat(vaultIdentifier))_114 let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: self.vaultType)_114 ?? panic("Could not get contract name from identifier: ".concat(vaultIdentifier))_114_114 /* --- Reference the signer's Vault --- */_114 //_114 // Borrow a reference to the FungibleToken Vault, configuring if necessary_114 let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName)_114 ?? panic("Could not borrow ViewResolver from FungibleToken contract")_114 let vaultData = viewResolver.resolveContractView(_114 resourceType: self.vaultType,_114 viewType: Type<FungibleTokenMetadataViews.FTVaultData>()_114 ) as! FungibleTokenMetadataViews.FTVaultData? ?? panic("Could not resolve FTVaultData view")_114 // If the vault does not exist, create it and publish according to the contract's defined configuration_114 if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) == nil {_114 signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath)_114_114 signer.capabilities.unpublish(vaultData.receiverPath)_114 signer.capabilities.unpublish(vaultData.metadataPath)_114_114 let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath)_114 let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath)_114_114 signer.capabilities.publish(receiverCap, at: vaultData.receiverPath)_114 signer.capabilities.publish(metadataCap, at: vaultData.metadataPath)_114 }_114 self.receiver = signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath)_114 ?? panic("Could not borrow Vault from storage path")_114_114 /* --- Configure a ScopedFTProvider --- */_114 //_114 // Calculate the bridge fee - bridging from EVM consumes no storage, so flat fee_114 let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)_114 // Issue and store bridge-dedicated Provider Capability in storage if necessary_114 if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {_114 let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(_114 /storage/flowTokenVault_114 )_114 signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)_114 }_114 // Copy the stored Provider capability and create a ScopedFTProvider_114 let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(_114 from: FlowEVMBridgeConfig.providerCapabilityStoragePath_114 ) ?? panic("Invalid Provider Capability found in storage.")_114 let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)_114 self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(_114 provider: providerCapCopy,_114 filters: [ providerFilter ],_114 expiration: getCurrentBlock().timestamp + 1.0_114 )_114 }_114_114 execute {_114 // Execute the bridge request_114 let vault: @{FungibleToken.Vault} <- self.coa.withdrawTokens(_114 type: self.vaultType,_114 amount: amount,_114 feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}_114 )_114 // Ensure the bridged vault is the correct type_114 assert(vault.getType() == self.vaultType, message: "Bridged vault type mismatch")_114 // Deposit the bridged token into the signer's vault_114 self.receiver.deposit(from: <-vault)_114 // Destroy the ScopedFTProvider_114 destroy self.scopedProvider_114 }_114}
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 be able 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 enable Cadence-native projects to
specify the representation of their assets in EVM. Implementing this view is not required for assets to be bridged, 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. In order 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 enables token URI updates
on subsequent bridge requests.
Opting 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 enable bridging, you can simply override the
default implementation and return true
.
In both cases, allowsBridging()
gates onboarding to the bridge. Once onboarded - 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: