Token, NFT and smart contract flows · Lesson 5 of 6
Lending, bridges and protocol accounting
8 min read
Lending protocols and bridges hold the largest pools of value in decentralized finance, and they fail in the largest ways. For an analyst they raise two kinds of question: what did a given address do (deposit, borrow, get liquidated, borrow for a single transaction, move funds to another chain), and does the protocol's accounting still add up. Both are answered from events and state reads, as long as you know which address is acting on whose behalf.
The lending event model
Most lending markets follow the same pattern: users supply an asset and receive an interest-bearing claim, borrow against that collateral, repay, withdraw, and are liquidated if their collateral falls below a threshold. Aave v3 is a good reference because its pool contract emits one event per action:
| Event | Key fields | Analyst note |
|---|---|---|
Supply | reserve, user, onBehalfOf, amount | user acted; onBehalfOf receives the position |
Withdraw | reserve, user, to, amount | to can differ from the position owner |
Borrow | reserve, user, onBehalfOf, amount, interestRateMode, borrowRate | borrowing can be delegated to another address |
Repay | reserve, user, repayer, amount, useATokens | anyone can repay someone else's debt |
LiquidationCall | collateralAsset, debtAsset, user, debtToCover, liquidatedCollateralAmount, liquidator, receiveAToken | user is the borrower being liquidated |
The split between who acted and whose position changed is the main trap. A deposit made by a router or a smart account on behalf of a user shows the router as user and the real owner in onBehalfOf. Repayments by third parties are a useful link: an address that repays another address's debt usually has a relationship with it.
Supply positions are represented by tokens (aTokens in Aave, cTokens in Compound-style markets). Aave's aToken balances grow continuously as interest accrues, without a Transfer event for each increase, so they behave like the rebasing tokens described in the lesson Reading token transfers: read balanceOf at a block rather than summing transfers. Debt tokens are usually non-transferable, and the borrower's debt also grows between events.
A decoded liquidation:
{
"event": "LiquidationCall",
"collateralAsset": "0x4d2e…77c1",
"debtAsset": "0x7d1f…c3a0",
"user": "0xd3b7…05fa",
"debtToCover": "10000000000",
"liquidatedCollateralAmount": "3500000000000000000",
"liquidator": "0x6c9a…e810",
"receiveAToken": false
}
With the debt asset a 6-decimal stablecoin and the collateral WETH at 3,000, the liquidator repaid 10,000 of the borrower's debt and received 3.5 WETH worth 10,500: a 5% liquidation bonus, the borrower's loss and the liquidator's gross profit before gas and swap costs. Liquidators are usually bots and often use flash loans to fund the repayment. To study market stress, group liquidation events by block and collateral asset: clusters line up with sharp moves in the price oracle the protocol uses, and a run of liquidations of the same borrower across several blocks shows a position being unwound in pieces, each one restoring health only briefly.
Flash loans in traces
A flash loan lends any amount with no collateral, on one condition: it must be repaid, plus a small premium, before the transaction ends, or the whole transaction reverts. Flash loans are legitimate tools for liquidations, collateral swaps and arbitrage, and they are also the standard way attackers obtain temporary capital. In logs they are easy to miss; in the trace they have a fixed shape:
CALL Bot.run()
CALL LendingPool.flashLoanSimple(receiver=Bot, asset=USDC, amount=1,000,000)
CALL USDC.transfer(Bot, 1,000,000) -- funds leave the reserve
CALL Bot.executeOperation(USDC, 1,000,000, premium, initiator, params)
CALL DEX / lending / governance contracts ... -- the actual strategy
CALL USDC.approve(LendingPool, 1,000,000 + premium)
CALL USDC.transferFrom(Bot, reserve, 1,000,000 + premium)
LOG FlashLoan(target=Bot, asset=USDC, amount=1,000,000, premium)
The pool sends the funds, calls back into the receiver's executeOperation, lets it do anything, then pulls back principal plus premium. Everything between the callback and the repayment is the strategy you need to analyze. Uniswap-style flash swaps and other vaults follow the same send, callback, check pattern with their own function names.
When reporting, net the flash loan out: the principal enters and leaves in the same transaction and is not the actor's money. The Beanstalk governance attack of April 17, 2022 shows why the distinction matters: the attacker borrowed about $1 billion through flash loans from several protocols, used the resulting voting power to pass and execute a malicious proposal in the same transaction through an emergency function, and left with about 24,830 ETH (roughly $76 million) from a loss of about $181 million. The capital was borrowed; the profit was real.
Bridge lock-and-mint flows
The lesson Layer 2s, sidechains and bridges as data sources introduces bridge deposit and withdrawal events, and the lesson Cross-chain tracing covers following funds across them. Here the focus is the accounting. In a lock-and-mint bridge:
- The user deposits tokens into a bridge escrow contract on the source chain, which emits a deposit event with the destination chain, recipient and a nonce.
- A verification layer (a validator set, a light client, an optimistic challenge window or a zero-knowledge proof) attests that the deposit happened.
- A minter on the destination chain issues the same amount of a wrapped token, a Transfer from the zero address to the recipient.
Withdrawals reverse it: burn on the destination, release from escrow on the source. Event names differ between bridges; an illustrative pair matched by nonce:
{
"source": {"chain_id": 1, "event": "Deposit", "escrow": "0x2b9e…a0c4",
"token": "0x7d1f…c3a0", "recipient": "0xa1b2…9f0e",
"amount": "250000000000", "dest_chain_id": 42161, "nonce": 88231},
"destination": {"chain_id": 42161, "event": "Transfer", "token": "0x51c8…9d02",
"from": "0x0000000000000000000000000000000000000000",
"to": "0xa1b2…9f0e", "value": "250000000000", "nonce": 88231}
}
Both legs show 250,000,000,000 base units, which is 250,000 tokens at 6 decimals. Check the decimals on both sides: wrapped versions do not always keep the original's decimals. Other bridge models avoid wrapped tokens altogether: liquidity networks pay out from pools on the destination, and some issuers burn and mint their own native token on each chain. Each model leaves a different reconciliation trail.
Reconciling protocol balances
The core invariant of a lock-and-mint bridge is simple: wrapped supply on the destination should never exceed collateral held in escrow on the source, apart from deposits in transit. The same discipline applies to lending: the supply tokens issued should match the underlying held by the protocol plus the outstanding debt.
-- Cumulative escrowed collateral vs wrapped supply, one token, per day
SELECT day,
SUM(net_locked_units) OVER (ORDER BY day) AS cum_locked,
SUM(net_minted_units) OVER (ORDER BY day) AS cum_minted,
SUM(net_locked_units - net_minted_units) OVER (ORDER BY day) AS backing_gap
FROM daily_bridge_flows -- built from escrow transfers and zero-address mints/burns
WHERE bridge = 'example-bridge' AND token_symbol = 'USDC'
ORDER BY day;
daily_bridge_flows holds, per day, net transfers into the escrow on the source chain and mints minus burns of the wrapped token on the destination, both already converted to whole tokens. Window sums turn them into running totals. A small positive gap is normal while deposits are in transit; a negative gap means unbacked tokens exist. Better still, compare state directly: the escrow's balanceOf against the wrapped token's totalSupply() at matched times.
Public incidents show what breaks:
| Case | What happened | How it shows in reconciliation |
|---|---|---|
| Wormhole, February 2, 2022 | A signature-verification flaw let the attacker mint 120,000 wETH on Solana with no deposit; 93,750 were bridged back and redeemed for ETH on Ethereum | Wrapped supply jumps with no matching escrow inflow, then escrow drains; the parent company later replaced the ETH |
| Ronin, March 2022 | Attackers controlled five of nine validator keys and signed withdrawals of about 173,600 ETH and 25.5 million USDC | Escrow falls with no matching burns; it went unnoticed for six days |
| Nomad, August 1, 2022 | An upgrade's initialization marked a zero value as a trusted root, so unproven messages passed; about $190 million taken by over 300 addresses copying the first exploit | Hundreds of releases with no matching deposits on the source chain |
The Ronin delay is the strongest argument for running such checks continuously: a monitor comparing escrow balances with burns would have flagged the withdrawals within blocks. For lending protocols, the parallel check compares total supply tokens with the underlying the protocol holds plus total debt, and tracks utilization (borrows divided by supply), which spikes when a market is being drained or a bank run begins.
Exam focus: supply, borrow, repay, withdraw and liquidation events and the difference between the acting address and onBehalfOf; why aToken-style balances must be read from state; computing a liquidation bonus from a decoded LiquidationCall; the trace shape of a flash loan (send, executeOperation callback, repay with premium) and netting principal out of reports; the Beanstalk flash-loan governance attack; lock-and-mint and burn-and-release flows, matching legs by nonce and checking decimals; the reconciliation invariant (wrapped supply ≤ escrowed collateral) and how the Wormhole, Ronin and Nomad incidents would appear in it.
Practice questions
Check your understanding. These questions do not count towards the exam.
Sign in to save your progress and continue where you left off. It is free. Sign in