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

Add Cadence transaction batching EVM calls #12

Merged
merged 24 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
761c724
forge install: random-coin-toss
sisyphusSmiling Oct 31, 2024
9f2a8de
remove submodule
sisyphusSmiling Oct 31, 2024
f0453d2
forge install: random-coin-toss
sisyphusSmiling Oct 31, 2024
8f6e417
update foundry.toml
sisyphusSmiling Oct 31, 2024
a37c417
update foundry remappings
sisyphusSmiling Oct 31, 2024
161e07b
add CadenceArchUtils library & test coverage on random mint
sisyphusSmiling Nov 1, 2024
5976256
update dependencies
sisyphusSmiling Nov 1, 2024
c73ef70
replace local dep with foundry installed dep
sisyphusSmiling Nov 1, 2024
aa01cbb
add custom errors to ERC721
sisyphusSmiling Nov 4, 2024
6ded160
add bundled Cadence txn
sisyphusSmiling Nov 4, 2024
54b4550
add stepwise Cadence txns
sisyphusSmiling Nov 4, 2024
26f071e
remove unused GitHub cadence tests workflow
sisyphusSmiling Nov 4, 2024
71206e8
remove WETH9 contract
sisyphusSmiling Nov 5, 2024
9dba454
update transaction comments
sisyphusSmiling Nov 5, 2024
ee20308
update README with testnet deployments
sisyphusSmiling Nov 5, 2024
0975463
remove onflow/random-coin-toss
sisyphusSmiling Nov 7, 2024
f5fa952
add Cadence tests & helper solidity contract
sisyphusSmiling Nov 7, 2024
bffd632
forge install: flow-sol-utils
sisyphusSmiling Nov 7, 2024
c4b6971
update solidity dependencies to use flow-sol-utils
sisyphusSmiling Nov 7, 2024
20fe203
Merge branch 'gio/add-random-mint' into gio/add-cadence-test
sisyphusSmiling Nov 7, 2024
029bde7
Merge branch 'gio/add-random-mint' into gio/add-batched-calls
sisyphusSmiling Nov 7, 2024
bbecea4
Merge branch 'gio/add-batched-calls' into gio/add-cadence-test
sisyphusSmiling Nov 7, 2024
d203cc9
fix failing Cadence tests & remove debug tests
sisyphusSmiling Nov 7, 2024
7856847
Merge branch 'main' into gio/add-batched-calls
sisyphusSmiling Nov 7, 2024
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
30 changes: 0 additions & 30 deletions .github/workflows/cadence_test.yml

This file was deleted.

3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "solidity/lib/forge-std"]
path = solidity/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "solidity/lib/flow-sol-utils"]
path = solidity/lib/flow-sol-utils
url = https://github.com/onflow/flow-sol-utils
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
### Batched Cadence EVM Execution Example
# Batched Cadence EVM Execution Example

> This repo contains an example of how to batch EVM execution on Flow using Cadence.

:building_construction: WIP
:building_construction: Currently work in progress.

## Deployments

The relevant contracts can be found at the following addresses on Flow Testnet:

|Contract|Address|
|---|---|
|`MaybeMintERC72`|[`0xdbC43Ba45381e02825b14322cDdd15eC4B3164E6`](https://evm-testnet.flowscan.io/address/0xdbc43ba45381e02825b14322cddd15ec4b3164e6?tab=contract_code)|
|`WFLOW`|[`0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e`](https://evm-testnet.flowscan.io/token/0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e?tab=contract_code)|
140 changes: 140 additions & 0 deletions cadence/transactions/bundled/wrap_and_mint.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import "FungibleToken"
import "FlowToken"
import "EVM"

/// This transaction demonstrates how multiple EVM calls can be batched in a single Cadence transaction via
/// CadenceOwnedAccount (COA), performing the following actions:
///
/// 1. Configures a COA in the signer's account if needed
/// 2. Funds the signer's COA with enough FLOW to cover the WFLOW cost of minting an ERC721 token
/// 3. Wraps FLOW as WFLOW - EVM call 1
/// 4. Approves the example MaybeMintERC721 contract which accepts WFLOW to move the mint amount - EVM call 2
/// 5. Attempts to mint an ERC721 token - EVM call 3
///
/// Importantly, the transaction is reverted if any of the EVM interactions fail returning the account to the original
/// state before the transaction was executed across Cadence & EVM.
///
/// For more context, see https://github.com/onflow/batched-evm-exec-example
///
/// @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) {

let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount
let mintCost: UFix64
let wflowAddress: EVM.EVMAddress
let erc721Address: EVM.EVMAddress

prepare(signer: auth(SaveValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) {
/* COA configuration & assigment */
//
let storagePath = /storage/evm
sisyphusSmiling marked this conversation as resolved.
Show resolved Hide resolved
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 {
// 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)
}

// 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
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
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 {
/* Wrap FLOW in EVM as WFLOW */
//
// Encode calldata & set value
let depositCalldata = EVM.encodeABIWithSignature("deposit()", [])
let value = EVM.Balance(attoflow: 0)
value.setFLOW(flow: self.mintCost)
// Call the WFLOW contract, wrapping the sent FLOW
let wrapResult = self.coa.call(
to: self.wflowAddress,
data: depositCalldata,
gasLimit: 15_000_000,
value: value
)
assert(
wrapResult.status == EVM.Status.successful,
message: "Wrapping FLOW as WFLOW failed: ".concat(wrapResult.errorMessage)
)

/* Approve the ERC721 address for the mint amount */
//
// 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)]
)
// Call the WFLOW contract, approving the ERC721 address to move the mint amount
let approveResult = self.coa.call(
to: self.wflowAddress,
data: approveCalldata,
gasLimit: 15_000_000,
value: EVM.Balance(attoflow: 0)
)
assert(
approveResult.status == EVM.Status.successful,
message: "Approving ERC721 address on WFLOW contract failed: ".concat(approveResult.errorMessage)
)

/* Attempt to mint ERC721 */
//
// Encode the mint() calldata
let mintCalldata = EVM.encodeABIWithSignature("mint()", [])
// Call the ERC721 contract, attempting to mint
let mintResult = self.coa.call(
to: self.erc721Address,
data: mintCalldata,
gasLimit: 15_000_000,
value: EVM.Balance(attoflow: 0)
)
// If mint fails, all other actions in this transaction are reverted
assert(
mintResult.status == EVM.Status.successful,
message: "Minting ERC721 token failed: ".concat(mintResult.errorMessage)
)
}
}

65 changes: 65 additions & 0 deletions cadence/transactions/stepwise/0_create_coa.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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.
///
/// @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

prepare(signer: auth(SaveValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) {
pre {
amount > 0.0: "The funding amount must be greater than zero"
}
/* COA configuration & assigment */
//
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)

// 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)
?? 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)
}
}

77 changes: 77 additions & 0 deletions cadence/transactions/stepwise/1_wrap_flow.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import "FungibleToken"
import "FlowToken"
import "EVM"

/// This transaction wraps FLOW as WFLOW, sourcing the wrapped FLOW from the signer's FlowToken Vault in the amount of
/// 1.0 FLOW to cover the mint cost of 1 MaybeMintERC721 token. If a CadenceOwnedAccount (COA) is not configured in the
/// signer's account, one is configured, allowing the Flow account to interact with Flow's EVM runtime.
///
/// While not interesting on its own, this transaction demonstrates a single step in the bundled EVM execution example,
/// showcasing how Cadence can be used to atomically orchestrate multiple EVM interactions in a single transaction.
///
/// For more context, see https://github.com/onflow/batched-evm-exec-example
///
/// @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 */
//
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
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
self.wflowAddress = EVM.addressFromString(wflowAddressHex)
}

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 {
/* Wrap FLOW in EVM as WFLOW */
//
// Encode calldata & set value
let depositCalldata = EVM.encodeABIWithSignature("deposit()", [])
let value = EVM.Balance(attoflow: 0)
value.setFLOW(flow: self.mintCost)
// Call the WFLOW contract, wrapping the sent FLOW
let wrapResult = self.coa.call(
to: self.wflowAddress,
data: depositCalldata,
gasLimit: 15_000_000,
value: value
)
assert(
wrapResult.status == EVM.Status.successful,
message: "Wrapping FLOW as WFLOW failed: ".concat(wrapResult.errorMessage)
)
}
}
Loading