Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a98fe0d
feat(sra): implement Service Rewards Actor per FIP-0118
LinZexiao Aug 13, 2026
fba807e
fix(sra): enforce input-domain bounds — overflow DoS (V1/V2/V3) + rem…
LinZexiao Aug 13, 2026
a85c97c
test(sra): add adversarial input matrix for external write functions …
LinZexiao Aug 13, 2026
9d976c5
docs(sra): document QA-system hardening (S2-S5) and fix src/test line…
LinZexiao Aug 13, 2026
fb4c14e
fix(ci): make forge lint --deny notes and foundry.lock pass
LinZexiao Aug 13, 2026
308cc0e
refactor(sra): address wjmelements review — Epoch typing, storage lib…
LinZexiao Aug 13, 2026
c015a35
docs(sra): track D1 f099 burn FIP follow-up (FIP-0118)
LinZexiao Aug 13, 2026
01d6bf4
docs(sra): complete per-category run commands in How to Run
LinZexiao Aug 13, 2026
6b7c850
fix(sra): sync Epoch uint64 narrowing (cherry-picked 8c3eff9)
LinZexiao Aug 14, 2026
c01625b
refactor(sra): make EPOCHS_PER_QUARTER public Epoch, use scratch-memo…
LinZexiao Aug 14, 2026
316341a
refactor(sra): Epoch-ify remaining quarter/window/hold immutables
LinZexiao Aug 14, 2026
46af2aa
refactor(sra): single params pointer in _checkPriceBand
LinZexiao Aug 14, 2026
32135ab
docs(sra): record EIP-170 size blocker and halmos-out-of-CI decision
LinZexiao Aug 14, 2026
6a5cb25
refactor(sra): add qEnd external view, make aggregatedFPV revert befo…
LinZexiao Aug 18, 2026
f3bb1aa
refactor(sra): adopt FIP-0118 off-chain FIL conversion (FIPs#1275)
LinZexiao Aug 19, 2026
f48d54e
chore(sra): clean up comments and condense design doc
LinZexiao Aug 19, 2026
533c3c6
refactor(sra): adopt FixedU18 for all USD computations
LinZexiao Aug 20, 2026
04fd2bb
chore(sra): drop halmos ComputeSharesCheck (FixedU18 symbolic limit)
LinZexiao Aug 20, 2026
c80111c
docs(sra): drop removed-check references in security tables
LinZexiao Aug 20, 2026
dcc196d
ci(sra): run halmos in a dedicated workflow
LinZexiao Aug 23, 2026
1ce28fb
refactor(sra): FIP-0118 review adaptation + three-piece aggregate mirror
LinZexiao Aug 23, 2026
b7376fe
fix(ci): pin halmos 0.1.13 for the symbolic workflow
LinZexiao Aug 23, 2026
fb940aa
fix(sra): future bound quarter SubmitShares is an all-zero no-op
LinZexiao Aug 23, 2026
befeb05
fix(sra): prevent activeQ regression and align remove exclusion with …
LinZexiao Aug 23, 2026
b521e61
fix(sra): allow skipping gap quarters and drive the quarter clock by …
LinZexiao Aug 24, 2026
387ceb9
feat(sra): port the uint64 id identity rework onto b521e61
LinZexiao Aug 24, 2026
48f6e01
refactor(sra): strip review-process references and line-number anchors
LinZexiao Aug 24, 2026
2701498
docs(sra): drop the id-identity impl documents
LinZexiao Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/halmos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Halmos

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
halmos:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: v1.7.1
cache: true

- name: Set up Python
# halmos 0.1.x predates the halmos 0.3 CLI (which dropped
# --no-test-constructor and changed solver defaults); pin the version
# verified locally (QuarterWindowCheck 2/2 PASS) on a Python it supports.
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install Dependencies
run: |
forge install

- name: Build (AST for halmos)
# halmos reads the forge build-info (AST) from out/; the default forge
# build embeds it — the explicit pass makes the artifact deterministic
# for the symbolic run (extra_output metadata only, no via-ir build).
run: |
forge build --force --extra-output metadata

- name: Run symbolic verification
# QuarterWindowCheck only (the former ComputeSharesCheck was removed with
# the FixedU18 adoption — see docs/sra-design.md §4.5). Runs independently
# of the main test CI.
Comment on lines +42 to +44

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rm

run: |
pip install halmos==0.1.13
halmos --contract QuarterWindowCheck --no-test-constructor
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ jobs:
forge install

- name: Run tests
# Symbolic verification (halmos) runs in its own workflow (halmos.yml),
# independent of this CI — it needs halmos plus a separate `forge build --ast`
# pass (see docs/sra-design.md §4.5).
run: |
forge test
1,032 changes: 1,032 additions & 0 deletions docs/sra-design.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion foundry.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
}
},
"lib/fvm-solidity": {
"rev": "ea1fe65367d7539236be111916a6e2781bcf7a1b"
"branch": {
"name": "main",
"rev": "ea1fe65367d7539236be111916a6e2781bcf7a1b"
}
},
"lib/safe-smart-account": {
"tag": {
Expand Down
834 changes: 834 additions & 0 deletions src/ServiceRewardsActor.sol

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions src/lib/SraStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
pragma solidity ^0.8.36;

import {Epoch} from "./Epoch.sol";
import {FixedU18} from "./FixedU18.sol";

// ----------------------------------------------------------------------------
// SRA ERC-7201 storage layout (4 namespaces) + precomputed slots, in a shared
// library so the #5 proxy refactor can use the exact same namespace definitions
// between proxy and implementation — a single source of truth for the storage layout.
//
// Identity model: a uint64 id is the orchestrator identity; an address is only the
// current effective wallet mapping (activeIdOf). bindings/fpv/freeze history all
// key on the id, so replace is an O(1) wallet re-point and historical quarter data
// survives an operator-address change without migration. ids are allocated
// monotonically and never reused (0 is the unregistered sentinel), so a removed
// id stays resolvable — a released binding pair is "unclaimed" iff the bound id's
// admitted flag is false.
// ----------------------------------------------------------------------------

library SraStorage {
struct OrchestratorInfo {
address wallet; // current effective wallet (replace updates this; the share map writes this) — 20B
bool admitted; // admitted — 1B
// Frozen-at-E+POST flag: exactly "was this orchestrator frozen at the close of the
// posting period of the active quarter" — the fpv-effectiveness test. It changes only
// before E+POST (freeze/unfreeze in the posting window set/clear it); from the
// verification window onward it is fixed. Contrast frozenSince, which tracks the
// current freeze state (0 = not frozen) for admission checks and freeze/unfreeze symmetry.
bool frozenAtPostEnd; // 1B
Epoch frozenSince; // current freeze state: 0 = not frozen; > 0 = frozen since this epoch — 8B
// 30B packed into slot0 (successor field removed — the id-keyed model needs no alias chain)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't slot 0

you can note the packing in fewer words

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't fill the code with tombstones documenting previous versions. We won't need to do that until after we deploy the first version.

// Contribution slots (mirror): fpv = active-quarter contribution (0 = not posted),
// prevFpv = previous-quarter contribution mirror, exclusion-fixed at mirror advance
// (prevFpv <- frozenAtPostEnd ? 0 : fpv; fpv = 0). submitShares reads fpv for the
// active quarter (q == activeQ) and prevFpv for the previous one (q == activeQ - 1).
FixedU18 fpv; // slot1
FixedU18 prevFpv; // slot2
Comment on lines +33 to +38

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we mirror instead of using the totalUsd mapping?

}

/// @custom:storage-location erc7201:Solstice.SRA.Registry
struct SraStorageRegistry {
mapping(uint64 id => OrchestratorInfo) orchestrators; // id is the identity (monotonic, never reused)
mapping(address orch => uint64 id) activeIdOf; // current effective address -> id (0 = unregistered sentinel)
mapping(bytes32 pairId => uint64 id) bindings; // pairId = keccak256(abi.encode(payer, operator))
uint64 nextId; // id allocator (constructor sets 1; 0 is the unregistered sentinel)
Comment thread
wjmelements marked this conversation as resolved.
uint64[] admittedIds; // enumerable admitted (incl. frozen); length doubles as the count
}

/// @custom:storage-location erc7201:Solstice.SRA.AdmittedLists
struct SraStorageLists {
mapping(address => bool) stablecoins; // admitted stablecoins (valued at face USD)
mapping(address => bool) filecoinPayContracts; // admitted Filecoin Pay contracts
Comment thread
wjmelements marked this conversation as resolved.
address[] stablecoinList; // needed for exclusive updates (design-gap completion)
address[] filecoinPayList;
}
Comment on lines +50 to +56

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to store these on-chain, or can we just emit them as an event?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — the actual consumers of the allowlist are all off-chain (indexers/verifiers can read from the event stream), so it's fully event-ifiable technically. Currently the spec defines it as on-chain registry state ("the whitelist is SRA state"), one advantage being read-path convenience (direct lookup of current state, no event replay). I think event-ifying it like the 1277 parameters (unified model + storage savings) is a reasonable evolution in the same direction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so too. The volume script already has to read the logs for this contract anyway.


/// @custom:storage-location erc7201:Solstice.SRA.Quarter
struct SraStorageQuarter {
// activeQ: the quarter the mirror has advanced to (postVolume/correctVolume set it on
// the first write of a new quarter — the advance trigger). The previous quarter's
// per-orchestrator contributions live in prevFpv (exclusion-fixed at the advance);
// only these two quarters retain per-orchestrator values (spec: CorrectVolume is
// bounded by the verification window, so no historical corrections exist).
uint64 activeQ;
uint64 lastSubmittedQ; // anti-replay: last submitted quarter + 1 (0 = none; q+1 encoding so quarter 0 does not collide with the sentinel; monotonic, no reset)
// Quarter counter array: per-quarter USD aggregate (aggregatedFPV O(1) for every
// quarter, fixed once the mirror advances — spec determinism: the registry is constant
// within a quarter, so the aggregate cannot drift with later remove/replace).
mapping(uint64 Q => FixedU18) totalUsd;
}

/// @custom:storage-location erc7201:Solstice.SRA.Params
struct SraStorageParams {
uint256 minLot; // MIN_LOT (FIP §2.3: governs the off-chain conversion, not an on-chain computation)
uint256 priceBand; // PRICE_BAND (basis points; same — authoritative parameter for the off-chain indexer)
}

// keccak256(abi.encode(uint256(keccak256(namespace)) - 1)) & ~bytes32(uint256(0xff)) — precomputed and hardcoded
bytes32 internal constant REGISTRY_SLOT = 0xb7fd4b054ced95f43476af93bf71636318271f9e64f7661dc52f0fb4c1a54400;
bytes32 internal constant LISTS_SLOT = 0x6b063b99e710dc539d819b661c65b9a94a4c91adbbbff20449f292eda97f9300;
bytes32 internal constant QUARTER_SLOT = 0x347e624280399e1e720d839edbd7cd00c80c69bf34cd8ee59e27f691732af300;
bytes32 internal constant PARAMS_SLOT = 0xe21afbd697880784c3da970abdca3a316f22b4c4fc74f2fceb073d8e55bcad00;

function registry() internal pure returns (SraStorageRegistry storage r) {
assembly ("memory-safe") {
r.slot := REGISTRY_SLOT
}
}

function lists() internal pure returns (SraStorageLists storage l) {
assembly ("memory-safe") {
l.slot := LISTS_SLOT
}
}

function quarter() internal pure returns (SraStorageQuarter storage q) {
assembly ("memory-safe") {
q.slot := QUARTER_SLOT
}
}

function params() internal pure returns (SraStorageParams storage p) {
assembly ("memory-safe") {
p.slot := PARAMS_SLOT
}
}
}
28 changes: 28 additions & 0 deletions src/lib/SraTypes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
pragma solidity ^0.8.36;

import {FixedU18} from "./FixedU18.sol";

// ----------------------------------------------------------------------------
// Top-level SRA types (test files import from this file: Pair / FPV)
// ----------------------------------------------------------------------------

/// @notice (payer, operator) binding pair. The design's §2.3.1 inline tuple-array signature is
/// illegal in Solidity 0.8.36 (Error 3546); replaced with a named struct (ABI encoding is still a tuple array).
struct Pair {
address payer;
address operator;
}

// forge-lint: disable-next-item(pascal-case-struct) — FPV is the FIP-0118 spec term (public ABI-facing type)
/// @notice Quarterly FPV: a single USD-denominated total (FIP-0118 §2.3, FIPs#1275: FIL→USD conversion moved
/// off-chain, so the SRA no longer stores pricing periods). `usd` is the face-USD stablecoin volume plus
/// the off-chain-converted FIL volume; `usd == 0` means not posted
/// (PostVolume rejects zero, CorrectVolume(0) clears).
/// @dev FixedU18: 18-decimal fixed-point USD (1 USD = 1e18 integer). Adopted per the SWA interface
/// (IServiceRewardsActor.aggregatedFPV returns FixedU18) so every USD-consuming computation is
/// type-safe against integer/fixed-point mixing (1 vs 1e18 magnitude errors). MAX_FPV_USD(1e30)
/// wraps as 1e48 < uint256.max — no narrowing at the storage write. One storage slot.
struct FPV {
FixedU18 usd; // single USD total for the quarter (FPV_i(Q)), 18-decimal fixed point; 0 = not posted
}
Loading
Loading