What are Spells?
Spells are sequences of connector functions that compose into complex DeFi transactions. A spell can contain any number of actions across any number of protocols, all executed atomically in a single transaction.
Think of spells as a recipe - each step (connector method call) is an ingredient, and the final cast is cooking the meal.
Core Concepts
Atomic Execution
All actions in a spell are executed atomically - either all succeed or all fail. This guarantees consistency and prevents partial execution that could leave your funds in an unintended state.
Composability
Spells leverage the composability of DeFi protocols. You can chain actions together where the output of one step becomes the input of the next, creating complex strategies.
Gas Efficiency
By batching multiple operations into a single transaction, spells significantly reduce gas costs compared to executing each operation separately.
Spell Structure
Each spell consists of individual actions with three components:
type Spell = {
connector : string // The protocol connector name (e.g., 'AAVE-V3-A')
method : string // The connector method to call (e.g., 'deposit')
args : any [] // Arguments for the method
}
Creating Spells
Basic Spell Creation
Create a new spell instance using the Spell() method:
const spell = dsa . Spell ()
Adding Actions
Add actions to your spell using the add() method:
spell . add ({
connector: 'AAVE-V3-A' ,
method: 'deposit' ,
args: [
'0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' , // ETH address
'1000000000000000000' , // 1 ETH (in wei)
0 , // getId (0 = don't use stored value)
0 // setId (0 = don't store value)
]
})
The add() method returns the spell instance, enabling method chaining for a fluent API.
Casting Spells
Once you’ve added all desired actions, execute the spell using the cast() method:
const txHash = await spell . cast ()
console . log ( 'Transaction hash:' , txHash )
Cast Parameters
You can pass optional parameters to customize the transaction:
await spell . cast ({
from: '0x...' , // Sender address (defaults to connected account)
gasPrice: '50' , // Gas price in gwei
value: '1000000000000000000' , // ETH to send with transaction (in wei)
nonce: 42 // Transaction nonce
})
Practical Examples
Example 1: Supply and Borrow
Deposit ETH to Aave and borrow DAI:
const spell = dsa . Spell ()
// Deposit 1 ETH to Aave V3
spell . add ({
connector: 'AAVE-V3-A' ,
method: 'deposit' ,
args: [
'0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' , // ETH
web3 . utils . toWei ( '1' , 'ether' ),
0 ,
0
]
})
// Borrow 1000 DAI from Aave V3
spell . add ({
connector: 'AAVE-V3-A' ,
method: 'borrow' ,
args: [
'0x6B175474E89094C44Da98b954EedeAC495271d0F' , // DAI address
web3 . utils . toWei ( '1000' , 'ether' ),
2 , // Variable rate mode
0 ,
0
]
})
// Execute the spell
const txHash = await spell . cast ()
Example 2: Yield Optimization
Move funds from Compound to Aave for better yield:
const spell = dsa . Spell ()
// Withdraw DAI from Compound
spell . add ({
connector: 'COMPOUND-V3-A' ,
method: 'withdraw' ,
args: [
daiMarket ,
daiAddress ,
withdrawAmount ,
0 ,
1 // Store withdrawn amount in ID 1
]
})
// Deposit withdrawn DAI to Aave
spell . add ({
connector: 'AAVE-V3-A' ,
method: 'deposit' ,
args: [
daiAddress ,
0 , // Use amount from ID 1
1 , // Get amount from ID 1
0
]
})
await spell . cast ()
Example 3: Swap and Supply
Swap tokens and supply to a lending protocol:
const spell = dsa . Spell ()
// Swap USDC to DAI using Uniswap V3
spell . add ({
connector: 'UNISWAP-V3-SWAP-A' ,
method: 'swap' ,
args: [
usdcAddress , // Token in
daiAddress , // Token out
swapAmount , // Amount to swap
0 , // Min amount out (slippage protection)
0 , // getId
1 // setId - store output amount
]
})
// Supply swapped DAI to Aave
spell . add ({
connector: 'AAVE-V3-A' ,
method: 'deposit' ,
args: [
daiAddress ,
0 , // Use stored amount
1 , // getId - use amount from previous step
0
]
})
await spell . cast ()
Advanced Features
Storage IDs (getId/setId)
Storage IDs enable passing values between spell actions. This is crucial for creating dynamic strategies where one action’s output feeds into another.
Use setId to store a value for use in subsequent actions: spell . add ({
connector: 'COMPOUND-V3-A' ,
method: 'withdraw' ,
args: [ market , token , amount , 0 , 5 ] // Store in ID 5
})
getId - Retrieving Values
Use getId to retrieve a previously stored value: spell . add ({
connector: 'AAVE-V3-A' ,
method: 'deposit' ,
args: [ token , 0 , 5 , 0 ] // Use value from ID 5
})
Storage ID 0 has special meaning : It means “use the literal value” rather than looking up a stored value.
Flash Loan Strategies
Flash loans enable capital-efficient strategies by borrowing funds within a transaction:
const spell = dsa . Spell ()
// 1. Borrow via flash loan
spell . add ({
connector: 'INSTAPOOL-D' ,
method: 'flashBorrow' ,
args: [
daiAddress ,
flashLoanAmount ,
0 ,
0
]
})
// 2. Use borrowed funds for arbitrage/liquidation/refinancing
spell . add ({
connector: 'YOUR-STRATEGY' ,
method: 'execute' ,
args: [ /* ... */ ]
})
// 3. Repay flash loan + fee
spell . add ({
connector: 'INSTAPOOL-D' ,
method: 'flashPayback' ,
args: [
daiAddress ,
0 ,
0
]
})
await spell . cast ()
Flash loans must be repaid within the same transaction. Ensure your strategy generates enough to cover the loan amount plus fees.
Encoding Spells
For advanced use cases, you can encode spells without executing them:
const spell = dsa . Spell ()
spell . add ({ /* ... */ })
// Get encoded spell data
const encodedSpells = await spell . encodeSpells ()
console . log ( encodedSpells )
// {
// targets: ['0x...', '0x...'],
// spells: ['0x...', '0x...']
// }
// Get full cast ABI encoding
const castABI = await spell . encodeCastABI ()
This is useful for:
Building custom transaction handlers
Integrating with multisig wallets
Creating meta-transactions
Testing and simulation
Gas Estimation
Estimate gas costs before casting:
const spell = dsa . Spell ()
spell . add ({ /* ... */ })
const estimatedGas = await spell . estimateCastGas ()
console . log ( `Estimated gas: ${ estimatedGas } ` )
Transaction Callbacks
Monitor transaction status with callbacks:
await spell . cast ({
onReceipt : ( receipt ) => {
console . log ( 'Transaction confirmed:' , receipt . transactionHash )
},
onConfirmation : ( confirmationNumber , receipt ) => {
console . log ( `Confirmation ${ confirmationNumber } :` , receipt . transactionHash )
}
})
Error Handling
Spells fail atomically - if any action fails, the entire transaction reverts:
try {
const spell = dsa . Spell ()
spell . add ({
connector: 'AAVE-V3-A' ,
method: 'deposit' ,
args: [ /* ... */ ]
})
await spell . cast ()
console . log ( 'Spell cast successfully!' )
} catch ( error ) {
console . error ( 'Spell failed:' , error . message )
// Handle failure - transaction was reverted, no state changes occurred
}
Common failure reasons include:
Insufficient balance
Slippage tolerance exceeded
Protocol-specific errors (e.g., collateral ratio too low)
Gas estimation failure
Best Practices
1. Test Before Mainnet
Always test your spells on testnets or using simulation mode before executing on mainnet:
const dsa = new DSA ({
web3: web3 ,
mode: 'simulation' ,
publicKey: '0x...'
})
2. Handle Max Values
Use the provided max value constant for “max” operations:
spell . add ({
connector: 'AAVE-V3-A' ,
method: 'withdraw' ,
args: [
tokenAddress ,
dsa . maxValue , // Withdraw entire balance
0 ,
0
]
})
3. Optimize Gas
Order actions to minimize state changes and optimize gas usage:
// Good: Batch similar operations
spell . add ({ connector: 'AAVE-V3-A' , method: 'deposit' , args: [ token1 , ... ] })
spell . add ({ connector: 'AAVE-V3-A' , method: 'deposit' , args: [ token2 , ... ] })
// Less optimal: Interleave different protocols
spell . add ({ connector: 'AAVE-V3-A' , method: 'deposit' , args: [ ... ] })
spell . add ({ connector: 'COMPOUND-V3-A' , method: 'supply' , args: [ ... ] })
spell . add ({ connector: 'AAVE-V3-A' , method: 'borrow' , args: [ ... ] })
4. Use Storage IDs for Dynamic Amounts
When dealing with variable amounts (e.g., after swaps), use storage IDs:
// Store swap output amount
spell . add ({
connector: 'UNISWAP-V3-SWAP-A' ,
method: 'swap' ,
args: [ buyToken , sellToken , amount , minReturn , 0 , 1 ] // setId = 1
})
// Use stored amount in next action
spell . add ({
connector: 'AAVE-V3-A' ,
method: 'deposit' ,
args: [ sellToken , 0 , 1 , 0 ] // getId = 1
})
5. Validate Parameters
Ensure all parameters are correct before casting:
const validateSpell = ( spell ) => {
if ( ! spell . data . length ) {
throw new Error ( 'Spell has no actions' )
}
spell . data . forEach (( action , index ) => {
if ( ! action . connector ) throw new Error ( `Action ${ index } : missing connector` )
if ( ! action . method ) throw new Error ( `Action ${ index } : missing method` )
if ( ! action . args ) throw new Error ( `Action ${ index } : missing args` )
})
}
validateSpell ( spell )
await spell . cast ()
Common Patterns
Refinancing Move debt between protocols to optimize interest rates
Leveraging Build leveraged positions by borrowing and re-supplying
Yield Farming Automate yield farming strategies across multiple protocols
Portfolio Rebalancing Adjust positions across protocols to maintain target allocation
Next Steps
Connectors Explore available protocol connectors
API Reference View the complete Spell API