diff --git a/cadence/tests/flow_evm_bridge_tests.cdc b/cadence/tests/flow_evm_bridge_tests.cdc index 06efe10d..b22c0141 100644 --- a/cadence/tests/flow_evm_bridge_tests.cdc +++ b/cadence/tests/flow_evm_bridge_tests.cdc @@ -182,7 +182,7 @@ fun setup() { arguments: [] ) Test.expect(err, Test.beNil()) - + /* Integrate EVM bridge contract */ // Set factory as registrar in registry @@ -335,7 +335,7 @@ fun testDeployERC721Succeeds() { exampleERCAccount ) Test.expect(erc721DeployResult, Test.beSucceeded()) - + // Get ERC721 & ERC20 deployed contract addresses let evts = Test.eventsOfType(Type()) Test.assertEqual(21, evts.length) @@ -350,12 +350,12 @@ fun testDeployERC20Succeeds() { exampleERCAccount ) Test.expect(erc20DeployResult, Test.beSucceeded()) - + // Get ERC721 & ERC20 deployed contract addresses let evts = Test.eventsOfType(Type()) Test.assertEqual(22, evts.length) erc20AddressHex = getEVMAddressHexFromEvents(evts, idx: 21) - + } access(all) @@ -601,7 +601,7 @@ fun testOnboardAndBridgeNFTToEVMSucceeds() { var requiresOnboarding = typeRequiresOnboardingByIdentifier(exampleNFTIdentifier) ?? panic("Problem getting onboarding status for type") Test.assertEqual(true, requiresOnboarding) - + // Execute bridge NFT to EVM - should also onboard the NFT type bridgeNFTToEVM( signer: alice, @@ -654,7 +654,7 @@ fun testOnboardAndCrossVMTransferNFTToEVMSucceeds() { var requiresOnboarding = typeRequiresOnboardingByIdentifier(exampleNFTIdentifier) ?? panic("Problem getting onboarding status for type") Test.assertEqual(true, requiresOnboarding) - + // Execute bridge NFT to EVM recipient - should also onboard the NFT type let crossVMTransferResult = executeTransaction( "../transactions/bridge/nft/bridge_nft_to_any_evm_address.cdc", @@ -719,7 +719,7 @@ access(all) fun testOnboardAndBridgeTokensToEVMSucceeds() { // Revert to state before ExampleNFT was onboarded Test.reset(to: snapshot) - + var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) var cadenceBalance = getBalance(ownerAddr: alice.address, storagePathIdentifier: "exampleTokenVault") ?? panic("Could not get ExampleToken balance") @@ -767,7 +767,7 @@ access(all) fun testOnboardAndCrossVMTransferTokensToEVMSucceeds() { // Revert to state before ExampleNFT was onboarded Test.reset(to: snapshot) - + var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) var cadenceBalance = getBalance(ownerAddr: alice.address, storagePathIdentifier: "exampleTokenVault") ?? panic("Could not get ExampleToken balance") @@ -1029,8 +1029,47 @@ fun testBridgeCadenceNativeNFTToEVMSucceeds() { Test.assertEqual(true, isOwnerResult.returnValue as! Bool? ?? panic("Problem getting owner status")) } +access(all) +fun testCrossVMTransferCadenceNativeNFTFromEVMSucceeds() { + snapshot = getCurrentBlockHeight() + // Configure recipient's Collection first, using generic setup transaction + let setupCollectionResult = executeTransaction( + "../transactions/example-assets/setup/setup_generic_nft_collection.cdc", + [exampleNFTIdentifier], + bob + ) + Test.expect(setupCollectionResult, Test.beSucceeded()) + + let aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) + + let associatedEVMAddressHex = getAssociatedEVMAddressHex(with: exampleNFTIdentifier) + Test.assertEqual(40, associatedEVMAddressHex.length) + + // Assert ownership of the bridged NFT in EVM + var aliceIsOwner = isOwner(of: UInt256(mintedNFTID), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(true, aliceIsOwner) + + // Execute bridge NFT from EVM to Cadence recipient (Bob in this case) + let crossVMTransferResult = executeTransaction( + "../transactions/bridge/nft/bridge_nft_to_any_cadence_address.cdc", + [ exampleNFTAccount.address, "ExampleNFT", UInt256(mintedNFTID), bob.address ], + alice + ) + Test.expect(crossVMTransferResult, Test.beSucceeded()) + + // Assert ownership of the bridged NFT in EVM has transferred + aliceIsOwner = isOwner(of: UInt256(mintedNFTID), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(false, aliceIsOwner) + + // Assert the NFT is now in Bob's Collection + let bobOwnedIDs = getIDs(ownerAddr: bob.address, storagePathIdentifier: "cadenceExampleNFTCollection") + Test.assertEqual(1, bobOwnedIDs.length) + Test.assertEqual(mintedNFTID, bobOwnedIDs[0]) +} + access(all) fun testBridgeCadenceNativeNFTFromEVMSucceeds() { + Test.reset(to: snapshot) let aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) let associatedEVMAddressHex = getAssociatedEVMAddressHex(with: exampleNFTIdentifier) @@ -1196,11 +1235,59 @@ fun testBridgeCadenceNativeTokenToEVMSucceeds() { Test.assertEqual(expectedEVMBalance, evmBalance) } +access(all) +fun testCrossVMTransferCadenceNativeTokenFromEVMSucceeds() { + snapshot = getCurrentBlockHeight() + // Configure recipient's Vault first, using generic setup transaction + let setupVaultResult = executeTransaction( + "../transactions/example-assets/setup/setup_generic_vault.cdc", + [exampleTokenIdentifier], + bob + ) + Test.expect(setupVaultResult, Test.beSucceeded()) + + let associatedEVMAddressHex = getAssociatedEVMAddressHex(with: exampleTokenIdentifier) + Test.assertEqual(40, associatedEVMAddressHex.length) + + var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) + + // Confirm Alice is starting with 0.0 balance in their Cadence Vault + let preCadenceBalance = getBalance(ownerAddr: alice.address, storagePathIdentifier: "exampleTokenVault") + ?? panic("Problem getting ExampleToken balance") + Test.assertEqual(0.0, preCadenceBalance) + + // Get Alice's ERC20 balance & convert to UFix64 + var evmBalance = balanceOf(evmAddressHex: aliceCOAAddressHex, erc20AddressHex: associatedEVMAddressHex) + let decimals = getTokenDecimals(erc20AddressHex: associatedEVMAddressHex) + let ufixValue = uint256ToUFix64(evmBalance, decimals: decimals) + // Assert the converted balance is equal to the originally minted amount that was bridged in the previous step + Test.assertEqual(exampleTokenMintAmount, ufixValue) + + // Execute bridge tokens from EVM to Cadence recipient (Bob in this case) + let crossVMTransferResult = executeTransaction( + "../transactions/bridge/nft/bridge_tokens_to_any_cadence_address.cdc", + [ exampleTokenAccount.address, "ExampleToken", evmBalance, bob.address ], + alice + ) + Test.expect(crossVMTransferResult, Test.beSucceeded()) + + // Confirm ExampleToken balance has been bridged back to Alice's Cadence vault + let recipientCadenceBalance = getBalance(ownerAddr: bob.address, storagePathIdentifier: "exampleTokenVault") + ?? panic("Problem getting ExampleToken balance") + Test.assertEqual(ufixValue, recipientCadenceBalance) + + // Confirm ownership on EVM side with Alice COA as owner of ERC721 representation + evmBalance = balanceOf(evmAddressHex: aliceCOAAddressHex, erc20AddressHex: associatedEVMAddressHex) + Test.assertEqual(UInt256(0), evmBalance) +} + access(all) fun testBridgeCadenceNativeTokenFromEVMSucceeds() { + Test.reset(to: snapshot) + let associatedEVMAddressHex = getAssociatedEVMAddressHex(with: exampleTokenIdentifier) Test.assertEqual(40, associatedEVMAddressHex.length) - + var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) // Confirm Alice is starting with 0.0 balance in their Cadence Vault diff --git a/cadence/transactions/bridge/nft/bridge_nft_to_any_cadence_address.cdc b/cadence/transactions/bridge/nft/bridge_nft_to_any_cadence_address.cdc new file mode 100644 index 00000000..f0212c1f --- /dev/null +++ b/cadence/transactions/bridge/nft/bridge_nft_to_any_cadence_address.cdc @@ -0,0 +1,101 @@ +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. +/// Also know that the recipient Flow account must have a Receiver capable of receiving the this bridged NFT accessible +/// via published Capability at the token's standard path. +/// NOTE: The ERC721 must have first been onboarded to the bridge. This can be checked via the method +/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress) +/// +/// @param nftContractAddress: The Flow account address hosting the NFT-defining Cadence contract +/// @param nftContractName: The name of the NFT-defining Cadence contract +/// @param id: The ERC721 id of the NFT to bridge to Cadence from EVM +/// @param recipient: The Flow account address to receive the bridged NFT +/// +transaction(nftContractAddress: Address, nftContractName: String, id: UInt256, recipient: Address) { + + let nftType: Type + let receiver: &{NonFungibleToken.Receiver} + 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") + + // Get the ERC721 contract address for the given NFT type + self.nftType = FlowEVMBridgeUtils.buildCompositeType( + address: nftContractAddress, + contractName: nftContractName, + resourceName: "NFT" + ) ?? panic("Could not construct NFT type") + + /* --- Reference the recipient's NFT Receiver --- */ + // + // 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") + // Configure the signer's account for this NFT + 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.receiver = getAccount(recipient).capabilities.borrow<&{NonFungibleToken.Receiver}>(collectionData.publicPath) + ?? panic("Could not borrow Receiver from recipient's public capability 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} + ) + // Deposit the bridged NFT into the signer's collection + self.receiver.deposit(token: <-nft) + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} diff --git a/cadence/transactions/bridge/tokens/bridge_tokens_to_any_cadence_address.cdc b/cadence/transactions/bridge/tokens/bridge_tokens_to_any_cadence_address.cdc new file mode 100644 index 00000000..becc344d --- /dev/null +++ b/cadence/transactions/bridge/tokens/bridge_tokens_to_any_cadence_address.cdc @@ -0,0 +1,109 @@ +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. The full amount to be transferred is sourced from EVM, so it's assumed the signer has sufficient +/// balance of the ERC20 to bridging into Cadence. Also know that the recipient Flow account must have a Receiver +/// capable of receiving the bridged tokens accessible via published Capability at the token's standard path. +/// +/// NOTE: The ERC721 must have first been onboarded to the bridge. This can be checked via the method +/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress) +/// +/// @param tokenContractAddress: The Flow account address hosting the FT-defining Cadence contract +/// @param tokenContractName: The name of the Vault-defining Cadence contract +/// @param amount: The amount of tokens to bridge from EVM +/// @param recipient: The Flow account address to receive the bridged tokens +/// +transaction(tokenContractAddress: Address, tokenContractName: String, amount: UInt256, recipient: Address) { + + let vaultType: Type + let receiver: &{FungibleToken.Receiver} + 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") + + // Get the ERC20 contract address for the given FungibleToken Vault type + self.vaultType = FlowEVMBridgeUtils.buildCompositeType( + address: tokenContractAddress, + contractName: tokenContractName, + resourceName: "Vault" + ) ?? panic("Could not construct Vault type of: " .concat(tokenContractAddress.toString()).concat(".").concat(tokenContractName).concat(".Vault")) + + /* --- 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 = getAccount(recipient).capabilities.borrow<&{FungibleToken.Receiver}>(vaultData.receiverPath) + ?? panic("Could not borrow Vault from recipient's account") + + /* --- 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} + ) + // Deposit the bridged token into the signer's vault + self.receiver.deposit(from: <-vault) + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} diff --git a/cadence/transactions/example-assets/setup/setup_generic_nft_collection.cdc b/cadence/transactions/example-assets/setup/setup_generic_nft_collection.cdc new file mode 100644 index 00000000..a4a5c664 --- /dev/null +++ b/cadence/transactions/example-assets/setup/setup_generic_nft_collection.cdc @@ -0,0 +1,31 @@ +import "NonFungibleToken" +import "MetadataViews" + +import "FlowEVMBridgeUtils" + +transaction(nftIdentifier: String) { + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + // Gather identifying information about the NFT and its defining contract + let nftType = CompositeType(nftIdentifier) ?? panic("Invalid NFT identifier: ".concat(nftIdentifier)) + let contractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: nftType) + ?? panic("Could not derive contract address from identifier: ".concat(nftIdentifier)) + let contractName = FlowEVMBridgeUtils.getContractName(fromType: nftType) + ?? panic("Could not derive contract name from identifier: ".concat(nftIdentifier)) + // Borrow the contract and resolve its collection data + let nftContract = getAccount(contractAddress).contracts.borrow<&{NonFungibleToken}>(name: contractName) + ?? panic("No such NFT contract found") + let data = nftContract.resolveContractView( + resourceType: nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve collection data for NFT type: ".concat(nftIdentifier)) + + // Create a new collection and save it to signer's storage at the collection's default storage path + signer.storage.save(<-data.createEmptyCollection(), to: data.storagePath) + + // Issue a public Collection capability and publish it to the collection's default public path + signer.capabilities.unpublish(data.publicPath) + let receiverCap = signer.capabilities.storage.issue<&{NonFungibleToken.Collection}>(data.storagePath) + signer.capabilities.publish(receiverCap, at: data.publicPath) + } +} diff --git a/cadence/transactions/example-assets/setup/setup_generic_vault.cdc b/cadence/transactions/example-assets/setup/setup_generic_vault.cdc new file mode 100644 index 00000000..bb4b0700 --- /dev/null +++ b/cadence/transactions/example-assets/setup/setup_generic_vault.cdc @@ -0,0 +1,33 @@ +import "FungibleToken" +import "NonFungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" + +import "FlowEVMBridgeUtils" + +transaction(vaultIdentifier: String) { + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + // Gather identifying information about the Vault and its defining contract + let vaultType = CompositeType(vaultIdentifier) ?? panic("Invalid Vault identifier: ".concat(vaultIdentifier)) + let contractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: vaultType) + ?? panic("Could not derive contract address from identifier: ".concat(vaultIdentifier)) + let contractName = FlowEVMBridgeUtils.getContractName(fromType: vaultType) + ?? panic("Could not derive contract name from identifier: ".concat(vaultIdentifier)) + // Borrow the contract and resolve its Vault data + let ftContract = getAccount(contractAddress).contracts.borrow<&{FungibleToken}>(name: contractName) + ?? panic("No such FungibleToken contract found") + let data = ftContract.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve collection data for Vault type: ".concat(vaultIdentifier)) + + // Create a new collection and save it to signer's storage at the collection's default storage path + signer.storage.save(<-data.createEmptyVault(), to: data.storagePath) + + // Issue a public Collection capability and publish it to the collection's default public path + signer.capabilities.unpublish(data.receiverPath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(data.storagePath) + signer.capabilities.publish(receiverCap, at: data.receiverPath) + } +}