Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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(...);
  }
}
Execution flow:
  1. User calls lock(data)
  2. Vif calls back lockCallback(data) on caller
  3. Caller performs operations (updating transient deltas)
  4. Caller settles debts and takes credits
  5. Vif verifies all deltas are zero
  6. 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 automatically

Example: 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:

ScenarioWithout Flash AccountingWith Flash AccountingSavings
Simple swap2 transfers (in + out)2 transfers0%
Multi-hop (3 hops)6 transfers2 transfers~67%
Limit order + claim4 transfers2 transfers~50%
Flash loan + arbitrageImpossible or expensive2 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 USDC

This prevents griefing attacks where someone creates tiny credits that are expensive to clear.

Related Concepts