Repository navigation
Conversation
Eleven op-codes Hipo's treasury and loan contracts use every round were missing from hipo_finance.xml, so they reach callers as bare hex: 0x574a297b, 0x6a31d344, 0x071d07cc, 0x2f0b5b3b, 0x23274435, 0x4f173d3e, 0x089cd4d0, 0x407cb243, 0xcd0f2116, 0x8b556813 and 0x5e2d81f4. A round-end trace is mostly made of them. Three are declared as ext_in rather than internal. treasury.fc routes participate_in_election, vset_changed and finish_participation through route_external_message, which accepts those three and nothing else; no internal handler takes them. finish_participation carries a uint32 query_id where every other op in this file uses uint64. Declared as uint64 it reads 32 bits of query id and 32 bits of round_since as one number, and the round the message is about is gone. Also fixes borrower_reward_share in request_loan, which has been wrong since this file was added. The contract widened it from 8 bits out of 255 to 16 out of 65535 (contracts/schema.tlb, and store_uint(.., 16) in treasury.fc). Declared as uint8 it still decodes, because the field is followed by a reference rather than more bits - it just yields the high byte. One borrower's share of 1799 has been reported as 7 in every request_loan on mainnet. Tests decode one real mainnet message per op, and pin the two field widths that are easy to get wrong and impossible to notice: with the old uint8 the new test reports "borrower_reward_share = 7, want 1799". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
On the red
Happy to split the |
Eleven op-codes were listed and ten were counted. The tongo half is now open as tonkeeper/tongo#504, with the per-op decode tests the plan asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I declared it uint32 on the strength of contracts/schema.tlb, which says uint32. The contract does not: treasury.fc reads load_uint(64), then load_uint(32), then end_parse(). schema.tlb is the file that is wrong, and I should have checked the FunC rather than the documentation of it. The message is 128 bits. Read as uint64/uint32 it yields query_id 1788202855 and round_since 1788104456 with nothing left over. Read as uint32/uint32 it yields query_id 0 and round_since 1788202855 - the low half of the query id, presented as the round - and leaves 32 bits unread. Both are plausible timestamps, so nothing about the output looks wrong. Every case now also asserts the decode consumed the whole body, which is the only thing that distinguishes the two. That check is the point of the test rather than a detail: a field declared too narrow decodes fine and reports a wrong number, and only the leftover bits give it away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
borrower_reward_share widened from 8 bits to 16 in Hipo's 2026-09-05 release, so a request sent before it no longer matches the current declaration at all - it underflows and falls back to raw hex. Explorers reclassify history, so that would quietly un-decode every loan request the protocol has ever had up to that date. Declaring the old layout alongside restores them. The two cannot be confused: multipleMsgsDecoder prefers whichever reading consumes the body exactly, and each layout does that only for its own era - the old body underflows the uint16 reading, and the new body leaves 8 bits over in the uint8 one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The older layout was pinned with a body built in the test, because the corpus I had to hand only went back as far as the release that changed it. Querying toncenter by op-code turned up a real one, sent on 2026-09-04, the day before. It carries a reward share of 8 on the old scale out of 255 - the same bid the contract's migration note uses as its example of what becomes 2056 out of 65535. No constructed cells remain in this file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hipo's treasury upgraded on 2026-09-21 and borrower_reward_share left the message: the treasury now sets one share for every loan in a round and refuses a request that names its own, throwing at end_parse. Requests from tomorrow's round onward carry the shorter body, so without this they decode as raw hex. The unversioned name keeps meaning the layout in force, as it did when the 8-bit era became _v1, so the era that named a 16-bit share is now _v2. All three are distinguished the same way as before, by which reading consumes the body exactly, and the decoder already prefers a complete read. The body for the new layout is built by this package from the real one above it rather than taken from a block: the upgrade landed at 14:19:35 UTC, after the last request of the open round, so no message in this shape exists on chain yet. It should be replaced with a real one once it does.
|
Pushed The unversioned name keeps meaning the layout in force, which is what it meant when the 8-bit era became One thing to know about the test: the body for the new layout is built by the package from the real 16-bit message above it, rather than taken from a block. The upgrade landed after the last request of the open round, so no message in this shape exists on chain yet — the first will be in the next round. I'll swap in a real one once it does; flag it if you would rather the commit wait for that. No rush on review, but the PR has been open since the 18th, so a ping in case it slipped off the list. The |
Hipo's next treasury release makes max_stake, a coins field, a required part of request_loan, right after min_payment: the most the loan stakes in total, and 0 for no cap. Once it lands, every request carries it, so without this they would decode as raw hex. The unversioned name keeps meaning the newest layout, as before, so the one that names nothing after min_payment is now _v3. The four are still told apart by which reading consumes the body exactly: max_stake is 4 + 8n bits, never the 0, 8 or 16 bits the older layouts leave, so no body reads completely under two of them. The _v3 test message is now a real one (sent 2026-09-26), replacing the one this package built before any existed. The two max_stake bodies are that message with the field added, built with @ton/core: the release is not deployed yet. They should be replaced with real ones once it is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hipo's stake-cap release is deployed (2026-09-26 08:49 UTC), and requests carrying max_stake are now on chain. The capped fixture is one of them, sent 09:39 UTC; the one with max_stake 0 stays built by hand until a request with no cap is sent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A request with max_stake 0 has now been sent (2026-09-26 14:37 UTC, round 1790463752), so the last hand-built fixture is replaced by it. Every request_loan fixture is now a real message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
send_unstake_all, proxy_unstake_all and unstake_all carry the unstake-all flow from the treasury through the parent to the holder's wallet; withdraw_surplus is one op and layout in the wallet, parent, treasury and librarian. Each is checked against the FunC, and the real ones against every mainnet message carrying them (6 withdraw_surplus, 4 of each unstake-all op, all consumed exactly). send_unstake_all has never been sent, so its test builds one. top_up, withdraw_jettons and the quote ops stay undeclared: declarations decode globally, and most mainnet messages with those op codes go to contracts that are not Hipo's. The round-lifecycle comment now says what treasury.fc does (the three ops are also routed internally, though only ever sent as externals), and take_borrower_fee moves under the burner it is sent to.
…youts hipo_finance_request_loan shipped in v1.23.4 with the uint8 share, and this branch had repurposed the name for the newest layout. Following the precedent in bidask.xml, the released name keeps its body and the later layouts are added as _v2 (uint16 share), _v3 (no share) and _v4 (max_stake), so the change is purely additive for API consumers. Comments are cut down to the style of the other schemas.
Follow-up to #499, which covered Hipo's staking messages. This adds the loan and round-lifecycle ops, the later
request_loanlayouts, and the unstake-all chain. It is additive: nothing released in #499 changes.Ops added
participate_in_election,vset_changedandfinish_participation(ext_in, which is how they are sent),decide_loan_requests,process_loan_requests,recover_stakes,proxy_new_stake,proxy_recover_stake,request_rejected,take_profit,take_borrower_fee. A round-end trace is mostly made of these, and today it reads as unnamed op codes.send_unstake_all,proxy_unstake_all,unstake_all.withdraw_surplus: one op and layout in the wallet, parent, treasury and librarian.request_loanlayoutsFollowing the
bidask_swap_fallback/_v2precedent, the released name keeps its body and later layouts are versioned:min_paymenthipo_finance_request_loanborrower_reward_share:uint8hipo_finance_request_loan_v2borrower_reward_share:uint16hipo_finance_request_loan_v3hipo_finance_request_loan_v4max_stake:(VarUInteger 16)Each layout consumes the body exactly only for its own era, so
multipleMsgsDecoderpicks the right one. Before this, every request since 2026-09-05 decoded the share as its high byte (1799 read as 7) or fell back to raw hex.Left out on purpose
top_up,withdraw_jettonsand the quote ops: most mainnet messages with their op codes belong to other protocols (97 of 105top_up, all 154withdraw_jettons), so declaring them would mislabel that traffic.Tests
abi/hipo_finance_test.godecodes a real mainnet message for every op (exceptsend_unstake_all, never sent, so built), and asserts the decode read the whole body. That check is what catches a wrong field width, which otherwise decodes to a plausible number. Layouts were checked against the FunC sources.go-generateandabipass. Theunit-testsfailures are outsideabi:TestConvertBlockchainConfigfails the same way on master (run 34974885155),TestGeneratedMethod3fails on master locally too, andTestParseAddressis liteserver flakiness.