Skip to content

learn-token: contract upgrade mechanism, claim gas estimation, event indexing, allowance cleanup - #362

Merged
DeFiVC merged 3 commits into
ChainLearnOfficial:mainfrom
gideononiru:feature/learn-token-upgrade-gas-events-cleanup
Aug 29, 2026
Merged

learn-token: contract upgrade mechanism, claim gas estimation, event indexing, allowance cleanup#362
DeFiVC merged 3 commits into
ChainLearnOfficial:mainfrom
gideononiru:feature/learn-token-upgrade-gas-events-cleanup

Conversation

@gideononiru

Copy link
Copy Markdown
Contributor

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

  • `cargo test -p learn-token --lib` — 42/42 passing (7 new: upgrade auth-gating + getters, 2× estimate_claim_gas, 3× cleanup_expired_allowances)
  • `cargo test --test token_tests` — 20/20 passing (2 new: event-topic indexing assertions for transfer_from and reward_claimed)
  • `cargo test --test token_flow` (integration) — 5/5 passing, no regressions
  • Not attempted: exercising the actual wasm-code-swap effect of `upgrade()` end-to-end — that needs a second compiled wasm artifact and an upload harness, out of scope for this unit-test pass. Auth gating, storage bookkeeping, and event emission around it are covered.

gideononiru and others added 3 commits August 29, 2026 23:52
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.
@drips-wave

drips-wave Bot commented Aug 29, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@DeFiVC
DeFiVC merged commit 0b1f71a into ChainLearnOfficial:main Aug 29, 2026
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.

. Add storage cleanup for expired data . Add event indexing improvements . Add gas estimation function . Add contract upgrade mechanism

2 participants