DEV Community

9xj89gzrtw-hue
9xj89gzrtw-hue

Posted on

How to verify an on-chain bounty ledger down to the wei (open-source Python engine)

A "who earned what" CSV request is the most under-estimated job in on-chain data. It sounds like eth_getLogs plus a groupBy. It is not, and the ways it breaks are specific and repeatable.

I got tired of re-deriving the same five rules on every bounty, so I wrote them into an engine: ledgerkit, MIT, ~1k lines of Python. The design goal was not "produce a CSV". It was produce a CSV and a second computation that can disagree with it.

The pipeline

fetch  ->  build  ->  verify
logs       ledger     reconciliation
Enter fullscreen mode Exit fullscreen mode
pip install eth-abi

# 1. every log the contract ever emitted
python -m ledgerkit fetch --chain arbitrum \
    --address 0xdffe8a4a4103f968ffd61fd082d08c41dcf9b940 \
    --from-block 0 --out logs.json

# 2. fold them into a per-wallet ledger
python -m ledgerkit build --logs logs.json --fee-bps 250 \
    --csv wallets.csv --summary summary.json

# 3. reconcile against an independent recount
python -m ledgerkit verify --logs logs.json --fee-bps 250 \
    --expect-earned 3.726923002 --expect-paid 3.63374992695
Enter fullscreen mode Exit fullscreen mode

The middle step writes a file, so the aggregation can be audited without touching the network.

The five rules that make the numbers right

1. Earned is gross; paid out is net. The fee is read from the settlement event, never recomputed from a percentage. Protocols change their fee rate, and old settlements keep the old one. --fee-bps is a check, and a mismatch is reported rather than silently applied. If a contract took 3% two years ago and 2.5% now, a single --fee-bps 250 pass would silently rewrite history — the engine refuses.

2. Cancelled bounties pay nobody. A settlement event on a cancelled bounty is surfaced as a warning, because it means one of the two events is wrong. Silently trusting settlements is how a CSV ends up paying someone for a bounty that was cancelled an hour before.

3. A claim without a bounty is not guessed. If the creation event falls outside the fetched block range — extremely common with a --from-block set for speed — the claim is counted in claims_accepted but excluded from the money columns, under claims_valued, and reported. Inventing an amount is how a ledger drifts from reality in a way nobody notices until an accountant does.

4. Duplicate (tx, logIndex) pairs are dropped. Retried eth_getLogs pages are the single most common source of doubled totals, and public RPCs retry constantly. Deduplicating on the pair, not on the tx hash, is the only correct key: one transaction legitimately settles several claims.

5. Exclusions are explicit and shared. --exclude-tx 0x… removes known-bad settlements (exploits, test transactions) before aggregation, and the verifier applies the same list — so the builder and the checker cannot disagree about what was excluded.

NFTs get their own rule: ownership is a last-transfer problem, not a sum. Balances are rebuilt from deltas (mint +1, burn −1, transfer −1/+1) and compared per holder.

What verify actually does

Every check is a second computation, not a re-run of the builder:

check method
conservation earned == paid_out + fees, wei-exact
raw_recount_earned / _fees money columns re-derived by slicing raw data words with int(hex, 16), bypassing the ABI decoder entirely
raw_recount_settlements settlement count from raw topics vs the ledger's claims_valued
no_negative_values, paid_out_within_earned per-wallet sanity
no_empty_rows every row carries activity — no phantom wallets
fee_rate_matches total fees against --fee-bps, within rounding
nft_balances ERC-721 balances from transfer deltas, optionally against balanceOf on chain

Real output, against the golden fixture (3,467 logs from a live escrow contract):

ok   expected earned 3.726923002 ETH, got 3.726923002
ok   expected paid 3.63374992695 ETH, got 3.63374992695
ok   conservation: earned=3726923002000000000 paid_out=3633749926950000000 fees=93173075050000000 diff=0
ok   raw_recount_earned: raw=3726923002000000000 ledger=3726923002000000000
ok   raw_recount_fees: raw=93173075050000000 ledger=93173075050000000
ok   raw_recount_settlements: raw=278 ledger=278
ok   no_negative_values: 0 wallet(s) below zero
ok   paid_out_within_earned: 0 wallet(s) paid more than earned
ok   no_empty_rows: 0 wallet(s) carry no activity
ok   fee_rate_matches: expected=93173075050000000 (at 250 bps) actual=93173075050000000
ok   nft_balances: 81 holder(s) from deltas vs 81 in ledger, 722 vs 722 tokens (0 token(s) with unseen history skipped)
Enter fullscreen mode Exit fullscreen mode

Any mismatch exits non-zero, so it drops into CI as-is:

- run: python -m ledgerkit verify --logs logs.json --fee-bps 250 --expect-earned 3.726923002
Enter fullscreen mode Exit fullscreen mode

Why integer arithmetic is not optional

Every money value stays integer wei until formatting. 3.726923002 ETH is never a float, because 3726923002000000000 in a float64 is 3726923002000000000.0 right up until it is not, and the failure shows up as a one-wei disagreement between two reports that should be identical. The verifier compares integers. That is the whole trick.

What the engine will not do for you

It does not know your protocol. Mapping an unfamiliar contract's events — which event is a settlement, which is a cancellation, whether the fee is inside the transfer or taken before it — is the actual work, and it is per-contract. The engine gives you the invariants; the mapping is judgment.

That is also why the honest version of this service is not a SaaS dashboard. It is: read the contract, write the mapping, run the ledger, hand over a reconciliation that survives a second opinion — and say out loud which rows are unvalued and why.

Where to get it

Code, tests and the golden fixture: github.com/9xj89gzrtw-hue/ledgerkit.

If you want the run rather than the tool — a per-wallet ledger for your escrow or bounty contract with the verification report attached — the scope is agreed in a public GitHub issue first, and it is paid in USDC on Base. Details and an example of the same approach applied to a different data problem (411 DefiLlama fee feeds silently reporting zero) are on the store page:

→ 9xj89gzrtw-hue.github.io


Tip jar / custom data bounties

USDC on Base, any amount, same address the store uses:

0x72Aa6edDE71B94f6b99c8e52EdC02B5C75F74d1a

Custom work I take: on-chain ledgers with reconciliation (from $149), non-standard event mappings (from $249), DefiLlama fee/revenue adapters (from $99). Open an issue with the word quote and what you need — public and timestamped, no DM required.

Top comments (0)