Skip to content
Open
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
268 changes: 267 additions & 1 deletion contracts/FlowTransactionSchedulerUtils.cdc
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import "FlowTransactionScheduler"
import "FungibleToken"
import "FlowToken"
import "EVM"
import "MetadataViews"

/// FlowTransactionSchedulerUtils provides utility functionality for working with scheduled transactions
/// on the Flow blockchain. Currently, it only includes a Manager resource for managing scheduled transactions.
/// on the Flow blockchain.
///
/// In the future, this contract will be updated to include more functionality
/// to make it more convenient for working with scheduled transactions for various use cases.
Expand Down Expand Up @@ -561,6 +564,269 @@ access(all) contract FlowTransactionSchedulerUtils {
return getAccount(at).capabilities.borrow<&{Manager}>(self.managerPublicPath)
}

/*********************************************

COA Handler Utils

**********************************************/

access(all) event COAHandlerExecutionError(id: UInt64, owner: Address?, coaAddress: String?, errorMessage: String)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just throwing an idea out there - a failure case event might be helpful at the TransactionHandler interface level. Thinking an event + emitter method so block explorers could watch for interface-level events and add failure states.

// inside FlowTransactionScheduler

access(all) event HandlerExecutionFailed(
    id: UInt64,
    handlerType: String,
    address: Address?,
    uuid: UInt64,
    error: String
)

access(all) resource interface TransactionHandler {
    // ...
    // default implementation emits an event - can be overridden
    access(contract) fun emitExecutionFailed(
        id: UInt64,
        error: String
    ) {
        post {
            emit HandlerExecutionFailed(
                whileExecuting: whileExecuting,
                handlerType: self.getType().identifier,
                address: self.owner?.address,
                uuid: self.uuid,
                error: String
            )
        }
    }
}

// ---------

// inside consuming resource
access(all) resource COAHandler : TransactionHandler {
    access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
        // some failure state encountered
        self.emitExecutionFailed(id: id, error: "CALL_FAILED")
        return
    }
}

The main concern would be id spoofing, but maybe there's something workable there to consider in the future. This failure case event pattern is present in DeFiAction, FlowCron, now here and I'm assuming it might be helpful for other implementations as well. Standardizing the events would be useful in tooling.

In any case, this would be upgradeable, so non-blocking and beyond scope for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion! I created an issue for it: #570


access(all) view fun coaHandlerStoragePath(): StoragePath {
return /storage/coaScheduledTransactionHandler
}

access(all) view fun coaHandlerPublicPath(): PublicPath {
return /public/coaScheduledTransactionHandler
}

/// COATransactionHandler is a resource that wraps a capability to a COA (Contract Owned Account)
/// and implements the TransactionHandler interface to allow scheduling transactions for COAs.
/// This handler enables users to schedule transactions that will be executed on behalf of their COA.
access(all) resource COATransactionHandler: FlowTransactionScheduler.TransactionHandler {
/// The capability to the COA resource
access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>

/// The capability to the FlowToken vault
access(self) let flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>

init(coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
)
{
pre {
coaCapability.check(): "COA capability is invalid or expired"
flowTokenVaultCapability.check(): "FlowToken vault capability is invalid or expired"
}
self.coaCapability = coaCapability
self.flowTokenVaultCapability = flowTokenVaultCapability
}

access(self) fun emitError(id: UInt64, errorMessage: String) {
let coa = self.coaCapability.borrow()!
emit COAHandlerExecutionError(id: id, owner: self.owner?.address, coaAddress: coa.address().toString(),
errorMessage: errorMessage)
}

/// Execute the scheduled transaction using the COA
/// @param id: The ID of the scheduled transaction
/// @param data: Optional data passed to the transaction execution. In this case, the data must be a COAHandlerParams struct with valid values.
access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {

// Borrow the COA capability
let coa = self.coaCapability.borrow()
if coa == nil {
emit COAHandlerExecutionError(id: id, owner: self.owner?.address ?? Address(0x0), coaAddress: nil,
errorMessage: "COA capability is invalid or expired for scheduled transaction with ID \(id)")
return
}

// Parse the data into a list of COAHandlerParams
// If the data is a single COAHandlerParams struct, wrap it in a list
var params: [COAHandlerParams]? = data as? [COAHandlerParams]
if params == nil {
if let param = data as? COAHandlerParams {
params = [param]
}
}

// Iterate through all the COA transactions and execute them all
// If revertOnFailure is true for a transaction and any part of it fails, the entire scheduled transaction will be reverted
// If not but a part of the transaction fails, an error event will be emitted but the scheduled transaction will continue to execute the next transaction
//
if let transactions = params {
for index, txParams in transactions {
switch txParams.txType {
case COAHandlerTxType.DepositFLOW:
let vault = self.flowTokenVaultCapability.borrow()
if vault == nil {
if !txParams.revertOnFailure {
self.emitError(id: id, errorMessage: "FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id) and index \(index)")
continue
} else {
panic("FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id) and index \(index)")
}
}

if txParams.amount! > vault!.balance && !txParams.revertOnFailure {
self.emitError(id: id, errorMessage: "Insufficient FLOW in FlowToken vault for deposit into COA for scheduled transaction with ID \(id) and index \(index)")
continue
}

// Deposit the FLOW into the COA vault. If there isn't enough FLOW in the vault,
//the transaction will be reverted because we know revertOnFailure is true
coa!.deposit(from: <-vault!.withdraw(amount: txParams.amount!) as! @FlowToken.Vault)
case COAHandlerTxType.WithdrawFLOW:
let vault = self.flowTokenVaultCapability.borrow()
if vault == nil {
if !txParams.revertOnFailure {
self.emitError(id: id, errorMessage: "FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id) and index \(index)")
continue
} else {
panic("FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id) and index \(index)")
}
}

let amount = EVM.Balance(attoflow: 0)
amount.setFLOW(flow: txParams.amount!)

if amount.attoflow > coa!.balance().attoflow && !txParams.revertOnFailure {
self.emitError(id: id, errorMessage: "Insufficient FLOW in COA vault for withdrawal from COA for scheduled transaction with ID \(id) and index \(index)")
continue
}

// Withdraw the FLOW from the COA vault. If there isn't enough FLOW in the COA,
// the transaction will be reverted because we know revertOnFailure is true
vault!.deposit(from: <-coa!.withdraw(balance: amount))
case COAHandlerTxType.Call:
let result = coa!.call(to: txParams.callToEVMAddress!, data: txParams.data!, gasLimit: txParams.gasLimit!, value: txParams.value!)

if result.status != EVM.Status.successful {
if !txParams.revertOnFailure {
self.emitError(id: id, errorMessage: "EVM call failed for scheduled transaction with ID \(id) and index \(index) with error: \(result.errorCode):\(result.errorMessage)")
continue
} else {
panic("EVM call failed for scheduled transaction with ID \(id) and index \(index) with error: \(result.errorCode):\(result.errorMessage)")
}
}
}
}
} else {
self.emitError(id: id, errorMessage: "Invalid scheduled transaction data type for COA handler execution for tx with ID \(id)! Expected [FlowTransactionSchedulerUtils.COAHandlerParams] but got \(data.getType().identifier)")
return
}
}

/// Get the views supported by this handler
/// @return: Array of view types
access(all) view fun getViews(): [Type] {
return [
Type<COAHandlerView>(),
Type<StoragePath>(),
Type<PublicPath>(),
Type<MetadataViews.Display>()
]
}

/// Resolve a view for this handler
/// @param viewType: The type of view to resolve
/// @return: The resolved view data, or nil if not supported
access(all) fun resolveView(_ viewType: Type): AnyStruct? {
if viewType == Type<COAHandlerView>() {
return COAHandlerView(
coaOwner: self.coaCapability.borrow()?.owner?.address,
coaEVMAddress: self.coaCapability.borrow()?.address(),
)
}
if viewType == Type<StoragePath>() {
return FlowTransactionSchedulerUtils.coaHandlerStoragePath()
} else if viewType == Type<PublicPath>() {
return FlowTransactionSchedulerUtils.coaHandlerPublicPath()
} else if viewType == Type<MetadataViews.Display>() {
return MetadataViews.Display(
name: "COA Scheduled Transaction Handler",
description: "Scheduled Transaction Handler that can execute transactions on behalf of a COA",
thumbnail: MetadataViews.HTTPFile(
url: ""
)
)
}
return nil
}
}

/// Enum for COA handler execution type
access(all) enum COAHandlerTxType: UInt8 {
access(all) case DepositFLOW
access(all) case WithdrawFLOW
access(all) case Call

// TODO: Should we have other transaction types??
}

access(all) struct COAHandlerParams {

/// The type of transaction to execute
access(all) let txType: COAHandlerTxType

/// Indicates if the whole set of scheduled transactions should be reverted
/// if this one transaction fails to execute in EVM
access(all) let revertOnFailure: Bool

/// The amount of FLOW to deposit or withdraw
/// Not required for the Call transaction type
access(all) let amount: UFix64?

/// The following fields are only required for the Call transaction type
access(all) let callToEVMAddress: EVM.EVMAddress?
access(all) let data: [UInt8]?
access(all) let gasLimit: UInt64?
access(all) let value: EVM.Balance?

init(txType: UInt8, revertOnFailure: Bool, amount: UFix64?, callToEVMAddress: String?, data: [UInt8]?, gasLimit: UInt64?, value: UInt?) {
self.txType = COAHandlerTxType(rawValue: txType)
?? panic("Invalid COA transaction type enum")
self.revertOnFailure = revertOnFailure
if self.txType == COAHandlerTxType.DepositFLOW {
assert(amount != nil, message: "Amount is required for deposit but was not provided")
}
if self.txType == COAHandlerTxType.WithdrawFLOW {
assert(amount != nil, message: "Amount is required for withdrawal but was not provided")
}
if self.txType == COAHandlerTxType.Call {
assert(callToEVMAddress != nil && callToEVMAddress!.length == 40, message: "Call to EVM address is required for EVM call but was not provided or is invalid length (must be 40 hex chars)")
assert(data != nil, message: "Data is required for EVM call but was not provided")
assert(gasLimit != nil, message: "Gas limit is required for EVM call but was not provided")
assert(value != nil, message: "Value is required for EVM call but was not provided")
}
self.amount = amount
if callToEVMAddress != nil {
let decodedAddress = callToEVMAddress!.decodeHex()
assert(
decodedAddress.length == 20,
message:"Invalid EVM address length for scheduled transaction! Expected 20 bytes but got \(decodedAddress.length) bytes for EVM address \(callToEVMAddress!)"
)
self.callToEVMAddress = EVM.EVMAddress(bytes: decodedAddress.toConstantSized<[UInt8; 20]>()!)
} else {
self.callToEVMAddress = nil
}
self.data = data
self.gasLimit = gasLimit
if let unwrappedValue = value {
self.value = EVM.Balance(attoflow: unwrappedValue)
} else {
self.value = nil
}
}
}

/// View struct for COA handler metadata
access(all) struct COAHandlerView {
access(all) let coaOwner: Address?
access(all) let coaEVMAddress: EVM.EVMAddress?

// TODO: Should we include other metadata about the COA, like balance, code, etc???

init(coaOwner: Address?, coaEVMAddress: EVM.EVMAddress?) {
self.coaOwner = coaOwner
self.coaEVMAddress = coaEVMAddress
}
}

/// Create a COA transaction handler
/// @param coaCapability: Capability to the COA resource
/// @param metadata: Optional metadata about the handler
/// @return: A new COATransactionHandler resource
access(all) fun createCOATransactionHandler(
coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
): @COATransactionHandler {
return <-create COATransactionHandler(
coaCapability: coaCapability,
flowTokenVaultCapability: flowTokenVaultCapability,
)
}

/********************************************

Scheduled Transactions Metadata Views
Expand Down
10 changes: 10 additions & 0 deletions flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@
"hash": "610692c2092bd29d8f49aefc10b6e8ff3d4b3909331fece98f6966fbdcb4cdd4",
"aliases": {}
},
"EVM": {
"source": "mainnet://e467b9dd11fa00df.EVM",
"hash": "2a4782c7459dc5b72c034f67c8dd1beac6bb9b29104772a3e6eb6850718bb3b4",
"aliases": {
"emulator": "f8d6e0586b0a20c7",
"mainnet": "e467b9dd11fa00df",
"testnet": "8c5303eaa26202d6"
}
},
"FlowClusterQC": {
"source": "mainnet://8624b52f9ddcd04a.FlowClusterQC",
"hash": "5fd45018af7e3b7606caf437dd0b6be72f698556313754c12308b8d5b174051d",
Expand Down Expand Up @@ -319,6 +328,7 @@
"networks": {
"emulator": "127.0.0.1:3569",
"mainnet": "access.mainnet.nodes.onflow.org:9000",
"migration": "access-001.migrationtestnet1.nodes.onflow.org:9000",
"testing": "127.0.0.1:3569",
"testnet": "access.devnet.nodes.onflow.org:9000"
},
Expand Down
6 changes: 3 additions & 3 deletions lib/go/contracts/internal/assets/assets.go

Large diffs are not rendered by default.

Loading
Loading