diff --git a/docs/evm/cadence/vm-bridge.md b/docs/evm/cadence/vm-bridge.md new file mode 100644 index 0000000000..bf4b029200 --- /dev/null +++ b/docs/evm/cadence/vm-bridge.md @@ -0,0 +1,913 @@ +--- +title: Cross-VM Bridge +sidebar_label: Cross-VM Bridge +sidebar_position: 6 +--- + +# Cross-VM Bridge + +Flow provides the [Cross-VM Bridge](https://www.github.com/onflow/flow-evm-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)](./interacting-with-coa.md) as this is the only EVM account +type that can be controlled from the Cadence runtime. + +This [FLIP](https://github.com/onflow/flips/pull/233) 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`](https://contractbrowser.com/account/0xdfc20aee650fcbdf/contracts)|[`0x1e4aa0b87d10b141`](https://contractbrowser.com/account/0x1e4aa0b87d10b141/contracts)| +|`FlowEVMBridgeFactory.sol`|[`0xf8146b4aef631853f0eb98dbe28706d029e52c52`](https://evm-testnet.flowscan.io/address/0xF8146B4aEF631853F0eB98DBE28706d029e52c52)|[`0x1c6dea788ee774cf15bcd3d7a07ede892ef0be40`](https://evm.flowscan.io/address/0x1C6dEa788Ee774CF15bCd3d7A07ede892ef0bE40)| +|`FlowEVMBridgeDeploymentRegistry.sol`|[`0x8781d15904d7e161f421400571dea24cc0db6938`](https://evm-testnet.flowscan.io/address/0x8781d15904d7e161f421400571dea24cc0db6938)|[`0x8fdec2058535a2cb25c2f8cec65e8e0d0691f7b0`](https://evm.flowscan.io/address/0x8FDEc2058535A2Cb25C2f8ceC65e8e0D0691f7B0)| +|`FlowEVMBridgedERC20Deployer.sol`|[`0x4d45CaD104A71D19991DE3489ddC5C7B284cf263`](https://evm-testnet.flowscan.io/address/0x4d45CaD104A71D19991DE3489ddC5C7B284cf263)|[`0x49631Eac7e67c417D036a4d114AD9359c93491e7`](https://evm.flowscan.io/address/0x49631Eac7e67c417D036a4d114AD9359c93491e7)| +|`FlowEVMBridgedERC721Deployer.sol`|[`0x1B852d242F9c4C4E9Bb91115276f659D1D1f7c56`](https://evm-testnet.flowscan.io/address/0x1B852d242F9c4C4E9Bb91115276f659D1D1f7c56)|[`0xe7c2B80a9de81340AE375B3a53940E9aeEAd79Df`](https://evm.flowscan.io/address/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`](https://evm-testnet.flowscan.io/address/0x0000000000000000000000023f946FFbc8829BFD)| +|Mainnet|[`0x00000000000000000000000249250a5c27ecab3b`](https://evm.flowscan.io/address/0x00000000000000000000000249250a5C27Ecab3B)| + +## Interacting With the Bridge + +:::info + +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](../how-it-works.md). + +::: + +### 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 +[the `FungibleToken` interface](https://github.com/onflow/flow-ft/blob/master/contracts/FungibleToken.cdc) +and non-fungible tokens implement +[the `NonFungibleToken` interface](https://github.com/onflow/flow-nft/blob/master/contracts/NonFungibleToken.cdc). + +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 + +```cadence title="onboard_by_type.cdc" +// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/onboarding/onboard_by_type.cdc + +import "FungibleToken" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" + +/// This transaction onboards the asset type to the bridge, configuring the bridge to move assets between environments +/// NOTE: This must be done before bridging a Cadence-native asset to EVM +/// +/// @param type: The Cadence type of the bridgeable asset to onboard to the bridge +/// +transaction(type: Type) { + + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + + prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { + + /* --- Configure a ScopedFTProvider --- */ + // + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid Provider Capability found in storage.") + let providerFilter = ScopedFTProviders.AllowanceFilter(FlowEVMBridgeConfig.onboardFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Onboard the asset Type + FlowEVMBridge.onboardByType( + type, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + destroy self.scopedProvider + } + + post { + FlowEVMBridge.typeRequiresOnboarding(type) == false: + "Asset ".concat(type.identifier).concat(" was not onboarded to the bridge.") + } +} +``` +
+ +
+onboard_by_evm_address.cdc + +``` +// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/onboarding/onboard_by_evm_address.cdc + +import "FungibleToken" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" + +/// This transaction onboards the NFT type to the bridge, configuring the bridge to move NFTs between environments +/// NOTE: This must be done before bridging a Cadence-native NFT to EVM +/// +/// @param contractAddressHex: The EVM address of the contract defining the bridgeable asset to be onboarded +/// +transaction(contractAddressHex: String) { + + let contractAddress: EVM.EVMAddress + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + + prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { + /* --- Construct EVMAddress from hex string (no leading `"0x"`) --- */ + // + self.contractAddress = EVM.addressFromString(contractAddressHex) + + /* --- Configure a ScopedFTProvider --- */ + // + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid Provider Capability found in storage.") + let providerFilter = ScopedFTProviders.AllowanceFilter(FlowEVMBridgeConfig.onboardFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Onboard the EVM contract + FlowEVMBridge.onboardByEVMAddress( + self.contractAddress, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + destroy self.scopedProvider + } +} +``` +
+ +### 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..EVMVMBridgedNFT_0x1234.NFT`. + +To get the type identifier for a given NFT, you can use the following code: + +```cadence +// Where `nft` is either a @{NonFungibleToken.NFT} or &{NonFungibleToken.NFT} +nft.getType().identifier +``` + +You may also retrieve the type associated with a given EVM contract address using the following script: + +
+ +get_associated_type.cdc + +```cadence title="get_associated_type.cdc" +// source: https://github.com/onflow/flow-evm-bridge/blob/main/cadence/scripts/bridge/get_associated_type.cdc + +import "EVM" + +import "FlowEVMBridgeConfig" + +/// Returns the Cadence Type associated with the given EVM address (as its hex String) +/// +/// @param evmAddressHex: The hex-encoded address of the EVM contract as a String +/// +/// @return The Cadence Type associated with the EVM address or nil if the address is not onboarded. `nil` may also be +/// returned if the address is not a valid EVM address. +/// +access(all) +fun main(addressHex: String): Type? { + let address = EVM.addressFromString(addressHex) + return FlowEVMBridgeConfig.getTypeAssociated(with: address) +} +``` +
+ +Alternatively, given some onboarded Cadence type, you can retrieve the associated EVM address using the following +script: + +
+ +get_associated_address.cdc + +```cadence title="get_associated_address.cdc" +// source: https://github.com/onflow/flow-evm-bridge/blob/main/cadence/scripts/bridge/get_associated_evm_address.cdc + +import "EVM" + +import "FlowEVMBridgeConfig" + +/// Returns the EVM address associated with the given Cadence type (as its identifier String) +/// +/// @param typeIdentifier: The Cadence type identifier String +/// +/// @return The EVM address as a hex string if the type has an associated EVMAddress, otherwise nil +/// +access(all) +fun main(identifier: String): String? { + if let type = CompositeType(identifier) { + if let address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) { + return address.toString() + } + } + return nil +} +``` +
+ +#### 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 + +```cadence title="bridge_nft_to_evm.cdc" +// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/nft/bridge_nft_to_evm.cdc + +import "FungibleToken" +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// Bridges an NFT from the signer's collection in Cadence to the signer's COA in FlowEVM +/// +/// NOTE: This transaction also onboards the NFT to the bridge if necessary which may incur additional fees +/// than bridging an asset that has already been onboarded. +/// +/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier +/// @param id: The Cadence NFT.id of the NFT to bridge to EVM +/// +transaction(nftIdentifier: String, id: UInt64) { + + let nft: @{NonFungibleToken.NFT} + let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount + let requiresOnboarding: Bool + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + + prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { + /* --- Reference the signer's CadenceOwnedAccount --- */ + // + // Borrow a reference to the signer's COA + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA from provided gateway address") + + /* --- Construct the NFT type --- */ + // + // Construct the NFT type from the provided identifier + let nftType = CompositeType(nftIdentifier) + ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier)) + // Parse the NFT identifier into its components + let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: nftType) + ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier)) + let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: nftType) + ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier)) + + /* --- Retrieve the NFT --- */ + // + // Borrow a reference to the NFT collection, configuring if necessary + let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName) + ?? panic("Could not borrow ViewResolver from NFT contract") + let collectionData = viewResolver.resolveContractView( + resourceType: nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view") + let collection = signer.storage.borrow( + from: collectionData.storagePath + ) ?? panic("Could not access signer's NFT Collection") + + // Withdraw the requested NFT & calculate the approximate bridge fee based on NFT storage usage + let currentStorageUsage = signer.storage.used + self.nft <- collection.withdraw(withdrawID: id) + let withdrawnStorageUsage = signer.storage.used + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: currentStorageUsage - withdrawnStorageUsage + ) * 1.10 + // Determine if the NFT requires onboarding - this impacts the fee required + self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.nft.getType()) + ?? panic("Bridge does not support this asset type") + if self.requiresOnboarding { + approxFee = approxFee + FlowEVMBridgeConfig.onboardFee + } + + /* --- Configure a ScopedFTProvider --- */ + // + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid Provider Capability found in storage.") + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + pre { + self.nft.getType().identifier == nftIdentifier: + "Attempting to send invalid nft type - requested: ".concat(nftIdentifier) + .concat(", sending: ").concat(self.nft.getType().identifier) + } + + execute { + if self.requiresOnboarding { + // Onboard the NFT to the bridge + FlowEVMBridge.onboardByType( + self.nft.getType(), + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + } + // Execute the bridge + self.coa.depositNFT( + nft: <-self.nft, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} +``` +
+ +
+ +bridge_nft_from_evm.cdc + +```cadence title="bridge_nft_from_evm.cdc" +// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/nft/bridge_nft_from_evm.cdc + +import "FungibleToken" +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// This transaction bridges an NFT from EVM to Cadence assuming it has already been onboarded to the FlowEVMBridge +/// NOTE: The ERC721 must have first been onboarded to the bridge. This can be checked via the method +/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress) +/// +/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier +/// @param id: The ERC721 id of the NFT to bridge to Cadence from EVM +/// +transaction(nftIdentifier: String, id: UInt256) { + + let nftType: Type + let collection: &{NonFungibleToken.Collection} + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount + + prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) { + /* --- Reference the signer's CadenceOwnedAccount --- */ + // + // Borrow a reference to the signer's COA + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA from provided gateway address") + + /* --- Construct the NFT type --- */ + // + // Construct the NFT type from the provided identifier + self.nftType = CompositeType(nftIdentifier) + ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier)) + // Parse the NFT identifier into its components + let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType) + ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier)) + let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType) + ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier)) + + /* --- Reference the signer's NFT Collection --- */ + // + // Borrow a reference to the NFT collection, configuring if necessary + let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName) + ?? panic("Could not borrow ViewResolver from NFT contract") + let collectionData = viewResolver.resolveContractView( + resourceType: self.nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view") + if signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) == nil { + signer.storage.save(<-collectionData.createEmptyCollection(), to: collectionData.storagePath) + signer.capabilities.unpublish(collectionData.publicPath) + let collectionCap = signer.capabilities.storage.issue<&{NonFungibleToken.Collection}>(collectionData.storagePath) + signer.capabilities.publish(collectionCap, at: collectionData.publicPath) + } + self.collection = signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) + ?? panic("Could not borrow collection from storage path") + + /* --- Configure a ScopedFTProvider --- */ + // + // Calculate the bridge fee - bridging from EVM consumes no storage, so flat fee + let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid Provider Capability found in storage.") + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Execute the bridge + let nft: @{NonFungibleToken.NFT} <- self.coa.withdrawNFT( + type: self.nftType, + id: id, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + // Ensure the bridged nft is the correct type + assert( + nft.getType() == self.nftType, + message: "Bridged nft type mismatch - requeswted: ".concat(self.nftType.identifier) + .concat(", received: ").concat(nft.getType().identifier) + ) + // Deposit the bridged NFT into the signer's collection + self.collection.deposit(token: <-nft) + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} +``` +
+ +#### 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`](https://github.com/onflow/flow-evm-bridge/blob/main/cadence/contracts/bridge/interfaces/FlowEVMBridgeHandlerInterfaces.cdc) +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 + +```cadence title="bridge_tokens_to_evm.cdc" +// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/tokens/bridge_tokens_to_evm.cdc + +import "FungibleToken" +import "ViewResolver" +import "FungibleTokenMetadataViews" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// Bridges a Vault from the signer's storage to the signer's COA in EVM.Account. +/// +/// NOTE: This transaction also onboards the Vault to the bridge if necessary which may incur additional fees +/// than bridging an asset that has already been onboarded. +/// +/// @param vaultIdentifier: The Cadence type identifier of the FungibleToken Vault to bridge +/// - e.g. vault.getType().identifier +/// @param amount: The amount of tokens to bridge from EVM +/// +transaction(vaultIdentifier: String, amount: UFix64) { + + let sentVault: @{FungibleToken.Vault} + let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount + let requiresOnboarding: Bool + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + + prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { + /* --- Reference the signer's CadenceOwnedAccount --- */ + // + // Borrow a reference to the signer's COA + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA from provided gateway address") + + /* --- Construct the Vault type --- */ + // + // Construct the Vault type from the provided identifier + let vaultType = CompositeType(vaultIdentifier) + ?? panic("Could not construct Vault type from identifier: ".concat(vaultIdentifier)) + // Parse the Vault identifier into its components + let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: vaultType) + ?? panic("Could not get contract address from identifier: ".concat(vaultIdentifier)) + let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: vaultType) + ?? panic("Could not get contract name from identifier: ".concat(vaultIdentifier)) + + /* --- Retrieve the funds --- */ + // + // Borrow a reference to the FungibleToken Vault + let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName) + ?? panic("Could not borrow ViewResolver from FungibleToken contract") + let vaultData = viewResolver.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? ?? panic("Could not resolve FTVaultData view") + let vault = signer.storage.borrow( + from: vaultData.storagePath + ) ?? panic("Could not access signer's FungibleToken Vault") + + // Withdraw the requested balance & calculate the approximate bridge fee based on storage usage + let currentStorageUsage = signer.storage.used + self.sentVault <- vault.withdraw(amount: amount) + let withdrawnStorageUsage = signer.storage.used + // Approximate the bridge fee based on the difference in storage usage with some buffer + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: currentStorageUsage - withdrawnStorageUsage + ) * 1.10 + // Determine if the Vault requires onboarding - this impacts the fee required + self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.sentVault.getType()) + ?? panic("Bridge does not support this asset type") + if self.requiresOnboarding { + approxFee = approxFee + FlowEVMBridgeConfig.onboardFee + } + + /* --- Configure a ScopedFTProvider --- */ + // + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid Provider Capability found in storage.") + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + pre { + self.sentVault.getType().identifier == vaultIdentifier: + "Attempting to send invalid vault type - requested: ".concat(vaultIdentifier) + .concat(", sending: ").concat(self.sentVault.getType().identifier) + } + + execute { + if self.requiresOnboarding { + // Onboard the Vault to the bridge + FlowEVMBridge.onboardByType( + self.sentVault.getType(), + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + } + // Execute the bridge + self.coa.depositTokens( + vault: <-self.sentVault, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} +``` +
+ +
+ +bridge_tokens_from_evm.cdc + +```cadence title="bridge_tokens_from_evm.cdc" +// source: https://www.github.com/onflow/flow-evm-bridge/blob/main/cadence/transactions/bridge/tokens/bridge_tokens_from_evm.cdc + +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" +import "MetadataViews" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// This transaction bridges fungible tokens from EVM to Cadence assuming it has already been onboarded to the +/// FlowEVMBridge. +/// +/// NOTE: The ERC20 must have first been onboarded to the bridge. This can be checked via the method +/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress) +/// +/// @param vaultIdentifier: The Cadence type identifier of the FungibleToken Vault to bridge +/// - e.g. vault.getType().identifier +/// @param amount: The amount of tokens to bridge from EVM +/// +transaction(vaultIdentifier: String, amount: UInt256) { + + let vaultType: Type + let receiver: &{FungibleToken.Vault} + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount + + prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) { + /* --- Reference the signer's CadenceOwnedAccount --- */ + // + // Borrow a reference to the signer's COA + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA from provided gateway address") + + /* --- Construct the Vault type --- */ + // + // Construct the Vault type from the provided identifier + self.vaultType = CompositeType(vaultIdentifier) + ?? panic("Could not construct Vault type from identifier: ".concat(vaultIdentifier)) + // Parse the Vault identifier into its components + let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.vaultType) + ?? panic("Could not get contract address from identifier: ".concat(vaultIdentifier)) + let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: self.vaultType) + ?? panic("Could not get contract name from identifier: ".concat(vaultIdentifier)) + + /* --- Reference the signer's Vault --- */ + // + // Borrow a reference to the FungibleToken Vault, configuring if necessary + let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName) + ?? panic("Could not borrow ViewResolver from FungibleToken contract") + let vaultData = viewResolver.resolveContractView( + resourceType: self.vaultType, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? ?? panic("Could not resolve FTVaultData view") + // If the vault does not exist, create it and publish according to the contract's defined configuration + if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) == nil { + signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) + + signer.capabilities.unpublish(vaultData.receiverPath) + signer.capabilities.unpublish(vaultData.metadataPath) + + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + + signer.capabilities.publish(receiverCap, at: vaultData.receiverPath) + signer.capabilities.publish(metadataCap, at: vaultData.metadataPath) + } + self.receiver = signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) + ?? panic("Could not borrow Vault from storage path") + + /* --- Configure a ScopedFTProvider --- */ + // + // Calculate the bridge fee - bridging from EVM consumes no storage, so flat fee + let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid Provider Capability found in storage.") + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Execute the bridge request + let vault: @{FungibleToken.Vault} <- self.coa.withdrawTokens( + type: self.vaultType, + amount: amount, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + // Ensure the bridged vault is the correct type + assert(vault.getType() == self.vaultType, message: "Bridged vault type mismatch") + // Deposit the bridged token into the signer's vault + self.receiver.deposit(from: <-vault) + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} +``` +
+ +## 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](https://github.com/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: + +```cadence +access(all) struct URI: MetadataViews.File { + /// The base URI prefix, if any. Not needed for all URIs, but helpful + /// for some use cases For example, updating a whole NFT collection's + /// image host easily + access(all) let baseURI: String? + /// The URI string value + /// NOTE: this is set on init as a concatenation of the baseURI and the + /// value if baseURI != nil + access(self) let value: String + + access(all) view fun uri(): String + +} + +access(all) struct EVMBridgedMetadata { + access(all) let name: String + access(all) let symbol: String + + access(all) let uri: {MetadataViews.File} +} +``` + +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](https://github.com/onflow/flow-evm-bridge/blob/main/cadence/contracts/utils/SerializeMetadata.cdc) 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](https://github.com/onflow/flow-evm-bridge/blob/main/solidity/src/interfaces/BridgePermissions.sol) 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](https://github.com/onflow/flow-evm-bridge/blob/main/cadence/contracts/bridge/interfaces/IBridgePermissions.cdc). +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](https://github.com/onflow/flips/blob/main/application/20231222-evm-vm-bridge.md) + +### Additional Resources + +For the current state of Flow EVM across various task paths, see the following resources: + +- [Flow EVM Equivalence forum post](https://forum.flow.com/t/evm-equivalence-on-flow-proposal-and-path-forward/5478) +- [EVM Integration FLIP #223](https://github.com/onflow/flips/pull/225/files) +- [Gateway & JSON RPC FLIP #235](https://github.com/onflow/flips/pull/235) \ No newline at end of file diff --git a/docs/evm/vm-bridge.md b/docs/evm/vm-bridge.md deleted file mode 100644 index 04d23d261e..0000000000 --- a/docs/evm/vm-bridge.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Cross-VM Bridge -sidebar_label: Cross-VM Bridge -sidebar_position: 7 ---- - -# Cross-VM Bridge - -Flow provides the cross-VM bridge which enables Cadence to interact with EVM on Flow in order to bridge tokens, fungible and non-fungible, from one VM environment to the other. The cross-VM bridge is a contract-based, trustless protocol enabling the automated bridging of tokens from Cadence into EVM with  the corresponding ERC-20 and ERC-721 token types. In the opposite direction, it supports bridging of arbitrary EVM ERC-20 and ERC-721 tokens into the corresponding Cadence 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 , 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 as cross-VM bridging functionality is currently not available natively in EVM. - -This [FLIP](https://github.com/onflow/flips/pull/233) outlines the architecture and implementation of the VM bridge.