Connectors
Flow Actions are being reviewed and finalized in FLIP 339. The specific implementation may change as a part of this process.
These tutorials will be updated, but you may need to refactor your code if the implementation changes.
Connectors are the bridge between external DeFi protocols and the standardized Flow Actions primitive interfaces. They act as protocol adapters that translate protocol-specific APIs into the universal language of Flow Actions. Think of them as "drivers" that provide a connection between software and piece of hardware without the software developer needing to know how the hardware expects commands to be delivered, or an MCP enabling an agent to use an API in a standardized manner. Flow Actions act as "money LEGOs" with which you can compose various complex operations with simple transactions. These are the benefits of connectors:
- Abstraction Layer: Connectors act like a universal translator between your application and various DeFi protocols
- Standardized Interface: All connectors implement the same core methods, making them interchangeable
- Protocol Integration: They handle the complex interactions with different DeFi services (swaps, staking, lending, etc.)
How Connectors Work
Abstraction Layer
Connectors sit between your application logic and protocol-specific contracts:
_10Your DeFi Strategy → Flow Actions Connector → Protocol Contract → Blockchain State
Interface Implementation
Each connector implements one or more of the five primitive interfaces:
_10// Example: A connector implementing the Sink primitive_10access(all) struct MyProtocolSink: DeFiActions.Sink {_10 // Protocol-specific configuration_10 access(self) let protocolConfig: MyProtocol.Config_10 _10 // DeFiActions required methods_10 access(all) fun getSinkType(): Type { ... }_10 access(all) fun minimumCapacity(): UFix64 { ... }_10 access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { ... }_10}
All connectors implement these standard methods:
_18// Identity & Component Info_18fun getComponentInfo(): ComponentInfo_18fun copyID(): UniqueIdentifier?_18fun setID(_ id: UniqueIdentifier?)_18_18// Type-specific methods_18fun getSinkType(): Type // Sink only_18fun getSourceType(): Type // Source only _18fun inType() / outType(): Type // Swapper only_18_18// Core operations_18fun minimumCapacity(): UFix64 // Sink_18fun depositCapacity(from: &Vault) // Sink_18fun minimumAvailable(): UFix64 // Source_18fun withdrawAvailable(maxAmount: UFix64): @Vault // Source_18fun swap(quote: Quote?, inVault: @Vault): @Vault // Swapper_18fun getPrice(baseAsset: Type, quoteAsset: Type): UFix64 // PriceOracle_18fun flashLoan(amount: UFix64, callback: Function) // Flasher
Composition Pattern
Connectors can be combined to create sophisticated workflows:
_10// Claim rewards → Swap to different token → Stake in new pool_10ProtocolA.RewardsSource → SwapConnectors.SwapSource → ProtocolB.StakingSink
Connector Library
🔄 SOURCE Primitive Implementations
Connector | Location | Protocol | Purpose |
---|---|---|---|
VaultSource | FungibleTokenConnectors | Generic FungibleToken | Withdraw from vaults with minimum balance protection |
VaultSinkAndSource | FungibleTokenConnectors | Generic FungibleToken | Combined vault operations (dual interface) |
SwapSource | SwapConnectors | Generic (composes with Swappers) | Source tokens then swap before returning |
PoolRewardsSource | IncrementFiStakingConnectors | IncrementFi Staking | Claim staking rewards from pools |
⬇️ SINK Primitive Implementations
Connector | Location | Protocol | Purpose |
---|---|---|---|
VaultSink | FungibleTokenConnectors | Generic FungibleToken | Deposit to vaults with capacity limits |
VaultSinkAndSource | FungibleTokenConnectors | Generic FungibleToken | Combined vault operations (dual interface) |
SwapSink | SwapConnectors | Generic (composes with Swappers) | Swap tokens before depositing to inner sink |
PoolSink | IncrementFiStakingConnectors | IncrementFi Staking | Stake tokens in staking pools |
🔀 SWAPPER Primitive Implementations
Connector | Location | Protocol | Purpose |
---|---|---|---|
MultiSwapper | SwapConnectors | Generic (DEX aggregation) | Aggregate multiple swappers for optimal routing |
Swapper | IncrementFiSwapConnectors | IncrementFi DEX | Token swapping through SwapRouter |
Zapper | IncrementFiPoolLiquidityConnectors | IncrementFi Pools | Single-token liquidity provision |
UniswapV2EVMSwapper | UniswapV2SwapConnectors | Flow EVM Bridge | Cross-VM UniswapV2-style swapping |
💰 PRICEORACLE Primitive Implementations
Connector | Location | Protocol | Purpose |
---|---|---|---|
PriceOracle | BandOracleConnectors | Band Protocol | External price feeds with staleness validation |
⚡ FLASHER Primitive Implementations
Connector | Location | Protocol | Purpose |
---|---|---|---|
Flasher | IncrementFiFlashloanConnectors | IncrementFi DEX | Flash loans through SwapPair contracts |
Guide to Building Connectors
Choose Your Primitive
First, determine which Flow Actions primitive(s) your connector will implement:
Primitive | When to Use | Example Use Cases |
---|---|---|
Source | Your protocol provides tokens | Vault withdrawals, reward claiming, unstaking |
Sink | Your protocol accepts tokens | Vault deposits, staking, loan repayments |
Swapper | Your protocol exchanges tokens | DEX trades, cross-chain bridges, LP provision |
PriceOracle | Your protocol provides price data | Oracle feeds, TWAP calculations |
Flasher | Your protocol offers flash loans | Arbitrage opportunities, liquidations |
Analyze Your Protocol
Study your target protocol to understand:
- Contract interfaces and method signatures
- Required parameters and data structures
- Error conditions and failure modes
- Fee structures and payment mechanisms
- Access controls and permissions
Design Your Connector
Plan your connector implementation:
- Configuration parameters needed for initialization
- Capability requirements for protocol access
- Error handling strategy for graceful failures
- Resource management for token handling
- Event emission for traceability
Implement the Interface
Create your connector struct implementing the chosen primitive interface(s).
Add Safety Features
Implement safety mechanisms:
- Capacity checking before operations
- Balance validation after operations
- Graceful error handling with no-ops
- Resource cleanup for empty vaults
Support Flow Actions Standards
Add required Flow Actions support:
- IdentifiableStruct implementation
- UniqueIdentifier management
- ComponentInfo for introspection
- Event emission integration
Best Practices
Error Handling
- Graceful Failures: Return empty results instead of panicking
- Validation: Check all inputs and preconditions
- Resource Safety: Properly handle vault resources in all paths
_13// Good: Graceful failure_13access(all) fun minimumCapacity(): UFix64 {_13 if let pool = self.poolCapability.borrow() {_13 return pool.getAvailableCapacity()_13 }_13 return 0.0 // Graceful failure_13}_13_13// Bad: Panics on failure _13access(all) fun minimumCapacity(): UFix64 {_13 let pool = self.poolCapability.borrow()! // Will panic if invalid_13 return pool.getAvailableCapacity()_13}
Capacity and Balance Checking
- Always Check First: Validate capacity/availability before operations
- Respect Limits: Work within available constraints
- Handle Edge Cases: Zero amounts, maximum values, empty vaults
_14access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {_14 // Check capacity first_14 let capacity = self.minimumCapacity()_14 if capacity == 0.0 { return }_14 _14 // Calculate actual deposit amount_14 let availableAmount = from.balance_14 let depositAmount = capacity < availableAmount ? capacity : availableAmount_14 _14 // Handle edge case_14 if depositAmount == 0.0 { return }_14 _14 // Proceed with deposit..._14}
Type Safety
- Validate Types: Ensure vault types match expected types
- Early Returns: Fail fast on type mismatches
- Clear Error Messages: Help developers understand issues
_10access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {_10 // Type validation_10 if from.getType() != self.getSinkType() {_10 return // No-op for wrong token type_10 }_10 _10 // Continue with deposit..._10}
Event Integration
- Leverage Post-conditions: Flow Actions interfaces emit events automatically
- Provide Context: Include relevant information in events
- Support Traceability: Use UniqueIdentifiers consistently
Resource Management
- Handle Empty Vaults: Use
DeFiActionsUtils.getEmptyVault()
for consistent empty vault creation - Destroy Properly: Clean up resources in all code paths
- Avoid Resource Leaks: Ensure all vaults are handled appropriately
Capability Management
- Validate Capabilities: Check capabilities before using them
- Handle Revocation: Gracefully handle revoked capabilities
- Proper Entitlements: Use correct entitlement levels (auth vs unauth)
Documentation
- Clear Comments: Explain protocol-specific logic
- Usage Examples: Show how to use your connectors
- Integration Patterns: Demonstrate composition with other connectors
Integration into Flow Actions
We will now go over the process of building a connector and integrating it with Flow Actions. Specifically, we will showcase the process of using the VaultSink connector in the FungibleTokenConnectors. It only performs basic token deposits to a vault with capacity limits, implements the Sink interface, has minimal external dependencies (only FungibleToken standard), and requires simple configuration (max balance, deposit vault capability,and unique ID).
The VaultSink
connector is already deployed and working in Flow Actions. Let's examine how it's integrated:
Location: cadence/contracts/connectors/FungibleTokenConnectors.cdc
Contract: FungibleTokenConnectors
Connector: VaultSink
struct that defines the interaction with the connector.
Deploy Your Connector Contract
Deploy your connector contract with the following command:
_10flow project deploy
In your 'flow.json' you will find:
_12{_12 "contracts": {_12 "FungibleTokenConnectors": {_12 "source": "./cadence/contracts/connectors/FungibleTokenConnectors.cdc",_12 "aliases": {_12 "emulator": "f8d6e0586b0a20c7",_12 "testnet": "...",_12 "mainnet": "..."_12 }_12 }_12 }_12}
Create Usage Transactions
Create transaction templates for using your connectors:
_23// Transaction: save_vault_sink.cdc_23import "FungibleTokenConnectors"_23import "DeFiActions"_23import "FungibleToken"_23_23transaction(maxBalance: UFix64) {_23 prepare(signer: auth(Storage, Capabilities) &Account) {_23 // Get vault capability for deposits_23 let vaultCap = signer.capabilities.get<&{FungibleToken.Receiver}>(_23 /public/flowTokenReceiver_23 )_23 _23 // Create the VaultSink connector_23 let vaultSink = FungibleTokenConnectors.VaultSink(_23 max: maxBalance,_23 depositVault: vaultCap,_23 uniqueID: nil_23 )_23 _23 // Save to storage for later use_23 signer.storage.save(vaultSink, to: /storage/FlowTokenVaultSink)_23 }_23}
Real Usage Transaction: VaultSink
Here's the actual working transaction that creates a VaultSink:
_43// File: cadence/transactions/fungible-token-stack/save_vault_sink.cdc_43import "FungibleToken"_43import "FungibleTokenMetadataViews"_43import "FlowToken"_43import "FungibleTokenConnectors"_43_43transaction(receiver: Address, vaultPublicPath: PublicPath, sinkStoragePath: StoragePath, max: UFix64?) {_43 let depositVault: Capability<&{FungibleToken.Vault}>_43 let signer: auth(SaveValue) &Account_43_43 prepare(signer: auth(SaveValue) &Account) {_43 // Get the receiver's vault capability_43 self.depositVault = getAccount(receiver).capabilities.get<&{FungibleToken.Vault}>(vaultPublicPath)_43 self.signer = signer_43 }_43_43 pre {_43 self.signer.storage.type(at: sinkStoragePath) == nil:_43 "Collision at sinkStoragePath \(sinkStoragePath.toString())"_43 self.depositVault.check(): "Invalid deposit vault capability"_43 }_43_43 execute {_43 // Create the VaultSink connector_43 let sink = FungibleTokenConnectors.VaultSink(_43 max: max, // Maximum capacity (nil = unlimited)_43 depositVault: self.depositVault, // Where tokens will be deposited_43 uniqueID: nil // No unique ID for this example_43 )_43 _43 // Save the connector for later use_43 self.signer.storage.save(sink, to: sinkStoragePath)_43 _43 log("VaultSink created and saved!")_43 log("Max capacity: ".concat(max?.toString() ?? "unlimited"))_43 log("Receiver: ".concat(receiver.toString()))_43 }_43_43 post {_43 self.signer.storage.type(at: sinkStoragePath) == Type<FungibleTokenConnectors.VaultSink>():_43 "VaultSink was not stored correctly"_43 }_43}
Execute this transaction:
_10flow transactions send cadence/transactions/fungible-token-stack/save_vault_sink.cdc \_10 --arg Address:0x01cf0e2f2f715450 \_10 --arg PublicPath:"/public/FlowTokenReceiver" \_10 --arg StoragePath:"/storage/FlowTokenSink" \_10 --arg "UFix64?":1000.0 \_10 --signer emulator
Create Combinations Examples
Show how your connectors work with existing Flow Actions components:
_30// Example: Using VaultSink in a real deposit workflow_30import "FungibleTokenConnectors"_30import "FlowToken"_30_30transaction(depositAmount: UFix64) {_30 prepare(signer: auth(BorrowValue) &Account) {_30 // 1. Load the saved VaultSink_30 let sink = signer.storage.borrow<&FungibleTokenConnectors.VaultSink>(_30 from: /storage/FlowTokenSink_30 ) ?? panic("VaultSink not found - create one first!")_30 _30 // 2. Create a simple source (your own vault)_30 let flowVault = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(_30 from: /storage/FlowTokenVault_30 ) ?? panic("FlowToken vault not found")_30 _30 // 3. Check sink capacity before depositing_30 let capacity = sink.minimumCapacity()_30 log("Sink capacity: ".concat(capacity.toString()))_30 _30 if capacity >= depositAmount {_30 // 4. Execute Source → Sink workflow_30 let tokens <- flowVault.withdraw(amount: depositAmount)_30 sink.depositCapacity(from: tokens)_30 log("Deposited ".concat(depositAmount.toString()).concat(" FLOW through VaultSink!"))_30 } else {_30 log("Insufficient sink capacity: ".concat(capacity.toString()))_30 }_30 }_30}
Add to Existing Workflows
The VaultSink can be used in advanced Flow Actions workflows:
_51// Example: VaultSink in AutoBalancer (real integration pattern)_51import "DeFiActions"_51import "FungibleTokenConnectors" _51import "BandOracleConnectors"_51_51transaction() {_51 prepare(signer: auth(SaveValue, BorrowValue, IssueStorageCapabilityController) &Account) {_51 // 1. Create rebalancing sink using VaultSink pattern_51 let rebalanceCap = getAccount(signer.address)_51 .capabilities.get<&{FungibleToken.Receiver}>(/public/FlowTokenReceiver)_51 _51 let rebalanceSink = FungibleTokenConnectors.VaultSink(_51 max: nil, // No limit for rebalancing_51 depositVault: rebalanceCap,_51 uniqueID: nil_51 )_51 _51 // 2. Create rebalancing source _51 let sourceCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &FlowToken.Vault>(_51 /storage/FlowTokenVault_51 )_51 let rebalanceSource = FungibleTokenConnectors.VaultSource(_51 min: 100.0, // Keep 100 FLOW minimum_51 withdrawVault: sourceCap,_51 uniqueID: nil_51 )_51 _51 // 3. Create price oracle_51 let priceOracle = BandOracleConnectors.PriceOracle(_51 unitOfAccount: Type<@FlowToken.Vault>(),_51 staleThreshold: 3600,_51 feeSource: rebalanceSource,_51 uniqueID: nil_51 )_51 _51 // 4. Create AutoBalancer using VaultSink pattern_51 let autoBalancer <- DeFiActions.createAutoBalancer(_51 oracle: priceOracle,_51 vaultType: Type<@FlowToken.Vault>(),_51 lowerThreshold: 0.9,_51 upperThreshold: 1.1,_51 rebalanceSink: rebalanceSink, // Uses VaultSink!_51 rebalanceSource: rebalanceSource, // Uses VaultSource!_51 uniqueID: nil_51 )_51 _51 signer.storage.save(<-autoBalancer, to: /storage/FlowAutoBalancer)_51 _51 log("AutoBalancer created using VaultSink/VaultSource pattern!")_51 }_51}
For Your Own Connectors
When building your own connectors, follow the VaultSink pattern:
- Keep constructors simple - minimal required parameters
- Validate inputs - check capabilities and preconditions
- Handle errors gracefully - no-ops instead of panics
- Support Flow Actions standards - UniqueIdentifier, ComponentInfo
- Test thoroughly - create usage transactions like the ones shown
- Document clearly - show real integration examples
Conclusion
The Flow Actions framework provides a comprehensive set of connectors that successfully implement the 5 fundamental DeFi primitives across multiple protocols:
- 20+ Connector Implementations spanning basic vault operations to complex cross-VM swapping
- 4 Protocol Integrations: Generic FungibleToken, IncrementFi, Band Oracle, Flow EVM
- Composable Architecture: Connectors can be combined to create sophisticated financial workflows
- Safety-First Design: Graceful error handling and resource safety throughout
- Event-Driven Traceability: Full workflow tracking and debugging capabilities
This framework enables developers to build sophisticated DeFi strategies while maintaining the simplicity and reliability of standardized primitive interfaces. The modular design allows for easy extension to additional protocols while preserving composability and atomic execution guarantees.