Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update stepwise txn & add confirmation scripts #16

Merged
merged 5 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
43 changes: 43 additions & 0 deletions cadence/scripts/allowance.cdc
Original file line number Diff line number Diff line change
@@ -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<auth(BorrowValue) &Account>(coaHost)
let coa = flowAccount.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(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<UInt256>()], 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
}
42 changes: 42 additions & 0 deletions cadence/scripts/balance_of.cdc
Original file line number Diff line number Diff line change
@@ -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<auth(BorrowValue) &Account>(coaHost)
let coa = flowAccount.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(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<UInt256>()], 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
}
16 changes: 16 additions & 0 deletions cadence/scripts/get_evm_address.cdc
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions cadence/scripts/get_evm_balance.cdc
Original file line number Diff line number Diff line change
@@ -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()
}
39 changes: 39 additions & 0 deletions cadence/scripts/token_uri.cdc
Original file line number Diff line number Diff line change
@@ -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<auth(BorrowValue) &Account>(coaHost)
let coa = flowAccount.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(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<String>()], 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
}
7 changes: 5 additions & 2 deletions cadence/transactions/bundled/wrap_and_mint.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -137,4 +141,3 @@ transaction(wflowAddressHex: String, maybeMintERC721AddressHex: String) {
)
}
}

69 changes: 17 additions & 52 deletions cadence/transactions/stepwise/0_create_coa.cdc
Original file line number Diff line number Diff line change
@@ -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<auth(EVM.Call) &EVM.CadenceOwnedAccount>(from: storagePath)
// Ensure a borrowable COA reference is available
let coa = signer.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(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<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
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"))
}
}

36 changes: 36 additions & 0 deletions cadence/transactions/stepwise/1_fund_coa.cdc
Original file line number Diff line number Diff line change
@@ -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<auth(EVM.Call) &EVM.CadenceOwnedAccount>(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<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<auth(EVM.Call) &EVM.CadenceOwnedAccount>(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<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
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)
}

Expand Down
Loading