DeFi Actions
DeFi Actions are composable primitives that enable complex DeFi operations through simple, reusable components. FYV leverages DeFi Actions to build sophisticated yield strategies from modular building blocks. This document explains the DeFi Actions framework and how it powers FYV's composability.
What are DeFi Actions?
DeFi Actions is a framework of composable smart contract components that implement common DeFi operations as standardized interfaces. Rather than building monolithic strategies, developers compose Actions like building blocks to create complex flows.
Key principles:
- Single Responsibility: Each Action does one thing well
- Composability: Actions can be chained and combined
- Standardized Interfaces: Consistent APIs across implementations
- Reusability: Same Actions used across multiple strategies
Core Action Types
FYV uses three main categories of DeFi Actions:
1. Swap Actions (SwapConnectors)
Convert one token type to another via decentralized exchanges.
Interface:
_13pub resource interface SwapConnector {_13 // Swap input tokens for output tokens_13 pub fun swap(_13 vaultIn: @FungibleToken.Vault,_13 amountOutMin: UFix64_13 ): @FungibleToken.Vault_13_13 // Get expected output for given input_13 pub fun quote(amountIn: UFix64): UFix64_13_13 // Get swap route information_13 pub fun getRoute(): SwapRoute_13}
Implementations:
- UniswapV3SwapConnectors: Swap via Uniswap V3 pools on Flow EVM
- TeleportCustodySwapConnectors: Swap via Teleport custody protocol
- IncrementSwapConnectors: Swap via Increment DEX
Example usage:
_11// Swap MOET → FLOW via Uniswap V3_11let swapConnector <- UniswapV3SwapConnectors.createConnector(_11 tokenIn: Type<@MOET.Vault>(),_11 tokenOut: Type<@FlowToken.Vault>(),_11 poolFee: 3000 // 0.3% fee tier_11)_11_11let flowVault <- swapConnector.swap(_11 vaultIn: <-moetVault,_11 amountOutMin: 95.0 // 5% slippage tolerance_11)
2. Sink Actions (SinkConnectors)
Deposit tokens into yield-generating protocols.
Interface:
_10pub resource interface SinkConnector {_10 // Deposit tokens to yield protocol_10 pub fun deposit(vault: @FungibleToken.Vault)_10_10 // Get current deposited balance_10 pub fun getBalance(): UFix64_10_10 // Get vault metadata_10 pub fun getVaultInfo(): VaultInfo_10}
Implementations:
- ERC4626SinkConnectors: Deposit to ERC4626-compliant vaults
- DrawDownSink: Bridge to ALP borrowing positions
- StakingSinkConnectors: Stake tokens in staking protocols
Example usage:
_10// Deposit to ERC4626 vault_10let sinkConnector <- ERC4626SinkConnectors.createConnector(_10 vaultAddress: 0x123..., // ERC4626 vault address_10 tokenType: Type<@YieldToken.Vault>()_10)_10_10sinkConnector.deposit(vault: <-yieldTokens)_10// Tokens now earning yield in ERC4626 vault
3. Source Actions (SourceConnectors)
Withdraw tokens from yield-generating protocols.
Interface:
_10pub resource interface SourceConnector {_10 // Withdraw specified amount_10 pub fun withdraw(amount: UFix64): @FungibleToken.Vault_10_10 // Withdraw all available balance_10 pub fun withdrawAll(): @FungibleToken.Vault_10_10 // Get available withdrawal amount_10 pub fun getAvailableBalance(): UFix64_10}
Implementations:
- ERC4626SourceConnectors: Withdraw from ERC4626 vaults
- TopUpSource: Provide liquidity from ALP positions
- UnstakingSourceConnectors: Unstake from staking protocols
Example usage:
_10// Withdraw from ERC4626 vault_10let sourceConnector <- ERC4626SourceConnectors.createConnector(_10 vaultAddress: 0x123...,_10 tokenType: Type<@YieldToken.Vault>()_10)_10_10let withdrawn <- sourceConnector.withdraw(amount: 100.0)_10// Yield tokens withdrawn from vault
Action Composition
The power of DeFi Actions comes from composition—chaining multiple Actions to create complex flows.
Example: TracerStrategy Composition
TracerStrategy composes five Actions to implement leveraged yield farming:
1. Borrow Action (DrawDownSink):
_10// Borrow MOET from ALP position_10let borrowAction <- DrawDownSink.create(positionCap: positionCapability)_10borrowAction.deposit(vault: <-initialCollateral)_10// Position auto-borrows MOET
2. Swap Action #1 (MOET → YieldToken):
_11// Convert borrowed MOET to yield tokens_11let swapAction1 <- UniswapV3SwapConnectors.createConnector(_11 tokenIn: Type<@MOET.Vault>(),_11 tokenOut: Type<@YieldToken.Vault>(),_11 poolFee: 3000_11)_11_11let yieldTokens <- swapAction1.swap(_11 vaultIn: <-moetVault,_11 amountOutMin: 95.0_11)
3. Sink Action (YieldToken → ERC4626):
_10// Deposit yield tokens to earn_10let sinkAction <- ERC4626SinkConnectors.createConnector(_10 vaultAddress: 0x789...,_10 tokenType: Type<@YieldToken.Vault>()_10)_10_10sinkAction.deposit(vault: <-yieldTokens)_10// Now earning yield
4. Source Action (ERC4626 → YieldToken):
_10// Withdraw when rebalancing needed_10let sourceAction <- ERC4626SourceConnectors.createConnector(_10 vaultAddress: 0x789...,_10 tokenType: Type<@YieldToken.Vault>()_10)_10_10let withdrawn <- sourceAction.withdraw(amount: excessAmount)
5. Swap Action #2 (YieldToken → FLOW):
_12// Convert back to collateral_12let swapAction2 <- UniswapV3SwapConnectors.createConnector(_12 tokenIn: Type<@YieldToken.Vault>(),_12 tokenOut: Type<@FlowToken.Vault>(),_12 poolFee: 3000_12)_12_12let flowCollateral <- swapAction2.swap(_12 vaultIn: <-withdrawn,_12 amountOutMin: 95.0_12)_12// Deposit back to position as additional collateral
Composition Diagram
_17graph LR_17 Collateral[FLOW Collateral] -->|1. Deposit| Borrow[DrawDownSink]_17 Borrow -->|2. Borrow| MOET[MOET Tokens]_17 MOET -->|3. Swap| Swap1[UniswapV3Swap]_17 Swap1 -->|4. Convert| Yield[YieldTokens]_17 Yield -->|5. Deposit| Sink[ERC4626Sink]_17 Sink -->|6. Earn| Vault[ERC4626 Vault]_17_17 Vault -->|7. Withdraw| Source[ERC4626Source]_17 Source -->|8. Convert| Swap2[UniswapV3Swap]_17 Swap2 -->|9. Return| Collateral_17_17 style Borrow fill:#f9f_17 style Swap1 fill:#bfb_17 style Sink fill:#bbf_17 style Source fill:#bbf_17 style Swap2 fill:#bfb
Strategy Composer Pattern
The StrategyComposer pattern assembles Actions into complete strategies:
_21pub resource StrategyComposer {_21 // Action components_21 access(self) let borrowAction: @DrawDownSink_21 access(self) let swapToYieldAction: @SwapConnector_21 access(self) let sinkAction: @SinkConnector_21 access(self) let sourceAction: @SourceConnector_21 access(self) let swapToCollateralAction: @SwapConnector_21_21 // Compose into strategy_21 pub fun composeStrategy(): @Strategy {_21 let strategy <- create TracerStrategy(_21 borrowAction: <-self.borrowAction,_21 swapToYield: <-self.swapToYieldAction,_21 sink: <-self.sinkAction,_21 source: <-self.sourceAction,_21 swapToCollateral: <-self.swapToCollateralAction_21 )_21_21 return <-strategy_21 }_21}
Benefits of this pattern:
- Flexibility: Swap any Action implementation without changing strategy logic
- Testability: Mock Actions for testing strategies in isolation
- Reusability: Same Actions used across multiple strategies
- Upgradability: Replace Actions with improved versions
Creating Custom Strategies
Developers can create custom strategies by composing different Actions:
Example: Conservative Stablecoin Strategy
_19pub resource ConservativeStrategy {_19 // Simplified strategy: just deposit to yield vault_19 access(self) let sinkAction: @ERC4626SinkConnector_19 access(self) let sourceAction: @ERC4626SourceConnector_19_19 pub fun deposit(vault: @FungibleToken.Vault) {_19 // Direct deposit, no borrowing or swapping_19 self.sinkAction.deposit(vault: <-vault)_19 }_19_19 pub fun withdraw(amount: UFix64): @FungibleToken.Vault {_19 // Direct withdrawal_19 return <-self.sourceAction.withdraw(amount: amount)_19 }_19_19 pub fun getBalance(): UFix64 {_19 return self.sinkAction.getBalance()_19 }_19}
Example: Multi-Vault Strategy
_21pub resource MultiVaultStrategy {_21 // Diversify across multiple vaults_21 access(self) let vaults: @{String: SinkConnector}_21_21 pub fun deposit(vault: @FungibleToken.Vault) {_21 let amount = vault.balance_21_21 // Split across 3 vaults_21 let vault1Amount = amount * 0.4_21 let vault2Amount = amount * 0.3_21 let vault3Amount = amount * 0.3_21_21 let vault1 <- vault.withdraw(amount: vault1Amount)_21 let vault2 <- vault.withdraw(amount: vault2Amount)_21 let vault3 <- vault_21_21 self.vaults["vault1"]?.deposit(vault: <-vault1)_21 self.vaults["vault2"]?.deposit(vault: <-vault2)_21 self.vaults["vault3"]?.deposit(vault: <-vault3)_21 }_21}
Action Registry
The ActionRegistry maintains available Action implementations:
_24pub contract ActionRegistry {_24 // Registry of available Actions_24 access(contract) var swapConnectors: {String: Type}_24 access(contract) var sinkConnectors: {String: Type}_24 access(contract) var sourceConnectors: {String: Type}_24_24 // Register new Action_24 pub fun registerSwapConnector(name: String, type: Type) {_24 self.swapConnectors[name] = type_24 }_24_24 // Get available Actions_24 pub fun getAvailableSwapConnectors(): [String] {_24 return self.swapConnectors.keys_24 }_24_24 // Create Action instance_24 pub fun createSwapConnector(name: String, config: {String: AnyStruct}): @SwapConnector {_24 let connectorType = self.swapConnectors[name]_24 ?? panic("Connector not found")_24_24 return <-create connectorType(config: config)_24 }_24}
Benefits:
- Discovery: Users can enumerate available Actions
- Versioning: Multiple versions of same Action can coexist
- Governance: Community can vote on adding new Actions
Advanced Composition Patterns
1. Sequential Composition
Chain Actions in sequence:
_10// FLOW → MOET → YieldToken → ERC4626_10let result <- action1.execute(input: <-flowVault)_10 |> action2.execute(input: <-result)_10 |> action3.execute(input: <-result)_10 |> action4.execute(input: <-result)
2. Parallel Composition
Execute multiple Actions concurrently:
_10// Deposit to 3 vaults simultaneously_10async {_10 vault1.deposit(vault: <-split1)_10 vault2.deposit(vault: <-split2)_10 vault3.deposit(vault: <-split3)_10}
3. Conditional Composition
Choose Actions based on conditions:
_10if ratio > 1.05 {_10 // Withdraw and swap_10 let withdrawn <- sourceAction.withdraw(amount: excess)_10 let collateral <- swapAction.swap(vaultIn: <-withdrawn)_10} else if ratio < 0.95 {_10 // Borrow and swap_10 let borrowed <- borrowAction.borrow(amount: deficit)_10 let yieldTokens <- swapAction.swap(vaultIn: <-borrowed)_10}
4. Recursive Composition
Actions that contain other Actions:
_10pub resource CompositeAction: SwapConnector {_10 // Multi-hop swap composed of single-hop swaps_10 access(self) let hop1: @SwapConnector_10 access(self) let hop2: @SwapConnector_10_10 pub fun swap(vaultIn: @FungibleToken.Vault): @FungibleToken.Vault {_10 let intermediate <- self.hop1.swap(vaultIn: <-vaultIn)_10 return <-self.hop2.swap(vaultIn: <-intermediate)_10 }_10}
Best Practices
Keep Actions Small: Each Action should have single, clear responsibility.
Use Interfaces: Depend on Action interfaces, not concrete implementations.
Handle Failures: Implement proper error handling and revert logic.
Document Dependencies: Clearly specify required Action sequences.
Version Actions: Track Action versions for compatibility.
Test Composition: Unit test Actions individually, integration test compositions.
Summary
DeFi Actions provide the composability framework that powers FYV's flexibility through modular Actions for swaps, deposits, and withdrawals, standardized interfaces enabling interchangeability, composition patterns supporting complex strategies, and the registry system allowing Action discovery and versioning.
Key components:
- SwapConnectors: Token conversion via DEXes
- SinkConnectors: Deposits to yield protocols
- SourceConnectors: Withdrawals from yield protocols
- StrategyComposer: Assembles Actions into strategies
- ActionRegistry: Discovers and versions Actions
DeFi Actions are like LEGO blocks for DeFi strategies. By composing simple, reusable Actions, FYV enables sophisticated yield farming flows while maintaining clean separation of concerns and allowing easy customization.