Skip to content

fix: extend new balances and roles when created - #276

Open
hpmaxi wants to merge 9 commits into
feat/full-wiring-checkfrom
fix/ttl-on-creation
Open

hpmaxi wants to merge 9 commits into
feat/full-wiring-checkfrom
fix/ttl-on-creation

Conversation

@hpmaxi

@hpmaxi hpmaxi commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #327

A new share balance or role entry started at the network's minimum lifetime, and the next call that read it paid to extend it. On testnet that minimum is about seven days, so a transaction simulated before the entry existed failed for too low a fee. The first redemption, which creates the vault's own share balance, hit this. OpenZeppelin extends an entry when it reads it, not when it creates it.

Now every call that writes a share balance or a freeze, and every role grant in the vault, the token and the oracle, reads back what it wrote through OpenZeppelin's own reads. A new entry reaches its extend target in the transaction that creates it, and the same code runs on every network. The kit spec's TTL policy lists each network's settings and what frontends should do.

Changes

  • The share token reads back each balance it writes after mint, burn, transfer, transfer_from, forced_transfer, legal_transfer and recover_balance
  • It reads back each freeze entry after set_address_frozen, freeze_partial_tokens, unfreeze_partial_tokens, and the freezes recover_balance carries
  • The vault, the token and the oracle read back the four entries a grant writes (the role, its member-list slot, the member count, the role list) after each grant in their constructors, in grant_role and in an applied GrantRole change
  • The read-backs call OpenZeppelin's own reads, so its thresholds apply unchanged; no network value is hardcoded
  • balance and total_supply extend the token's instance before they read, so a wind-down that only reads balances keeps the token alive
  • 21 new tests set the ledger's minimum lifetime to testnet's (120,960 ledgers) and read each new entry's TTL from inside its contract; two more set mainnet's and check the day a balance (91) and a role (31) are first renewed
  • The kit spec's TTL policy is rewritten, and the testing guide lists the new tests

Deviations

Acceptance criteria

  • No balance or role entry is created without reaching its extend target in the same transaction
  • Each path has a test that sets the ledger's minimum lifetime as low as testnet's
  • The TTL policy states both networks' values and the advice for frontends

Test plan

Automated tests

  1. cargo test --locked: 705 passed (440 vault, 101 oracle, 96 token, 36 compliance, 7 identity verifier, 19 timelock, 6 storage). 24 are new. On the base, the 21 testnet ones fail with each new entry at 120,959 ledgers, the network minimum; the two mainnet ones pass there too, since that minimum is longer than any target. The last one lets the token's instance fall below its threshold and fails on the base, where a balance read does not extend it. Each commit passes on its own.
  2. cargo clippy --locked --all-targets -p share-token -p async-vault -p nav-oracle -- -D warnings, and the vault with --features short_wind_down: clean.
  3. CI runs the harness deploy, happy path and guards on a local network.

Manual verification

  1. Start a local network: docker run --rm -d -p 8000:8000 --name stellar-local stellar/quickstart --local --enable core,rpc. Its minimum lifetime is testnet's, 120,960 ledgers.
  2. Run stellar contract build, then CARGO_TARGET_DIR=target/short-wind-down stellar contract build --package async-vault --features short_wind_down.
  3. Run npm run build:clients, then HARNESS_NETWORK=local npm run deploy -w scripts/harness, then test:happy-path, test:guards and test:wind-down with npm run <script> -w scripts/harness.
  4. You should see 24, 50 and 6 tests pass. A role entry the deploy wrote lives until its ledger plus 1,555,200, and a share balance the happy path wrote until its ledger plus 518,400, not plus 120,960.

Wasm sizes: share token 42,919 bytes (was 42,490), oracle 33,080 (was 32,937), vault 73,163 (was 73,041). Compliance and identity verifier are unchanged.

Breaking changes

No entrypoint, argument, event or error changes. Fees move: on testnet, the call that creates a share balance, freeze or role entry pays for 30 days (90 for a role) instead of about 7, and the next caller no longer pays the difference.

Checklist

  • Self-reviewed my own diff
  • Tests added or updated
  • Docs updated (if applicable)
  • No unrelated changes bundled in

Screenshots

None.

OpenZeppelin extends a balance or freeze entry when it reads it, not
when it creates it, so a new one started at the network's minimum
lifetime and the next caller paid to extend it. On testnet that minimum
is about seven days, and a transaction simulated before the entry
existed then failed for too low a fee; the first redemption, which
creates the vault's own share balance, hit this. Every token call that
writes a balance or a freeze entry now reads it back through
OpenZeppelin's own reads, so a new entry reaches its extend target in
the call that creates it. The tests set the ledger's minimum lifetime
to testnet's.
A role grant writes up to four persistent entries: the account's role,
its place in the member list, the member count and the list of roles.
OpenZeppelin extends them when it reads them, not when it creates them,
so the first keeper or investor whose call checked the role paid to
extend it from the network's minimum. Every grant the vault, the share
token and the oracle make (in their constructors, in grant_role and in
an applied GrantRole change) now reads those entries back through
OpenZeppelin's own reads, so the granter pays and each entry reaches
the role lifetime in the same call.
The kit spec said OpenZeppelin extends balances when it writes them; it
extends them only when it reads them. The TTL policy now says the
contracts read back each balance, freeze and role entry they write, and
lists each network's minimum lifetime and longest extension (testnet
about 7 days, mainnet about 120, read over RPC), what that means on
each network, why the kit keeps one set of values everywhere, who pays,
and what frontends should do. The testing guide lists the new tests.
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
strata-vault-kit-app Ready Ready Preview Oct 22, 2026 2:31pm UTC

Request Review

The mainnet test only checked that a new balance starts at the network's
120-day minimum. It now also checks the spec's claim about renewals: a
read on day 90 leaves the balance alone, and a read on day 91 renews it
to 30 days. A second test does the same for a role, which a role check
renews on day 31 and not on day 30. Test comments are one line each.
The TTL policy said every role check renews a role's entries and that
any read or write extends an entry. A role check extends only that
account's role entry; the member-list slot, the member count and the
role list are extended only by grants and by calls that list role
members. Writing an entry never extends it on its own: the kit's storage
helpers extend on every read and write, OpenZeppelin only when it reads
an existing entry, so the kit reads back what it creates. A revoke moves
entries with plain writes and extends none of them.
@hpmaxi
hpmaxi marked this pull request as ready for review October 20, 2026 05:59
balance and total_supply now extend the share token's instance before
they read, like every call that moves shares. A keeper's wind-down round,
or a claim that burns nothing, only reads the token, so a quiet wind-down
could otherwise let the token's instance archive.
…_supply

Both are read-only views, and a view's doc carries no authorization section, like the oracle's views.

This branch was successfully deployed

1 active deployment
Preview — d23f45bd Deployed Oct 22, 2026 by vercel[bot]
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.

1 participant