Skip to content

Tag every transfer correctly. You still will not have the books.

This is the transaction from the home page, in full. The card there shows the eight transfers your ledger sees; here is what they hide.

The transaction: 0xf732d5cc…9ff4b2e9e Ethereum, block 25515871. Aave v3 and Euler v2, through a solver. Public, not ours, not a customer’s. Open it in another tab and check me as you go.

It emitted 36 logs. Fourteen move a token, across 17 addresses, and most of that is noise: solver plumbing, routing hops, other people’s business sharing the block. Discard everything the wallet is not a party to and eight transfers remain.

Eight is a number a person can hold, so this is not a story about volume. The wallet did one thing, once. The trouble starts after the noise is gone.

Standard crypto ledgers key on the address, finds the 8 related transfers, and tabulates them with direction, token, amount, and an address on the other end. The cash cancels: USDC in equals USDC out, RLUSD in equals RLUSD out, to the wei. Now the ball’s in your court, how do you tag them?

Related transfers

The wallet is a party to every transfer below, so a ledger keyed on the address records them all. It is not wrong about any of them.

fromtotokenamount
0x0000…00000xb721…fa38variableDebtEthUSDC780,208.473253
0x09bd…faba0xb721…fa38USDC778,238.290021
0xb721…fa380x9bD5…2CbbUSDC186,534.550447
0xaF53…9BB20xb721…fa38RLUSD204,524.934814662449203000
0xb721…fa380x9bD5…2CbbUSDC591,703.739574
0xaF53…9BB20xb721…fa38RLUSD526,273.020496461240292674
0xb721…fa380x09bd…fabaRLUSD730,797.955311123689495674
0xb721…fa380x0000…0000variableDebtEthRLUSD730,794.568432444353918468

Each row lands in a queue with no account attached, and someone has to pick one. That is not a knock on anybody’s product; it is what the data model requires, because a transfer carries an amount and a counterparty and nothing else.

So your controller looks up an address, finds a Euler USDC vault, and writes a rule: USDC out to that address is a Euler deposit. Now look at two of the rows: same direction, token, counterparty, contract, block. One is a debt repayment; the other is a collateral deposit, on a different account. Opposite sides of the balance sheet, and nothing in either row tells them apart. Whatever rule the controller writes is wrong on one of them, and no one finds out, because there is nothing to check it against. (Two more rows are the same trap reversed: identical RLUSD inflows, one a withdrawal, one a borrow, an asset and a liability in the same row.)

So tagging here is not tedious. It is impossible without digging into the internals of both the Euler and Aave protocols, and understanding how they work.

So we dig in, so you do not have to. The account a transfer cannot give you is recoverable, but only at the source: we read each protocol at the smart-contract level, work out what every transaction actually did, and encode it as a parser. The journal below is generated programmatically, not tagged by hand. It is the output, the golden our parser is regression-tested against. Six actions, twelve splits.

What we book

Twelve entries, from six things that actually happened. Four of them land on two addresses the wallet-keyed ledger never looked at.

typeprotocolpositionsymbolamount
aaveBorrow
liabilityAave V3USDC-778,238.290021
assetUSDC778,238.290021
eulerRepay(a3d)
assetUSDC-186,534.550447
liabilityEuler0xb721…FA3DUSDC186,534.550447
eulerWithdraw(a3d)
assetEuler0xb721…FA3DRLUSD-204,524.934814662449203000
assetRLUSD204,524.934814662449203000
eulerDeposit(a3e)
assetUSDC-591,703.739574
assetEuler0xb721…FA3EUSDC591,703.739574
eulerBorrow(a3e)
liabilityEuler0xb721…FA3ERLUSD-526,273.020496461240292674
assetRLUSD526,273.020496461240292674
aaveRepay
assetRLUSD-730,797.955311123689495674
liabilityAave V3RLUSD730,797.955311123689495674

cashassetliability

Sort the twelve by account. Six are the wallet’s own cash, one per currency, and they net to zero. Those are the six your ledger already had. The other six are positions, and not one is a transfer, so not one was in the table above. Four sit on Euler sub-accounts your tool has never heard of.

The amounts were never the hard part; every figure here is already in the transfer table at the top. What a transfer does not carry is which account owes what. The amounts survive the trip. The accounts do not.

The journal is a book built forward from the transactions, and on its own it is still a claim. So we check it the other way: read each account’s balance straight from the chain, and reconcile the two, account by account. A book built from transfers has nothing to reconcile against; a chain balance has nothing to reconcile with. Holding both ends is the whole thing.

Account values are checked against the chain.

bookon chaindifference
Aave V3USDC-778,238.290021-778,238.33021-0.040189
walletUSDC000
EulerUSDC0xb721…FA3D186,534.550447186,534.167881-0.382566
EulerRLUSD0xb721…FA3D-204,524.934814662449203-204,524.808382350.126432312449203
walletRLUSD000
EulerUSDC0xb721…FA3E591,703.739574591,704.0723390.332765
EulerRLUSD0xb721…FA3E-526,273.020496461240292674-526,273.1553649286-0.134868467359707326
Aave V3RLUSD730,797.955311123689495674730,797.940324909-0.014986214689495674

They meet, account by account. The two cash accounts net to exactly zero. The six positions do not, and that is the point: each carries a block of accrued interest, a number no transfer moved, because interest emits no event. The reconciliation is what surfaces it, named rather than buried. We take reconciliation apart on its own page.

More of the accounting that breaks other tools: