diff --git a/README.md b/README.md index b868f70..d9c49b6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Batched Cadence EVM Execution Example -> This repo contains an example of how to batch EVM execution on Flow using Cadence. - -:building_construction: Currently work in progress. +> This repo contains an example of transaction batching EVM execution on Flow using Cadence. ## Deployments @@ -12,3 +10,9 @@ The relevant contracts can be found at the following addresses on Flow Testnet: |---|---| |`MaybeMintERC72`|[`0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2`](https://evm-testnet.flowscan.io/address/0xdbc43ba45381e02825b14322cddd15ec4b3164e6?tab=contract_code)| |`WFLOW`|[`0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e`](https://evm-testnet.flowscan.io/token/0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e?tab=contract_code)| + +## Walkthrough + +For a walkthrough through the contents of this repo, check out the [full guide] in Flow's developer docs. + +[full guide]: https://developers.flow.com/evm/cadence/batched-evm-transactions \ No newline at end of file diff --git a/cadence/scripts/allowance.cdc b/cadence/scripts/allowance.cdc new file mode 100644 index 0000000..57e6a3b --- /dev/null +++ b/cadence/scripts/allowance.cdc @@ -0,0 +1,43 @@ +import "EVM" + +/// Returns the ERC20 token allowance of the allowed address as approved by the owner +/// +/// @param coaHost: The Flow address storing the COA to use for the EVM call +/// @param tokenAddressHex: The hex-encoded EVM address to token to check the allowance for +/// @param ownerAddressHex: The hex-encoded EVM address of the entity who approved the allowed address +/// @param allowedAddressHex: The hex-encoded EVM address of the entity who has been approved +/// +/// @return The allowance allotted to the address, reverting if the given contract address does not implement the ERC20 method +/// "allowance(address,address)(uint256)" +/// +access(all) fun main(coaHost: Address, tokenAddressHex: String, ownerAddressHex: String, allowedAddressHex: String): UInt256 { + // Get the COA from the Flow account we'll use to make the EVM call + let flowAccount = getAuthAccount(coaHost) + let coa = flowAccount.storage.borrow(from: /storage/evm) + ?? panic("Could not find a COA in account ".concat(coaHost.toString())) + // Deserialize the contract address & owner address + let tokenAddress = EVM.addressFromString(tokenAddressHex) + let ownerAddress = EVM.addressFromString(ownerAddressHex) + let allowedAddress = EVM.addressFromString(allowedAddressHex) + + // Encode the calldata for the EVM call + let calldata = EVM.encodeABIWithSignature( + "allowance(address,address)", + [ownerAddress, allowedAddress] + ) + // Make the EVM call, targetting the contract and passing the encoded calldata + let res = coa.call( + to: tokenAddress, + data: calldata, + gasLimit: 15_000_000, + value: EVM.Balance(attoflow: 0) + ) + assert(res.status == EVM.Status.successful, message: "Error making allowance(address,address) call to ".concat(allowedAddressHex)) + + // Decode the calldata, ensure success & return + let decoded = EVM.decodeABI(types: [Type()], data: res.data) + assert(decoded.length == 1, message: "Expected 1 decoded value, got ".concat(decoded.length.toString())) + + // Cast the return value since .decodeABI returns [`AnyStruct`] + return decoded[0] as! UInt256 +} diff --git a/cadence/scripts/balance_of.cdc b/cadence/scripts/balance_of.cdc new file mode 100644 index 0000000..29b1a89 --- /dev/null +++ b/cadence/scripts/balance_of.cdc @@ -0,0 +1,42 @@ +import "EVM" + +/// Returns the balance of the owner (hex-encoded EVM address) of a given ERC20 fungible token defined +/// at the hex-encoded EVM contract address +/// +/// @param coaHost: The Flow address storing the COA to use for the EVM call +/// @param contractAddressHex: The hex-encoded EVM contract address of the ERC20 contract +/// @param ownerAddressHex: The hex-encoded EVM address to check the balance of +/// +/// @return The balance of the address, reverting if the given contract address does not implement the ERC20 method +/// "balanceOf(address)(uint256)" +/// +access(all) fun main(coaHost: Address, contractAddressHex: String, ownerAddressHex: String): UInt256 { + // Get the COA from the Flow account we'll use to make the EVM call + let flowAccount = getAuthAccount(coaHost) + let coa = flowAccount.storage.borrow(from: /storage/evm) + ?? panic("Could not find a COA in account ".concat(coaHost.toString())) + // Deserialize the contract address & owner address + let contractAddress = EVM.addressFromString(contractAddressHex) + let ownerAddress = EVM.addressFromString(ownerAddressHex) + + // Encode the calldata for the EVM call + let calldata = EVM.encodeABIWithSignature( + "balanceOf(address)", + [ownerAddress] + ) + // Make the EVM call, targetting the contract and passing the encoded calldata + let res = coa.call( + to: contractAddress, + data: calldata, + gasLimit: 15_000_000, + value: EVM.Balance(attoflow: 0) + ) + assert(res.status == EVM.Status.successful, message: "Error making balanceOf(address) call to ".concat(contractAddressHex)) + + // Decode the calldata, ensure success & return + let decoded = EVM.decodeABI(types: [Type()], data: res.data) + assert(decoded.length == 1, message: "Expected 1 decoded value, got ".concat(decoded.length.toString())) + + // Cast the return value since .decodeABI returns [`AnyStruct`] + return decoded[0] as! UInt256 +} diff --git a/cadence/scripts/get_evm_address.cdc b/cadence/scripts/get_evm_address.cdc new file mode 100644 index 0000000..43c30b1 --- /dev/null +++ b/cadence/scripts/get_evm_address.cdc @@ -0,0 +1,16 @@ +import "EVM" + +/// Returns the EVM address of a given Flow account as defined by the account's COA. +/// If a COA is not found, nil is returned. +/// +/// @param address: The Flow address to look up +/// +/// @return the serialized EVM address or nil if a COA is not found in the given account +/// +access(all) fun main(address: Address): String? { + let flowAccount = getAccount(address) + if let coa = flowAccount.capabilities.borrow<&EVM.CadenceOwnedAccount>(/public/evm) { + return coa.address().toString() + } + return nil +} diff --git a/cadence/scripts/get_evm_balance.cdc b/cadence/scripts/get_evm_balance.cdc new file mode 100644 index 0000000..e45412f --- /dev/null +++ b/cadence/scripts/get_evm_balance.cdc @@ -0,0 +1,9 @@ +import "EVM" + +/// Returns the FLOW balance of of a given EVM address in FlowEVM +/// +/// @param address: The hex-encoded EVM address for which to check the balance +/// +access(all) fun main(address: String): UFix64 { + return EVM.addressFromString(address).balance().inFLOW() +} diff --git a/cadence/scripts/token_uri.cdc b/cadence/scripts/token_uri.cdc new file mode 100644 index 0000000..4cadced --- /dev/null +++ b/cadence/scripts/token_uri.cdc @@ -0,0 +1,39 @@ +import "EVM" + +/// Returns the token URI of the requested ERC721 ID +/// +/// @param coaHost: The Flow address storing the COA to use for the EVM call +/// @param erc721AddressHex: The hex-encoded EVM contract address of the ERC721 contract +/// @param tokenID: The NFT ID for which to retrieve the token URI +/// +/// @return The token URI for the requested NFT +/// +access(all) fun main(coaHost: Address, erc721AddressHex: String, tokenID: UInt256): String { + // Get the COA from the Flow account we'll use to make the EVM call + let flowAccount = getAuthAccount(coaHost) + let coa = flowAccount.storage.borrow(from: /storage/evm) + ?? panic("Could not find a COA in account ".concat(coaHost.toString())) + // Deserialize the contract address & owner address + let erc721Address = EVM.addressFromString(erc721AddressHex) + + // Encode the calldata for the EVM call + let calldata = EVM.encodeABIWithSignature( + "tokenURI(uint256)", + [tokenID] + ) + // Make the EVM call, targetting the contract and passing the encoded calldata + let res = coa.call( + to: erc721Address, + data: calldata, + gasLimit: 15_000_000, + value: EVM.Balance(attoflow: 0) + ) + assert(res.status == EVM.Status.successful, message: "Error making tokenURI(uint256) call to ".concat(erc721AddressHex)) + + // Decode the calldata, ensure success & return + let decoded = EVM.decodeABI(types: [Type()], data: res.data) + assert(decoded.length == 1, message: "Expected 1 decoded value, got ".concat(decoded.length.toString())) + + // Cast the return value since .decodeABI returns [`AnyStruct`] + return decoded[0] as! String +} diff --git a/cadence/transactions/bundled/wrap_and_mint.cdc b/cadence/transactions/bundled/wrap_and_mint.cdc index 9424816..4945068 100644 --- a/cadence/transactions/bundled/wrap_and_mint.cdc +++ b/cadence/transactions/bundled/wrap_and_mint.cdc @@ -102,10 +102,14 @@ transaction(wflowAddressHex: String, maybeMintERC721AddressHex: String) { /* Approve the ERC721 address for the mint amount */ // + // Convert the mintAmount from UFix64 to UInt256 (given 18 decimal precision on WFLOW contract) + let ufixAllowance = EVM.Balance(attoflow: 0) + ufixAllowance.setFLOW(flow: self.mintCost) + let uintAllowance = UInt256(ufixAllowance.inAttoFLOW()) // Encode calldata approve(address,uint) calldata, providing the ERC721 address & mint amount let approveCalldata = EVM.encodeABIWithSignature( "approve(address,uint256)", - [self.erc721Address, UInt256(1_000_000_000_000_000_000)] + [self.erc721Address, uintAllowance] ) // Call the WFLOW contract, approving the ERC721 address to move the mint amount let approveResult = self.coa.call( @@ -137,4 +141,3 @@ transaction(wflowAddressHex: String, maybeMintERC721AddressHex: String) { ) } } - \ No newline at end of file diff --git a/cadence/transactions/stepwise/0_create_coa.cdc b/cadence/transactions/stepwise/0_create_coa.cdc index daced72..862ba6c 100644 --- a/cadence/transactions/stepwise/0_create_coa.cdc +++ b/cadence/transactions/stepwise/0_create_coa.cdc @@ -1,65 +1,30 @@ -import "FungibleToken" -import "FlowToken" import "EVM" -/// Creates a CadenceOwnedAccount (COA) & funds with the specified amount. -/// If a COA already exists in storage at /storage/evm, the transaction reverts. +/// Configures a CadenceOwnedAccount in the signer's account if one is not already stored. /// -/// @param amount: The amount of FLOW to fund the COA with, sourcing funds from the signer's FlowToken Vault -/// -transaction(amount: UFix64) { - - let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount - let fundingVault: @FlowToken.Vault +transaction { - prepare(signer: auth(SaveValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { - pre { - amount > 0.0: "The funding amount must be greater than zero" - } - /* COA configuration & assigment */ + prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability, UnpublishCapability) &Account) { + /* COA configuration & assignment */ // let storagePath = /storage/evm let publicPath = /public/evm // Configure a COA if one is not found in storage at the default path - if signer.storage.type(at: storagePath) != nil { - panic("CadenceOwnedAccount already exists at path ".concat(storagePath.toString())) - } - // Create & save the CadenceOwnedAccount (COA) Resource - let newCOA <- EVM.createCadenceOwnedAccount() - signer.storage.save(<-newCOA, to: storagePath) + if signer.storage.type(at: storagePath) == nil { + // Create & save the CadenceOwnedAccount (COA) Resource + let newCOA <- EVM.createCadenceOwnedAccount() + signer.storage.save(<-newCOA, to: storagePath) - // Unpublish any existing Capability at the public path if it exists - signer.capabilities.unpublish(publicPath) - // Issue & publish the public, unentitled COA Capability - let coaCapability = signer.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(storagePath) - signer.capabilities.publish(coaCapability, at: publicPath) + // Unpublish any existing Capability at the public path if it exists + signer.capabilities.unpublish(publicPath) + // Issue & publish the public, unentitled COA Capability + let coaCapability = signer.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(storagePath) + signer.capabilities.publish(coaCapability, at: publicPath) + } - // Assign the COA reference to the transaction's coa field - self.coa = signer.storage.borrow(from: storagePath) + // Ensure a borrowable COA reference is available + let coa = signer.storage.borrow(from: storagePath) ?? panic("A CadenceOwnedAccount (COA) Resource could not be found at path ".concat(storagePath.toString()) - .concat(" - ensure the COA Resource is created and saved at this path to enable EVM interactions")) - - // Borrow authorized reference to signer's FlowToken Vault - let sourceVault = signer.storage.borrow( - from: /storage/flowTokenVault - ) ?? panic("The signer does not store a FlowToken Vault object at the path " - .concat("/storage/flowTokenVault. ") - .concat("The signer must initialize their account with this vault first!")) - // Withdraw from the signer's FlowToken Vault - self.fundingVault <- sourceVault.withdraw(amount: amount) as! @FlowToken.Vault - } - - pre { - self.fundingVault.balance == amount: - "Expected amount =".concat(amount.toString()) - .concat(" but fundingVault.balance=").concat(self.fundingVault.balance.toString()) - } - - execute { - /* Fund COA */ - // - // Deposit the FLOW into the COA - self.coa.deposit(from: <-self.fundingVault) + .concat(" - ensure the COA Resource is created and saved at this path to enable EVM interactions")) } } - \ No newline at end of file diff --git a/cadence/transactions/stepwise/1_fund_coa.cdc b/cadence/transactions/stepwise/1_fund_coa.cdc new file mode 100644 index 0000000..02a5661 --- /dev/null +++ b/cadence/transactions/stepwise/1_fund_coa.cdc @@ -0,0 +1,36 @@ +import "FungibleToken" +import "FlowToken" +import "EVM" + +/// This transaction deposits FLOW from the signer's Cadence vault to their COA's EVM balance +/// +transaction { + + let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount + let fundingVault: @FlowToken.Vault + + prepare(signer: auth(BorrowValue) &Account) { + // Ensure a borrowable COA reference is available + let storagePath = /storage/evm + self.coa = signer.storage.borrow(from: storagePath) + ?? panic("A CadenceOwnedAccount (COA) Resource could not be found at path ".concat(storagePath.toString()) + .concat(" - ensure the COA Resource is created and saved at this path to enable EVM interactions")) + + /* Fund COA with cost of mint */ + // + // Borrow authorized reference to signer's FlowToken Vault + let sourceVault = signer.storage.borrow( + from: /storage/flowTokenVault + ) ?? panic("The signer does not store a FlowToken Vault object at the path " + .concat("/storage/flowTokenVault. ") + .concat("The signer must initialize their account with this vault first!")) + // Withdraw from the signer's FlowToken Vault + let mintCost = 1.0 + self.fundingVault <- sourceVault.withdraw(amount: mintCost) as! @FlowToken.Vault + } + + execute { + // Deposit the mint cost into the COA + self.coa.deposit(from: <-self.fundingVault) + } +} diff --git a/cadence/transactions/stepwise/1_wrap_flow.cdc b/cadence/transactions/stepwise/2_wrap_flow.cdc similarity index 67% rename from cadence/transactions/stepwise/1_wrap_flow.cdc rename to cadence/transactions/stepwise/2_wrap_flow.cdc index 00a6534..f93f9d8 100644 --- a/cadence/transactions/stepwise/1_wrap_flow.cdc +++ b/cadence/transactions/stepwise/2_wrap_flow.cdc @@ -14,38 +14,20 @@ import "EVM" /// @param wflowAddressHex: The EVM address hex of the WFLOW contract as a String /// transaction(wflowAddressHex: String) { - + let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount let mintCost: UFix64 let wflowAddress: EVM.EVMAddress - prepare(signer: auth(SaveValue, BorrowValue) &Account) { - /* COA configuration & assigment */ - // + prepare(signer: auth(BorrowValue) &Account) { + // Ensure a borrowable COA reference is available let storagePath = /storage/evm - // Assign the COA reference to the transaction's coa field self.coa = signer.storage.borrow(from: storagePath) ?? panic("A CadenceOwnedAccount (COA) Resource could not be found at path ".concat(storagePath.toString()) .concat(" - ensure the COA Resource is created and saved at this path to enable EVM interactions")) - - /* Fund COA with cost of mint */ - // - // Borrow authorized reference to signer's FlowToken Vault - let sourceVault = signer.storage.borrow( - from: /storage/flowTokenVault - ) ?? panic("The signer does not store a FlowToken Vault object at the path " - .concat("/storage/flowTokenVault. ") - .concat("The signer must initialize their account with this vault first!")) - // Withdraw from the signer's FlowToken Vault + // Assign the amount we'll deposit to WFLOW to cover the eventual ERC721 mint self.mintCost = 1.0 - let fundingVault <- sourceVault.withdraw(amount: self.mintCost) as! @FlowToken.Vault - // Deposit the mint cost into the COA - self.coa.deposit(from: <-fundingVault) - - /* Set the WFLOW contract address */ - // - // View the cannonical WFLOW contract at: - // https://evm-testnet.flowscan.io/address/0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e + // Deserialize the WFLOW address self.wflowAddress = EVM.addressFromString(wflowAddressHex) } diff --git a/cadence/transactions/stepwise/2_approve.cdc b/cadence/transactions/stepwise/3_approve.cdc similarity index 71% rename from cadence/transactions/stepwise/2_approve.cdc rename to cadence/transactions/stepwise/3_approve.cdc index 61338f0..65a92f8 100644 --- a/cadence/transactions/stepwise/2_approve.cdc +++ b/cadence/transactions/stepwise/3_approve.cdc @@ -12,49 +12,36 @@ import "EVM" /// @param maybeMintERC721AddressHex: The EVM address hex of the ERC721 contract as a String /// transaction(wflowAddressHex: String, maybeMintERC721AddressHex: String) { - + let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount let mintCost: UFix64 let wflowAddress: EVM.EVMAddress let erc721Address: EVM.EVMAddress - prepare(signer: auth(SaveValue, BorrowValue) &Account) { - /* COA assignment */ - // + prepare(signer: auth(BorrowValue) &Account) { + // Ensure a borrowable COA reference is available let storagePath = /storage/evm - // Assign the COA reference to the transaction's coa field self.coa = signer.storage.borrow(from: storagePath) ?? panic("A CadenceOwnedAccount (COA) Resource could not be found at path ".concat(storagePath.toString()) .concat(" - ensure the COA Resource is created and saved at this path to enable EVM interactions")) - - /* Fund COA with cost of mint */ - // + // Assign the amount we'll deposit to WFLOW to cover the eventual ERC721 mint self.mintCost = 1.0 - /* Set the WFLOW contract address */ - // - // View the cannonical WFLOW contract at: - // https://evm-testnet.flowscan.io/address/0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e + // Deserialize the WFLOW & ERC721 addresses self.wflowAddress = EVM.addressFromString(wflowAddressHex) - - /* Assign the ERC721 EVM Address */ - // - // Deserialize the provided ERC721 hex string to an EVM address self.erc721Address = EVM.addressFromString(maybeMintERC721AddressHex) } - pre { - self.coa.balance().inFLOW() >= self.mintCost: - "CadenceOwnedAccount holds insufficient FLOW balance to mint - " - .concat("Ensure COA has at least ".concat(self.mintCost.toString()).concat(" FLOW")) - } - execute { /* Approve the ERC721 address for the mint amount */ // + // Convert the mintAmount from UFix64 to UInt256 (given 18 decimal precision on WFLOW contract) + let ufixAllowance = EVM.Balance(attoflow: 0) + ufixAllowance.setFLOW(flow: self.mintCost) + let uintAllowance = UInt256(ufixAllowance.inAttoFLOW()) // Encode calldata approve(address,uint) calldata, providing the ERC721 address & mint amount let approveCalldata = EVM.encodeABIWithSignature( "approve(address,uint256)", - [self.erc721Address, UInt256(1_000_000_000_000_000_000)] + [self.erc721Address, uintAllowance] ) // Call the WFLOW contract, approving the ERC721 address to move the mint amount let approveResult = self.coa.call( @@ -69,4 +56,3 @@ transaction(wflowAddressHex: String, maybeMintERC721AddressHex: String) { ) } } - \ No newline at end of file diff --git a/cadence/transactions/stepwise/3_mint.cdc b/cadence/transactions/stepwise/4_mint.cdc similarity index 82% rename from cadence/transactions/stepwise/3_mint.cdc rename to cadence/transactions/stepwise/4_mint.cdc index 3bd5504..54bc943 100644 --- a/cadence/transactions/stepwise/3_mint.cdc +++ b/cadence/transactions/stepwise/4_mint.cdc @@ -13,24 +13,18 @@ import "EVM" /// @param wflowAddressHex: The EVM address hex of the WFLOW contract as a String /// @param maybeMintERC721AddressHex: The EVM address hex of the ERC721 contract as a String /// -transaction(wflowAddressHex: String, maybeMintERC721AddressHex: String) { +transaction(maybeMintERC721AddressHex: String) { let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount let erc721Address: EVM.EVMAddress - prepare(signer: auth(SaveValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { - /* COA assigment */ - // + prepare(signer: auth(BorrowValue) &Account) { + // Ensure a borrowable COA reference is available let storagePath = /storage/evm - - // Assign the COA reference to the transaction's coa field self.coa = signer.storage.borrow(from: storagePath) ?? panic("A CadenceOwnedAccount (COA) Resource could not be found at path ".concat(storagePath.toString()) .concat(" - ensure the COA Resource is created and saved at this path to enable EVM interactions")) - - /* Assign the ERC721 EVM Address */ - // - // Deserialize the provided ERC721 hex string to an EVM address + // Deserialize the ERC721 address self.erc721Address = EVM.addressFromString(maybeMintERC721AddressHex) }