Pull issue - #519
Open
ARBOR-L wants to merge 193 commits into
Open
Conversation
Closes the gap where total_supplied and total_shares_outstanding were clamped to zero silently, with no TotalSuppliedDustClampEvent or TotalSharesOutstandingDustClampEvent ever fired. Both events now capture the pre-clamp value before zeroing, per ARCM v3.11.1 Section III.6 / AYIS Section 4.4. withdraw.go was untracked prior to this commit.
Commits the accumulated Arbor lending protocol work that had been building up uncommitted on-device: core plugin scaffold, market/ lender/borrower state accessors, deposit/withdraw/create_market/ update_price/set_asset_tier handlers, interest accrual (AYIS), uint128 encoding helpers, compound interest math, arbor.proto/ arbor_events.proto/arbor_state.proto and generated code, asset tier logic, and the submit_tx.go / rpc test-harness scripts. Also includes small, targeted changes to core Canopy files (plugin.go, go.mod, tx.proto, account.proto, event.proto, plugin.proto) required for Arbor's custom transaction/event types and the currentHeight BeginBlock->DeliverTx tracking fix. This is a checkpoint commit, not a clean history — prior to this, substantial work existed only on-device with no version control. Going forward, changes should be committed incrementally per logical unit of work rather than accumulated.
…instead of market ID (20 bytes)
…_fund RPC query route
Core custody architecture live on-chain: deposit/withdraw/borrow/repay/
liquidate_position move real Account/Pool balances via escrow pools
(pool_id.go, custody_arith.go). Liquidation (liquidate_position.go) wired
to Layer 2 bad-debt draw-down (bad_debt_layer2.go, Layer2DrawDown) per
ARCM Section 9.2 -- all-or-nothing gate against R_fund, covered==false
branch confirmed live on-chain; covered==true branch not yet exercised.
Adds /v1/query/reservefund RPC route (rpc.go) for direct R_fund
visibility, mirroring handleQueryPool's pattern.
Known gaps, disclosed: interest_accrual.go's Insolvent-branch R_fund
routing is a TODO; C4's WillExhaustThisBlock lookahead (ARCM v3.11.1)
is not yet wired, pending Layer 4 (SumLenderBalancesInMarket, {28}
queue) which does not exist yet. See ARBOR_HANDOFF_LAYER2.md.
Root cause (confirmed live on devnet, liq-test-01): applyDebtDelta's
decrement branch (repay/liquidation) clamps market.TotalBorrowed to
zero whenever decrease >= TotalBorrowed, per ARCM v3.10 Section
19.2.1a's spec'd compare-before-subtract logic. This is correct,
intentional behavior -- but it was previously silent: no event,
no log, nothing to distinguish a real drift from a market with
genuinely zero debt. The original finding: a liquidation left
market.TotalBorrowed == 0 while the borrower position's own
debtPrincipal still read 3, no way to tell why after the fact.
Fix mirrors ARCM v3.11.1 Section III.6 / H4's dust-clamp pattern for
total_supplied: applyDebtDelta now returns a named clampedFrom value
carrying the pre-clamp TotalBorrowed whenever the clamp fires, and all
three callers (borrow.go discards it, repay.go and liquidate_position.go
build and emit EventTotalBorrowedDustClamp) surface it as a proper
chain event.
New: EventTotalBorrowedDustClamp{ market_id, source, decrease_amount,
pre_clamp_value }, registered in contract.go's EventTypeUrls.
Verified live: borrowed 10 on liq-test-01 (after refreshing stale eth/
usdc oracle prices and depositing collateral), repaid 14 against a
combined debtPrincipal of 14, confirmed via /v1/query/events-by-height
at height 6852 -- event fired with source="repay", decrease_amount=14,
pre_clamp_value=10, reference=<repay tx hash>. Decoded raw protobuf
bytes to confirm field values independent of the RPC's generic (and
mislabeled) event JSON view.
Merging origin/main (upstream canopy-network sync, go 1.24->1.26 bump) produced a go.mod with two 'go' directives (1.24.0 and 1.26), which go tooling rejects outright (repeated go statement). The active toolchain in this environment is already go1.26.0; set go.mod to match rather than pin back to the now-superseded 1.25.11 toolchain this plugin was previously built against.
…anch Previously, AccrueInterest's Insolvent-status branch (Step 8, AYIS Section 7 J1/K1) computed interestEarned but discarded it entirely -- the TODO comment noted R_fund accessors didn't exist yet, but GetReserveFund/SetReserveFundTry were already implemented and in use by the non-Insolvent path (Step 10). This was a real value leak: for any Insolvent market, every block's interest_earned would vanish rather than routing to R_fund as ARCM Section 9.3(1) requires. Currently unreachable in practice -- no code path can set market.Status = MarketStatus_INSOLVENT yet (Layer 4 / loss-factor socialization is not implemented) -- confirmed via full-codebase grep before and after this change. Fix is structurally verified (compiles clean, mirrors the exact GetReserveFund/SetReserveFundTry/overflow- freeze pattern already verified working at Step 10) but NOT live- verified on a real Insolvent market, since that state is not yet reachable on any chain. Live verification is pending Layer 4.
The endpoint previously returned only the raw BorrowerPosition proto via protojson.Marshal, whose debtPrincipal field is the stored principal as of the position's last write -- not the borrower's current owed debt. ScaledDebt()'s own doc comment (AYIS Section 6, ARCM Section 2.2) explicitly warns pos.DebtPrincipal alone must never be treated as current debt; this endpoint was doing exactly that implicitly, by exposing it under a field name a caller would reasonably assume is authoritative. Found via a live regression check on borrow-test-01: after a partial repay, debtPrincipal read 4001 while totalBorrowed (the aggregate) correctly read 4000 -- a 1-unit gap explained by interest accrued between borrow and repay via B_index growth, not a drift bug. Traced and confirmed via ScaledDebt()'s ceiling-division formula before concluding this. Now batches a second single-key read for the market's B_index alongside the existing position read (matching the QueryId-per-result pattern used in deposit.go/contract.go/price_resolve.go -- QueryId lives on PluginReadResult, not on individual PluginStateEntry entries), computes ScaledDebt(position, bIndexNow), and adds it as an additive currentDebt field. debtPrincipal and all other fields are unchanged for backward compatibility. If B_index is missing, currentDebt is simply omitted rather than failing the request. Live-verified: real query against borrow-test-01 after rebuild/ restart returned currentDebt: 4002 (one unit above debtPrincipal's 4001, consistent with further accrual since the repay), with all original fields intact.
First piece of Layer 4 (lender socialization), which per the July 2026
audit was entirely unimplemented -- zero code, not partial. This is the
foundational O(1) read every remaining Layer 4 piece depends on:
ApplyLossFactor() uses it as the exhaustion-check denominator, and
WillExhaustThisBlock() (ARCM v3.11.1 Section 9.3b Rule 3 -- the C4 fix
from the re-audit, already fully specified but blocked on this and the
{28} queue existing) will use the identical comparison one block earlier
as a lookahead.
Computes sum_i(balance_i) == total_shares_outstanding * s_rate *
loss_factor / RAY^2 exactly, per AYIS Section 5.4.2's own algebraic
argument -- no per-position iteration. Carries the same BitLen()
cast-safety guard (J2 precedent) MintShares()/RedeemShares() already
apply at the identical big.Int -> uint64 boundary shape, reusing
ErrShareOverflow rather than inventing a new error type for the same
underlying failure.
Compiles clean, both binaries rebuilt. NOT live-verified: nothing calls
this function yet -- it has zero behavioral effect on any running
transaction until ApplyLossFactor (next piece) wires it in. Structurally
verified only: real accessor signatures (GetSupplyIndex, GetLossFactor)
confirmed via direct inspection before writing, not assumed from the
spec pseudocode's own naming.
Second batch of Layer 4 pieces, building on SumLenderBalancesInMarket:
- proto: new LossFactorQueueEntry message ({28} record, AYIS Section
12.4). Per-market, not append-only -- a market has at most one
outstanding entry, matching K3's idempotency guard (a second enqueue
overwrites rather than accumulating a second bad-debt figure).
KeyForLossFactorQueue() changed from a bare prefix to per-market,
matching every other {16}-{28} key helper in this file; confirmed
zero existing callers before changing its signature.
- loss_factor_queue.go: PeekLossFactorQueue (read-only lookahead,
shared by ProcessLossFactorQueue's future drain step and
WillExhaustThisBlock's C4 lookahead), EnqueueLossFactorApplication,
DequeueLossFactorApplication.
- market_insolvency.go: GetMarketStatus, SetMarketInsolvent (no
dependency on index_overflow_halted by construction -- satisfies
ARCM v3.11.1 Section 9.3b Rule 1 structurally, not via a checked
guard), SetLossFactor, DecrementLayer4Pending (paired count/total
decrement per ARCM Section 9.2b).
Real mid-implementation correction: initially called EncodeUint128 as
a single-return function per the spec doc's pseudocode ('revert with
error' framing). Built failed real compilation -- this codebase's
actual EncodeUint128 returns ([]byte, *PluginError), a normal Go
error value rather than an implicit panic-and-revert. Fixed at all
three call sites rather than assumed correct from the spec alone.
Also fixed: three heredoc-written files (lender_balances.go,
loss_factor_queue.go, market_insolvency.go) had lost tab indentation
somewhere in the write path and failed gofmt -l. Reformatted with
gofmt -w and reverified clean before this commit -- lender_balances.go
was already pushed in the prior commit with this defect; this commit
corrects it.
DecrementLayer4Pending's underflow branch intentionally does NOT
attempt to emit EventLayer4PendingCountUnderflow (the proto message
exists and is registered, but every existing event-emission call site
in this codebase is DeliverTx-context with a local events slice this
BeginBlock-context function has no access to -- a real, documented,
pre-existing gap, not invented here).
All new code compiles clean (both binaries), gofmt-clean. NOT live-
verified: nothing calls any of these functions yet -- ApplyLossFactor
(next piece) is what wires SumLenderBalancesInMarket, SetLossFactor,
SetMarketInsolvent, and DecrementLayer4Pending together for the first
time.
…d self-liquidation guard market_insolvency.go: SetMarketInsolvent and DecrementLayer4Pending now take *Market in place instead of doing their own GetMarket/SaveMarket round-trips. Two independent read-mutate-save cycles within one tx's call graph race last-write-wins on any field, not just Status -- this was found via Status silently reverting to ACTIVE after a Layer 4 exhaustion despite loss_factor correctly persisting at 0. apply_loss_factor.go: ApplyLossFactor now takes *market directly and reads market.Status off the caller's struct instead of a third independent GetMarketStatus() call. Corrected a stale header comment claiming no caller invokes this function (liquidate_position.go does). liquidate_position.go: call site passes its own already-in-scope market struct through; the existing end-of-function SaveMarket(market) now correctly captures every mutation since there is only one copy of market in play for the whole function. Also adds a self-liquidation guard (ErrSelfLiquidation, code 238) blocking msg.Liquidator == msg.BorrowerAddress. rpc.go: adds /v1/query/lossfactor route. Verified live against devnet: layer4-test-03 and layer4-test-04 both show status=INSOLVENT post-liquidation with the fix in place; a subsequent borrow against test-03 correctly rejects, and a second liquidation against the same market hits the K3 idempotency path cleanly. Self-liquidation guard verified both for rejection and for a genuine second-address liquidator still succeeding.
…eads
Adds handleQueryAllMarkets ({16}), handleQueryPrices ({19} by asset), and
handleQueryAllBorrowerPositions ({17}, with server-side currentDebt via the
market B_index) to the plugin HTTP server, mirroring the existing BeginBlock
and price_resolve range walks. These let the frontend auto-discover every
market, drive the oracle freshness monitor, and render a global liquidation
view without a hand-pinned id list. Also adds plugin/go/.gitignore for the
built binary and oracle-heartbeat artifacts.
…reader fixes - adminGetKey reads the PascalCase PublicKey/PrivateKey the admin RPC actually returns (the Go struct has no json tags, so the old camelCase read was empty and browser connect failed with "No public key returned from admin RPC"). - Lender/borrower position readers strip a leading 0x from the address (the plugin hex.DecodeString wants bare hex; 0x => 400) and decode borrowIndexAtOpen from its base64 uint128 form instead of BigInt()-ing the base64 string (which threw and nulled the whole borrower read, emptying the portfolio). - Portfolio section: per-position health-factor pills (green/amber/red) plus an approaching-liquidation warning banner, from live positions x oracle prices x the on-chain tier LTV (computeHealthFactorScaled / TIER_PARAMS). All values read live from the ARBOR plugin; no mock data. Plugin RPC routes (all-markets / prices / all-borrower-positions) were already shipped in the prior commit; this is frontend-only.
…y/liquidate/withdraw_collateral All four custody-touching DeliverTx handlers were splitting their state mutations across 2-4 independent StateWrite calls instead of one. Per the Canopy builder docs' own canonical pattern (batch-read, batch-write -- operations in ONE StateWrite call are atomic; there is no cross-call transactional guarantee), a failure partway through any of these handlers could leave real custody already moved while dependent records (market.TotalBorrowed, BorrowerPosition, R_fund) never reflected it -- funds-out-with-no-debt-recorded and similar inconsistent states, on the exact code paths that move real value. liquidate_position.go: SaveMarket's own internal StateWrite collapsed into the existing liquidator/pool/position write. borrow.go: custody write and the market/position write (previously two separate StateWrite calls) collapsed into one. repay.go: up to four independent writes (custody, R_fund routing, SaveMarket, position) collapsed into one. collateral.go (withdraw_collateral): custody write and position write collapsed into one. Also corrects a stale header comment that claimed this handler was bookkeeping-only with no Account.Amount fund transfer occurring -- inaccurate relative to the real custody code beneath it. No business logic changed in any of the four -- every existing condition, guard, and error path is preserved exactly; only the commit point moved, from N writes to 1. Verified live against devnet for all four: - liquidate_position: real two-address liquidation (borrower vs. independent liquidator) on a fresh Tier-1 position pushed liquidatable via oracle price update. Full seizure (Tier 3 close factor) confirmed correct across liquidator account, both pools, and position deletion. - borrow: fresh market, deposit + collateral + borrow sequence: account credit, position debt, market.TotalBorrowed, and supply pool all confirmed to move together. - repay: partial repay against the borrow above: account debit, position debt reduction (position correctly NOT deleted, collateral remains), market.TotalBorrowed decrement, and supply pool credit all confirmed. - withdraw_collateral: partial withdrawal against the same position: account credit, position collateral reduction, and collateral pool debit all confirmed.
DeliverTx's switch statement had exactly one case, MessageSend -- every Arbor-specific message type (MessageCreateMarket, MessageDeposit, MessageWithdraw, MessageBorrow, MessageRepay, MessageLiquidatePosition, MessageDepositCollateral, MessageWithdrawCollateral, MessageUpdatePrice, MessagePauseMarket, MessageResumeMarket, MessageDeprecateMarket, MessageUpdateMarketParams, MessageSetAssetTier) fell to default and was rejected with ErrInvalidMessageCast(). No Arbor lending operation could execute on-chain against a fresh build of this source. Root cause: commit ae03baf ("Merge branch 'main' into main") merged upstream Canopy's generic send-only plugin template over this file's DeliverTx switch, silently discarding the Arbor-specific routing, with no merge conflict. CheckTx's own switch (unaffected by the merge) continued routing all 15 types correctly, so transactions were still admitted to the mempool -- but DeliverTx rejected every one of them at the point business logic would actually run. ContractConfig's SupportedTransactions/TransactionTypeUrls registration was NOT affected by this merge and did not need restoring; only the DeliverTx switch itself was reverted. This regression was not caught during tonight's earlier custody- atomicity fixes and live verification (liquidate_position.go, borrow.go, repay.go, withdraw_collateral) because the go-plugin binary running throughout those tests had been built earlier, from a source state that predated ae03baf reaching this checkout -- confirmed via objdump disassembly of DeliverTx showing full routing in that binary despite the committed source already being broken. Every one of tonight's earlier custody fixes is independently still correct and still verified; this was purely a separate, coincidental routing regression that a stale-but-working binary had been masking. Found via two independent AI security audits run against this commit (bf899fa) that flagged the routing gap; the discrepancy between their static-source finding and this session's own live-transaction verification was investigated and resolved by directly disassembling both the pre-fix and post-fix go-plugin binaries. Restored by mirroring CheckTx's already-correct case list and order exactly, calling the existing, unmodified DeliverMessage* handlers (none of which needed any change). Verified live against devnet post-fix: rebuilt both canopy and go-plugin binaries, confirmed via objdump that DeliverTx's compiled code now calls all 14 Arbor DeliverMessage* handlers plus DeliverMessageSend, restarted the node, and submitted a real deposit_collateral transaction -- collateralQuantity on the target position increased by exactly the submitted amount, confirming the fix is live and correct, not just present in source.
…casts liquidate_position.go had two unguarded big.Int -> uint64 casts, identified by an independent AI audit and confirmed live: - collateralSeized.Uint64() (ARCM Section 8, non-bad-debt path) -- collateralSeized is computed from oracle prices with no prior bound. An extreme debtPrice/collateralPrice ratio from a single oracle submitter (MinReporters=1 on devnet) could push it past 64 bits, silently wrapping the amount credited to the liquidator and debited from the collateral pool. - badDebtNative.Uint64() (ARCM Section 9.2, Layer 2 bad-debt path) -- previously called twice (Layer2DrawDown, ApplyLossFactor), both unguarded, with an explicit [DISCLOSED] comment acknowledging the gap rather than closing it. Same oracle-price-ratio dependency; a wraparound here would corrupt both the R_fund debit and the loss-factor lender haircut by the same wrong, understated amount. Both now guarded with BitLen() > 64 checks before the cast, matching this codebase's existing pattern (deposit.go's sharesBig guard, withdraw.go's tokensBig guard) -- reject via new error codes 239/240 rather than silently truncate. badDebtNative's second call site now reuses the single guarded uint64 value instead of re-casting unguarded a second time. On the bad-debt path, collateralSeized is reassigned to pos.CollateralQuantity (already uint64-derived, safe by construction) before its own guard runs, so the new check is a no-op there -- it only has teeth on the non-bad-debt path where collateralSeized is freshly computed from oracle prices. Verified live against devnet: rebuilt both binaries, restarted the node, ran a real liquidation (price-manipulated position, Tier 3 full close factor, full collateral seizure) through the newly-guarded code path -- succeeded exactly as before, confirming the guards don't interfere with normal-magnitude values and only reject genuinely out-of-range ones. MinReporters=1 (the devnet-only oracle quorum override that lowers the bar for triggering this class of bug) is intentionally left untouched per this session's own scoping -- restoring it to a real quorum is a deployment-config decision for when devnet work is complete, not a code fix.
Visual system (CSS-only, data path untouched): - globals.css brand layer: brand tokens (teal #2FD6C0 / violet #7C6CF2 / gold #F2B84B), ambient aurora field, lit-edge .glass panels, .brand-glyph asset marks, .btn-brand gradient buttons, neon .util-track gauges, type ramp. - life layer: slow masthead gradient shift + drifting aurora (reduced-motion safe). - refine layer: solid high-contrast display title with a brand-gradient accent rule, faint fixed structural grid, card/button micro-feedback, brand focus ring. - Class swaps site-wide (home/portfolio/monitor/oracle/liquidation/forms): flat bg-white/[0.03] -> .glass, indigo/emerald monograms -> .brand-glyph, indigo buttons -> .btn-brand, util bars -> .util-track, headings -> .display-title/.section-h. Brand assets + chrome: - public/logo-mark.svg (the real ARBOR icon mark, transparent bg) + header brand swap replacing the placeholder gradient square. - Home masthead collapsed to a single "Protocol overview" display line. Functional fix: - Portfolio panels no longer deadlock: tables always mount when connected so the per-position rows query and report; the empty-state copy moved to an in-table fallback row. Lending + borrowing positions (with live HF pills) now render. All values remain read live from the ARBOR plugin; no mock data.
ScaledDebt() (AYIS Section 6) previously had no BitLen() overflow guard
on its final cast, disclosed as a deliberate v1.11-era carve-out
("no amplification path analogous to MintShares()/RedeemShares()/
SumLenderBalancesInMarket()") -- a design assumption, not a proven
bound, per Arbor Handoff Part 2 item 2.
- Added ErrScaledDebtOverflow (code 241), matching the existing
ErrCollateralSeizedOverflow/ErrBadDebtNativeOverflow style.
- Changed ScaledDebt() signature from uint64 to (uint64, *PluginError),
added the same BitLen() > 64 guard pattern used in deposit.go,
withdraw.go, and liquidate_position.go.
- Updated all 6 call sites: 4 DeliverTx handlers (borrow.go, repay.go,
collateral.go, liquidate_position.go) now revert on overflow via
PluginDeliverResponse.Error; 2 RPC query sites (rpc.go) degrade
gracefully by omitting/falling back to raw debtPrincipal, matching
the existing missing-bIndexRaw fallback pattern -- no transaction
to revert in a read-only query context.
- Added scaled_debt_test.go: regression case confirming normal-magnitude
values are unaffected, and a deliberately constructed overflow case
(MaxUint64 debtPrincipal, artificial borrowIndexAtOpen=1) confirming
the guard actually fires with correct arithmetic, not just compiles.
Live-verified: both binaries rebuilt (core + plugin), go build/vet
clean (vet output unrelated, pre-existing Canopy-core-only findings),
node restarted, RPC reads against real chain state (borrow-test-01,
layer4-test-02, layer4-test-04) confirm correct currentDebt values
through the guarded path before and after restart.
…le TODOs
Scaffolding for ARCM Section 9.2's Layer 3 (protocol treasury), the
missing layer in the bad-debt waterfall between Layer 2 (R_fund,
market-isolated) and Layer 4 (lender socialization, loss_factor).
Layer 3 itself is NOT built by this commit -- no draw-down function,
no waterfall wiring, no funding mechanism. This is state-layer
scaffolding only, following the same order used for every prior
Arbor addition (proto/state key, then accessors, before any logic
wires into it).
state_keys.go:
- PrefixTreasury = []byte{40}. NOT {30}, despite {30} being the next
free integer after {29} (PrefixAssetTier) -- {30}-{39} is reserved
for future NASM/NUSD coordination (confirmed as a deliberate prior
decision, not a stale assumption). {40} chosen with deliberate
headroom above that reservation rather than sitting adjacent to it,
so NASM can claim {30}-{39} without Treasury being the first thing
it collides with.
- KeyForTreasury() -- NOT market-keyed, unlike every other key
builder in this file. T_fund is a single global uint128 balance,
not per-market, mirroring KeyForGovernanceParams()/
KeyForBackstopQueue()'s existing zero-argument JoinLenPrefix shape.
state_accessors.go:
- GetTreasury / SetTreasuryTry / SetTreasury, mirroring
GetReserveFund / SetReserveFundTry / SetReserveFund's exact
three-function shape and BeginBlock-freeze-vs-DeliverTx-revert
contract (Principle 14), adapted for a global rather than
per-market accumulator. No caller exists yet for any of these --
write-side contracts are added alongside read-side ones rather
than deferred until a caller needs them, matching this codebase's
existing SetReserveFund precedent.
bad_debt_layer2.go, interest_accrual.go:
- Comment-only corrections. Both files carried TODO/gap comments
written before Layer 4 (ApplyLossFactor, EnqueueLossFactorApplication,
PeekLossFactorQueue, SumLenderBalancesInMarket), repay.go, and
liquidate_position.go existed. Re-verified directly against the
real files rather than re-assumed: Layer 4 machinery now exists
and is wired in (liquidate_position.go calls ApplyLossFactor on a
Layer 2 miss); Treasury accessors now exist (this commit).
ProcessLossFactorQueue (BeginBlock drain) and WillExhaustThisBlock
(C4 lookahead, AYIS v1.11.1 Section 7 Step 8 revised) remain
genuinely unbuilt -- re-confirmed, not just re-stated. No logic
changes in either file.
Explicitly NOT done by this commit, confirmed by direct inspection:
- Layer 3 draw-down function (Layer2DrawDown analog against T_fund)
- Waterfall wiring (liquidate_position.go's Layer 2-miss path still
falls straight through to ApplyLossFactor/Layer 4)
- Funding mechanism (fee skim or otherwise) -- T_fund has no writer
anywhere in the codebase yet
- WillExhaustThisBlock / C4 fix
Verified: gofmt clean on all 4 files, go build ./... exit 0,
go vet ./contract/... exit 0.
Reverses the single-shared-treasury design from the prior session's scaffolding (abb783a): a shared T_fund meant a NUSD-side bad-debt event could drain Layer 3 protection Arbor lenders were counting on, and vice versa -- a hidden risk coupling between two products that should be independent. Reopened and reversed the same session it was introduced, before any caller depended on the shared design. - state_keys.go: PrefixTreasuryArbor/PrefixTreasuryNASM at {40}/{41} respectively, KeyForTreasuryArbor()/KeyForTreasuryNASM(), replacing the single PrefixTreasury/KeyForTreasury. {40} kept for Arbor to minimize churn (already live). - state_accessors.go: GetTreasuryArbor/GetTreasuryNASM, SetTreasuryArborTry/SetTreasuryNASMTry, SetTreasuryArbor/SetTreasuryNASM -- 6 functions replacing the original 3, mirroring GetReserveFund/SetReserveFundTry/SetReserveFund's exact BeginBlock-freeze-vs-DeliverTx-revert contract (Principle 14) per pool. - bad_debt_layer3.go: Layer3DrawDown split into Layer3DrawDownArbor and Layer3DrawDownNASM -- distinct functions rather than a parameterized single function, so a caller cannot mix up pools at the type level. Binary-gate contract (identical to Layer2DrawDown) unchanged. - arbor_events.proto / arbor_events.pb.go: added EventReserveFundDrawDown (retroactive fix -- Layer2DrawDown had no event since it went live) and EventTreasuryDrawDown, both inserted before the Layer 4 event section in ARCM waterfall order. EventTreasuryDrawDown carries a new pool field ("arbor" | "nasm") so an observer can distinguish which isolated pool fired a draw. Verified: gofmt clean (files touched this commit only -- pre-existing repo-wide formatting drift in unrelated files left untouched, per project convention), go build ./... exit 0, go vet ./... exit 0. No caller wired yet -- liquidate_position.go's Layer 2-miss fallthrough still calls ApplyLossFactor/Layer 4 directly, unaware Layer 3 exists. That wiring is the next unit of work, deliberately not included here.
…data - RevealObserver now re-scans on every route change (isomorphic layout effect), fixing the home page rendering blank after client-side navigation: the mount-only observer left navigated-to .reveal sections stuck at opacity:0 under reveal-armed. - Header: 8-item nav collapses to a hamburger + glass sheet below md; inline on md+. - layout: richer metadata (title/OG/favicon = logo-mark) + self-hosted Space Grotesk (display) / Manrope (body) via next/font; body className carries the font vars. - Liquidation "all healthy" note now only asserts health for priced positions. Data path untouched; all values still read live from the ARBOR plugin.
The Tailwind Play CDN (PostCSS is disabled in this project) ignores the opacity modifier on arbitrary hex colors, so bg-[#070a12]/95 rendered transparent — the mobile nav sheet showed the page bleeding through, and the sticky header / wallet popover had the same latent bug (only hidden at scroll-top over dark space). Replace those three with real-CSS classes in globals.css (always applied, like .glass): .arbor-surface (frosted header), .arbor-surface-solid (opaque menu), .arbor-popover (frosted wallet dropdown). A safety-net regex also strips any other stray bg-[#hex]/NN to solid so no surface can go transparent again. Data path untouched.
Resolves the 3.2 open question from the treasury-split session (HANDOFF_LAYER3_SPLIT.md) by having Layer2DrawDown, Layer3DrawDownArbor, and Layer3DrawDownNASM all return their post-draw balance as a second value, then uses that to complete the 3.1 restructure: - liquidate_position.go: Layer 2 miss now falls through to Layer3DrawDownArbor before Layer 4 (ApplyLossFactor), completing the four-layer waterfall order instead of skipping straight from Layer 2 to Layer 4. Stale "Layer 3 does not exist" comment corrected. - Emits EventReserveFundDrawDown on a Layer 2 cover and EventTreasuryDrawDown (pool: "arbor") on a Layer 3 cover, using the real post-draw balances now returned by the draw-down functions. - rpc.go: adds /v1/query/treasury?pool=arbor|nasm, the last balance in the waterfall with no query surface (R_fund and loss_factor already had one). No marketId param -- T_fund is a single global balance per pool, matching KeyForTreasuryArbor/KeyForTreasuryNASM's own no-arg shape. Verified: gofmt -l, go build ./..., go vet ./... all clean from the plugin/go module root (not ~/arbor -- these are separate go.mod trees; ~/arbor's own ./... does not reach plugin/go/contract at all). Not yet done, left for follow-up: - No live devnet/RPC verification of this session's changes. - NASM's own waterfall (Layer3DrawDownNASM has no caller anywhere yet). - treasury_cut funding mechanism, still unbuilt.
The menu panel itself is now opaque (prior fix), but its backdrop was a transparent click-catcher, so the bright home page showed through undimmed below the dropdown and read as "bleeding". Add a CDN-proof .arbor-scrim (near-black + slight blur, hand-written CSS so the Play CDN opacity bug cannot affect it) on the menu backdrop so opening the drawer dims/softens the page like a real mobile overlay. Wallet popover backdrop left unchanged. Data path untouched.
The new logo's symbol (a merkle/index tree drawn as a literal tree/canopy with gold leaf-nodes) is the strongest mark yet, but its as-authored cream seal is tuned for light/framed surfaces, not the dark inline header (a cream tile + forest green would clash with the ink/teal/violet glass UI). So the one artwork is used in two roles, generated from a single shared geometry set (the tree is identical in both, just re-skinned): - public/logo-tree.svg: transparent tree recolored to the brand gradient (teal->violet) with our gold leaf-nodes, for the header, plus an ambient teal/violet drop-shadow glow (.arbor-mark) so it reads as lit on the ink. - public/logo-seal.svg: the cream seal as-authored, for favicon / OG / apple-touch (self-framed, correct where a transparent mark would vanish). Header <img> repointed to the tree; metadata icon/OG repointed to the seal. Data path untouched. (Byte-exact seal override: cp your arbor-logo.svg to public/logo-seal.svg — same filename, no code change.)
- Empty/whitespace EVM field → signs 'evm:none' (was signing 'evm:') - Non-empty EVM → lowercased before signing (was signing raw case) - Payload also normalized so we send what we signed - Mirrors quest_xp.go questXPHandleLink normalization byte-for-byte
- Remove 'X connected.' / 'Linked!' status display from IdentityLinkCard (redundant — card shows connected state inline) - Add 'Export CSV' button to Weekly leaderboard card - CSV includes rank, address, evm_address, xp columns - EVM column populates when backend includes evmAddress in leaderboard response (backend patch needed in quest_xp.go)
- refetchIntervalInBackground: true on price + liquidation queries so backgrounded phone tabs keep polling instead of freezing - staleTime 20s -> 5s: returning tabs refetch instead of painting cache - default refresh interval 25s -> 15s - Chain data verified fresh (price 2 blocks old at query time); the stale reads were client cache artifacts, not backend staleness
- Add AUTHORITY_ADDRESSES set with e35a3b07e600817905858fca98a0420637b44f63 - Authority users auto-see advanced wallet options (no toggle needed) - Admin RPC tab visible only to authority addresses - Authority badge displayed next to wallet address when connected - Non-authority users: Admin RPC hidden even in advanced mode
- Header chip shows amber ADMIN pill whenever the connected address is in AUTHORITY_ADDRESSES — regardless of login method (MetaMask derive, paste key, import, device cache, Admin RPC all set the same address) - Wallet modal shows Admin banner for authority addresses - effectiveAdvanced = advancedMode || isAuthority: authority access is reactive, not mount-time — connecting after modal open still unlocks - Toggle hidden for authority users (their advanced set is always on); non-authority users still never see Admin RPC
- New /admin page: authority status card, Admin RPC key fetch + connect, monitoring quick-links (Monitor / Liquidation / Oracle / Governance) - Non-authority or disconnected visitors get a plain 'Not authorized' card - Header 'Authority' nav item now renders when EITHER the frontend authority set (e35a3b...4f63) OR the on-chain protocol role matches - Header component now imports useWalletStore to access connected address for the nav gate check
- Events page transformed into full Explorer console - Live network widget: pulsing orb, animated gradient grid, glow-pulse, real-time block height + market count + ticking tx counter - Address dossier: any address (or one-click 'Use my wallet') shows asset balances (BTC/ETH/USDC), NUSD balance, borrower/lender positions per market, and NASM vault ownership — all read live from the plugin - Privacy-first: every lookup is a direct browser→node read of public state; no search history stored or relayed (visible note in UI) - Motion polish: staggered fade-up cards, pulse/glow/grid-drift keyframes with prefers-reduced-motion respected by existing global rules
- The explorer rewrite accidentally replaced the Events & activity log section; restore it from git as components/explorer/ActivityLog.tsx - Explorer page now stacks: live network widget + address dossier on top, full activity log (Activity/Failed/Consensus tabs, paginated tx table, waterfall activity) below a divider — nothing removed - Section title refined to 'Activity log' to sit inside the explorer
- Previous restore used cwd-relative paths in 'git show rev:path', which resolves from repo root — every lookup failed, and the wrong-commit state (explorer mounted twice) shipped - ActivityLog.tsx now rebuilt from the last commit containing the real activity log: Activity/Failed/Consensus tabs, paginated tx table, waterfall section with refresh - Events page = explorer widgets on top, activity log below, once each
- Network widget's simulated TX ticker replaced with the explored address's real transaction total (queryTxsBySender, 15s refresh), labeled 'Address TXs' so the figure is honest - Explorer lookup now drives the activity log too: activeAddress is passed as externalAddress and synced into the log's lookup state, so dossier + tx table + Failed/Consensus tabs all switch together - Activity log's own input remains as fallback when nothing is looked up
- Add accountsChanged listener with cleanup, scoped to sessions that actually originated from MetaMask (paste/import/admin sessions are left untouched) - Track ETH<->Arbor address origin so a session restored via the device-cache path is still recognized as MetaMask-derived and stays reactive to account switches
Device-cache silent reconnect previously trusted a cached MetaMask-derived session without checking whether MetaMask's active account had changed while the tab was closed. Now it verifies via eth_accounts before restoring; on mismatch it falls through to deriving fresh for whatever account is actually connected, which always requires a real signature rather than silently trusting stale state.
On mobile browser-extension bridges window.ethereum can be injected after page hydration instead of before it, unlike desktop Chrome. Auto-reconnect and the accountsChanged listener both checked synchronously on mount and gave up permanently if the provider wasn't there yet -- explaining why a manual tap on Connect worked (more time had passed) but page load alone didn't. Both now wait for either an already-present provider or the ethereum#initialized event, bounded by a timeout.
eth_accounts is spec'd to never prompt, but on this MetaMask/ browser-extension bridge it was returning empty even for an already-authorized origin -- explaining why only the Connect button (which uses eth_requestAccounts) worked. Switched the reconnect check to eth_requestAccounts, which resolves silently for an already-authorized origin and only prompts when authorization genuinely isn't granted. Gated behind a local 'has connected via MetaMask before' cache check so first-time visitors never see an unprompted permission popup.
- Quetta/Rabby inject window.ethereum asynchronously after page load; the mount-time silent-reconnect effect checked before injection and gave up, so refresh showed 'Connect wallet' until a manual tap - Poll for window.ethereum every 250ms up to 6s before attempting the MetaMask silent path; device-cache path (no extension needed) still runs first and is unaffected
- The device-cache path previously verified MetaMask-derived sessions against getAlreadyConnectedEthAccount() BEFORE any provider-injection wait; on mobile the probe returned null (extension not injected yet / wallet locked), read as 'account changed', and abandoned the cache - Device-cache restore is now unconditional: the cached key is the user's own previously-connected key, restored with zero provider involvement -- refresh reconnects instantly for every connect method - MetaMask silent path (with 6s injection wait) remains as fallback for returning MM users without a device-cache entry - MetaMask connect now also writes the device cache (cachePrivateKey), closing the gap where only paste/generate/import populated it - Account-switch safety: users who change MetaMask accounts simply disconnect/reconnect manually
…estore - Manual disconnect no longer resurrects: a wasConnected ref marks sessions where isConnected was true; the silent-reconnect effect skips its own trigger when the flip came from a user disconnect, so the connect modal opens disconnected and manual choice works - Refresh restore for pre-device-cache MetaMask sessions: step 2 now reads the localStorage derived-key cache directly (keyed by eth address) with zero provider probing -- the mobile getAlreadyConnectedEthAccount() probe (popup/lock-fail) was the reason refresh reconnects never worked on Quetta/Rabby - Behavior matrix: refresh -> instant restore; disconnect -> stays disconnected until manual connect; cache clear -> clean first visit
- handleMetaMaskConnect cached the device key via
useWalletStore.getState().address right after connectFromRawKey; if the
store hadn't committed the address, the write silently skipped and
refresh had nothing to restore. Use derived.address (synchronous).
- Silent-restore failures were swallowed by .catch(() => {}); the effect
now records a step trace (addr/key/eth/mkey/conn/err) rendered as a
tiny line in the connect modal, so any future mobile-restore failure
is diagnosable from a screenshot instead of guesswork
- Silent reconnect effect now waits 100ms before attempting
connectFromRawKey, matching Praxis's 'await window.blsReady' pattern
- Without this, the noble-curves BLS library may not have finished
initialization at React mount time, causing getPublicKey() to throw
silently and the .catch(() => {}) to swallow the error
- Manual connect worked because by tap-time the lib was fully loaded;
this fixes the mount-time race so refresh reconnects work too
- Previous patch had 'disconected' (one n) in the useState generic, causing the state declaration to not match the usage sites - Now declared as useState<'changed' | 'disconnected' | null>(null) matching all setWalletDrift calls throughout the component
… sessions - The wallet-sync patch's step 2 (re-arm mmEthAddress from the origin map after device-cache restore) was discarded when the first patch run aborted before writing the file; it was never re-applied - Consequence: restored sessions had mmEthAddress=null, so the drift detection effect never armed and Rabby account switches after a refresh were invisible -- exactly the gap Praxis doesn't have (it sets its equivalent on every load) - Also render the reconnect trace in the main connect modal (it was only in the keystore-unlock branch, so it never showed in testing)
…t fully disconnect from wallet/dApp permissions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.