Skip to content

Commit

Permalink
Add Cadence transaction batching EVM calls (#12)
Browse files Browse the repository at this point in the history
* forge install: random-coin-toss

* remove submodule

* forge install: random-coin-toss

* update foundry.toml

* update foundry remappings

* add CadenceArchUtils library & test coverage on random mint

* update dependencies

* replace local dep with foundry installed dep

* add custom errors to ERC721

* add bundled Cadence txn

* add stepwise Cadence txns

* remove unused GitHub cadence tests workflow

* remove WETH9 contract

* update transaction comments

* update README with testnet deployments

* remove onflow/random-coin-toss

* add Cadence tests & helper solidity contract

* forge install: flow-sol-utils

* update solidity dependencies to use flow-sol-utils

* fix failing Cadence tests & remove debug tests
  • Loading branch information
sisyphusSmiling authored Nov 7, 2024
1 parent 74422d2 commit 5aa3bb2
Show file tree
Hide file tree
Showing 14 changed files with 702 additions and 39 deletions.
30 changes: 0 additions & 30 deletions .github/workflows/cadence_test.yml

This file was deleted.

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)|
38 changes: 38 additions & 0 deletions cadence/tests/test_helpers.cdc

Large diffs are not rendered by default.

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

/// Configures a COA in the signer's Flow account, funding with the specified amount. If the COA already exists, the
/// transaction reverts.
///
transaction(amount: UFix64) {
let coa: &EVM.CadenceOwnedAccount
let sentVault: @FlowToken.Vault

prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) {
let storagePath = /storage/evm
let publicPath = /public/evm

// Revert if the CadenceOwnedAccount already exists
if signer.storage.type(at: storagePath) != nil {
panic("Storage collision - Resource already stored at path=".concat(storagePath.toString()))
}

// Configure the CadenceOwnedAccount
signer.storage.save<@EVM.CadenceOwnedAccount>(<-EVM.createCadenceOwnedAccount(), to: storagePath)
let addressableCap = signer.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(storagePath)
signer.capabilities.unpublish(publicPath)
signer.capabilities.publish(addressableCap, at: publicPath)

// Reference the CadeceOwnedAccount
self.coa = signer.storage.borrow<auth(EVM.Owner) &EVM.CadenceOwnedAccount>(from: /storage/evm)
?? panic("Missing or mis-typed CadenceOwnedAccount at /storage/evm")

// Withdraw the amount from the signer's FlowToken vault
let vaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
from: /storage/flowTokenVault
) ?? panic("Could not borrow reference to the owner's Vault!")
self.sentVault <- vaultRef.withdraw(amount: amount) as! @FlowToken.Vault
}

pre {
self.sentVault.balance == amount:
"Expected amount =".concat(amount.toString()).concat(" but sentVault.balance=").concat(self.sentVault.balance.toString())
}

execute {
// Deposit the amount into the CadenceOwnedAccount if the balance is greater than zero
if self.sentVault.balance > 0.0 {
self.coa.deposit(from: <-self.sentVault)
} else {
destroy self.sentVault
}
}
}
56 changes: 56 additions & 0 deletions cadence/tests/transactions/deploy.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import "FungibleToken"
import "FlowToken"

import "EVM"

/// Deploys a compiled solidity contract from bytecode to the EVM, with the signer's COA as the deployer
///
transaction(bytecode: String, gasLimit: UInt64, value: UFix64) {

let coa: auth(EVM.Deploy) &EVM.CadenceOwnedAccount
var sentVault: @FlowToken.Vault?

prepare(signer: auth(BorrowValue) &Account) {

let storagePath = StoragePath(identifier: "evm")!
self.coa = signer.storage.borrow<auth(EVM.Deploy) &EVM.CadenceOwnedAccount>(from: storagePath)
?? panic("Could not borrow reference to the signer's bridged account")

// Rebalance Flow across VMs if there is not enough Flow in the EVM account to cover the value
let evmFlowBalance: UFix64 = self.coa.balance().inFLOW()
if self.coa.balance().inFLOW() < value {
let withdrawAmount: UFix64 = value - evmFlowBalance
let vaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
from: /storage/flowTokenVault
) ?? panic("Could not borrow reference to the owner's Vault!")

self.sentVault <- vaultRef.withdraw(amount: withdrawAmount) as! @FlowToken.Vault
} else {
self.sentVault <- nil
}
}

execute {

// Deposit Flow into the EVM account if necessary otherwise destroy the sent Vault
if self.sentVault != nil {
self.coa.deposit(from: <-self.sentVault!)
} else {
destroy self.sentVault
}

let valueBalance = EVM.Balance(attoflow: 0)
valueBalance.setFLOW(flow: value)
// Finally deploy the contract
let evmResult = self.coa.deploy(
code: bytecode.decodeHex(),
gasLimit: gasLimit,
value: valueBalance
)
assert(
evmResult.status == EVM.Status.successful && evmResult.deployedContract != nil,
message: "EVM deployment failed with error code: ".concat(evmResult.errorCode.toString())
.concat(" and message: ").concat(evmResult.errorMessage)
)
}
}
19 changes: 19 additions & 0 deletions cadence/tests/transactions/move_block.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import "EVM"

transaction {
let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount
prepare(signer: auth(BorrowValue) &Account) {
self.coa = signer.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm)
?? panic("A CadenceOwnedAccount (COA) Resource could not be found at path /storage/evm")
}

execute {
let res = self.coa.call(
to: self.coa.address(),
data: [],
gasLimit: 15_000_000,
value: EVM.Balance(attoflow: 0)
)
assert(res.status == EVM.Status.successful, message: "Empty EVM call failed")
}
}
101 changes: 101 additions & 0 deletions cadence/tests/wrap_and_mint_tests.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import Test
import BlockchainHelpers
import "test_helpers.cdc"

import "EVM"

access(all) let serviceAccount = Test.serviceAccount()

access(all) var coaAddress: String = ""
access(all) var wflowAddress: String = ""
access(all) var erc721Address: String = ""

access(all)
fun setup() {
// Create & fund a CadenceOwnedAccount
let coaRes = executeTransaction(
"./transactions/create_coa.cdc",
[100.0],
serviceAccount
)
Test.expect(coaRes, Test.beSucceeded())

// Extract COA address from event
let coaEvts = Test.eventsOfType(Type<EVM.CadenceOwnedAccountCreated>())
let coaEvt = coaEvts[0] as! EVM.CadenceOwnedAccountCreated
coaAddress = coaEvt.address

// Deploy WFLOW
let wflowDeployRes = executeTransaction(
"./transactions/deploy.cdc",
[getWFLOWBytecode(), UInt64(15_000_000), 0.0],
serviceAccount
)
Test.expect(wflowDeployRes, Test.beSucceeded())

// Extract WFLOW address from event
var txnExecEvts = Test.eventsOfType(Type<EVM.TransactionExecuted>())
let wflowEvt = txnExecEvts[2] as! EVM.TransactionExecuted
wflowAddress = wflowEvt.contractAddress

// Deploy ERC721
let constructorArgs = [
"Maybe Mint ERC721",
"MAYBE",
EVM.addressFromString(wflowAddress),
UInt256(1_000_000_000_000_000_000),
EVM.addressFromString(coaAddress)
]
// Encode constructor args as ABI and then as hex
let argsBytecode = String.encodeHex(EVM.encodeABI(
constructorArgs
))
// Append the encoded constructor args to the compiled bytecode
let finalBytecode = String.join([getERC721Bytecode(), argsBytecode], separator: "")
let erc721DeployRes = executeTransaction(
"./transactions/deploy.cdc",
[finalBytecode, UInt64(15_000_000), 0.0],
serviceAccount
)
Test.expect(erc721DeployRes, Test.beSucceeded())

// Extract ERC721 address from event
txnExecEvts = Test.eventsOfType(Type<EVM.TransactionExecuted>())
let erc721Evt = txnExecEvts[3] as! EVM.TransactionExecuted
erc721Address = erc721Evt.contractAddress
}

access(all)
fun testWrapAndMintSucceeds() {
let user = Test.createAccount()
mintFlow(to: user, amount: 10.0)

// Executes the wrap_and_mint.cdc transaction
// - Creates a COA
// - Funds the COA with FLOW to cover mint cost
// - Wraps FLOW as WFLOW
// - Approves ERC721 contract to mint
// - Mints ERC721 <- may fail so we retry here until success (can't mock CadenceArch calls in Cadence tests atm)
wrapAndMintUntilSuccess(iter: 5, signer: user, wflow: wflowAddress, erc721: erc721Address)
}

access(all)
fun wrapAndMintUntilSuccess(iter: Int, signer: Test.TestAccount, wflow: String, erc721: String) {
var i = 0
var success = false
while i < iter {
let res = executeTransaction(
"../transactions/bundled/wrap_and_mint.cdc",
[wflow, erc721],
signer
)
if res.error == nil {
success = true
break
} else {
i = i + 1
moveBlock()
}
}
Test.assert(success)
}
Loading

0 comments on commit 5aa3bb2

Please sign in to comment.