diff --git a/BACKLOG.md b/BACKLOG.md index c0ee64d..89821a3 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -9,16 +9,16 @@ Legend: `[ ]` open, `[~]` in flight, `[x]` done. ## P0 - [ ] Third-party security audit (events + profile). Cost ~$30-80k for a Soroban-experienced firm. Scope it to the 2.0.0 / 1.2.1 tree. -- [ ] **Upgrade mainnet to events 2.0.0 + profile 1.2.1** (`docs/upgrade-runbook.md`, Section 8). Carries every threat-model fix (`docs/threat-model.md` v1.1, Section 4.1.2), restores the 17,280-ledger upgrade timelock (EoP.10: the deployed 1.7.0 / 1.2.0 builds have none), and finishes the 1.7.0 migration that never ran: events still reports a 1.6.0 marker, and events 1 and 2 (`…609`, `…610`, both Completed with zero escrow) are still in the old layout and fail to decode. Rehearsed on testnet 2026-10-02 (events `CBEODVJG…` at 2.0.0, 185 events through `migrate_events`, `migrate` stamped). Backend first: read pages of 90, refund cranks of 15, hackathon selections split at 20 per call, `forfeit_milestone` on milestone rejection, `claim_refund` for held refunds, and no calls to the removed participation entry points. Then profile propose / apply / migrate, and events pause / propose / apply / one `migrate_events` page / `migrate` / unpause, all under the deployed zero timelock. `deploy_mainnet.sh` now refuses to unpause events until `migrate` has stamped, the gate the August upgrade lacked. +- [ ] **Upgrade mainnet to events 1.8.0 + profile 1.2.1** (`docs/upgrade-runbook.md`, Section 8). Events 1.8.0 is the 2.0.0 code under a 1.x label (tag `events-v1.8.0`), so the backend keeps grants and held-refund claims off on mainnet until a 2.x upgrade. Carries every threat-model fix (`docs/threat-model.md` v1.1, Section 4.1.2), restores the 17,280-ledger upgrade timelock (EoP.10: the deployed 1.7.0 / 1.2.0 builds have none), and finishes the 1.7.0 migration that never ran: events still reports a 1.6.0 marker, and events 1 and 2 (`…609`, `…610`, both Completed with zero escrow) are still in the old layout and fail to decode. Rehearsed on testnet 2026-10-02 (events `CBEODVJG…` at 2.0.0, 185 events through `migrate_events`, `migrate` stamped). Backend first: read pages of 90, refund cranks of 15, hackathon selections split at 20 per call, `forfeit_milestone` on milestone rejection, `claim_refund` for held refunds, and no calls to the removed participation entry points. Then profile propose / apply / migrate, and events pause / propose / apply / one `migrate_events` page / `migrate` / unpause, all under the deployed zero timelock. `deploy_mainnet.sh` now refuses to unpause events until `migrate` has stamped, the gate the August upgrade lacked. ## P1 -- [ ] **Events 2.1.0 (grant milestone splits) to mainnet**, as its own upgrade after 2.0.0, waiting the 17,280-ledger timelock. Before it goes: rerun Scout on the 2.1.0 code and add it to `docs/threat-model.md` Section 6 (the scan covers 2.0.0; the testnet grant run already passed on 2.1.0, `docs/audit/testnet-grant-run-2.1.0.md`). Refresh the storage replay from 2.0.0 once mainnet runs it (`docs/storage-compatibility.md`). +- [ ] **Events 2.1.0 (grant milestone splits) to mainnet**, as its own upgrade after 1.8.0, waiting the 17,280-ledger timelock. Before it goes: rerun Scout on the 2.1.0 code and add it to `docs/threat-model.md` Section 6 (the scan covers 2.0.0; the testnet grant run already passed on 2.1.0, `docs/audit/testnet-grant-run-2.1.0.md`). Refresh the storage replay from 1.8.0 once mainnet runs it (`docs/storage-compatibility.md`). - [ ] Per-event prize claim window: `PRIZE_CLAIM_WINDOW_SECS` (90 days) is a module constant because adding a field to `EventRecord` requires a storage migration. Move it onto `EventRecord` at the next real migration window, per the per-event-config rule in `CLAUDE.md`. - [ ] `Error` enum headroom: events uses 42 of the 50-case `contracterror` cap after 2.0.0, and retired codes are never reused. Plan a consolidation before the cap, not when the next variant fails to compile. - [ ] Grant committee multi-sig primitive (dedicated signer set + quorum at the contract level vs the current address-level multi-sig). - [ ] Multi-token support audit: verify the whitelist mechanism handles tokens with non-Stellar 7-decimal scales (currently assumed uniform). -- [ ] Decide whether to appoint a crowdfunding release validator on mainnet once 2.0.0 is live, and who holds the key (`docs/admin-custody-policy.md` §4.1). Until one accepts, the admin multisig co-signs every crowdfunding release. +- [ ] Decide whether to appoint a crowdfunding release validator on mainnet once 1.8.0 is live, and who holds the key (`docs/admin-custody-policy.md` §4.1). Until one accepts, the admin multisig co-signs every crowdfunding release. ## P2 (roadmap) diff --git a/README.md b/README.md index 81abc2e..c391a96 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ boundless-contract/ |-----|----------------| | `docs/ARCHITECTURE.md` | What lives on chain and why, roles, cross-contract calls | | `docs/DEPLOYMENT.md` | Fresh deploy on testnet or mainnet, token registration, admin rotation | -| `docs/upgrade-runbook.md` | Upgrading live contracts, including mainnet 1.7.0 to 2.0.0 | +| `docs/upgrade-runbook.md` | Upgrading live contracts, including mainnet 1.7.0 to 1.8.0 | | `docs/contract-ops-runbook.md` | Building, simulating, signing and submitting admin transactions | | `docs/multisig-guide.md` | Plain-English guide for signers; provisioning the multisig | | `docs/admin-custody-policy.md` | Custody policy: signers, thresholds, rotation, release validator | diff --git a/docs/admin-custody-policy.md b/docs/admin-custody-policy.md index 0f99fd3..f781e85 100644 --- a/docs/admin-custody-policy.md +++ b/docs/admin-custody-policy.md @@ -29,7 +29,7 @@ The admin authority on `boundless-events` gates: - `set_admin(new_admin)`: rotate the admin authority (two-step with `accept_admin`). - `pause()`: emergency stop on every event-lifecycle call. Admin calls keep working. - `unpause()`: resume them. -- `propose_upgrade(wasm_hash, version)`, `apply_upgrade()`, `cancel_pending_upgrade()`: replace contract logic. Mainnet builds from events 2.0.0 and profile 1.2.1 make `apply_upgrade` wait 17,280 ledgers (about a day) after the proposal. `migrate_events(max_events)` and `migrate()` finish an upgrade (`docs/upgrade-runbook.md`). +- `propose_upgrade(wasm_hash, version)`, `apply_upgrade()`, `cancel_pending_upgrade()`: replace contract logic. Mainnet builds from events 1.8.0 and profile 1.2.1 make `apply_upgrade` wait 17,280 ledgers (about a day) after the proposal. `migrate_events(max_events)` and `migrate()` finish an upgrade (`docs/upgrade-runbook.md`). - `register_supported_token(token)` / `deregister_supported_token(token)`: token whitelist. - `propose_release_validator(target)`, `cancel_pending_release_validator()`, `clear_release_validator()`: appoint, withdraw a proposal for, or remove the co-signer for crowdfunding milestone releases (from events 2.0.0; see §4.1). The proposed key must accept with its own signature. While none is appointed, the admin co-signs those releases itself. diff --git a/docs/multisig-guide.md b/docs/multisig-guide.md index c251c53..7776413 100644 --- a/docs/multisig-guide.md +++ b/docs/multisig-guide.md @@ -577,7 +577,7 @@ On the events contract the admin **can:** - Change the default fee rate (`set_fee_bps`). - Change the fee account address (`set_fee_account`). This one needs all three signatures. - Pause the contract in an emergency (`pause`) and resume after a fix (`unpause`). -- Propose a contract upgrade (`propose_upgrade`). On current mainnet builds the upgrade cannot apply until about a day (17,280 ledgers) after the proposal, so users can see it coming. The deployed events 1.7.0 and profile 1.2.0 builds have no such wait; the 2.0.0 upgrade restores it. +- Propose a contract upgrade (`propose_upgrade`). On current mainnet builds the upgrade cannot apply until about a day (17,280 ledgers) after the proposal, so users can see it coming. The deployed events 1.7.0 and profile 1.2.0 builds have no such wait; the 1.8.0 upgrade restores it. - Apply a proposed upgrade after that wait (`apply_upgrade`), or cancel it (`cancel_pending_upgrade`). - Run the post-upgrade migration (`migrate_events`, then `migrate`). - Add or remove tokens from the whitelist (`register_supported_token`, `deregister_supported_token`). diff --git a/docs/storage-compatibility.md b/docs/storage-compatibility.md index 7189fd7..ee64505 100644 --- a/docs/storage-compatibility.md +++ b/docs/storage-compatibility.md @@ -59,7 +59,7 @@ To check that the fixture still reproduces from the history archive (Stellar CLI ## Refreshing for the next upgrade -Once mainnet runs 2.0.0, the replay should start from 2.0.0 instead: +Once mainnet runs 1.8.0 (tag `events-v1.8.0`), the replay should start from 1.8.0 instead: 1. Fetch the deployed wasm for both contracts by hash into the fixtures directory and replace the two `mainnet-*.wasm` files. diff --git a/docs/threat-model.md b/docs/threat-model.md index 0bcc071..9796081 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -4,7 +4,7 @@ **Date:** 8 October 2026 **Prepared for:** Stellar Development Foundation, Soroban Security Audit Bank **Prepared by:** Boundless Engineering -**Contracts under review:** `boundless-events` 2.0.0 and `boundless-profile` 1.2.1, plus the grant milestone split that `boundless-events` 2.1.0 adds (Tamp.20), which reaches mainnet in a later upgrade. Mainnet runs 1.7.0 and 1.2.0 until the upgrade in Section 4.4. +**Contracts under review:** `boundless-events` 2.0.0 and `boundless-profile` 1.2.1, plus the grant milestone split that `boundless-events` 2.1.0 adds (Tamp.20), which reaches mainnet in a later upgrade. Mainnet runs 1.7.0 and 1.2.0 until the upgrade in Section 4.4, which installs the 2.0.0 code as events 1.8.0. **Repository:** github.com/boundlessfi/boundless-contract **Changes in 1.3:** events 2.1.0 lets a grant pay its milestones fixed, unequal shares of each award (`ReleaseKind::Split`), adding Tamp.20. The testnet grant run was repeated on 2.1.0 with a split grant; the static analysis covers 2.0.0 and is due again for 2.1.0. **Changes in 1.2:** reworded for clarity, retired threats moved to their own table, flow steps renumbered after the participation removal, statuses limited to four defined values, and Section 4 reorganized. @@ -163,7 +163,7 @@ The model relies on these assumptions. A threat that needs one of them to fail i | Parameter | Value | |---|---| | Maximum fee | 1,000 bps (10%) | -| Upgrade timelock | 17,280 ledgers (about one day) on mainnet builds from 2.0.0; 0 on testnet builds. The deployed 1.7.0 build has 0 (Section 4.4). | +| Upgrade timelock | 17,280 ledgers (about one day) on mainnet builds from 2.0.0, including its 1.8.0 mainnet label; 0 on testnet builds. The deployed 1.7.0 build has 0 (Section 4.4). | | Upgrade proposal expiry | 518,400 ledgers (about 30 days) | | Admin rotation and release validator acceptance windows | 120,960 ledgers (about 7 days) each | | Manager proposal expiry | 17,280 ledgers (about one day) | @@ -388,7 +388,7 @@ Of the 73 active threats, 10 are P1, 23 are P2, 23 are P3 and 17 are P4. | **EoP.7** | The owner must sign the same transaction as the co-signer, the money goes only to the owner, and the amount is calculated from the pool. The backend prepares the transaction only for the signed-in owner. | Mitigated | | **EoP.8** | The manager can select, cancel and rotate. It cannot change the owner (no function exists), receive refunds, pay milestones, or change floors. It can award any address, including its own: judging is off chain by design, and the owner chooses whom to delegate to. `ManagerChanged` is on chain before any selection. | Accepted | | **EoP.9** | The recipient must hold an award in the grant roster, and the amount comes from that award (grants) or from `remaining_escrow / milestones_left` (crowdfunding), never from the caller. Each milestone is settled once and must be in range. | Mitigated | -| **EoP.10** | Mainnet builds have a 17,280-ledger upgrade timelock from events 2.0.0 and profile 1.2.1; testnet builds keep zero, and `deploy_mainnet.sh` always builds without the testnet setting. The deployed 1.7.0 and 1.2.0 builds have no timelock until the upgrade in Section 4.4, which itself runs without one. Tests: `apply_upgrade_waits_out_the_timelock`, `apply_upgrade_waits_out_the_timelock_profile`. | Mitigated | +| **EoP.10** | Mainnet builds have a 17,280-ledger upgrade timelock from events 2.0.0 (1.8.0 on mainnet) and profile 1.2.1; testnet builds keep zero, and `deploy_mainnet.sh` always builds without the testnet setting. The deployed 1.7.0 and 1.2.0 builds have no timelock until the upgrade in Section 4.4, which itself runs without one. Tests: `apply_upgrade_waits_out_the_timelock`, `apply_upgrade_waits_out_the_timelock_profile`. | Mitigated | | **EoP.11** | `admin_slash_reputation` requires the admin and a non-empty reason, emits `AdminReputationSlashed`, and is the only way to lower a score. | Mitigated | | **EoP.12** | Both migration functions require the admin, the cursor only moves forward, and `migrate` runs once per version. | Mitigated | | **EoP.13** | After the first binding, a change is `propose_events_contract`, a 17,280-ledger wait, then `accept_events_contract` within 7 days, all admin-only and all emitting events. The wait only means something once the profile's upgrade timelock is deployed (EoP.10). | Mitigated | @@ -461,10 +461,10 @@ These threats concerned on-chain applications and submissions, which 2.0.0 remov | Item | What is needed | |---|---| -| Mainnet upgrade | Mainnet runs events 1.7.0 and profile 1.2.0. Every fix in Section 4.2 takes effect only after the admin multisig upgrades both, to events 2.0.0 and profile 1.2.1, following `docs/upgrade-runbook.md` Section 8. The deployed builds have no timelock, so this upgrade applies immediately; every later one waits 17,280 ledgers (EoP.10). | +| Mainnet upgrade | Mainnet runs events 1.7.0 and profile 1.2.0. Every fix in Section 4.2 takes effect only after the admin multisig upgrades both, to events 1.8.0 (the 2.0.0 code, tag `events-v1.8.0`) and profile 1.2.1, following `docs/upgrade-runbook.md` Section 8. The deployed builds have no timelock, so this upgrade applies immediately; every later one waits 17,280 ledgers (EoP.10). | | Old-layout records (Tamp.10) | Two mainnet event records cannot be read until converted. Both are completed events with no escrow, so no money is affected. Mainnet holds eight events, so one `migrate_events` call during the upgrade converts the two and leaves the others unchanged, and `migrate` then stamps the version. | | Backend changes that must ship first | Page winner and contributor reads at 90 or fewer and run refunds in batches of 15 or fewer (DoS.17); pages of 100 and batches of 25 already fail on the deployed build for large events. Split hackathon selections above 20 winners (DoS.17). Call `forfeit_milestone` when a grant milestone is rejected, or the grant can never be cancelled (EoP.14). Support `claim_refund` for refunds set aside (DoS.14). Stop calling the participation functions that 2.0.0 removed. | -| Events 2.1.0 | Grant milestone splits (Tamp.20) run on testnet from 8 October 2026 and reach mainnet in a separate upgrade after 2.0.0, which waits the 17,280-ledger timelock (`docs/upgrade-runbook.md` Section 9). | +| Events 2.1.0 | Grant milestone splits (Tamp.20) run on testnet from 8 October 2026 and reach mainnet in a separate upgrade after 1.8.0, which waits the 17,280-ledger timelock (`docs/upgrade-runbook.md` Section 9). | ### 4.5 Accepted risks diff --git a/docs/upgrade-runbook.md b/docs/upgrade-runbook.md index 32e874b..c2e7915 100644 --- a/docs/upgrade-runbook.md +++ b/docs/upgrade-runbook.md @@ -5,7 +5,7 @@ mainnet every admin step is a 2-of-3 multisig transaction that `./deploy_mainnet.sh` prepares and checks but never submits; on testnet `./deploy_and_upgrade.sh` signs with the local admin identity. Section 8 is the specific procedure for taking mainnet from events 1.7.0 and profile 1.2.0 -to events 2.0.0 and profile 1.2.1. +to events 1.8.0 and profile 1.2.1. Related: `docs/contract-ops-runbook.md` (signing and submitting a multisig transaction), `docs/admin-custody-policy.md` (who signs what), @@ -34,7 +34,7 @@ There is no `upgrade()` entry point. Both contracts expose `version()`, The timelock that applies is the one compiled into the wasm that is live when you propose, not the one you are proposing. The deployed mainnet builds, events 1.7.0 and profile 1.2.0, were compiled with 0 for the August rollout; -events 2.0.0 and profile 1.2.1 restore 17,280. +events 1.8.0 and profile 1.2.1 restore 17,280, as does every later build. Pausing events blocks every event-lifecycle call: `create_event`, `add_funds`, `select_winners`, `claim_prize`, `claim_milestone`, `forfeit_milestone`, @@ -263,7 +263,15 @@ Confirm the proposal with `get_pending_events_contract` before preparing the accept. `deploy_mainnet.sh` refuses upgrade steps while a rotation is pending, so finish or cancel it first. -## 8. Mainnet 1.7.0 to 2.0.0 +## 8. Mainnet 1.7.0 to 1.8.0 + +Mainnet takes the 2.0.0 code under a 1.x label. Tag `events-v1.8.0` (commit +`7a7295f` on branch `release/events-1.8.0`) is the 2.0.0 commit `8b4f054` with +only the events version changed, so this branch keeps building 2.1.0 for +testnet. The backend turns on +grants and held-refund claims for events 2.0.0 or later, so both stay off on +mainnet until a 2.x upgrade. A refund 1.8.0 holds back during a cancellation +stays claimable on chain and can be paid once that upgrade lands. ### 8.1 Where mainnet stands @@ -272,7 +280,7 @@ so finish or cancel it first. | Contract | `CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ` | `CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC` | | Live version | 1.7.0 | 1.2.0 | | Migration marker | 1.6.0 | none | -| Target | 2.0.0 | 1.2.1 | +| Target | 1.8.0 | 1.2.1 | | Upgrade timelock of the live build | 0 | 0 | The admin of both is the 2-of-3 multisig @@ -286,7 +294,7 @@ Events holds 8 events. The 1.7.0 `migrate_events` never ran (there is no pre-1.7.0 record layout and fail to decode on read. Both are Completed with zero escrow. The other six are in the current layout. -Because the live builds have a zero timelock, each 2.0.0 and 1.2.1 proposal +Because the live builds have a zero timelock, each 1.8.0 and 1.2.1 proposal can be applied as soon as it lands, in the same session. Every upgrade after this one waits 17,280 ledgers. @@ -294,6 +302,8 @@ Re-read all of this with `upgrade-status` and `get_event` before signing. ### 8.2 What has to ship in the backend first +All of this is in production (nestjs #867 and #874). + - Read winners and contributors in pages of at most 90 rows. - Crank cancellation refunds in batches of at most 15 per `process_cancel_batch`. @@ -303,19 +313,26 @@ Re-read all of this with `upgrade-status` and `get_event` before signing. - Use `claim_refund` and `get_unclaimed_refund` for refunds the contract had to hold back. - Stop calling the participation entry points (`apply`, `apply_to_bounty`, - `remove_applicant`, `submit`) and the applicant and submission reads; 2.0.0 + `remove_applicant`, `submit`) and the applicant and submission reads; 1.8.0 removes them and participation lives in the backend. ### 8.3 Steps -Rehearsed on testnet on 2026-10-02: events `CBEODVJG…` went to 2.0.0 (wasm -`70791d71…`), 185 events paged through `migrate_events` in 24 calls, then -`migrate` stamped 2.0.0. If testnet profile still reports 1.2.0, upgrade it to -1.2.1 there first. +The same code was rehearsed on testnet under its 2.0.0 label on 2026-10-02: +events `CBEODVJG…` went to 2.0.0 (wasm `70791d71…`), 185 events paged through +`migrate_events` in 24 calls, then `migrate` stamped 2.0.0. Testnet has since +moved to 2.1.0, so the 1.8.0 build itself is proven by the storage replay on +the tag (`./scripts/test-storage-compat.sh`). -Build both wasms per Section 2 and set the environment: +Check out `events-v1.8.0`, build both wasms per Section 2 and set the +environment. The builds must hash to events +`acf70ca019f75ee6bc60f7806254dfb3508fd5a76634c708977e6030870dc654` and profile +`8a8a1208801c36f5d6e0b90eca4783f3d6789a10b26cf40b062782bfae5731c0`. ```sh +git checkout events-v1.8.0 +./scripts/build-release-linux.sh + export STELLAR_NETWORK=boundless-mainnet export ADMIN_SOURCE=GCVK72I6TVJVDTTY4UKU6MQT4QJ2T2AAG3NULNEUDM46L3UOQYDSO4O2 export EVENTS_ID=CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ @@ -326,11 +343,11 @@ PROFILE_WASM=target/release-linux/boundless_profile.wasm ./deploy_mainnet.sh upgrade-status ./deploy_mainnet.sh upload-profile-wasm "$PROFILE_WASM" 1.2.1 -./deploy_mainnet.sh upload-events-wasm "$EVENTS_WASM" 2.0.0 +./deploy_mainnet.sh upload-events-wasm "$EVENTS_WASM" 1.8.0 ``` Profile first. Either order is safe: events 1.7.0 never calls the -`slash_reputation` entry point that 1.2.1 removes, and events 2.0.0 only calls +`slash_reputation` entry point that 1.2.1 removes, and events 1.8.0 only calls functions that 1.2.0 already has. Profile stays unpaused. Each line below is one signing ceremony; submit it and wait for it to land before the next. @@ -346,18 +363,18 @@ Then events: ```sh ./deploy_mainnet.sh prepare-pause-events events-pause.xdr -./deploy_mainnet.sh prepare-propose-upgrade-events "$EVENTS_WASM" 2.0.0 events-propose.xdr -./deploy_mainnet.sh verify-proposed-upgrade-events "$EVENTS_WASM" 2.0.0 -./deploy_mainnet.sh prepare-apply-upgrade-events "$EVENTS_WASM" 2.0.0 events-apply.xdr +./deploy_mainnet.sh prepare-propose-upgrade-events "$EVENTS_WASM" 1.8.0 events-propose.xdr +./deploy_mainnet.sh verify-proposed-upgrade-events "$EVENTS_WASM" 1.8.0 +./deploy_mainnet.sh prepare-apply-upgrade-events "$EVENTS_WASM" 1.8.0 events-apply.xdr ./deploy_mainnet.sh prepare-migrate-events-page 8 events-page.xdr -./deploy_mainnet.sh prepare-migrate-upgrade-events 2.0.0 events-migrate.xdr -./deploy_mainnet.sh prepare-unpause-events 2.0.0 events-unpause.xdr -./deploy_mainnet.sh verify-upgrade-events "$EVENTS_WASM" 2.0.0 +./deploy_mainnet.sh prepare-migrate-upgrade-events 1.8.0 events-migrate.xdr +./deploy_mainnet.sh prepare-unpause-events 1.8.0 events-unpause.xdr +./deploy_mainnet.sh verify-upgrade-events "$EVENTS_WASM" 1.8.0 ``` One page of 8 converts all eight events, so the page command should report 0 remaining. If more events exist by then, repeat the page until it reports 0; -`migrate` refuses with error 97 until it does. Until 2.0.0 is applied, +`migrate` refuses with error 97 until it does. Until 1.8.0 is applied, `upgrade-status` cannot read the release validator (1.7.0 has no such function). @@ -375,7 +392,7 @@ After the unpause: Rollback: before the events apply, cancel and unpause with `UNPAUSE_UNMIGRATED=1` (the marker was already 1.6.0). After the apply, a -rollback to 1.7.0 waits the 17,280-ledger timelock that 2.0.0 carries, with +rollback to 1.7.0 waits the 17,280-ledger timelock that 1.8.0 carries, with events paused throughout; after the first page, fix forward. ## 9. Events 2.1.0 (grant milestone splits) @@ -389,7 +406,8 @@ wasm `c742cb96…` uploaded and proposed (`fc713ba2…`), applied (`d3f1e8c0…` 10 `migrate_events` pages, then `migrate` stamped 2.1.0 (`d91f1ddc…`). Grants created on 2.0.0 read and pay as before. -Mainnet takes 2.1.0 only after Section 8 has put it on 2.0.0, as a separate -upgrade that waits the 17,280-ledger timelock. The backend sends a split only +Mainnet takes 2.1.0 only after Section 8 has put it on 1.8.0, as a separate +upgrade that waits the 17,280-ledger timelock. That upgrade also turns on +grants and held-refund claims, which the backend allows from 2.0.0. The backend sends a split only to a contract that reports 2.1.0 or later, so nothing has to ship first.