OpenZeppelin / OpenZeppelin/stellar-contracts
🐞 [Bug]: total supply moved to persistent storage without a migration path
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 95
- Forks
- 68
- Avg merge
- 4d 42m
- Merged PRs (30d)
- 20
Description
Summary
#795 moved the fungible total supply from instance storage (FungibleStorageKey::TotalSupply) to a persistent entry (TotalSupplyStorageKey::TotalSupply). A contract deployed from an earlier version and upgraded in place keeps its balances but reads its supply as 0; the old value stays in instance storage. There is no migration helper and no note about it.
Effects after the upgrade
S is the pre-upgrade supply, A the assets a vault holds.
TotalSupply:total_supply()under-reports byS.burnandburn_frompanic withError(Contract, #104)(decrease_total_supply,amount > supply) until post-upgrade mints have raised the new counter above the burn amount.Capped:check_capuses the under-reported supply, soStokens overcapcan be minted.RWA: same asTotalSupplyforburn.Vault:withdrawandredeempanic with#104for every holder. Withdecimals_offset = 0, the first deposit that mints a share (#883 rejects smaller ones) isD = A + 1and mints one share. A pre-upgrade holder can then redeem one share forfloor((A + D + 1) / 2) = A + 1assets, the whole deposit.
Migration
#[contracttype] unit variants serialize as Vec[Symbol(<variant>)], so FungibleStorageKey::TotalSupply and TotalSupplyStorageKey::TotalSupply produce the same XDR (checked with to_xdr on both types, soroban-sdk 27.0.2). 0.9 code can read the old value with e.storage().instance().get::<_, i128>(&TotalSupplyStorageKey::TotalSupply), and no 0.9 key collides with the leftover entry.
Proposal
Helper in total_supply/storage.rs
/// Moves a total supply recorded by `stellar-tokens` 0.8 or earlier from
/// `instance` storage to the `persistent` entry used by this extension and
/// returns the migrated amount, `0` when there is nothing to migrate.
///
/// # Security Warning
///
/// ⚠️ SECURITY RISK: This function has NO AUTHORIZATION CONTROLS ⚠️
///
/// Call it from an admin-restricted `migrate` entrypoint guarded by
/// `stellar_contract_utils::upgradeable::get_schema_version`.
pub fn migrate_total_supply(e: &Env) -> i128 {
let key = TotalSupplyStorageKey::TotalSupply;
let Some(legacy) = e.storage().instance().get::<_, i128>(&key) else {
return 0;
};
increase_total_supply(e, legacy);
e.storage().instance().remove(&key);
legacy
}
The legacy value is added to the persistent counter rather than written over it, because mints between upgrade and migrate start that counter from 0. Removing the instance entry makes a second call a no-op.
Tests: migrated value, removed entry, additive case, no-op case, and one test asserting that a test-local copy of the 0.8 FungibleStorageKey::TotalSupply encodes to the same XDR as the new key.
examples/upgradeable/v1 → v2 demonstrate this migration
v1/v2 currently migrate a Config { rate } → Config { rate, active } shape change, which lazy-v1/lazy-v2 and the Pattern 1 snippet in the upgradeable module docs already cover.
v1: a fungible token with the pre-0.9 layout on 0.9 crates:Basefor balances and metadata, an owner-onlymintcallingBase::mintand bumping ani128ininstanceunder a local#[contracttype] enum LegacyStorageKey { TotalSupply }, andtotal_supply()reading it.v2: the same token onCompose<(TotalSupply,)>+FungibleTotalSupply, plusmigrate(operator)under#[only_role(operator, "migrator")], guarded byget_schema_version(e) < 2, callingmigrate_total_supplythenset_schema_version(e, 2).- Test in
v1: mint1_000, upgrade, asserttotal_supply() == 0andtry_burnfails with#104, migrate, assert1_000, burn, assert a secondmigrateis rejected. A second test mints onv2before migrating. upgradertest switches to the token; rebuild thetestdatawasm files and snapshots; update the example list at the end of theupgradeablemodule docs.
A real 0.8 v1 cannot be a workspace member (0.9 removed FungibleToken::total_supply and 0.8 pins another SDK line), so v1 reproduces the layout instead.
Docs
v0.9.0release notes: an "Upgrading from 0.8 or earlier" section with the failure modes and themigrate_total_supplyrecipe.total_supply/mod.rsmodule docs and theTotalSupplybullet inpackages/tokens/README.md: point at the helper and the example.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with total_supply/storage.rs and the existing examples/upgradeable/v1 and v2 migration flow, then review the stated storage and authorization requirements. Add migration coverage for migrated, removed, additive, and no-op values, update the upgrade example and its testdata and snapshots, and revise total_supply/mod.rs, packages/tokens/README.md, and the v0.9.0 release notes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- blockchain
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100