diff --git a/cadence/tests/flow_evm_bridge_tests.cdc b/cadence/tests/flow_evm_bridge_tests.cdc index 12b61784..3a95166e 100644 --- a/cadence/tests/flow_evm_bridge_tests.cdc +++ b/cadence/tests/flow_evm_bridge_tests.cdc @@ -25,7 +25,8 @@ access(all) let exampleNFTIdentifier = "A.0000000000000008.ExampleNFT.NFT" access(all) let exampleNFTTokenName = "Example NFT" access(all) let exampleNFTTokenDescription = "Example NFT token description" access(all) let exampleNFTTokenThumbnail = "https://examplenft.com/thumbnail.png" -access(all) var mintedNFTID: UInt64 = 0 +access(all) var mintedNFTID1: UInt64 = 0 +access(all) var mintedNFTID2: UInt64 = 0 // ExampleToken access(all) let exampleTokenIdentifier = "A.0000000000000010.ExampleToken.Vault" @@ -516,22 +517,38 @@ fun testMintExampleNFTSucceeds() { Test.expect(hasCollection, Test.beSucceeded()) Test.assertEqual(true, hasCollection.returnValue as! Bool? ?? panic("Problem getting collection status")) - let mintExampleNFTResult = executeTransaction( + var mintExampleNFTResult = executeTransaction( "../transactions/example-assets/example-nft/mint_nft.cdc", [alice.address, exampleNFTTokenName, exampleNFTTokenDescription, exampleNFTTokenThumbnail, [], [], []], exampleNFTAccount ) Test.expect(mintExampleNFTResult, Test.beSucceeded()) - let aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") + var aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") Test.assertEqual(1, aliceOwnedIDs.length) - let events = Test.eventsOfType(Type()) + var events = Test.eventsOfType(Type()) Test.assertEqual(1, events.length) - let evt = events[0] as! NonFungibleToken.Deposited - mintedNFTID = evt.id + var evt = events[0] as! NonFungibleToken.Deposited + mintedNFTID1 = evt.id + + mintExampleNFTResult = executeTransaction( + "../transactions/example-assets/example-nft/mint_nft.cdc", + [alice.address, exampleNFTTokenName, exampleNFTTokenDescription, exampleNFTTokenThumbnail, [], [], []], + exampleNFTAccount + ) + Test.expect(mintExampleNFTResult, Test.beSucceeded()) + + aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") + Test.assertEqual(2, aliceOwnedIDs.length) + + events = Test.eventsOfType(Type()) + Test.assertEqual(2, events.length) + evt = events[1] as! NonFungibleToken.Deposited + mintedNFTID2 = evt.id - Test.assertEqual(aliceOwnedIDs[0], mintedNFTID) + Test.assert(mintedNFTID1 != mintedNFTID2) + Test.assertEqual(true, aliceOwnedIDs.contains(mintedNFTID1) && aliceOwnedIDs.contains(mintedNFTID2)) } access(all) @@ -694,8 +711,7 @@ fun testOnboardAndBridgeNFTToEVMSucceeds() { var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) var aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") - Test.assertEqual(1, aliceOwnedIDs.length) - let aliceID = aliceOwnedIDs[0] + Test.assertEqual(2, aliceOwnedIDs.length) var requiresOnboarding = typeRequiresOnboardingByIdentifier(exampleNFTIdentifier) ?? panic("Problem getting onboarding status for type") @@ -705,7 +721,7 @@ fun testOnboardAndBridgeNFTToEVMSucceeds() { bridgeNFTToEVM( signer: alice, nftIdentifier: exampleNFTIdentifier, - nftID: aliceID, + nftID: mintedNFTID1, bridgeAccountAddr: bridgeAccount.address, beFailed: false ) @@ -726,12 +742,12 @@ fun testOnboardAndBridgeNFTToEVMSucceeds() { // Confirm the NFT is no longer in Alice's Collection aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") - Test.assertEqual(0, aliceOwnedIDs.length) + Test.assertEqual(1, aliceOwnedIDs.length) // Confirm ownership on EVM side with Alice COA as owner of ERC721 representation let isOwnerResult = executeScript( "../scripts/utils/is_owner.cdc", - [UInt256(mintedNFTID), aliceCOAAddressHex, associatedEVMAddressHex] + [UInt256(mintedNFTID1), aliceCOAAddressHex, associatedEVMAddressHex] ) Test.expect(isOwnerResult, Test.beSucceeded()) Test.assertEqual(true, isOwnerResult.returnValue as! Bool? ?? panic("Problem getting owner status")) @@ -744,8 +760,7 @@ fun testOnboardAndCrossVMTransferNFTToEVMSucceeds() { var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) var aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") - Test.assertEqual(1, aliceOwnedIDs.length) - let aliceID = aliceOwnedIDs[0] + Test.assertEqual(2, aliceOwnedIDs.length) let recipient = getCOAAddressHex(atFlowAddress: bob.address) @@ -756,7 +771,7 @@ fun testOnboardAndCrossVMTransferNFTToEVMSucceeds() { // 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", - [ exampleNFTIdentifier, aliceID, recipient ], + [ exampleNFTIdentifier, mintedNFTID1, recipient ], alice ) Test.expect(crossVMTransferResult, Test.beSucceeded()) @@ -777,15 +792,11 @@ fun testOnboardAndCrossVMTransferNFTToEVMSucceeds() { // Confirm the NFT is no longer in Alice's Collection aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") - Test.assertEqual(0, aliceOwnedIDs.length) + Test.assertEqual(1, aliceOwnedIDs.length) // Confirm ownership on EVM side with Alice COA as owner of ERC721 representation - let isOwnerResult = executeScript( - "../scripts/utils/is_owner.cdc", - [UInt256(mintedNFTID), recipient, associatedEVMAddressHex] - ) - Test.expect(isOwnerResult, Test.beSucceeded()) - Test.assertEqual(true, isOwnerResult.returnValue as! Bool? ?? panic("Problem getting owner status")) + var aliceIsOwner = isOwner(of: UInt256(mintedNFTID1), ownerEVMAddrHex: recipient, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(true, aliceIsOwner) } access(all) @@ -1085,7 +1096,7 @@ fun testPauseBridgeSucceeds() { Test.assertEqual(true, isPausedResult.returnValue as! Bool? ?? panic("Problem getting pause status")) var aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") - Test.assertEqual(1, aliceOwnedIDs.length) + Test.assertEqual(2, aliceOwnedIDs.length) var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) @@ -1111,8 +1122,10 @@ fun testPauseBridgeSucceeds() { access(all) fun testBridgeCadenceNativeNFTToEVMSucceeds() { + snapshot = getCurrentBlockHeight() + var aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") - Test.assertEqual(1, aliceOwnedIDs.length) + Test.assertEqual(2, aliceOwnedIDs.length) var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) @@ -1120,7 +1133,7 @@ fun testBridgeCadenceNativeNFTToEVMSucceeds() { bridgeNFTToEVM( signer: alice, nftIdentifier: exampleNFTIdentifier, - nftID: aliceOwnedIDs[0], + nftID: mintedNFTID1, bridgeAccountAddr: bridgeAccount.address, beFailed: false ) @@ -1130,26 +1143,110 @@ fun testBridgeCadenceNativeNFTToEVMSucceeds() { // Confirm the NFT is no longer in Alice's Collection aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") - Test.assertEqual(0, aliceOwnedIDs.length) + Test.assertEqual(1, aliceOwnedIDs.length) // Confirm ownership on EVM side with Alice COA as owner of ERC721 representation let isOwnerResult = executeScript( "../scripts/utils/is_owner.cdc", - [UInt256(mintedNFTID), aliceCOAAddressHex, associatedEVMAddressHex] + [UInt256(mintedNFTID1), aliceCOAAddressHex, associatedEVMAddressHex] ) Test.expect(isOwnerResult, Test.beSucceeded()) Test.assertEqual(true, isOwnerResult.returnValue as! Bool? ?? panic("Problem getting owner status")) - let isNFTLocked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID) + let isNFTLocked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID1) Test.assertEqual(true, isNFTLocked) - let metadata = resolveLockedNFTView(bridgeAddress: bridgeAccount.address, nftTypeIdentifier: exampleNFTIdentifier, id: UInt256(mintedNFTID), viewIdentifier: Type().identifier) + let metadata = resolveLockedNFTView(bridgeAddress: bridgeAccount.address, nftTypeIdentifier: exampleNFTIdentifier, id: UInt256(mintedNFTID1), viewIdentifier: Type().identifier) Test.assert(metadata != nil, message: "Expected NFT metadata to be resolved from escrow but none was returned") } +access(all) +fun testBatchBridgeCadenceNativeNFTToEVMSucceeds() { + let tmp = snapshot + Test.reset(to: snapshot) + snapshot = tmp + + var aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") + Test.assertEqual(2, aliceOwnedIDs.length) + + var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) + + // Execute bridge to EVM + let bridgeResult = executeTransaction( + "../transactions/bridge/nft/batch_bridge_nft_to_evm.cdc", + [ exampleNFTIdentifier, aliceOwnedIDs ], + alice + ) + Test.expect(bridgeResult, Test.beSucceeded()) + + let associatedEVMAddressHex = getAssociatedEVMAddressHex(with: exampleNFTIdentifier) + Test.assertEqual(40, associatedEVMAddressHex.length) + + // Confirm the NFT is no longer in Alice's Collection + aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") + Test.assertEqual(0, aliceOwnedIDs.length) + + // Confirm ownership on EVM side with Alice COA as owner of ERC721 representation + var aliceIsOwner = isOwner(of: UInt256(mintedNFTID1), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(true, aliceIsOwner) + aliceIsOwner = isOwner(of: UInt256(mintedNFTID2), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(true, aliceIsOwner) + + let isNFT1Locked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID1) + let isNFT2Locked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID2) + Test.assertEqual(true, isNFT1Locked) + Test.assertEqual(true, isNFT2Locked) + + let metadata1 = resolveLockedNFTView(bridgeAddress: bridgeAccount.address, nftTypeIdentifier: exampleNFTIdentifier, id: UInt256(mintedNFTID1), viewIdentifier: Type().identifier) + let metadata2 = resolveLockedNFTView(bridgeAddress: bridgeAccount.address, nftTypeIdentifier: exampleNFTIdentifier, id: UInt256(mintedNFTID2), viewIdentifier: Type().identifier) + Test.assert(metadata1 != nil, message: "Expected NFT metadata to be resolved from escrow but none was returned") + Test.assert(metadata2 != nil, message: "Expected NFT metadata to be resolved from escrow but none was returned") +} + +access(all) +fun testBatchBridgeCadenceNativeNFTFromEVMSucceeds() { + snapshot = getCurrentBlockHeight() + + var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address) + let associatedEVMAddressHex = getAssociatedEVMAddressHex(with: exampleNFTIdentifier) + Test.assertEqual(40, associatedEVMAddressHex.length) + + // Confirm ownership on EVM side with Alice COA as owner of ERC721 representation + var aliceIsOwner = isOwner(of: UInt256(mintedNFTID1), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(true, aliceIsOwner) + aliceIsOwner = isOwner(of: UInt256(mintedNFTID2), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(true, aliceIsOwner) + + // Execute bridge from EVM + let bridgeResult = executeTransaction( + "../transactions/bridge/nft/batch_bridge_nft_from_evm.cdc", + [ exampleNFTIdentifier, [UInt256(mintedNFTID1), UInt256(mintedNFTID2)] ], + alice + ) + Test.expect(bridgeResult, Test.beSucceeded()) + + // Confirm the NFT is no longer in Alice's Collection + let aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") + Test.assertEqual(2, aliceOwnedIDs.length) + + // Confirm ownership on EVM side with Alice COA as owner of ERC721 representation + aliceIsOwner = isOwner(of: UInt256(mintedNFTID1), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(false, aliceIsOwner) + aliceIsOwner = isOwner(of: UInt256(mintedNFTID2), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + Test.assertEqual(false, aliceIsOwner) + + let isNFT1Locked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID1) + let isNFT2Locked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID2) + Test.assertEqual(false, isNFT1Locked) + Test.assertEqual(false, isNFT2Locked) +} + access(all) fun testCrossVMTransferCadenceNativeNFTFromEVMSucceeds() { + let tmp = snapshot + Test.reset(to: snapshot) snapshot = getCurrentBlockHeight() + // Configure recipient's Collection first, using generic setup transaction let setupCollectionResult = executeTransaction( "../transactions/example-assets/setup/setup_generic_nft_collection.cdc", @@ -1164,27 +1261,27 @@ fun testCrossVMTransferCadenceNativeNFTFromEVMSucceeds() { Test.assertEqual(40, associatedEVMAddressHex.length) // Assert ownership of the bridged NFT in EVM - var aliceIsOwner = isOwner(of: UInt256(mintedNFTID), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + var aliceIsOwner = isOwner(of: UInt256(mintedNFTID1), 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", - [ exampleNFTIdentifier, UInt256(mintedNFTID), bob.address ], + [ exampleNFTIdentifier, UInt256(mintedNFTID1), 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) + aliceIsOwner = isOwner(of: UInt256(mintedNFTID1), 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]) + Test.assertEqual(mintedNFTID1, bobOwnedIDs[0]) - let isNFTLocked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID) + let isNFTLocked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID1) Test.assertEqual(false, isNFTLocked) } @@ -1197,26 +1294,26 @@ fun testBridgeCadenceNativeNFTFromEVMSucceeds() { Test.assertEqual(40, associatedEVMAddressHex.length) // Assert ownership of the bridged NFT in EVM - var aliceIsOwner = isOwner(of: UInt256(mintedNFTID), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + var aliceIsOwner = isOwner(of: UInt256(mintedNFTID1), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) Test.assertEqual(true, aliceIsOwner) // Execute bridge from EVM bridgeNFTFromEVM( signer: alice, nftIdentifier: exampleNFTIdentifier, - erc721ID: UInt256(mintedNFTID), + erc721ID: UInt256(mintedNFTID1), bridgeAccountAddr: bridgeAccount.address, beFailed: false ) // Assert ownership of the bridged NFT in EVM has transferred - aliceIsOwner = isOwner(of: UInt256(mintedNFTID), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) + aliceIsOwner = isOwner(of: UInt256(mintedNFTID1), ownerEVMAddrHex: aliceCOAAddressHex, erc721AddressHex: associatedEVMAddressHex) Test.assertEqual(false, aliceIsOwner) // Assert the NFT is back in Alice's Collection let aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection") Test.assertEqual(1, aliceOwnedIDs.length) - Test.assertEqual(mintedNFTID, aliceOwnedIDs[0]) + Test.assertEqual(true, aliceOwnedIDs.contains(mintedNFTID1)) } access(all) @@ -1282,7 +1379,7 @@ fun testPauseByTypeSucceeds() { bridgeNFTToEVM( signer: alice, nftIdentifier: exampleNFTIdentifier, - nftID: aliceOwnedIDs[0], + nftID: mintedNFTID1, bridgeAccountAddr: bridgeAccount.address, beFailed: true ) diff --git a/cadence/transactions/bridge/nft/batch_bridge_nft_from_evm.cdc b/cadence/transactions/bridge/nft/batch_bridge_nft_from_evm.cdc new file mode 100644 index 00000000..d2beb2a9 --- /dev/null +++ b/cadence/transactions/bridge/nft/batch_bridge_nft_from_evm.cdc @@ -0,0 +1,116 @@ +import "FungibleToken" +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// This transaction bridges NFTs from EVM to Cadence assuming the NFT 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 ids: The ERC721 ids of the NFTs to bridge to Cadence from EVM +/// +transaction(nftIdentifier: String, ids: [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 signer's account at path /storage/evm") + + /* --- 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 with name " + .concat(nftContractName).concat(" and address ") + .concat(nftContractAddress.toString())) + let collectionData = viewResolver.resolveContractView( + resourceType: self.nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier)) + 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 a NonFungibleToken Collection from the signer's storage path " + .concat(collectionData.storagePath.toString())) + + /* --- Configure a ScopedFTProvider --- */ + // + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length)) + // 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 FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Iterate over the provided ids + for id in ids { + // 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 - requested: ".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 + } +} diff --git a/cadence/transactions/bridge/nft/batch_bridge_nft_to_any_cadence_address.cdc b/cadence/transactions/bridge/nft/batch_bridge_nft_to_any_cadence_address.cdc new file mode 100644 index 00000000..9de56dd1 --- /dev/null +++ b/cadence/transactions/bridge/nft/batch_bridge_nft_to_any_cadence_address.cdc @@ -0,0 +1,118 @@ +import "FungibleToken" +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// This transaction bridges NFTs from EVM to Cadence assuming the NFT 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 nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier +/// @param ids: The ERC721 ids of the NFTs to bridge to Cadence from EVM +/// @param recipient: The Flow account address to receive the bridged NFT +/// +transaction(nftIdentifier: String, ids: [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 signer's account at path /storage/evm") + + /* --- 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 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 with name " + .concat(nftContractName).concat(" and address ") + .concat(nftContractAddress.toString())) + let collectionData = viewResolver.resolveContractView( + resourceType: self.nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier)) + // 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 NonFungibleToken Receiver from recipient's public capability path") + + /* --- Configure a ScopedFTProvider --- */ + // + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length)) + // 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 FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Iterate over the provided ids + for id in ids { + // 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 - requested: ".concat(self.nftType.identifier) + .concat(", received: ").concat(nft.getType().identifier) + ) + // 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/nft/batch_bridge_nft_to_any_evm_address.cdc b/cadence/transactions/bridge/nft/batch_bridge_nft_to_any_evm_address.cdc new file mode 100644 index 00000000..76b24d01 --- /dev/null +++ b/cadence/transactions/bridge/nft/batch_bridge_nft_to_any_evm_address.cdc @@ -0,0 +1,131 @@ +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 provided recipient 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 +/// @param recipient: The hex-encoded EVM address to receive the NFT +/// +transaction(nftIdentifier: String, ids: [UInt64], recipient: String) { + + let nftType: Type + let collection: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection} + 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 signer's account at path /storage/evm") + + /* --- 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)) + + /* --- 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 with name " + .concat(nftContractName).concat(" and address ") + .concat(nftContractAddress.toString())) + let collectionData = viewResolver.resolveContractView( + resourceType: self.nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier)) + self.collection = signer.storage.borrow( + from: collectionData.storagePath + ) ?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path " + .concat(collectionData.storagePath.toString())) + + // Withdraw the requested NFT & set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length)) + // Determine if the NFT requires onboarding - this impacts the fee required + self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.nftType) + ?? panic("Bridge does not support the requested asset type ".concat(nftIdentifier)) + // Add the onboarding fee if onboarding is necessary + 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 FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + if self.requiresOnboarding { + // Onboard the NFT to the bridge + FlowEVMBridge.onboardByType( + self.nftType, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + } + + // Iterate over requested IDs and bridge each NFT to the provided recipient in EVM + for id in ids { + // Withdraw the NFT & ensure it is the correct type + let nft <-self.collection.withdraw(withdrawID: id) + assert( + nft.getType() == self.nftType, + message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier) + .concat(", received: ").concat(nft.getType().identifier) + ) + // Execute the bridge to EVM + let recipientEVMAddress = EVM.addressFromString(recipient) + FlowEVMBridge.bridgeNFTToEVM( + token: <-nft, + to: EVM.addressFromString(recipient), + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + } + + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} diff --git a/cadence/transactions/bridge/nft/batch_bridge_nft_to_evm.cdc b/cadence/transactions/bridge/nft/batch_bridge_nft_to_evm.cdc new file mode 100644 index 00000000..e07c0c90 --- /dev/null +++ b/cadence/transactions/bridge/nft/batch_bridge_nft_to_evm.cdc @@ -0,0 +1,128 @@ +import "FungibleToken" +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// Bridges NFTs (from the same collection) 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, ids: [UInt64]) { + + let nftType: Type + let collection: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection} + 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 signer's account at path /storage/evm") + + /* --- 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)) + + /* --- 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 with name " + .concat(nftContractName).concat(" and address ") + .concat(nftContractAddress.toString())) + let collectionData = viewResolver.resolveContractView( + resourceType: self.nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier)) + self.collection = signer.storage.borrow( + from: collectionData.storagePath + ) ?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path " + .concat(collectionData.storagePath.toString())) + + // Withdraw the requested NFT & set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length)) + // Determine if the NFT requires onboarding - this impacts the fee required + self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.nftType) + ?? panic("Bridge does not support the requested asset type ".concat(nftIdentifier)) + // Add the onboarding fee if onboarding is necessary + 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 FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + if self.requiresOnboarding { + // Onboard the NFT to the bridge + FlowEVMBridge.onboardByType( + self.nftType, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + } + + // Iterate over requested IDs and bridge each NFT to the signer's COA in EVM + for id in ids { + // Withdraw the NFT & ensure it's the correct type + let nft <-self.collection.withdraw(withdrawID: id) + assert( + nft.getType() == self.nftType, + message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier) + .concat(", received: ").concat(nft.getType().identifier) + ) + // Execute the bridge to EVM for the current ID + self.coa.depositNFT( + nft: <-nft, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + } + + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} diff --git a/cadence/transactions/bridge/nft/bridge_nft_from_evm.cdc b/cadence/transactions/bridge/nft/bridge_nft_from_evm.cdc index 3e813a43..43c73b06 100644 --- a/cadence/transactions/bridge/nft/bridge_nft_from_evm.cdc +++ b/cadence/transactions/bridge/nft/bridge_nft_from_evm.cdc @@ -31,7 +31,7 @@ transaction(nftIdentifier: String, id: UInt256) { // // 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") + ?? panic("Could not borrow COA signer's account at path /storage/evm") /* --- Construct the NFT type --- */ // @@ -48,11 +48,14 @@ transaction(nftIdentifier: String, id: UInt256) { // // 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") + ?? panic("Could not borrow ViewResolver from NFT contract with name " + .concat(nftContractName).concat(" and address ") + .concat(nftContractAddress.toString())) let collectionData = viewResolver.resolveContractView( resourceType: self.nftType, viewType: Type() - ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view") + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier)) if signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) == nil { signer.storage.save(<-collectionData.createEmptyCollection(), to: collectionData.storagePath) signer.capabilities.unpublish(collectionData.publicPath) @@ -60,12 +63,15 @@ transaction(nftIdentifier: String, id: UInt256) { 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") + ?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path " + .concat(collectionData.storagePath.toString())) /* --- Configure a ScopedFTProvider --- */ // - // Calculate the bridge fee - bridging from EVM consumes no storage, so flat fee - let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) // 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( @@ -76,7 +82,8 @@ transaction(nftIdentifier: String, id: UInt256) { // Copy the stored Provider capability and create a ScopedFTProvider let providerCapCopy = signer.storage.copy>( from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid Provider Capability found in storage.") + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( provider: providerCapCopy, @@ -95,7 +102,7 @@ transaction(nftIdentifier: String, id: UInt256) { // Ensure the bridged nft is the correct type assert( nft.getType() == self.nftType, - message: "Bridged nft type mismatch - requeswted: ".concat(self.nftType.identifier) + message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier) .concat(", received: ").concat(nft.getType().identifier) ) // Deposit the bridged NFT into the signer's collection 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 index 2c3f3aa3..ada8f29f 100644 --- a/cadence/transactions/bridge/nft/bridge_nft_to_any_cadence_address.cdc +++ b/cadence/transactions/bridge/nft/bridge_nft_to_any_cadence_address.cdc @@ -33,7 +33,7 @@ transaction(nftIdentifier: String, id: UInt256, recipient: Address) { // // 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") + ?? panic("Could not borrow COA signer's account at path /storage/evm") /* --- Construct the NFT type --- */ // @@ -50,11 +50,14 @@ transaction(nftIdentifier: String, id: UInt256, recipient: Address) { // // 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") + ?? panic("Could not borrow ViewResolver from NFT contract with name " + .concat(nftContractName).concat(" and address ") + .concat(nftContractAddress.toString())) let collectionData = viewResolver.resolveContractView( resourceType: self.nftType, viewType: Type() - ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view") + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier)) // 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) @@ -63,12 +66,14 @@ transaction(nftIdentifier: String, id: UInt256, recipient: Address) { 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") + ?? panic("Could not borrow NonFungibleToken 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) + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) // 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( @@ -79,7 +84,8 @@ transaction(nftIdentifier: String, id: UInt256, recipient: Address) { // Copy the stored Provider capability and create a ScopedFTProvider let providerCapCopy = signer.storage.copy>( from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid Provider Capability found in storage.") + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( provider: providerCapCopy, @@ -98,7 +104,7 @@ transaction(nftIdentifier: String, id: UInt256, recipient: Address) { // Ensure the bridged nft is the correct type assert( nft.getType() == self.nftType, - message: "Bridged nft type mismatch - requeswted: ".concat(self.nftType.identifier) + message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier) .concat(", received: ").concat(nft.getType().identifier) ) // Deposit the bridged NFT into the signer's collection diff --git a/cadence/transactions/bridge/nft/bridge_nft_to_any_evm_address.cdc b/cadence/transactions/bridge/nft/bridge_nft_to_any_evm_address.cdc index 2a33a33e..46953e1e 100644 --- a/cadence/transactions/bridge/nft/bridge_nft_to_any_evm_address.cdc +++ b/cadence/transactions/bridge/nft/bridge_nft_to_any_evm_address.cdc @@ -42,25 +42,28 @@ transaction(nftIdentifier: String, id: UInt64, recipient: String) { // // 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") + ?? panic("Could not borrow ViewResolver from NFT contract with name " + .concat(nftContractName).concat(" and address ") + .concat(nftContractAddress.toString())) let collectionData = viewResolver.resolveContractView( resourceType: nil, viewType: Type() - ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view") + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(nftType.identifier)) let collection = signer.storage.borrow( from: collectionData.storagePath - ) ?? panic("Could not access signer's NFT Collection") + ) ?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path " + .concat(collectionData.storagePath.toString())) // 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 + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) // 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") + ?? panic("Bridge does not support the requested asset type ".concat(nftIdentifier)) + // Add the onboarding fee if onboarding is necessary if self.requiresOnboarding { approxFee = approxFee + FlowEVMBridgeConfig.onboardFee } @@ -77,7 +80,8 @@ transaction(nftIdentifier: String, id: UInt64, recipient: String) { // Copy the stored Provider capability and create a ScopedFTProvider let providerCapCopy = signer.storage.copy>( from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid Provider Capability found in storage.") + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( provider: providerCapCopy, @@ -100,7 +104,7 @@ transaction(nftIdentifier: String, id: UInt64, recipient: String) { feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} ) } - // Execute the bridge transaction + // Execute the bridge to EVM let recipientEVMAddress = EVM.addressFromString(recipient) FlowEVMBridge.bridgeNFTToEVM( token: <-self.nft, diff --git a/cadence/transactions/bridge/nft/bridge_nft_to_evm.cdc b/cadence/transactions/bridge/nft/bridge_nft_to_evm.cdc index 82986f80..ed216f15 100644 --- a/cadence/transactions/bridge/nft/bridge_nft_to_evm.cdc +++ b/cadence/transactions/bridge/nft/bridge_nft_to_evm.cdc @@ -32,7 +32,7 @@ transaction(nftIdentifier: String, id: UInt64) { // // 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") + ?? panic("Could not borrow COA signer's account at path /storage/evm") /* --- Construct the NFT type --- */ // @@ -49,25 +49,28 @@ transaction(nftIdentifier: String, id: UInt64) { // // 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") + ?? panic("Could not borrow ViewResolver from NFT contract with name " + .concat(nftContractName).concat(" and address ") + .concat(nftContractAddress.toString())) let collectionData = viewResolver.resolveContractView( resourceType: nftType, viewType: Type() - ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view") + ) as! MetadataViews.NFTCollectionData? + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(nftType.identifier)) let collection = signer.storage.borrow( from: collectionData.storagePath - ) ?? panic("Could not access signer's NFT Collection") + ) ?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path " + .concat(collectionData.storagePath.toString())) - // Withdraw the requested NFT & calculate the approximate bridge fee based on NFT storage usage - let currentStorageUsage = signer.storage.used + // Withdraw the requested NFT & set a cap on the withdrawable bridge fee self.nft <- collection.withdraw(withdrawID: id) - let withdrawnStorageUsage = signer.storage.used var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( - bytes: currentStorageUsage - withdrawnStorageUsage - ) * 1.10 + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) // 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") + ?? panic("Bridge does not support the requested asset type ".concat(nftIdentifier)) + // Add the onboarding fee if onboarding is necessary if self.requiresOnboarding { approxFee = approxFee + FlowEVMBridgeConfig.onboardFee } @@ -84,7 +87,8 @@ transaction(nftIdentifier: String, id: UInt64) { // Copy the stored Provider capability and create a ScopedFTProvider let providerCapCopy = signer.storage.copy>( from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid Provider Capability found in storage.") + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( provider: providerCapCopy, diff --git a/cadence/transactions/bridge/tokens/bridge_tokens_from_evm.cdc b/cadence/transactions/bridge/tokens/bridge_tokens_from_evm.cdc index 55d20a54..1f61be93 100644 --- a/cadence/transactions/bridge/tokens/bridge_tokens_from_evm.cdc +++ b/cadence/transactions/bridge/tokens/bridge_tokens_from_evm.cdc @@ -34,7 +34,7 @@ transaction(vaultIdentifier: String, amount: UInt256) { // // 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") + ?? panic("Could not borrow COA signer's account at path /storage/evm") /* --- Construct the Vault type --- */ // @@ -51,11 +51,14 @@ transaction(vaultIdentifier: String, amount: UInt256) { // // 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") + ?? panic("Could not borrow ViewResolver from FungibleToken contract with name" + .concat(tokenContractName).concat(" and address ") + .concat(tokenContractAddress.toString())) let vaultData = viewResolver.resolveContractView( resourceType: self.vaultType, viewType: Type() - ) as! FungibleTokenMetadataViews.FTVaultData? ?? panic("Could not resolve FTVaultData view") + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData view for Vault type ".concat(self.vaultType.identifier)) // 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) @@ -70,12 +73,14 @@ transaction(vaultIdentifier: String, amount: UInt256) { 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") + ?? panic("Could not borrow FungibleToken Vault from storage path ".concat(vaultData.storagePath.toString())) /* --- Configure a ScopedFTProvider --- */ // - // Calculate the bridge fee - bridging from EVM consumes no storage, so flat fee - let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) // 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( @@ -86,7 +91,8 @@ transaction(vaultIdentifier: String, amount: UInt256) { // Copy the stored Provider capability and create a ScopedFTProvider let providerCapCopy = signer.storage.copy>( from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid Provider Capability found in storage.") + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( provider: providerCapCopy, @@ -103,7 +109,11 @@ transaction(vaultIdentifier: String, amount: UInt256) { 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") + assert( + vault.getType() == self.vaultType, + message: "Bridged vault type mismatch - requested: ".concat(self.vaultType.identifier) + .concat(", received: ").concat(vault.getType().identifier) + ) // Deposit the bridged token into the signer's vault self.receiver.deposit(from: <-vault) // Destroy the ScopedFTProvider 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 index ed576a31..111e9249 100644 --- a/cadence/transactions/bridge/tokens/bridge_tokens_to_any_cadence_address.cdc +++ b/cadence/transactions/bridge/tokens/bridge_tokens_to_any_cadence_address.cdc @@ -37,7 +37,7 @@ transaction(vaultIdentifier: String, amount: UInt256, recipient: Address) { // // 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") + ?? panic("Could not borrow COA signer's account at path /storage/evm") /* --- Construct the Vault type --- */ // @@ -54,11 +54,14 @@ transaction(vaultIdentifier: String, amount: UInt256, recipient: Address) { // // 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") + ?? panic("Could not borrow ViewResolver from FungibleToken contract with name" + .concat(tokenContractName).concat(" and address ") + .concat(tokenContractAddress.toString())) let vaultData = viewResolver.resolveContractView( resourceType: self.vaultType, viewType: Type() - ) as! FungibleTokenMetadataViews.FTVaultData? ?? panic("Could not resolve FTVaultData view") + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData view for Vault type ".concat(self.vaultType.identifier)) // 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) @@ -73,12 +76,14 @@ transaction(vaultIdentifier: String, amount: UInt256, recipient: Address) { 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") + ?? panic("Could not borrow FungibleToken Vault from storage path ".concat(vaultData.storagePath.toString())) /* --- Configure a ScopedFTProvider --- */ // - // Calculate the bridge fee - bridging from EVM consumes no storage, so flat fee - let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) // 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( @@ -89,7 +94,8 @@ transaction(vaultIdentifier: String, amount: UInt256, recipient: Address) { // Copy the stored Provider capability and create a ScopedFTProvider let providerCapCopy = signer.storage.copy>( from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid Provider Capability found in storage.") + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( provider: providerCapCopy, @@ -106,7 +112,11 @@ transaction(vaultIdentifier: String, amount: UInt256, recipient: Address) { 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") + assert( + vault.getType() == self.vaultType, + message: "Bridged vault type mismatch - requested: ".concat(self.vaultType.identifier) + .concat(", received: ").concat(vault.getType().identifier) + ) // Deposit the bridged token into the signer's vault self.receiver.deposit(from: <-vault) // Destroy the ScopedFTProvider diff --git a/cadence/transactions/bridge/tokens/bridge_tokens_to_any_evm_address.cdc b/cadence/transactions/bridge/tokens/bridge_tokens_to_any_evm_address.cdc index e967e8f5..b82c6e9a 100644 --- a/cadence/transactions/bridge/tokens/bridge_tokens_to_any_evm_address.cdc +++ b/cadence/transactions/bridge/tokens/bridge_tokens_to_any_evm_address.cdc @@ -45,26 +45,26 @@ transaction(vaultIdentifier: String, amount: UFix64, recipient: String) { // // Borrow a reference to the FungibleToken Vault let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName) - ?? panic("Could not borrow ViewResolver from FungibleToken contract") + ?? panic("Could not borrow ViewResolver from FungibleToken contract with name" + .concat(tokenContractName).concat(" and address ") + .concat(tokenContractAddress.toString())) let vaultData = viewResolver.resolveContractView( resourceType: nil, viewType: Type() - ) as! FungibleTokenMetadataViews.FTVaultData? ?? panic("Could not resolve FTVaultData view") + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData view for Vault type ".concat(vaultType.identifier)) let vault = signer.storage.borrow( from: vaultData.storagePath - ) ?? panic("Could not access signer's FungibleToken Vault") + ) ?? panic("Could not borrow FungibleToken Vault from storage path ".concat(vaultData.storagePath.toString())) - // Withdraw the requested balance & calculate the approximate bridge fee based on storage usage - let currentStorageUsage = signer.storage.used + // Withdraw the requested balance & set a cap on the withdrawable bridge fee 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 + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) // 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") + ?? panic("Bridge does not support the requested asset type ".concat(vaultIdentifier)) if self.requiresOnboarding { approxFee = approxFee + FlowEVMBridgeConfig.onboardFee } @@ -81,7 +81,8 @@ transaction(vaultIdentifier: String, amount: UFix64, recipient: String) { // Copy the stored Provider capability and create a ScopedFTProvider let providerCapCopy = signer.storage.copy>( from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid Provider Capability found in storage.") + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( provider: providerCapCopy, diff --git a/cadence/transactions/bridge/tokens/bridge_tokens_to_evm.cdc b/cadence/transactions/bridge/tokens/bridge_tokens_to_evm.cdc index 6fc0dd6d..5371e43d 100644 --- a/cadence/transactions/bridge/tokens/bridge_tokens_to_evm.cdc +++ b/cadence/transactions/bridge/tokens/bridge_tokens_to_evm.cdc @@ -32,7 +32,7 @@ transaction(vaultIdentifier: String, amount: UFix64) { // // 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") + ?? panic("Could not borrow COA signer's account at path /storage/evm") /* --- Construct the Vault type --- */ // @@ -49,26 +49,26 @@ transaction(vaultIdentifier: String, amount: UFix64) { // // Borrow a reference to the FungibleToken Vault let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName) - ?? panic("Could not borrow ViewResolver from FungibleToken contract") + ?? panic("Could not borrow ViewResolver from FungibleToken contract with name" + .concat(tokenContractName).concat(" and address ") + .concat(tokenContractAddress.toString())) let vaultData = viewResolver.resolveContractView( resourceType: vaultType, viewType: Type() - ) as! FungibleTokenMetadataViews.FTVaultData? ?? panic("Could not resolve FTVaultData view") + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData view for Vault type ".concat(vaultType.identifier)) let vault = signer.storage.borrow( from: vaultData.storagePath - ) ?? panic("Could not access signer's FungibleToken Vault") + ) ?? panic("Could not borrow FungibleToken Vault from storage path ".concat(vaultData.storagePath.toString())) - // Withdraw the requested balance & calculate the approximate bridge fee based on storage usage - let currentStorageUsage = signer.storage.used + // Withdraw the requested balance & set a cap on the withdrawable bridge fee 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 + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) // 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") + ?? panic("Bridge does not support the requested asset type ".concat(vaultIdentifier)) if self.requiresOnboarding { approxFee = approxFee + FlowEVMBridgeConfig.onboardFee } @@ -85,7 +85,8 @@ transaction(vaultIdentifier: String, amount: UFix64) { // Copy the stored Provider capability and create a ScopedFTProvider let providerCapCopy = signer.storage.copy>( from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid Provider Capability found in storage.") + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( provider: providerCapCopy,