Connectors
We are reviewing and finalizing Flow Actions in FLIP 339. The specific implementation may change as a part of this process.
We will update these tutorials, but you may need to refactor your code if the implementation changes.
Overview
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 a piece of hardware without the software developer needing to know how the hardware expects to receive commands, or an MCP allowing 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 decentralized finance (DeFi) protocols.
- Standardized Interface: All connectors implement the same core methods, which makes them interchangeable.
- Protocol Integration: They handle the complex interactions with different DeFi services (swaps, staking, lending, and so on).
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
You can combine Connetors 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 how to build a connector and integrate 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
You can use VaultSink 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 five 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: Combine Connectors 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 allows 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.