Every ledger entry a Tribe Builder contract owns, how long it lives, who renews it and what that costs. The design
behind it is STORAGE_DIET.md; this page is the operating reference and must change with any
storage layout change (the e2e tests protocol_runs_on_chain_state_only and
a_dao_kept_alive_by_the_sweep_alone_works_after_a_year fail when the inventory drifts).
Numbers are testnet, measured 2026-10-12. Rent rates move with the size of the network's live state, so
re-read them before quoting mainnet costs (stellar network settings, the Renewal tab's quote, or
renew-code-ttl.mjs --dry-run).
| Setting | Testnet value | What it bounds |
|---|---|---|
max_entry_ttl |
3,110,400 ledgers (180 days) | The longest any extension can reach |
min_persistent_ttl |
120,960 ledgers (7 days) | The TTL a new persistent entry or new code is born with |
tx_max_footprint_entries |
400 | Keys in one renewal transaction |
tx_max_disk_read_entries / _bytes |
200 / 200,000 | Entries and bytes one transaction reads (binds the sweep) |
tx_max_write_bytes |
132,096 | Bytes one transaction writes (binds restores) |
tx_max_instructions |
400,000,000 | CPU per transaction |
tx_max_contract_events_size_bytes |
16,384 | Events plus return value per invocation |
| Transaction fee | u32: 4,294,967,295 stroops (~429 XLM) | The most one transaction can pay |
Ledger time is estimated at 5 seconds, 17,280 ledgers per day, everywhere in the app and scripts.
Rent is charged per byte of key plus value, for every ledger an entry's TTL is extended. An extension pays only for the ledgers it adds: extending an entry that already has 170 days left to 180 days pays for 10 days. So rent is linear in time: renewing for 180 days twice costs the same as renewing for 30 days twelve times, less the per-transaction overhead. The Renewal tab defaults to 180 days for that reason.
| Entry kind | Measured rate | How |
|---|---|---|
| Contract data and instances | ~0.0098 XLM per KB per day | Simulated ExtendFootprintTTL of the Manager and the six DAO 0 instances (884 and 6,420 bytes, 167.7 → 180 days): 98 stroops per byte-day |
| Contract code | ~0.051 XLM per KB per day | A 35,604-byte code entry for ~164 days simulated at 2,998,799,289 stroops (testnet, 2026-10-12): 514 stroops per byte-day, about five times data |
| What | 30 days | 120 days | 180 days | One year of upkeep |
|---|---|---|---|---|
| 1 KB of data | 0.29 XLM | 1.2 XLM | 1.8 XLM | 3.6 XLM |
| A DAO's six instances (6.4 KB) | 1.9 XLM | 7.5 XLM | 11.2 XLM | 23 XLM |
| Reference DAO sweep (32 KB, below) | 9.5 XLM | 38 XLM | 57 XLM | ~115 XLM |
| One token's artwork record (~19 B) | ~0.07 XLM |
The reference sweep was 117 KB (~420 XLM a year, plus ~240 XLM a year more for every year of daily auctions) before D32–D38 (below). Who pays which part of all this, role by role, is in STORAGE_COSTS.md.
Creating a DAO pays the six new instances up to 170 days (about 10.6 XLM at this rate), because a new entry is born at 7 days, under the 60-day instance threshold. The app's fee guard is therefore 50 XLM, not 10 (see Fee guard).
| State | Extended to | When at or under | Source |
|---|---|---|---|
Contract instances (module config, admin, wiring, Live, CurrentHash) |
170 days | 60 days | common::ttl::extend_instance, on every state-changing call |
| Shared contract code | 4 days | 2 days | common::ttl::extend_instance (code bumped separately, D27); the platform prepays 170 days |
Persistent entries (common::ttl::extend_persistent) |
365 days, clamped to the network maximum | 30 days | Read or written by a contract call |
Governor ProposalTime(id), ProposalId(seq) |
120 days | 30 days | Covers the longest lifecycle (3 × 30-day delays + 14 days to execute) from one touch |
Marketplace Listing(id) (resales) |
30 days | 7 days | Expired listings can be cleared by anyone |
| OpenZeppelin owners, balances, votes, checkpoints | 30 days | 29 days | The library, when the entry is read or written |
| Asset contract (SAC) balances | 30 days | 29 days | The host, when the balance is read or written |
The host extends when the TTL is at or below the threshold (common test thresholds_include_their_bound). A
threshold well under the target means ordinary reads do not pay rent on every touch. Getters simulated by a client
are not transactions and renew nothing.
Every Tribe Builder key is namespaced: (Symbol("builder"), [Symbol(Variant), args…]) (D30, D37). OpenZeppelin keys
keep OZ's own shape. Off-chain code builds these keys with apps/web/src/lib/storage-key.ts, whose bytes are
pinned against the Rust encoding. The namespace adds 28 bytes per key and no host allocation (the symbol stays
inline in a Val). Each crate's storage::key_samples lists every
variant through an exhaustive match, and its storage_keys_do_not_collide test keeps the Tribe Builder enums disjoint.
"Sweep" means the DAO's renewal sweep covers the key. "Holder" means it renews when its owner acts and is restored by the app before the owner's next transaction (see Archived member state). Sizes are from the reference DAO below.
| Key | Class | Holds | Renewed by |
|---|---|---|---|
Admin |
instance | The module admin (launch admin, then the Treasury) | instance |
CurrentHash, StorageVersion |
instance | The running wasm hash and storage layout version; version() looks the hash up in the Manager registry |
instance |
Live |
instance | Whether the DAO has launched | instance |
| Key | Class | Holds | Size | Renewed by |
|---|---|---|---|---|
MetadataContract, Manager, Treasury |
instance | Core wiring | instance | |
Modules, Settings, TransferAgents |
instance | The module registry (≤ MAX_MODULES entries), the membership switches, the transfer agents |
instance | |
MintAllowance(module) |
persistent | What a module may still mint; absent at zero, u32::MAX unlimited |
~170 B | Sweep (one per registered module with an allowance) |
OZ Metadata, TokenIdCounter, NumTotalSupplyCheckpoints |
instance | Name, symbol, next id, checkpoint count | instance | |
OZ TotalSupplyCheckpoint(i) |
persistent | Supply history | ~180 B each | Sweep: the latest only (D33) |
OZ Owner(t), Balance(a) held by the Treasury or a registered module |
persistent | The DAO's own tokens (unsold auction lots, pool stock, anything given back) | ~130 B | Sweep (holdings) |
OZ Owner(t), Balance(a), Delegatee(a), NumCheckpoints(a), DelegateCheckpoint(a, i), VotingUnits(a) of members |
persistent | Per-member ownership and votes | Holder | |
OZ Approval(t), ApprovalForAll(o, op) |
temporary | Approvals; expire by design | Never renewed |
token_uri checks existence by the id counter, not Owner(t), so a member's token keeps rendering after their
30-day ownership entry archives.
| Key | Class | Holds | Bound | Renewed by |
|---|---|---|---|---|
Settings |
instance | The token address and the renderer base token_uri points images at |
≤ 256-byte URL | instance |
Profile |
instance | profile_hash and profile_seq of the event-only profile JSON (≤ 4,096 bytes, in events only) |
Fixed | instance |
ItemCounts, IpfsGroupCount |
instance | Item count per property (≤ 16), groups in use; all a mint reads | instance | |
Manager, Treasury |
instance | Wiring | instance | |
PropertyNames |
persistent | Every property name (≤ 16 × 64 B), in one entry | ~460 B for 16 | Sweep |
Items(property, page) |
persistent | Up to 32 items: their names, plus one byte each for the IPFS group | ≤ 32 pages per property; ~580 B for 32 short names | Sweep |
IpfsGroups |
persistent | Every IPFS group (≤ 64) in one vector | ~1.4 KB | Sweep |
AttributesPage(p) |
persistent | Packed trait records of 64 tokens | one per 64 minted tokens; ~1.2 KB per full page | Sweep |
An attribute record is a header byte (the trait count; the high bit marks two-byte indexes, needed only past 256 items in a property) and one byte per trait: 17 bytes for 16 traits. Every mint writes its token's record, empty until artwork exists, so the pages up to the minted count always exist and an archived one is detectable (D32).
| Key | Class | Holds | Size | Renewed by |
|---|---|---|---|---|
Treasury, QueueDelay, Manager, ProposalCount |
instance | Wiring, settings, the proposal counter | instance | |
ProposalTime(id) |
persistent | (proposer, 49 packed bytes): snapshot, window, eta, state, seq and the quorum fixed at creation (D38) |
~270 B | Sweep while live |
ProposalId(seq) |
persistent | The id of proposal seq, so proposals are listable from chain |
~170 B | Sweep while live |
OZ VotingDelay, VotingPeriod, ProposalThreshold, NumQuorumCheckpoints, TokenContract |
instance | Governance settings | instance | |
OZ QuorumCheckpoint(i) |
persistent | Quorum rate history | ~170 B each | Sweep: the latest only (D33) |
OZ ProposalVote(id) |
persistent | Vote tallies; OZ renews it 30 days per vote or queue, the sweep to the full term while live | Sweep while live, when present | |
OZ HasVoted(id, voter) |
persistent | One vote record, 30 days | Holder |
Each proposal stores its quorum when it is created, from the voting supply at the previous ledger (the snapshot
ledger, read from the two latest checkpoints only) and the current rate, so proposal_state, queue and
execute never look up historical checkpoints. Old ones may archive; the quorum(ledger) view for a past
ledger then needs them restored. Ended proposals leave the sweep; the id list then has gaps. A contract call
that reads an archived key fails as a whole, so proposal_ids(start, limit) fails for any page holding an
archived id; chain-reading clients read the ProposalId(seq) entries raw with getLedgerEntries instead
(apps/web/src/lib/chain-reads/proposals.ts), where an archived entry is simply absent.
Modules list their own sweep keys (DaoModule::renewal_keys(cursor, limit), at most 200 per page; membership-models R3):
the sweep reads the Token's registry and pages through each module by simulation, then reads which of the listed
keys exist. A module registered from outside the Manager without renewal_keys has only its instance renewed, and
Manage → Storage renewal says so.
| Key | Class | Holds | Renewed by |
|---|---|---|---|
Treasury Governor, Manager |
instance | Wiring | instance |
Auction Config, Auction, Manager, PaymentTokenLocked |
instance | Settings and the running auction | instance |
Auction PendingRefund(bidder) |
persistent | A refund the bidder can withdraw | Holder |
Marketplace Config, NextPoolId, OpenPools |
instance | Settings, the pool counter, the open pools (≤ MAX_OPEN_POOLS) with their page counts |
instance |
Marketplace Listing(id) |
persistent | An open resale | Sweep while open (renewal_keys lists every slot up to the minted count; only present ones are renewed) |
Marketplace Pool(id), PoolStock(id, page) |
persistent | An open pool's terms and its stocked ids, 64 per page (D15 superseded: a 100-token pool is 3 entries, not 100) | Sweep while open |
Marketplace PoolBuyer(id, buyer) |
persistent | How many a buyer took from a pool | Holder |
ClaimMinter Config, NextListId, AssuranceList |
instance | Wiring and payment asset, the list counter, the assurance list | instance |
ClaimMinter List(id) |
persistent | ClaimList { root, tree_uri, asset, price, expires_at, total, minted, claimers, threshold_claimers, deadline, escrow }; tree_uri is ipfs://, ≤ 256 B |
Sweep |
ClaimMinter Claim(id, claimer) |
persistent | ClaimRecord { claimed, escrowed }: progress and any escrowed payment |
Holder |
| Asset `Balance(treasury | module) on every asset a module reports (renewal_keys assets`), the auction's payment asset, and every open pool's or list's asset |
persistent (SAC) | The DAO's funds and escrow in the assets it trades in |
Asset Balance(treasury) on any other asset (donations, fees in a former payment asset) |
persistent (SAC) | Funds the inventory cannot discover from chain | Restored when a proposal spends them (the proposal page restores before signing); add them to the sweep with renew-dao.mjs --asset |
| Key | Class | Holds | Renewed by |
|---|---|---|---|
ManagerAdmin, FactoryPaused, Current*Wasm (including CurrentClaimsWasm), CurrentManagerVersion |
instance | Platform admin, pause flag, current hashes | Platform |
Implementation(hash), LatestImplementation(name) |
persistent | The registry version() reads, and each name's latest |
Platform (renew-code-ttl.mjs) |
UpgradeApproval(from, to), PendingAdmin |
persistent | Approved upgrade paths, an admin handoff | Used soon after they are written; restore an old one if needed |
PendingDao(token) |
persistent | A created DAO awaiting launch; removed at launch | Sweep, when present |
Assurance(token) |
persistent | An assurance drive (its ClaimMinter, list, deadline, launch config); removed at launch, kept after a failed drive | Sweep, when present |
ModuleCount(token, kind) |
persistent | How many modules of a kind create_module deployed (the n in the next one's salt) |
Sweep, when present |
SlugToDao(slug), DaoSlug(token) |
persistent | The DAO's /dao link, both directions (claimed at launch, or when a drive opens) |
Sweep |
16 properties of 19 items, 500 tokens, 50 proposals of which 10 are live, one token and one balance held by the
DAO (e2e reference_dao_sweep_fits_one_transaction, which prints this table):
| Class | Entries | Bytes | Before D32–D38 |
|---|---|---|---|
| Instances (core four, Auction, Marketplace) | 6 | 6,508 | 6,372 |
AttributesPage |
8 | 9,592 | 75,164 |
Items |
16 | 8,560 | 25,008 |
ProposalTime + ProposalId |
20 | 4,320 | 5,560 |
PropertyNames |
1 | 452 | 2,368 (16 keys) |
IpfsGroups |
1 | 1,432 | 1,436 |
MintAllowance (the started Auction's) |
1 | 168 | 688 as four MintAuthority grants |
DAO holdings (Owner, Balance) |
2 | 264 | not covered |
| Slug (both keys) | 2 | 348 | 356 |
| Latest supply and quorum checkpoints | 2 | 352 | 352, growing ~180 B per auction day |
| Total | 59 | 31,996 | 117,304 |
One ExtendFootprintTTL transaction holds it: 59 of 400 entries and 32 KB of the 200 KB read limit. A ClaimMinter
adds its instance and one List entry per list (a few hundred bytes, mostly its tree_uri); an open pool adds its Pool entry and one PoolStock page per 64
stocked tokens. At about 19
bytes per token, roughly 9,000 tokens fit before the read limit splits the sweep into a second transaction, and
the sweep no longer grows with time. Tokens the DAO itself keeps (unsold lots the Treasury holds) add ~130 bytes
each on top of that, so a DAO that accumulates unsold tokens reaches the limit sooner. Listing the inventory for
a large DAO reads every ownership entry (one getLedgerEntries call per 100 tokens).
Other measured budgets on the same fixtures:
| Call | CPU | Write bytes | Entries read / written | Event bytes |
|---|---|---|---|---|
create_dao with a 4 KB profile |
9.2 M | 6,024 | 25 / 9 | 7,380 |
propose, 20 actions, longest description |
3.3 M | 1,536 | 12 / 4 | 6,856 |
The DAO: one sweep. Manage → Storage renewal lists every key in the Sweep column from chain reads and each module's renewal_keys,
shows each class's earliest expiry, simulates the price for 30, 90, 180 (the default) or any number of days, shows
what a year costs at that rate, and renews them in one ExtendFootprintTTL transaction. Archived keys, including
ones the RPC returns with a past live-until, are restored first with RestoreFootprint, quoted the same way.
Anyone can renew; the signing wallet pays. A DAO that wants to reimburse the renewer does it by proposal. The same
logic runs on the command line:
node scripts/renew-dao.mjs --network testnet --token C... --days 180 --source <identity> [--dry-run]The e2e test a_dao_kept_alive_by_the_sweep_alone_works_after_a_year keeps an idle DAO alive with nothing but
this sweep (and the platform renewal) every 150 days while archiving everything else that expires. After a year it
settles an auction, a new member proposes and executes a Treasury spend, a year-old claim list is claimed, and an
old token renders. Its negative control shows an idle Treasury balance archiving when holdings are left out.
Sources: inventory and transactions, report and quote, tab, CLI.
The platform: code and registry. Code is never in a DAO's sweep: seven ~35 KB code entries alone exceed the 200 KB read limit. Contracts bump code only to 4 days when under 2, so if the platform's renewal lapses a member pays a few days of code rent at most. The platform prepays 170 days:
node scripts/renew-code-ttl.mjs deploys/builder-testnet-manager.json --days 170 [--token C...] [--dry-run]It finds code from the Manager's current_implementations() and each name's latest implementation, the Manager's
running wasm_hash(), and what every known DAO contract runs (DAO deploy artifacts, --token with the modules
its registry lists, or the indexer's manager.module_versions with DATABASE_URL), so a DAO that skipped an
upgrade keeps its code alive. It also renews each hash's Implementation(hash) record, every
LatestImplementation(name), and the Manager instance.
Code rent is the platform's largest cost: a dry run on 2026-10-12 quoted about 900 XLM to bring nine code hashes
to 170 days (Auction 264 XLM, Marketplace 251 XLM, Governor ~300 XLM). Every extension is simulated first; one
whose resource fee would exceed 300 XLM (70 % of the u32 fee cap) is done in steps (the Governor's in that dry run:
102 days for 178 XLM, then the rest). deploy-manager.mjs and upgrade-contract.mjs extend new code to 170 days
at upload the same way, since new code is born at 7 days. A platform dashboard is deferred (TODOS.md).
Members: on use. A member's ownership, balance, vote and claim entries renew for 30 days whenever they act.
A member back after more than a month may find their entries archived. The app restores them before the member's transaction, with one extra wallet signature and a small fee:
- calls the app builds in the browser (votes, proposals, admin actions, bids, artwork, slug, upgrades) set the
SDK's
restoreoption (restoreArchivedState), so aRestoreFootprintruns first; - server-prepared flows (token actions, marketplace, claims, Treasury funding) return an unsigned
RestoreFootprintwith status 409 (code: restore-required); the client signs and submits it, waits, and prepares again (withArchivedRestore).
Sources: restore-required.ts, restore-archived-client.ts.
Every app transaction signs through signWithWallet, which refuses a transaction whose maximum fee exceeds
50 XLM and explains that it would be paying shared code rent. The design called for 10 XLM, but creating a DAO
legitimately costs about 10 XLM of instance rent on testnet. Before D27 a single code bump cost ~300 XLM, well
above the guard. The Renewal tab passes the price the member already saw as the allowance.
stellar-sdk 17.1.0 counted the resource fee twice when AssembledTransaction.sign rebuilt a transaction, which
doubled every displayed Soroban fee and overflowed u32 above ~214 XLM. patches/@stellar__stellar-sdk@17.1.0.patch
fixes it; apps/web/src/lib/fee/sdk-sign-fee.test.ts fails if a new SDK version brings it back. Reported
upstream as stellar/js-stellar-sdk#1807 with fix
#1808; drop the patch once a release includes it.
Extension and restoration are separate operations. A key the inventory expects but RPC does not return, or returns
with a past liveUntilLedgerSeq, is archived; the Renewal tab and renew-dao.mjs restore it before extending. A
restore needs the write-bytes budget (132,096 bytes), so a large archived DAO restores in several transactions.
renew-code-ttl.mjs extends but does not restore code; archived code needs a RestoreFootprint from the operator.
Per-holder rights such as refunds and claim markers are not deleted by archival: their owner or anyone holding the key can restore them. Keep SECURITY_MODEL.md and MONITORING.md aligned with these limits.