learn-token: contract upgrade mechanism, claim gas estimation, event indexing, allowance cleanup - #362
Merged
DeFiVC merged 3 commits intoAug 29, 2026
Conversation
Closes ChainLearnOfficial#200 Every event in events.rs used a single-symbol topic and pushed every address/id into the data payload. Soroban's getEvents RPC filters match topics positionally with server-side indexing but never inspects the data payload, so "every transfer touching address X" or "every reward claimed for course Y" required fetching and decoding every event of that type and filtering client-side. Moved the field(s) an indexer is most likely to filter by into additional topic slots, keeping topics[0] (the event-name symbol) unchanged so any indexer already filtering on it keeps working: - transfer: (transfer, from, to) — matches the SEP-41 reference token convention. - transfer_from: (transfer_from, from, to) — from/to occupy the same topic positions as transfer, so "everything that moved address X's tokens" is one topic shape across both event kinds; spender stays in data. - burn / burn_from: (burn[/burn_from], from) — same topic position as transfer's balance-reducing party. - mint: (mint, to) - approve / allowance_expired: (approve[/allowance_expired], owner, spender) — both frequently queried together, and the same pair lets an indexer correlate an allowance's creation with its expiry. - reward_claimed: (reward, learner, course_id) — also fixes a doc-comment bug where two different (and both stale) "Topics:" lines had been pasted on top of each other. - whitelist_updated: (whitelist_updated, address) Left progress_tracker_updated, restriction_updated, and snapshot_created as single-symbol topics — they're rare, admin-only, contract-wide config events with no per-address query pattern to index. Updated the one existing test asserting an exact topics/data shape (transfer_from) and added two tests asserting the new indexed fields are actually present as queryable topics (not just still-present somewhere in the payload) for transfer_from and reward_claimed. `cargo test -p learn-token --lib` and `cargo test --test token_tests` — all passing (35 + 20, no regressions; +2 new).
…leanup Closes ChainLearnOfficial#198 Closes ChainLearnOfficial#199 Closes ChainLearnOfficial#201 ## ChainLearnOfficial#198 — Contract upgrade mechanism Added `upgrade(new_wasm_hash)` (admin-only, via `env.deployer().update_current_contract_wasm`), plus `wasm_hash()` and `upgrade_version()` getters. State is preserved across the upgrade by construction — a Soroban upgrade replaces only the executable code at the contract's address, never its storage, so every balance, allowance, and other entry survives untouched with zero migration code needed. The new wasm hash and an incrementing version counter are stored on-chain and an `upgraded` event is emitted on every call. (`ContractMetadata.version`, added in ChainLearnOfficial#107, is a compile-time constant baked into whichever wasm is currently installed — it changes when a new wasm build bumps `CONTRACT_VERSION`, but doesn't by itself count *how many times* this specific deployed instance has been upgraded, which is what `upgrade_version()` tracks.) ## ChainLearnOfficial#199 — Gas estimation for claim_reward Added `estimate_claim_gas(learner, course_id, quiz_id)`, a read-only function that re-runs claim_reward's full validation path (already- claimed check, quiz score via the progress-tracker, score bounds, reward cap, supply cap) with zero state changes and returns a `ClaimEstimate { would_succeed, estimated_reward, failure_reason }`. Worth being upfront about scope here: a Soroban contract has no way to introspect its own CPU/resource-fee cost — that's computed by the host during the client-side `simulateTransaction` RPC call, which no contract invocation can perform on itself. What this function provides instead is a deterministic preview of whether the real `claim_reward` call would succeed right now and for how much, so a caller can decide whether it's worth submitting a transaction (and paying its real fee) before doing so. That's the on-chain-buildable piece of "know your cost before submitting." ## ChainLearnOfficial#201 — Storage cleanup for expired allowances Added `cleanup_expired_allowances(owner)` (permissionless, same reasoning as the existing `prune_expired_allowance`: it only removes data that's already expired and therefore already worthless) plus `allowance_spender_count(owner)` for storage-size visibility. Soroban contract storage has no key-enumeration API, so a bulk "remove every expired allowance for this owner" function has no way to discover which spenders an owner has ever approved without the contract maintaining its own index. Added a per-owner spender registry (`AllowanceSpenders`, a deduplicated `Vec<Address>`), recorded by `approve`/`increase_allowance`, that `cleanup_expired_allowances` walks: for each tracked spender it checks expiry via the existing `check_allowance_expired`, removes and emits `allowance_expired` for anything expired, and keeps the registry itself compacted to only still-active spenders afterward. ## Test plan `cargo test -p learn-token --lib` — 42/42 passing (7 new: 2 for estimate_claim_gas, 3 for cleanup_expired_allowances, 2 for upgrade's version/hash getters and admin-auth gating). `cargo test --test token_flow` (integration) — 5/5 passing, no regressions. Did not attempt to test the actual wasm-code-swap effect of `upgrade()` end-to-end — that needs a second compiled wasm artifact and a harness that uploads it, which is out of scope for a unit-test pass; the auth gating, storage bookkeeping, and event emission around it are covered instead.
|
@gideononiru Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
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.
Summary
#198 — Contract upgrade mechanism
Added `upgrade(new_wasm_hash)` (admin-only, via `env.deployer().update_current_contract_wasm`), plus `wasm_hash()` and `upgrade_version()` getters. State is preserved by construction — a Soroban upgrade replaces only the code at the contract's address, never its storage. The new wasm hash and an incrementing version counter are persisted and an `upgraded` event is emitted on every call.
#199 — Gas estimation for claim_reward
Added `estimate_claim_gas(learner, course_id, quiz_id)`: a read-only function that re-runs `claim_reward`'s full validation path (already-claimed, quiz score via the progress-tracker, score bounds, reward cap, supply cap) with zero state changes, returning `ClaimEstimate { would_succeed, estimated_reward, failure_reason }`.
Flagging the scope honestly: a Soroban contract can't introspect its own CPU/resource-fee cost — that's a client-side `simulateTransaction` RPC concern, not something any contract invocation can compute about itself. What's actually buildable on-chain, and what this implements, is a deterministic preview of whether the real claim would succeed and for how much, so a caller can decide whether it's worth submitting a transaction before doing so.
#200 — Event indexing for efficient querying
Every event in `events.rs` used a single-symbol topic with every address/id in the data payload. Soroban's `getEvents` RPC filters match topics positionally with server-side indexing but never inspects `data`, so querying "every transfer touching address X" required scanning and decoding every transfer event. Moved the field(s) most likely to be filtered on into additional topic slots (keeping topics[0], the event name, unchanged so existing symbol-only filters keep working): `transfer`/`transfer_from` index `(from, to)` in the same positions (matching the SEP-41 reference convention), `burn`/`burn_from` index `from`, `mint` indexes `to`, `approve`/`allowance_expired` index `(owner, spender)`, `reward_claimed` indexes `(learner, course_id)`, `whitelist_updated` indexes `address`. Also fixed a doc-comment bug on `reward_claimed` where two different stale "Topics:" lines had been pasted on top of each other. Rare admin/config-only events (`progress_tracker_updated`, `restriction_updated`, `snapshot_created`) were left as single-symbol topics — no per-address query pattern to index.
#201 — Storage cleanup for expired allowances
The repo already had a permissionless `prune_expired_allowance(owner, spender)` for a single pair. Soroban storage has no key-enumeration API, so a bulk "remove every expired allowance for this owner" needs its own index of which spenders an owner has ever approved. Added a deduplicated per-owner spender registry (recorded by `approve`/`increase_allowance`) and `cleanup_expired_allowances(owner)` (permissionless, same reasoning as `prune_expired_allowance`: it only ever removes already-worthless expired data) that walks the registry, removes and emits `allowance_expired` for anything expired, and compacts the registry to the remaining active spenders. Added `allowance_spender_count(owner)` for storage-size visibility.
Test plan