Skip to content

Commit

Permalink
add cross VM txfr to Cadence txns + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sisyphusSmiling committed Jun 25, 2024
1 parent 836696e commit c8c4fd0
Show file tree
Hide file tree
Showing 5 changed files with 370 additions and 9 deletions.
105 changes: 96 additions & 9 deletions cadence/tests/flow_evm_bridge_tests.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ fun setup() {
arguments: []
)
Test.expect(err, Test.beNil())

/* Integrate EVM bridge contract */

// Set factory as registrar in registry
Expand Down Expand Up @@ -335,7 +335,7 @@ fun testDeployERC721Succeeds() {
exampleERCAccount
)
Test.expect(erc721DeployResult, Test.beSucceeded())

// Get ERC721 & ERC20 deployed contract addresses
let evts = Test.eventsOfType(Type<EVM.TransactionExecuted>())
Test.assertEqual(21, evts.length)
Expand All @@ -350,12 +350,12 @@ fun testDeployERC20Succeeds() {
exampleERCAccount
)
Test.expect(erc20DeployResult, Test.beSucceeded())

// Get ERC721 & ERC20 deployed contract addresses
let evts = Test.eventsOfType(Type<EVM.TransactionExecuted>())
Test.assertEqual(22, evts.length)
erc20AddressHex = getEVMAddressHexFromEvents(evts, idx: 21)

}

access(all)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
101 changes: 101 additions & 0 deletions cadence/transactions/bridge/nft/bridge_nft_to_any_cadence_address.cdc
Original file line number Diff line number Diff line change
@@ -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<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(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<MetadataViews.NFTCollectionData>()
) 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<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(
/storage/flowTokenVault
)
signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)
}
// Copy the stored Provider capability and create a ScopedFTProvider
let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(
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
}
}
Original file line number Diff line number Diff line change
@@ -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<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(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<FungibleTokenMetadataViews.FTVaultData>()
) 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<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(
/storage/flowTokenVault
)
signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)
}
// Copy the stored Provider capability and create a ScopedFTProvider
let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(
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
}
}
Loading

0 comments on commit c8c4fd0

Please sign in to comment.