This guide explains how to use Weiroll to compose complex operation sequences in Flux Protocol, enabling managers and liquidators to execute sophisticated multi-step transactions efficiently.
Weiroll is a scripting language and virtual machine (VM) for composing sequences of smart contract calls into a single atomic transaction. Think of it as a "programmable callback" that lets you encode complex operations off-chain and execute them on-chain in one transaction.
Key Concepts
Commands: Encoded function calls represented as bytes32 values
State: Array of variables (inputs/outputs) that commands can reference
Planner: Off-chain tool (JavaScript library) for building command sequences
VM: On-chain executor that interprets and executes command sequences
How Weiroll Works
Example Weiroll Script:
Why Flux Uses Weiroll
Flux's unified callback architecture creates a unique opportunity for Weiroll:
1. Unified Callback Pattern
Both managers (unlock) and liquidators share the same callback interface:
This means one Weiroll executor can handle both manager operations and liquidations.
2. Complex Operation Sequences
Manager workflows often require multiple steps:
Borrow → Swap → Deposit collateral
Withdraw → Repay → Rebalance
Move funds between positions → Adjust leverage
Weiroll lets you compose these atomically without writing custom callback contracts.
3. Capital-Free Liquidations
Liquidators can use locked_borrow() during callbacks to access vault capital:
No upfront capital needed—use the vault's liquidity during liquidation.
4. Flexibility Without Code Changes
Deploy one WeirollExecutor, then compose different operations by changing the script:
No new contract deployments
No contract upgrades
Change strategies on the fly
WeirollExecutor Architecture
Contract Structure
Execution Flow
Access Control
The executor has two access modes:
Self-call during callback: msg.sender == address(this)
When vault calls fluxUnlockCallback(), executor uses this.execute()
This converts memory arrays to calldata for the VM
Direct manager call: msg.sender == manager
Manager can test scripts directly
Useful for debugging without involving the vault
Getting Started
Step 1: Deploy Your WeirollExecutor
Use the factory's deployment helper:
Or deploy manually:
Step 2: Install Weiroll JavaScript Library
Note: Use ethers v5 (weiroll.js is not yet compatible with v6)
const planner = new weiroll.Planner();
// Create contract wrappers
const vault = weiroll.Contract.createContract(
new ethers.Contract(VAULT_ADDRESS, VAULT_ABI)
);
const asset = weiroll.Contract.createContract(
new ethers.Contract(ASSET_ADDRESS, ERC20_ABI)
);
// Amounts
const bondAmount = ethers.utils.parseEther('25000');
const borrowAmount = ethers.utils.parseEther('100000');
// Build script
// Note: Transfer bond to executor before calling unlock
planner.add(asset.approve(VAULT_ADDRESS, bondAmount));
planner.add(vault.locked_depositBond(bondAmount));
planner.add(vault.locked_borrow(borrowAmount));
const { commands, state } = planner.plan();
// Transfer bond to executor BEFORE calling unlock
asset.transfer(executorAddress, bondAmount);
// Execute script
vault.unlock(abi.encode(commands, state));
const planner = new weiroll.Planner();
// Contract wrappers
const vault = weiroll.Contract.createContract(
new ethers.Contract(VAULT_ADDRESS, VAULT_ABI)
);
const usdc = weiroll.Contract.createContract(
new ethers.Contract(USDC_ADDRESS, ERC20_ABI)
);
const weth = weiroll.Contract.createContract(
new ethers.Contract(WETH_ADDRESS, ERC20_ABI)
);
const uniswapRouter = weiroll.Contract.createContract(
new ethers.Contract(UNISWAP_ROUTER, UNISWAP_ABI)
);
const bondAmount = ethers.utils.parseUnits('25000', 6); // USDC has 6 decimals
const borrowAmount = ethers.utils.parseUnits('100000', 6);
// Build script
planner.add(usdc.approve(VAULT_ADDRESS, bondAmount));
planner.add(vault.locked_depositBond(bondAmount));
planner.add(vault.locked_borrow(borrowAmount));
// Withdraw borrowed USDC to executor
planner.add(vault.locked_withdrawFromWrapper(
BASE_WRAPPER,
WORKING_CAPITAL_POS_ID,
borrowAmount
));
// Swap USDC → WETH via Uniswap
planner.add(usdc.approve(UNISWAP_ROUTER, borrowAmount));
planner.add(uniswapRouter.swapExactTokensForTokens(
borrowAmount,
minWethOut,
[USDC_ADDRESS, WETH_ADDRESS],
EXECUTOR_ADDRESS,
deadline
));
// Deposit WETH as collateral
const wethAmount = weiroll.Library.wrap(WETH_RECEIVED); // Use return value from swap
planner.add(weth.approve(VAULT_ADDRESS, wethAmount));
planner.add(vault.locked_registerWrapper(WETH_WRAPPER));
planner.add(vault.locked_depositToWrapper(
WETH_WRAPPER,
ethers.constants.HashZero, // positionId
wethAmount
));
const { commands, state } = planner.plan();
const planner = new weiroll.Planner();
const vault = weiroll.Contract.createContract(
new ethers.Contract(VAULT_ADDRESS, VAULT_ABI)
);
// Constants for position IDs
const BOND_POS_ID = ethers.utils.hexZeroPad('0x01', 32);
const WORKING_CAPITAL_POS_ID = ethers.constants.HashZero;
// Move 5k from bond to working capital (to cover accrued interest)
const moveAmount = ethers.utils.parseEther('5000');
planner.add(vault.locked_moveFunds(
BOND_POS_ID,
WORKING_CAPITAL_POS_ID,
moveAmount
));
// Repay all debt (0 = full repayment)
planner.add(vault.locked_repay(0));
// Withdraw remaining bond (25k - 5k = 20k)
const withdrawAmount = ethers.utils.parseEther('20000');
planner.add(vault.locked_withdrawBond(withdrawAmount));
const { commands, state } = planner.plan();
const planner = new weiroll.Planner();
const vault = weiroll.Contract.createContract(
new ethers.Contract(VAULT_ADDRESS, VAULT_ABI)
);
const weth = weiroll.Contract.createContract(
new ethers.Contract(WETH_ADDRESS, ERC20_ABI)
);
const usdc = weiroll.Contract.createContract(
new ethers.Contract(USDC_ADDRESS, ERC20_ABI)
);
const uniswapRouter = weiroll.Contract.createContract(
new ethers.Contract(UNISWAP_ROUTER, UNISWAP_ABI)
);
const wethToWithdraw = ethers.utils.parseEther('10'); // 10 WETH
// Withdraw WETH from wrapper
planner.add(vault.locked_withdrawFromWrapper(
WETH_WRAPPER,
POSITION_ID,
wethToWithdraw
));
// Swap WETH → USDC
planner.add(weth.approve(UNISWAP_ROUTER, wethToWithdraw));
planner.add(uniswapRouter.swapExactTokensForTokens(
wethToWithdraw,
minUsdcOut,
[WETH_ADDRESS, USDC_ADDRESS],
EXECUTOR_ADDRESS,
deadline
));
// Deposit USDC to working capital
const usdcReceived = weiroll.Library.wrap(USDC_AMOUNT);
planner.add(usdc.approve(VAULT_ADDRESS, usdcReceived));
planner.add(vault.locked_depositToWrapper(
BASE_WRAPPER,
WORKING_CAPITAL_POS_ID,
usdcReceived
));
const { commands, state } = planner.plan();
// Manual callback (not using Weiroll for simplicity)
contract CapitalFreeLiquidator is IFluxUnlockCallback {
address public vault;
address public immutable liquidatorOwner;
address public immutable baseAssetWrapper;
IERC20 public asset;
constructor(address _owner, address _vault, address _asset, address _baseWrapper) {
liquidatorOwner = _owner;
vault = _vault;
asset = IERC20(_asset);
baseAssetWrapper = _baseWrapper;
asset.approve(_vault, type(uint256).max);
}
function fluxUnlockCallback(bytes calldata data) external override {
require(msg.sender == vault, "Only vault");
(uint256 minPayment,,) = abi.decode(data, (uint256, uint256, bytes));
// 1. Withdraw seized working capital
uint256 workingCapital = FluxVault(vault).getWorkingCapital(liquidatorOwner);
if (workingCapital > 0) {
FluxVault(vault).locked_withdrawFromWrapper(
IAsset(baseAssetWrapper),
WORKING_CAPITAL_POS_ID,
workingCapital
);
}
// 2. Withdraw seized bond if needed
uint256 balance = asset.balanceOf(address(this));
if (balance < minPayment) {
uint256 needed = minPayment - balance;
FluxVault(vault).locked_withdrawBond(needed);
}
// 3. Deposit minPayment to working capital
FluxVault(vault).locked_depositToWrapper(
IAsset(baseAssetWrapper),
bytes32(0),
minPayment
);
}
}
const planner = new weiroll.Planner();
const vault = weiroll.Contract.createContract(
new ethers.Contract(VAULT_ADDRESS, VAULT_ABI)
);
const asset = weiroll.Contract.createContract(
new ethers.Contract(ASSET_ADDRESS, ERC20_ABI)
);
// Note: minPayment is provided in callback data
const minPayment = weiroll.State.fromSlot(0); // First state variable from callback
// 1. Withdraw seized working capital
planner.add(vault.locked_withdrawFromWrapper(
BASE_WRAPPER,
WORKING_CAPITAL_POS_ID,
workingCapitalAmount // Query this beforehand
));
// 2. Withdraw seized bond (if needed)
planner.add(vault.locked_withdrawBond(bondNeeded));
// 3. Approve and deposit minPayment
planner.add(asset.approve(VAULT_ADDRESS, minPayment));
planner.add(vault.locked_depositToWrapper(
BASE_WRAPPER,
WORKING_CAPITAL_POS_ID,
minPayment
));
const { commands, state } = planner.plan();
// Liquidate with Weiroll script
vault.liquidate(managerToLiquidate, new IAsset[](0), weirollScript);
const planner = new weiroll.Planner();
const vault = weiroll.Contract.createContract(
new ethers.Contract(VAULT_ADDRESS, VAULT_ABI)
);
const asset = weiroll.Contract.createContract(
new ethers.Contract(ASSET_ADDRESS, ERC20_ABI)
);
const borrowAmount = ethers.utils.parseEther('50000'); // Borrow vault capital
const minPayment = weiroll.State.fromSlot(0);
// 1. Deposit small bond as liquidator
const liquidatorBond = ethers.utils.parseEther('5000');
planner.add(asset.approve(VAULT_ADDRESS, liquidatorBond));
planner.add(vault.locked_depositBond(liquidatorBond));
// 2. Borrow from vault
planner.add(vault.locked_borrow(borrowAmount));
// 3. Withdraw seized collateral
planner.add(vault.locked_withdrawFromWrapper(
WETH_WRAPPER,
seizedPositionId,
seizedAmount
));
// 4. Sell seized collateral on DEX
planner.add(weth.approve(UNISWAP_ROUTER, seizedAmount));
planner.add(uniswapRouter.swapExactTokensForTokens(
seizedAmount,
minUsdcOut,
[WETH_ADDRESS, USDC_ADDRESS],
EXECUTOR_ADDRESS,
deadline
));
// 5. Repay borrowed amount
planner.add(asset.approve(VAULT_ADDRESS, borrowAmount));
planner.add(vault.locked_repay(borrowAmount));
// 6. Deposit minPayment
planner.add(vault.locked_depositToWrapper(
BASE_WRAPPER,
WORKING_CAPITAL_POS_ID,
minPayment
));
// 7. Withdraw liquidator bond (get it back)
planner.add(vault.locked_withdrawBond(liquidatorBond));
const { commands, state } = planner.plan();
const planner = new weiroll.Planner();
const vault = weiroll.Contract.createContract(
new ethers.Contract(VAULT_ADDRESS, VAULT_ABI)
);
const weth = weiroll.Contract.createContract(
new ethers.Contract(WETH_ADDRESS, ERC20_ABI)
);
const usdc = weiroll.Contract.createContract(
new ethers.Contract(USDC_ADDRESS, ERC20_ABI)
);
const aggregator = weiroll.Contract.createContract(
new ethers.Contract(AGGREGATOR_ADDRESS, AGGREGATOR_ABI)
);
// 1. Withdraw seized WETH
planner.add(vault.locked_withdrawFromWrapper(
WETH_WRAPPER,
positionId,
wethAmount
));
// 2. Use 1inch/Paraswap aggregator for best price
planner.add(weth.approve(AGGREGATOR_ADDRESS, wethAmount));
planner.add(aggregator.swap(
WETH_ADDRESS,
USDC_ADDRESS,
wethAmount,
minUsdcOut,
routingData // Pre-calculated optimal route
));
// 3. Deposit proceeds to cover minPayment
const usdcReceived = weiroll.Library.wrap(SWAP_OUTPUT);
planner.add(usdc.approve(VAULT_ADDRESS, usdcReceived));
planner.add(vault.locked_depositToWrapper(
BASE_WRAPPER,
WORKING_CAPITAL_POS_ID,
minPayment
));
const { commands, state } = planner.plan();
const weiroll = require('@ensofinance/weiroll.js');
const planner = new weiroll.Planner();
const vault = weiroll.Contract.createContract(
new ethers.Contract(VAULT_ADDRESS, VAULT_ABI, provider)
);
// Now you can add vault function calls to the planner
planner.add(vault.locked_borrow(amount));
// Use return value from previous command
const swapOutput = vault.locked_borrow(amount); // Returns amount borrowed
planner.add(asset.approve(spender, swapOutput)); // Use returned value
// Access callback data
const minPayment = weiroll.State.fromSlot(0); // First variable in state array
// Monitor gas usage
const gasEstimate = await vault.estimateGas.unlock(script);
if (gasEstimate > 8_000_000) { // Block gas limit on some chains
console.warn('Script may exceed gas limit, consider splitting');
}