Skip to main content

Connectors

warning

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:


_10
Your 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
_10
access(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
_18
fun getComponentInfo(): ComponentInfo
_18
fun copyID(): UniqueIdentifier?
_18
fun setID(_ id: UniqueIdentifier?)
_18
_18
// Type-specific methods
_18
fun getSinkType(): Type // Sink only
_18
fun getSourceType(): Type // Source only
_18
fun inType() / outType(): Type // Swapper only
_18
_18
// Core operations
_18
fun minimumCapacity(): UFix64 // Sink
_18
fun depositCapacity(from: &Vault) // Sink
_18
fun minimumAvailable(): UFix64 // Source
_18
fun withdrawAvailable(maxAmount: UFix64): @Vault // Source
_18
fun swap(quote: Quote?, inVault: @Vault): @Vault // Swapper
_18
fun getPrice(baseAsset: Type, quoteAsset: Type): UFix64 // PriceOracle
_18
fun 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
_10
ProtocolA.RewardsSource → SwapConnectors.SwapSource → ProtocolB.StakingSink

Connector Library

🔄 SOURCE Primitive Implementations

ConnectorLocationProtocolPurpose
VaultSourceFungibleTokenConnectorsGeneric FungibleTokenWithdraw from vaults with minimum balance protection
VaultSinkAndSourceFungibleTokenConnectorsGeneric FungibleTokenCombined vault operations (dual interface)
SwapSourceSwapConnectorsGeneric (composes with Swappers)Source tokens then swap before returning
PoolRewardsSourceIncrementFiStakingConnectorsIncrementFi StakingClaim staking rewards from pools

⬇️ SINK Primitive Implementations

ConnectorLocationProtocolPurpose
VaultSinkFungibleTokenConnectorsGeneric FungibleTokenDeposit to vaults with capacity limits
VaultSinkAndSourceFungibleTokenConnectorsGeneric FungibleTokenCombined vault operations (dual interface)
SwapSinkSwapConnectorsGeneric (composes with Swappers)Swap tokens before depositing to inner sink
PoolSinkIncrementFiStakingConnectorsIncrementFi StakingStake tokens in staking pools

🔀 SWAPPER Primitive Implementations

ConnectorLocationProtocolPurpose
MultiSwapperSwapConnectorsGeneric (DEX aggregation)Aggregate multiple swappers for optimal routing
SwapperIncrementFiSwapConnectorsIncrementFi DEXToken swapping through SwapRouter
ZapperIncrementFiPoolLiquidityConnectorsIncrementFi PoolsSingle-token liquidity provision
UniswapV2EVMSwapperUniswapV2SwapConnectorsFlow EVM BridgeCross-VM UniswapV2-style swapping

💰 PRICEORACLE Primitive Implementations

ConnectorLocationProtocolPurpose
PriceOracleBandOracleConnectorsBand ProtocolExternal price feeds with staleness validation

⚡ FLASHER Primitive Implementations

ConnectorLocationProtocolPurpose
FlasherIncrementFiFlashloanConnectorsIncrementFi DEXFlash loans through SwapPair contracts

Guide to Building Connectors

Choose Your Primitive

First, determine which Flow Actions primitive(s) your connector will implement:

PrimitiveWhen to UseExample Use Cases
SourceYour protocol provides tokensVault withdrawals, reward claiming, unstaking
SinkYour protocol accepts tokensVault deposits, staking, loan repayments
SwapperYour protocol exchanges tokensDEX trades, cross-chain bridges, LP provision
PriceOracleYour protocol provides price dataOracle feeds, TWAP calculations
FlasherYour protocol offers flash loansArbitrage 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
_13
access(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
_13
access(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

_14
access(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

_10
access(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:


_10
flow 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
_23
import "FungibleTokenConnectors"
_23
import "DeFiActions"
_23
import "FungibleToken"
_23
_23
transaction(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
_43
import "FungibleToken"
_43
import "FungibleTokenMetadataViews"
_43
import "FlowToken"
_43
import "FungibleTokenConnectors"
_43
_43
transaction(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:


_10
flow 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
_30
import "FungibleTokenConnectors"
_30
import "FlowToken"
_30
_30
transaction(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)
_51
import "DeFiActions"
_51
import "FungibleTokenConnectors"
_51
import "BandOracleConnectors"
_51
_51
transaction() {
_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:

  1. Keep constructors simple - minimal required parameters
  2. Validate inputs - check capabilities and preconditions
  3. Handle errors gracefully - no-ops instead of panics
  4. Support Flow Actions standards - UniqueIdentifier, ComponentInfo
  5. Test thoroughly - create usage transactions like the ones shown
  6. 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.