You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(design): correct the legacy-grant framing and the landed #3785 dependency
Two facts from the full bot review that the earlier summary did not surface:
- #3785 is merged (2026-07-29), so "land it first or alongside" described a
dependency that no longer exists. Replaced with what actually remains: mock
entries are sweepable now, but TeeState::with_mocked_participant_attestations
still stores bare non-expiring Mock::Valid sentinels at init, which never fail
re-verification and so are never swept or granted.
- The legacy grandfather does not work the way we assumed when deciding to accept
it. A live node re-attests under rule 1, so its entry never fails
re-verification, is never swept, and yields no grant -- operators currently
running nodes get nothing and need nothing. Only *abandoned* entries convert to
grants. The number is still negligible today (14 mainnet, 31 testnet, nearly all
live) and the deploy-time count check still bounds it, but the rationale is
"abandoned entries are rare", not "it rewards our existing operators".
Also: say why prepay keeps the remainder instead of following the contract's
require_deposit + refund_to convention, and note that making the fee votable needs
the ConfigExt DTO plumbing and a borsh-schema snapshot, not just the migration.
Copy file name to clipboardExpand all lines: docs/design/operator-prepaid-attestation-storage.md
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -36,6 +36,8 @@ The counter is **available** grants, not lifetime total, so `0` means either "ne
36
36
| Method | Kind | Purpose |
37
37
|---|---|---|
38
38
|`prepay_attestation_storage(account_id)`|`#[payable]`| Grants `floor(attached / fee)`. Rejects below one fee, keeps any remainder. Permissionless — anyone may prepay for any account. |
39
+
40
+
Keeping the remainder deviates from the contract's usual `require_deposit` + `refund_to` pattern, deliberately: a grant is a discrete unit, so the leftover is at most one fee short of the next grant, and adding a transfer path to return sub-0.02 NEAR dust is not worth it. "No NEAR is ever returned" means exactly that — no withdrawal method, and no refund of an overpayment.
|`attestation_storage_fee() -> NearToken`| view | Current fee. |
41
43
@@ -140,13 +142,13 @@ Every new entry consumes a paid grant, so the contract funds no new attestation
140
142
141
143
Two accepted residuals:
142
144
143
-
-**Legacy entries yield a grant when swept**, since rule 3 does not distinguish them. That grandfathers the operators already running nodes — 14 entries on mainnet, 31 on testnet. The exposure is exactly "entries present at deploy become permanent capacity", so the count must be checked at deploy rather than assumed; a count in the thousands would mean the still-open drain was exploited first, and those entries should be purged rather than granted.
145
+
-**Abandoned pre-upgrade entries yield a grant when swept**, since rule 3 cannot distinguish them from paid ones. Note who this does *not* benefit: an operator running a live node re-attests under rule 1, so its entry never fails re-verification, is never swept, and yields nothing — live operators get no grant and need none. The grants go to whoever *abandoned* an entry. Today that is a negligible set (14 entries on mainnet and 31 on testnet, nearly all live), but the exposure is precisely "abandoned entries present at deploy become permanent capacity", so the count must be checked at deploy rather than assumed. A count in the thousands would mean the still-open drain was exploited before the release, and those entries should be purged rather than granted.
144
146
-**The contract carries the price risk on sold grants**, bounded by the number outstanding. It stores counts, not purchase prices, so it cannot retro-price them; a future update can address this if the gap becomes material.
145
147
146
148
## Implementation notes
147
149
148
-
1.Land [#3785](https://github.com/near/mpc/pull/3785)first or alongside, so abandoned entries are reclaimable.
149
-
2.**A state migration is required.** The grants map and the `Config` fee field both change `MpcContract`'s borsh layout: frozen snapshot module plus a `migrate()` arm the way `crates/contract/src/v3_13_0_state.rs` does it, and regenerated borsh-schema and ABI snapshots.
150
+
1.[#3785](https://github.com/near/mpc/pull/3785)has landed (`stamp_expiry_on_legacy_mocks`, `v3_13_0_state.rs:122`), so abandoned mock entries are already sweepable — no dependency to wait on. Reclaimability is not universal though: `TeeState::with_mocked_participant_attestations` still stores bare, non-expiring `Mock::Valid` sentinels at init, which never fail re-verification and so are never swept. Those stay contract-funded and never yield a grant.
151
+
2.**A state migration is required.** The grants map and the `Config` fee field both change `MpcContract`'s borsh layout: frozen snapshot module plus a `migrate()` arm the way `crates/contract/src/v3_13_0_state.rs` does it. Making the fee votable also needs the `ConfigExt` DTO plumbing in `crates/contract/src/dto_mapping.rs`, and both the borsh-schema and ABI snapshots regenerated.
150
152
3.**Re-validate the sweep gas budget.** Rule 3 adds a grants-map write per removed entry — a row *insert*, not an update, whenever the owner's row was deleted at zero — inside gas-bounded `clean_invalid_attestations`. Re-check `clean_invalid_attestations_tera_gas` and `RESHARE_CLEAN_INVALID_ATTESTATIONS_MAX_SCAN`, with a guard test of the kind [#3936](https://github.com/near/mpc/pull/3936) adds.
151
153
4. Check the stored-entry count at deploy, per [Security](#security).
152
154
5. Coordinate the runbook change with the release: operators onboarding after the upgrade must prepay or their node's submissions fail in a loop.
0 commit comments