An educational demo showcasing Flow Scheduled Transactions, Atomic Transactions, EVM DeFi Integration, and Sponsored Transactions - all on Flow blockchain.
This project demonstrates Flow's unique capabilities that aren't possible on other blockchains:
- Native Scheduled Transactions - No external keepers, oracles, or off-chain infrastructure
- Cadence + EVM Interoperability - Seamlessly interact with EVM DeFi protocols from Cadence
- Gas-Free UX - Metamask users never need FLOW tokens for gas
Unlike Chainlink Automation or off-chain keepers that require external infrastructure, fees, and trust assumptions, Flow's native Scheduled Transactions run directly onchain. This means zero external dependencies, no keeper fees, and guaranteed execution - all while staying fully decentralized.
Cadence smart contracts can directly call any EVM contract on Flow. This isn't a bridge or wrapped callβit's native interoperability where Cadence controls an EVM account (COA) and executes EVM transactions atomically.
Here, a Cadence contract executes a UniswapV3 swap by building the calldata and calling the EVM router:
File: cadence/contracts/DCAServiceEVM.cdc
access(all) contract DCAServiceEVM {
// Cadence contract owns an EVM account
access(self) let coa: @EVM.CadenceOwnedAccount
access(all) let routerAddress: EVM.EVMAddress // UniswapV3 Router
access(self) fun executeSwap(
tokenIn: EVM.EVMAddress,
tokenOut: EVM.EVMAddress,
amountIn: UInt256,
feeTier: UInt32
): UInt256 {
// Build UniswapV3 exactInput path: tokenIn (20 bytes) + fee (3 bytes) + tokenOut (20 bytes)
var pathBytes: [UInt8] = []
for byte in tokenIn.bytes { pathBytes.append(byte) }
pathBytes.append(UInt8((feeTier >> 16) & 0xFF))
pathBytes.append(UInt8((feeTier >> 8) & 0xFF))
pathBytes.append(UInt8(feeTier & 0xFF))
for byte in tokenOut.bytes { pathBytes.append(byte) }
// Build calldata: exactInput selector (0xb858183f) + encoded params
let selector: [UInt8] = [0xb8, 0x58, 0x18, 0x3f]
let calldata = selector.concat(self.encodeExactInputParams(pathBytes, ...))
// Execute EVM call from Cadence - this is the magic!
let result = self.coa.call(
to: self.routerAddress,
data: calldata,
gasLimit: 500_000,
value: EVM.Balance(attoflow: 0)
)
// Decode and return amount out
if result.status == EVM.Status.successful {
let decoded = EVM.decodeABI(types: [Type<UInt256>()], data: result.data)
return decoded[0] as! UInt256
}
return 0
}
}- Native interop - Cadence contracts can use any EVM DeFi protocol (Uniswap, Aave, etc.)
- Atomic execution - EVM calls are part of the Cadence transaction (all-or-nothing)
- No bridges - Not a wrapped call or message passing; direct EVM execution
- Full control - Cadence logic decides when/how to call EVM based on on-chain state
On EVM chains, wrapping ETHβWETH and approving a spender requires 2 separate transactions. On Flow, Cadence can execute both EVM calls in one atomic transactionβif either fails, both revert.
File: src/lib/cadence-transactions.ts - WRAP_AND_APPROVE_TX
transaction(amount: UFix64, spenderAddress: String, approvalAmount: UInt256) {
let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount
execute {
// ========== STEP 1: Wrap FLOW β WFLOW ==========
let depositCalldata: [UInt8] = [0xd0, 0xe3, 0x0d, 0xb0] // deposit()
let wrapResult = self.coa.call(
to: wflowAddress,
data: depositCalldata,
gasLimit: 100_000,
value: amountInWei // Send FLOW with the call
)
// If wrap fails, entire transaction reverts
// ========== STEP 2: Approve DCA Service ==========
// Build approve(address,uint256) calldata manually
var calldata: [UInt8] = [0x09, 0x5e, 0xa7, 0xb3] // approve selector
// Encode spender address (pad to 32 bytes)
var j = 0
while j < 12 { calldata.append(0x00); j = j + 1 }
for byte in spenderAddressBytes { calldata.append(byte) }
// Encode amount (32-byte big-endian)
let amountBytes = approvalAmount.toBigEndianBytes()
var k = 0
while k < (32 - amountBytes.length) { calldata.append(0x00); k = k + 1 }
for byte in amountBytes { calldata.append(byte) }
let approveResult = self.coa.call(
to: wflowAddress,
data: calldata,
gasLimit: 100_000,
value: EVM.Balance(attoflow: 0)
)
// If approve fails, wrap is also reverted - atomic!
}
}- 2 EVM transactions β 1 Cadence transaction - Better UX, lower fees
- Atomic guarantees - Either both succeed or both revert (no stuck approvals)
- No ABI libraries needed - Manual encoding works in pure Cadence
- Composable - Can chain unlimited EVM calls in one transaction
Flow validators natively execute scheduled transactionsβno Chainlink Keepers, no Gelato, no off-chain bots. Your handler runs exactly when scheduled, with cryptographic guarantees.
File: cadence/contracts/DCAHandlerEVMV4.cdc
/// Handler resource that executes scheduled DCA swaps
access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
/// Called automatically by Flow validators at the scheduled time
access(FlowTransactionScheduler.Execute)
fun executeTransaction(id: UInt64, data: AnyStruct?) {
let txData = data as! TransactionData
// Execute the DCA swap (calls into EVM via DCAServiceEVM)
let success = DCAServiceEVM.executePlan(planId: txData.planId)
// If successful and plan still active, schedule NEXT execution
if success {
self.scheduleNextExecution(
planId: txData.planId,
nextExecutionTime: plan.nextExecutionTime,
loopConfig: txData.loopConfig
)
}
}
/// Self-rescheduling: the handler schedules its own next run
access(self) fun scheduleNextExecution(
planId: UInt64,
nextExecutionTime: UFix64?,
loopConfig: LoopConfig
): Bool {
// Calculate fees based on execution effort and priority
let baseFee = FlowFees.computeFees(
inclusionEffort: 1.0,
executionEffort: UFix64(loopConfig.executionEffort) / 100_000_000.0
)
let priorityMultiplier = FlowTransactionScheduler.getConfig()
.priorityFeeMultipliers[loopConfig.priority]!
let feeAmount = baseFee * priorityMultiplier * 1.05 // 5% buffer
// Withdraw fees from pre-funded vault
let feeVault = loopConfig.feeProviderCap.borrow()!
let fees <- feeVault.withdraw(amount: feeAmount)
// Schedule next execution - Handler schedules itself!
let schedulerManager = loopConfig.schedulerManagerCap.borrow()!
let scheduledId = schedulerManager.scheduleByHandler(
handlerTypeIdentifier: self.getType().identifier,
handlerUUID: self.uuid,
data: TransactionData(planId: planId, loopConfig: loopConfig),
timestamp: nextExecutionTime!,
priority: loopConfig.priority,
executionEffort: loopConfig.executionEffort,
fees: <-fees as! @FlowToken.Vault
)
return scheduledId > 0
}
}- No off-chain infrastructure - Validators execute your handler, not external keepers
- Self-rescheduling loops - Handler schedules its own next execution (autonomous)
- Guaranteed execution - Scheduled transactions run or fees are refunded
- Priority levels - Low/Medium/High priority affects fee multiplier and execution order
- Pre-funded fee vault - Fees for all executions deposited upfront
Flow has a unique transaction model where every transaction has three roles that can be different accounts:
- Proposer - Provides sequence number (prevents replay attacks)
- Payer - Pays the gas fees
- Authorizer - Signs to authorize the transaction
This separation enables native gas sponsorship without smart contract paymasters.
Flow Wallet already sponsors transactions for its users. When a Flow Wallet user interacts with this app, the wallet infrastructure handles gas fees automatically using Flow's native payer separation.
Metamask users interact without paying gas for the scheduled transactions with a type of paymaster made possible through COAs and associated Cadence accounts:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Metamask User β
β - Signs EVM transactions (token approvals) β
β - Never needs FLOW tokens β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Relay β
β - Receives plan creation requests β
β - Service account acts as PROPOSER + PAYER + AUTHORIZER β
β - Signs Cadence transactions server-side β
β - Pays all gas fees from service account β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DCA Service (Shared COA) β
β - Executes swaps using pre-approved token allowances β
β - Scheduled transaction fees paid from funded fee vault β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
File: src/app/api/relay/route.ts
// Service account signs as proposer, payer, AND authorizer
const txId = await fcl.mutate({
cadence: createPlanTransaction,
args: (arg, t) => [...],
proposer: serviceSigner, // Service account
payer: serviceSigner, // Service account pays gas
authorizations: [serviceSigner],
limit: 9999,
});
// The serviceSigner uses the correct curve for each network
const signWithKey = (privateKey, msgHex, signatureAlgorithm, hashAlgorithm) => {
const curve = signatureAlgorithm === "ECDSA_secp256k1"
? secp256k1Curve // Testnet
: p256Curve; // Mainnet
const hash = hashAlgorithm === "SHA2_256"
? hashMessageSHA2(msgHex)
: hashMessageSHA3(msgHex);
// ... sign and return
};- Flow's 3-role transaction model enables native sponsorship
- No smart contract paymaster needed (unlike EVM chains)
- Backend service pays all Cadence gas fees
- Users only sign EVM transactions (Metamask), never Cadence transactions
- Scheduled transaction fees are pre-funded into a fee vault
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Creates DCA Plan β
β "Swap 0.1 WFLOW β USDF every hour" β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EVM Token Approval β
β User approves WFLOW to DCA Service's shared COA address β
β (One-time approval via Metamask) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Plan Scheduled via Backend β
β Service account schedules first execution via β
β FlowTransactionScheduler (gas sponsored) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β° At Scheduled Time
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Handler Executes Automatically β
β β
β 1. Pull WFLOW from user via COA (transferFrom) β
β 2. Execute swap on Uniswap V3 (WFLOW β USDF) β
β 3. Transfer USDF to user's EVM address β
β 4. Update plan statistics β
β 5. Reschedule next execution (autonomous loop) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
(Repeats until complete)
| Contract | Address | Purpose |
|---|---|---|
DCAServiceEVM |
0xca7ee55e4fc3251a (mainnet) |
Core DCA logic, plan management, EVM swaps |
DCAHandlerEVMV4 |
Same as above | Scheduled transaction handler |
The service uses a shared Cadence Owned Account (COA) to interact with EVM:
- Mainnet COA:
0x000000000000000000000002623833e1789dbd4a
Users approve ERC-20 tokens to this COA, which executes swaps on their behalf.
| Token | Mainnet Address |
|---|---|
| WFLOW | 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e |
| USDF | 0x2aaBea2058b5aC2D339b163C6Ab6f2b6d53aabED |
| USDC | 0xF1815bd50389c46847f0Bda824eC8da914045D14 |
# Clone the repository
git clone https://github.com/Aliserag/dcatoken.git
cd dcatoken
# Install dependencies
npm install
# Copy environment template
cp .env.example .env
# Edit .env with your private keys# Development
npm run dev
# Production build
npm run build && npm startOpen http://localhost:3000 in your browser.
# Check Flow CLI version
flow version # Should be v1.0+
# Test network connection
flow scripts execute cadence/scripts/evm/get_total_plans.cdc --network mainnetdcatoken/
βββ cadence/
β βββ contracts/
β β βββ DCAServiceEVM.cdc # Core service: plans, swaps, EVM calls
β β βββ DCAHandlerEVMV4.cdc # Scheduled transaction handler
β βββ transactions/
β β βββ evm/
β β βββ init_handler_v4.cdc # Initialize handler capabilities
β β βββ schedule_plan_v4.cdc # Schedule plan execution
β βββ scripts/
β βββ evm/ # Query scripts
βββ src/
β βββ app/
β β βββ api/relay/route.ts # Backend relay (gas sponsoring)
β βββ components/dca/ # React components
β βββ config/fcl-config.ts # FCL configuration
β βββ lib/
β βββ cadence-transactions.ts # Cadence templates
β βββ transaction-relay.ts # Relay API client
βββ flow.json # Flow project config
flow keys generateSave the private key securely. Never commit it to git.
Add your account to flow.json:
{
"accounts": {
"my-deployer": {
"address": "YOUR_ADDRESS",
"key": {
"type": "hex",
"index": 0,
"signatureAlgorithm": "ECDSA_P256",
"hashAlgorithm": "SHA3_256",
"privateKey": "${MY_PRIVATE_KEY}"
}
}
}
}flow project deploy --network testnetUpdate src/config/fcl-config.ts with your deployed contract addresses.
- Token Approvals: Users should only approve the amount they intend to DCA and not leave them open
- Slippage: Configure appropriate slippage tolerance (default: 1%)
- Service Account: Keep relay API private keys secure and rotate regularly
- COA Security: The shared COA can only execute approved operations
See CONTRIBUTING.md for guidelines.
MIT License - see LICENSE for details.
Built with Flow Scheduled Transactions and Cadence 1.0