Flash Accounting
Flash accounting is the innovative mechanism that allows Vif to batch multiple operations without transferring tokens until final settlement. This system uses transient storage (EIP-1153) to track per-token debts and credits globally.
How It Works
Instead of transferring tokens on every operation, Vif maintains running balances in transient storage:
// Transient storage (cleared after transaction)
mapping(address token => int256 delta) deltas;- Positive delta = credit (protocol owes tokens to user)
- Negative delta = debt (user owes tokens to protocol)
- Zero delta = balanced
The Lock Mechanism
All operations happen within a lock call:
contract MyRouter is ILockCallback {
function myAction() external {
// 1. Encode action data
bytes memory data = abi.encode(...);
// 2. Call lock - Vif will callback lockCallback()
bytes memory result = vif.lock(data);
// 3. Decode result
return abi.decode(result, ...);
}
function lockCallback(bytes calldata data) external returns (bytes memory) {
require(msg.sender == address(vif), "Only Vif can callback");
// 4. Perform operations (they update deltas)
vif.consume(...); // Market order
vif.make(...); // Limit order
// 5. Settle all debts and take all credits
_settle(token1);
_settle(token2);
_take(token3);
// 6. Return result data
return abi.encode(...);
}
}- User calls
lock(data) - Vif calls back
lockCallback(data)on caller - Caller performs operations (updating transient deltas)
- Caller settles debts and takes credits
- Vif verifies all deltas are zero
- If balanced: success; if not: revert entire transaction
Operations That Update Deltas
Operations That Only Modify Deltas
These operations don't transfer tokens - they only update transient storage:
// Market orders
vif.consume(taker, market, maxTick, fillVolume, fillWants, maxOffers);
// → Increases debt in inbound token (user gives)
// → Increases credit in outbound token (user receives)
// → May increase credit in native token (bounties)
// Limit orders
vif.make(maker, market, offerId, gives, tick, expiry, provision);
// → Increases debt in outbound token (maker provides)
// → May increase debt in native token (provision)
// → May increase credit in inbound token (claiming previous fills)
// Cancel orders
vif.cancel(market, offerId);
// → Increases credit in outbound token (remaining offer amount)
// → Increases credit in inbound token (filled amount)
// → Increases credit in native token (provision returned)
// Claim orders
vif.claim(market, offerId);
// → Increases credit in inbound token (filled amount)
// → If expired: returns outbound + provision as credits
// Clean expired offers
vif.clean(market, offerId);
// → Increases credit in native token (bounty)Operations That Transfer Tokens
These operations transfer tokens AND modify deltas:
// Settle debt (pull tokens from user)
vif.settle(token, amount, from);
// → Pulls `amount` tokens from `from` address
// → Decreases debt (increases delta by +amount)
// For native token:
vif.settle{value: amount}(address(0), amount, from);
// → Uses msg.value
// → Decreases debt by +amount
// Take credit (push tokens to user)
vif.take(token, amount, receiver);
// → Sends `amount` tokens to `receiver`
// → Decreases credit (decreases delta by -amount)
// Clear dust credit
vif.clear(token, amount);
// → Donates `amount` to protocol fees
// → Decreases credit (decreases delta by -amount)Example: Simple Swap
Here's how a swap works with flash accounting:
// User wants: 1 WETH for USDC (market order)
// Initial state: delta[WETH] = 0, delta[USDC] = 0
// 1. Execute market order
vif.consume(user, market, maxTick, 1 ether, true, 100);
// After: delta[WETH] = +1e18 (credit)
// delta[USDC] = -2000e6 (debt, user owes 2000 USDC)
// 2. Settle USDC debt
USDC.transferFrom(user, address(vif), 2000e6); // Actually done by vif.settle()
vif.settle(USDC, 2000e6, user);
// After: delta[WETH] = +1e18 (credit)
// delta[USDC] = 0 (balanced)
// 3. Take WETH credit
vif.take(WETH, 1 ether, user);
WETH.transfer(user, 1 ether); // Actually done by vif.take()
// After: delta[WETH] = 0 (balanced)
// delta[USDC] = 0 (balanced)
// Lock ends successfully - all deltas are zero ✓Example: Multi-Hop Swap
Flash accounting shines with multi-hop trades:
// User wants: WETH → USDC → DAI
// Initial state: all deltas = 0
// 1. Swap WETH for USDC
vif.consume(user, wethUsdcMarket, maxTick, 1 ether, true, 100);
// After: delta[WETH] = -1e18 (debt)
// delta[USDC] = +2000e6 (credit)
// 2. Swap USDC for DAI
vif.consume(user, usdcDaiMarket, maxTick, 2000e6, false, 100);
// After: delta[WETH] = -1e18 (debt)
// delta[USDC] = 0 (balanced! Credit netted with debt)
// delta[DAI] = +2000e18 (credit)
// 3. Settle and take
vif.settle(WETH, 1 ether, user); // Pay WETH debt
vif.take(DAI, 2000e18, user); // Receive DAI credit
// Notice: USDC was never transferred! ✓
// The protocol netted the intermediate token automaticallyExample: Flash Loan
Free flash loans are a side effect of the accounting system:
function flashLoan() external {
bytes memory data = ...;
vif.lock(data);
}
function lockCallback(bytes calldata data) external returns (bytes memory) {
// 1. Take a "loan" (create credit)
vif.take(WETH, 100 ether, address(this));
// After: delta[WETH] = -100e18 (debt)
// 2. Use the tokens
_doArbitrage(100 ether); // Make profit
// 3. Pay back (settle debt)
vif.settle(WETH, 100 ether, address(this));
// After: delta[WETH] = 0 (balanced)
return "";
}
// Lock ends successfully - no cost flash loan! ✓Example: Limit Order with Claim
Combining operations efficiently:
// Maker has an active offer that has been partially filled
// They want to claim the filled amount and update the offer
function updateOffer() external {
vif.lock(abi.encode(...));
}
function lockCallback(bytes calldata data) external returns (bytes memory) {
// 1. Create new offer (also claims old one)
(uint40 newOfferId, uint256 claimed) = vif.make(
maker,
market,
oldOfferId, // Edit existing offer
2 ether, // New amount
tick,
expiry,
provision
);
// After: delta[WETH] = -2e18 (debt, new offer amount)
// delta[USDC] = +claimed (credit, claimed from old fills)
// 2. Settle outbound debt with new tokens
vif.settle(WETH, 2 ether, maker);
// After: delta[WETH] = 0
// delta[USDC] = +claimed
// 3. Take inbound credit
vif.take(USDC, claimed, maker);
// After: delta[WETH] = 0
// delta[USDC] = 0
return abi.encode(newOfferId, claimed);
}
// In one transaction: claimed proceeds + updated offer ✓Balance Verification
At the end of every lock, Vif verifies:
for each token {
require(delta[token] == 0, "Unbalanced");
}If any token has a non-zero delta:
- The entire transaction reverts
- No state changes persist
- No tokens are transferred
This guarantees:
- Atomic operations - all or nothing
- No stuck funds - users can't accidentally leave credits
- Debt protection - users can't leave without settling
Gas Savings
Flash accounting provides substantial gas savings:
| Scenario | Without Flash Accounting | With Flash Accounting | Savings |
|---|---|---|---|
| Simple swap | 2 transfers (in + out) | 2 transfers | 0% |
| Multi-hop (3 hops) | 6 transfers | 2 transfers | ~67% |
| Limit order + claim | 4 transfers | 2 transfers | ~50% |
| Flash loan + arbitrage | Impossible or expensive | 2 transfers | ∞ |
Each avoided transfer saves ~21,000 gas (for ERC20) or ~30,000 gas (for native token).
Security Considerations
Reentrancy Protection
The lock mechanism provides implicit reentrancy protection:
- Only one lock can be active at a time
- Nested locks revert
- All state is verified before unlock
Native Token Handling
Native token (ETH/MATIC/etc.) uses address(0) as identifier:
// Settle native debt
vif.settle{value: 1 ether}(address(0), 1 ether, user);
// Take native credit
vif.take(address(0), 0.5 ether, user);Dust Credits
For very small credits that cost more gas to claim than they're worth:
// Clear dust (donates to protocol)
vif.clear(USDC, 1); // Clear 1 wei of USDCThis prevents griefing attacks where someone creates tiny credits that are expensive to clear.
Related Concepts
- Overview - Protocol introduction
- Creating Swaps - Practical examples
- Placing Limit Orders - Using flash accounting for making