Skip to content

bath: show Hipo's loans and validation rounds, and decode its logs - #988

Open
behrang wants to merge 6 commits into
tonkeeper:masterfrom
behrang:hipo-round-logs
Open

behrang wants to merge 6 commits into
tonkeeper:masterfrom
behrang:hipo-round-logs

Conversation

@behrang

@behrang behrang commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Hipo's staking now reads well on tonviewer (#939, #971). Its other half, the loans it makes to validators and the validation rounds they run in, still shows as raw contract calls, and the three external messages that drive a round show as "Something happened but we don't understand what". A round-end trace is up to a hundred transactions, most of them SmartContractExec rows.

This adds four generic action types, each with implementation: PoolImplementationType like DepositStake, so any lending pool could emit them:

Action When Carries
LoanRequest a validator asks a pool for a loan borrower, pool, round, loan, min_payment, GRAM attached
LoanIssue the pool lends and the loan is staked borrower, pool, loan contract, round, loan, accrued, collateral, min_payment
LoanRepayment the elector returns the loan and the pool splits the reward the above, plus recovered, stakers' reward, protocol fee, borrower payout, burn, shortfall
PoolRoundTransition a round moves on: election, began, validated, ended, settled pool, round; settled also has the round's total staked and recovered

What stays as it was: the elector legs stay ElectionsDepositStake and ElectionsRecoverStake, and every staker-facing action (DepositStake, WithdrawStake, WithdrawStakeRequest, JettonMint, JettonBurn) is unchanged. The new straws only fold in the loan and round legs around them. One staking addition: an unstake-all request (send_unstake_all, or the comment w) is now a WithdrawStakeRequest with no amount, rather than a bare transfer and two contract calls.

Where the figures come from. The treasury's messages do not carry them: loan_result, take_borrower_fee and take_profit carry only a query id. They are in external-out logs, whose topic is the 256-bit external destination and whose body has no op-code, so abi reads the round number as an op-code (a different one every round). pkg/core/hipo_logs.go reads them by topic, and ConvertMessage now decodes them, only when the treasury wrote them, so the trace view shows hipo_finance_log_repayment and its fields instead of a bogus op-code. The burner that receives the borrower fee logs the same way and is decoded too. Two treasury logs changed layout on 2026-09-05; the layout is chosen by when the log was written, since some old bodies also fit the new layout exactly. A log loan is paired with its proxy_new_stake by logical time, since it is written right after it; for that, BubbleTx now keeps its incoming message's lt and its sender's external-out messages.

Anchoring. Each new action names a borrower and amounts, so each is anchored on something only the treasury produces: a log is read only from the treasury's own transaction, a loan only from a message the treasury sent, and a round only from the treasury's own transactions. TestHipoLoanStrawsRejectForgeries covers the shapes an impostor could build.

One generic fix. Straw.Merge added the head bubble's accounts and transactions twice, so a straw that folds one child per pass into an action it built before doubled them each pass, and ran out of memory on a round that burns sixteen bills. They are now added once; accounts are only searched and transactions are compacted before use, so no other output changes.

Checked against mainnet. 1,327 real traces (every loan and round trace on the treasury since June, and the earlier staking corpus) were replayed through bath on this branch and on master and diffed: value flows are identical in every trace, the 421 traces without a new action are byte-identical, and the staker-facing action counts are unchanged. What changed is that the loan and round contract calls and transfers became the actions above. Every treasury and burner log in them (1,206) decodes. New goldens: hipo-loan-request, hipo-round-election, hipo-round-began, hipo-round-validated, hipo-round-end.

The message flows and the anchoring rules are documented at https://github.com/HipoFinance/contract/blob/main/docs/integration.md#explorer-actions. Happy to rename the types or narrow them if you would rather shape them differently.

🤖 Generated with Claude Code

behrang and others added 6 commits October 16, 2026 07:41
The treasury reports what a round did in external-out messages, and they carry what its
messages do not: loan_result, take_borrower_fee and take_profit carry only a query id, so how
a loan was repaid and split exists only in log::repayment. emit_log puts the topic in the
destination, a 256-bit external address, and starts the body at round_since with no op-code,
so abi read round_since as an op-code and reported a different one every round.

ConvertMessage now decodes them by topic, and only when the treasury wrote them, so the trace
view shows hipo_finance_log_repayment and its fields. Two of the logs changed layout with
Hipo's 2026-09-05 release. The layout is chosen by when the log was written, because an old
body can fit the new layout exactly: 8 of 542 real logs do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge started the new bubble's accounts and transactions from the head and then added the
head again with the rest of the mapping, so they doubled each time a straw extended an action
it had built before. A straw that folds one child per pass into an action doubled them once
per child, and ran out of memory on a round that burns sixteen bills. They now start empty
and the head is added once, after its builder, so an account a builder credits still lands.

Accounts are only ever searched and transactions are sorted and compacted before use, so no
action changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hipo's staking reads well since tonkeeper#939 and tonkeeper#971, but the loans it makes to validators, and the
rounds they run in, showed as raw contract calls, and the three external messages that drive
a round as nothing at all. This adds four generic action types, each with an
implementation like DepositStake, so any lending pool could emit them:

- LoanRequest: a validator asks the pool for a loan.
- LoanIssue: the pool lends, and the loan contract stakes it.
- LoanRepayment: the loan comes back and its reward is split between the stakers, the
  operator, the burner and the borrower; loan_result, take_borrower_fee and take_profit fold
  into it.
- PoolRoundTransition: election, began, validated, ended, settled. vset_changed arrives
  twice, for the round that began and for the one before it, whose stakes are now held.

The figures come from the treasury's logs. A loan's log is written one logical time after its
proxy_new_stake, so BubbleTx now keeps its incoming message's lt and its sender's
external-out messages. Each action is anchored on the treasury's own transaction or its own
logs, because these ops are public and the actions name a borrower. The elector legs stay
ElectionsDepositStake and ElectionsRecoverStake, and the staker-facing actions are unchanged:
over 1,327 real traces the value flows match master, and traces without a loan or round
action are identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
send_unstake_all, or the text comment "w" that wallets without custom payloads send, asks
the treasury to unstake a whole balance. It showed as a bare transfer followed by
proxy_unstake_all and unstake_all as contract calls. It is now a WithdrawStakeRequest with no
amount, which the API already shows as ALL; the burn and the payout below it stay their own
actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The burner that receives the borrower fee (take_borrower_fee) logs each step the same way,
with topics of its own: what it received, staked, swapped and burned. Its logs appear in every
round-end trace and showed as op-code 0x00000000. They are decoded only when the burner wrote
them. A log whose body starts with 32 zero bits is kept as text in Message.Body, so the logs
are decoded from the body cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- loan and round actions record the transfers they fold in, so extra no
  longer reports a repayment's stake, fees and profit or a round's
  recover/burn/mint legs as unexplained
- a refused loan request counts its bounced refund
- parse the repayment log in the check, so an unfit log does not match
  instead of reporting an error to sentry
- skip loans and repayments whose log names no borrower
- name a settled round by the last repayment log when last_bill_burned
  is missing, and leave a round of unknown start undescribed
- reuse the log ConvertMessage decoded, and only serialize ext-out
  bodies of the treasury and the burner
- add en/ru translations for the loan and round actions
- drop function-level comments and order declarations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants