DeploySEALED
@$!$╳$#█○╳╳▫^^*%╱╲□■#○╲?%◇■$#~%~▒&~!^□▒$▓▫▪╲@$○&?▓*▓*╳▒╲░█■~░■▒◆
Transaction ID
Execution Fee
0.00003523 FLOWTransaction Summary
DeployContract deployment
Contract deployment
Script Arguments
0nameString
DCATransactionHandlerV2
1codeString
import FlowTransactionScheduler from 0xe467b9dd11fa00df
import FlowTransactionSchedulerUtils from 0xe467b9dd11fa00df
import DCAVaultActions from 0x17ae3b1b0b0d50db
import DeFiActions from 0x17ae3b1b0b0d50db
import IncrementFiSwapperConnector from 0x17ae3b1b0b0d50db
import UniswapV2SwapperConnectorV2 from 0x17ae3b1b0b0d50db
import UniswapV3SwapperConnectorV2 from 0x17ae3b1b0b0d50db
import EVMTokenRegistry from 0x17ae3b1b0b0d50db
import SwapRouter from 0xa6850776a94e6551
import SwapFactory from 0xb063c16cac85dbd1
import FlowToken from 0x1654653399040a61
import FungibleToken from 0xf233dcee88fe0abe
import PriceOracle from 0x17ae3b1b0b0d50db
import SimplePriceOracle from 0x17ae3b1b0b0d50db
import EVM from 0xe467b9dd11fa00df
/// DCATransactionHandlerV2: Forte scheduled transaction handler for DCA purchases
///
/// This V2 contract implements the FlowTransactionScheduler.TransactionHandler interface
/// to enable automated, recurring DCA purchases executed by Forte's on-chain scheduler.
///
/// V2 Features:
/// - Full COA-based EVM swap execution
/// - Support for UniswapV2 and UniswapV3 on Flow EVM
/// - Token bridging between Cadence and EVM
/// - Cron-style recurring purchase scheduling
/// - FlowActions integration for DEX-agnostic swaps
///
access(all) contract DCATransactionHandlerV2 {
/// Events
access(all) event DCATransactionExecuted(planId: UInt64, scheduledTxId: UInt64, amountSpent: UFix64, amountReceived: UFix64)
access(all) event DCATransactionScheduled(planId: UInt64, executionTime: UFix64, priority: String)
access(all) event DCASeriesCompleted(planId: UInt64, totalExecutions: UInt64)
access(all) event DCATransactionFailed(planId: UInt64, scheduledTxId: UInt64, reason: String)
access(all) event HandlerCreated(address: Address)
/// EVM-specific events for monitoring
access(all) event EVMSwapExecuted(planId: UInt64, dexType: String, amountIn: UFix64, amountOut: UFix64)
access(all) event EVMSwapFailed(planId: UInt64, dexType: String, reason: String)
/// Storage paths
access(all) let HandlerStoragePath: StoragePath
access(all) let HandlerPublicPath: PublicPath
/// Execution data structure passed to scheduled transactions
/// NOTE: Minimal design to stay under Forte's 1KB data size limit
/// V2 includes COA capability for EVM swaps
access(all) struct ExecutionData {
/// DCA plan identifier
access(all) let planId: UInt64
/// Address of the vault owner
access(all) let vaultOwner: Address
/// Execution priority
access(all) let priority: FlowTransactionScheduler.Priority
/// Execution effort (gas budget)
access(all) let executionEffort: UInt64
/// For recurring DCA: interval between purchases
access(all) let interval: UFix64?
/// For recurring DCA: maximum executions (nil = unlimited)
access(all) let maxExec: UInt64?
/// Current execution count
access(all) var count: UInt64
/// Base timestamp for calculating next execution
access(all) let baseTime: UFix64
/// Required capabilities (minimal set)
access(all) let feeCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>
access(all) let mgrCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>
access(all) let handlerCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>
/// COA capability for EVM swaps (required for V2 EVM connectors)
access(all) let coaCap: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?
init(
planId: UInt64,
vaultOwner: Address,
priority: FlowTransactionScheduler.Priority,
executionEffort: UInt64,
interval: UFix64?,
maxExec: UInt64?,
count: UInt64,
baseTime: UFix64,
feeCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>,
mgrCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
handlerCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>,
coaCap: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?
) {
self.planId = planId
self.vaultOwner = vaultOwner
self.priority = priority
self.executionEffort = executionEffort
self.interval = interval
self.maxExec = maxExec
self.count = count
self.baseTime = baseTime
self.feeCap = feeCap
self.mgrCap = mgrCap
self.handlerCap = handlerCap
self.coaCap = coaCap
}
/// Check if this DCA series should continue
access(all) fun shouldContinue(): Bool {
if let max = self.maxExec {
return self.count < max
}
return true
}
/// Get next execution time (cron-style)
access(all) fun getNextExecutionTime(): UFix64 {
if let interval = self.interval {
let elapsedTime = getCurrentBlock().timestamp - self.baseTime
let elapsedIntervals = UInt64(elapsedTime / interval)
return self.baseTime + (UFix64(elapsedIntervals + 1) * interval)
}
panic("No interval set for recurring DCA")
}
/// Create updated config for next execution
access(all) fun withIncrementedCount(): ExecutionData {
return ExecutionData(
planId: self.planId,
vaultOwner: self.vaultOwner,
priority: self.priority,
executionEffort: self.executionEffort,
interval: self.interval,
maxExec: self.maxExec,
count: self.count + 1,
baseTime: self.baseTime,
feeCap: self.feeCap,
mgrCap: self.mgrCap,
handlerCap: self.handlerCap,
coaCap: self.coaCap
)
}
}
/// Transaction Handler resource
access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
/// Execute a scheduled DCA purchase
access(FlowTransactionScheduler.Execute) fun executeTransaction(
id: UInt64,
data: AnyStruct?
) {
pre {
id > 0: "Transaction ID must be positive"
data != nil: "Execution data cannot be nil"
}
// Parse execution data
let executionData = data as! ExecutionData?
?? panic("Invalid execution data")
// Get vault reference from owner's account
let vaultCap = getAccount(executionData.vaultOwner)
.capabilities.get<&DCAVaultActions.Vault>(DCAVaultActions.VaultPublicPath)
let vault = vaultCap.borrow()
?? panic("Could not borrow vault reference")
// Get the DCA plan
let plan = vault.getPlan(planId: executionData.planId)
?? panic("Plan not found")
// Verify plan is active
assert(plan.status == DCAVaultActions.PlanStatus.active, message: "Plan is not active")
assert(plan.isReadyForPurchase(), message: "Plan not ready for purchase")
// Check trigger conditions if set
var currentPrice: UFix64? = nil
if plan.triggerType != DCAVaultActions.TriggerType.none {
let targetSymbol = plan.tokenPair.targetToken
// Fetch price with 5 minute staleness tolerance
if let priceDataAny = SimplePriceOracle.getPriceWithMaxAge(symbol: targetSymbol, maxAge: 300.0) {
if let priceData = priceDataAny as? SimplePriceOracle.PriceData {
currentPrice = priceData.price
if let triggerPrice = plan.triggerPrice {
if plan.triggerDirection == DCAVaultActions.TriggerDirection.buyAbove {
assert(currentPrice! >= triggerPrice, message: "Price condition not met")
} else if plan.triggerDirection == DCAVaultActions.TriggerDirection.sellBelow {
assert(currentPrice! <= triggerPrice, message: "Price condition not met")
}
}
} else {
panic("Invalid price data format")
}
} else {
panic("Unable to fetch current price for ".concat(targetSymbol))
}
}
// Create swapper based on type from plan with COA capability
let swapper <- self.createSwapper(plan, coaCap: executionData.coaCap)
// Calculate amount after protocol fee (0.1%)
let protocolFeePercent = DCAVaultActions.getProtocolFeePercent()
let feeAmount = plan.purchaseAmount * protocolFeePercent
let amountAfterFee = plan.purchaseAmount - feeAmount
// Get quote for the swap
let quote = swapper.getQuote(
fromTokenType: plan.tokenPair.sourceTokenType,
toTokenType: plan.tokenPair.targetTokenType,
amount: amountAfterFee
)
// Execute the purchase through vault
vault.executePurchaseWithSwapper(
planId: executionData.planId,
swapper: <-swapper,
quote: quote,
currentPrice: currentPrice
)
emit DCATransactionExecuted(
planId: executionData.planId,
scheduledTxId: id,
amountSpent: plan.purchaseAmount,
amountReceived: quote.expectedAmount
)
// Check if EVM swap
if self.isEVMSwapper(plan.swapperType) {
emit EVMSwapExecuted(
planId: executionData.planId,
dexType: plan.swapperType,
amountIn: plan.purchaseAmount,
amountOut: quote.expectedAmount
)
}
// Re-schedule next execution if this is a recurring DCA and should continue
if executionData.shouldContinue() && plan.purchasesExecuted < plan.totalPurchases {
self.rescheduleNext(executionData)
} else {
emit DCASeriesCompleted(
planId: executionData.planId,
totalExecutions: executionData.count + 1
)
}
}
/// Create swapper for the specified DEX
/// V2: Uses COA-based connectors for EVM swaps
access(self) fun createSwapper(
_ plan: DCAVaultActions.DCAPlan,
coaCap: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?
): @{DeFiActions.Swapper} {
pre {
plan.swapperType.length > 0: "Swapper type cannot be empty"
}
// Handle Cadence DEXes (IncrementFi)
if plan.swapperType == "IncrementFi" || plan.swapperType == "increment" {
let routerAddress = self.getContractAddress(Type<SwapRouter>())
let pairAddress = self.getContractAddress(Type<SwapFactory>())
let sourceTypeId = self.getContractTypeId(plan.tokenPair.sourceTokenType)
let targetTypeId = self.getContractTypeId(plan.tokenPair.targetTokenType)
return <- IncrementFiSwapperConnector.createSwapper(
pairAddress: pairAddress,
routerAddress: routerAddress,
tokenKeyPath: [sourceTypeId, targetTypeId]
)
}
// Handle UniswapV2 EVM swapper (includes PunchSwap V2)
// Uses V2 connector with COA support
if plan.swapperType == "UniswapV2"
|| plan.swapperType == "uniswap-v2"
|| plan.swapperType == "punchswap-v2" {
return <- self.createUniswapV2Swapper(plan, coaCap: coaCap)
}
// Handle UniswapV3 EVM swapper (includes FlowSwap V3, PunchSwap V3)
// Uses V2 connector with COA support
if plan.swapperType == "UniswapV3"
|| plan.swapperType == "uniswap-v3"
|| plan.swapperType == "flowswap-v3"
|| plan.swapperType == "punchswap-v3" {
return <- self.createUniswapV3Swapper(plan, coaCap: coaCap)
}
panic("Unsupported swapper type: ".concat(plan.swapperType))
}
/// Create Uniswap V2 swapper with COA for EVM execution
access(self) fun createUniswapV2Swapper(
_ plan: DCAVaultActions.DCAPlan,
coaCap: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?
): @{DeFiActions.Swapper} {
let sourceTypeId = self.getContractTypeId(plan.tokenPair.sourceTokenType)
let targetTypeId = self.getContractTypeId(plan.tokenPair.targetTokenType)
let sourceEVMAddress = self.getEVMTokenAddress(plan.tokenPair.sourceTokenType)
let targetEVMAddress = self.getEVMTokenAddress(plan.tokenPair.targetTokenType)
let tokenPath: [EVM.EVMAddress] = [sourceEVMAddress, targetEVMAddress]
return <- UniswapV2SwapperConnectorV2.createSwapperWithDefaults(
tokenPath: tokenPath,
cadenceTokenPath: [sourceTypeId, targetTypeId],
coaCapability: coaCap,
inVaultType: plan.tokenPair.sourceTokenType,
outVaultType: plan.tokenPair.targetTokenType
)
}
/// Create Uniswap V3 swapper with COA for EVM execution
access(self) fun createUniswapV3Swapper(
_ plan: DCAVaultActions.DCAPlan,
coaCap: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?
): @{DeFiActions.Swapper} {
let sourceTypeId = self.getContractTypeId(plan.tokenPair.sourceTokenType)
let targetTypeId = self.getContractTypeId(plan.tokenPair.targetTokenType)
let tokenIn = self.getEVMTokenAddress(plan.tokenPair.sourceTokenType)
let tokenOut = self.getEVMTokenAddress(plan.tokenPair.targetTokenType)
return <- UniswapV3SwapperConnectorV2.createSwapperWithDefaults(
tokenIn: tokenIn,
tokenOut: tokenOut,
cadenceTokenIn: sourceTypeId,
cadenceTokenOut: targetTypeId,
coaCapability: coaCap,
inVaultType: plan.tokenPair.sourceTokenType,
outVaultType: plan.tokenPair.targetTokenType
)
}
/// Check if a swapper type is an EVM swapper
access(self) fun isEVMSwapper(_ swapperType: String): Bool {
return swapperType == "UniswapV2"
|| swapperType == "uniswap-v2"
|| swapperType == "UniswapV3"
|| swapperType == "uniswap-v3"
|| swapperType == "punchswap-v2"
|| swapperType == "punchswap-v3"
|| swapperType == "flowswap-v3"
}
/// Get EVM token address for a Cadence token type
access(self) fun getEVMTokenAddress(_ tokenType: Type): EVM.EVMAddress {
if let evmAddress = EVMTokenRegistry.getEVMAddress(tokenType) {
return evmAddress
}
if tokenType == Type<@FlowToken.Vault>() {
return EVMTokenRegistry.getFlowEVMAddress()
}
panic("No EVM address found for token type: ".concat(tokenType.identifier))
}
/// Extract contract address from Type identifier
access(self) fun getContractAddress(_ contractType: Type): Address {
let typeId = contractType.identifier
if typeId.length > 2 && typeId.slice(from: 0, upTo: 2) == "A." {
let parts = typeId.split(separator: ".")
if parts.length >= 2 {
let addrHex = parts[1]
return Address.fromString("0x".concat(addrHex)) ?? Address(0x0)
}
}
return Address(0x0)
}
/// Extract contract type identifier (strip resource name)
access(self) fun getContractTypeId(_ vaultType: Type): String {
let typeId = vaultType.identifier
let parts = typeId.split(separator: ".")
if parts.length >= 4 && parts[parts.length - 1] == "Vault" {
return "A.".concat(parts[1]).concat(".").concat(parts[2])
}
return typeId
}
/// Re-schedule next DCA purchase
access(self) fun rescheduleNext(_ data: ExecutionData) {
pre {
data.executionEffort >= 10: "Execution effort must be at least 10"
}
let nextTime = data.getNextExecutionTime()
assert(nextTime > getCurrentBlock().timestamp, message: "Next execution time must be in the future")
let updatedData = data.withIncrementedCount()
let estimation = FlowTransactionScheduler.estimate(
data: updatedData as AnyStruct,
timestamp: nextTime,
priority: data.priority,
executionEffort: data.executionEffort
)
let estimatedFee = estimation.flowFee ?? 0.01
assert(estimatedFee > 0.0, message: "Estimated fee must be positive")
let feeProvider = data.feeCap.borrow() ?? panic("Fee provider capability is invalid")
let feesVault <- feeProvider.withdraw(amount: estimatedFee)
let fees <- feesVault as! @FlowToken.Vault
let manager = data.mgrCap.borrow() ?? panic("Manager capability is invalid")
let scheduledId = manager.schedule(
handlerCap: data.handlerCap,
data: updatedData,
timestamp: nextTime,
priority: data.priority,
executionEffort: data.executionEffort,
fees: <-fees
)
emit DCATransactionScheduled(
planId: data.planId,
executionTime: nextTime,
priority: data.priority.rawValue.toString()
)
}
/// Get views for metadata
access(all) view fun getViews(): [Type] {
return [Type<StoragePath>(), Type<PublicPath>()]
}
/// Resolve view
access(all) fun resolveView(_ view: Type): AnyStruct? {
switch view {
case Type<StoragePath>():
return DCATransactionHandlerV2.HandlerStoragePath
case Type<PublicPath>():
return DCATransactionHandlerV2.HandlerPublicPath
default:
return nil
}
}
}
/// Create a new DCA transaction handler
access(all) fun createHandler(): @Handler {
return <- create Handler()
}
init() {
self.HandlerStoragePath = /storage/DCATransactionHandlerV2
self.HandlerPublicPath = /public/DCATransactionHandlerV2
}
}
Cadence Script
1transaction(name: String, code: String ) {
2 prepare(signer: auth(AddContract) &Account) {
3 signer.contracts.add(name: name, code: code.utf8 )
4 }
5 }