Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ jobs:
RATELOOP_E2E_PRODUCTION_BUILD: "true"
NEXT_PUBLIC_RATELOOP_E2E_PRODUCTION_BUILD: "true"
DATABASE_URL: "memory:"
NEXT_PUBLIC_FRONTEND_CODE: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
NEXT_PUBLIC_PONDER_URL: http://127.0.0.1:42069
NEXT_PUBLIC_TARGET_NETWORKS: "31337"
NEXT_PUBLIC_THIRDWEB_CLIENT_ID: 124332de491a2aa8b7ba8ea6db1ce693
Expand All @@ -172,6 +173,7 @@ jobs:
RATELOOP_E2E_PRODUCTION_BUILD: "true"
NEXT_PUBLIC_RATELOOP_E2E_PRODUCTION_BUILD: "true"
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/rateloop_app
NEXT_PUBLIC_FRONTEND_CODE: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
NEXT_PUBLIC_PONDER_URL: http://127.0.0.1:42069
NEXT_PUBLIC_TARGET_NETWORKS: "31337"
NEXT_PUBLIC_THIRDWEB_CLIENT_ID: 124332de491a2aa8b7ba8ea6db1ce693
Expand Down Expand Up @@ -221,6 +223,7 @@ jobs:
RATELOOP_E2E_PRODUCTION_BUILD: "true"
NEXT_PUBLIC_RATELOOP_E2E_PRODUCTION_BUILD: "true"
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/rateloop_app
NEXT_PUBLIC_FRONTEND_CODE: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
NEXT_PUBLIC_PONDER_URL: http://127.0.0.1:42069
NEXT_PUBLIC_TARGET_NETWORKS: "31337"
NEXT_PUBLIC_THIRDWEB_CLIENT_ID: 124332de491a2aa8b7ba8ea6db1ce693
Expand Down
1 change: 1 addition & 0 deletions docs/testing/base-sepolia-next-env.fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_TARGET_NETWORKS=84532
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// single-use. Inherits compiler/prover settings from base.conf.
// Run with: certoraRun certora/confs/launch-distribution-pool.conf
"override_base_config": "certora/confs/base.conf",
"files": ["contracts/LaunchDistributionPool.sol"],
"verify": "LaunchDistributionPool:certora/specs/LaunchDistributionPool.spec",
"files": ["certora/harnesses/LaunchDistributionPoolHarness.sol"],
"verify": "LaunchDistributionPoolHarness:certora/specs/LaunchDistributionPool.spec",
"msg": "RateLoop Phase 5 LaunchDistributionPool cap conservation"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ pragma solidity 0.8.35;
import { LaunchDistributionPool } from "../../contracts/LaunchDistributionPool.sol";

/// @title LaunchDistributionPoolHarness
/// @notice Exposes the policy's unverified-cap bps so Certora can prove the auxiliary
/// invariant `raterLaunchCap <= raterFullLaunchCap` (which needs bps <= 10000) in
/// the cap-conservation chain. Verification target for
/// certora/specs/LaunchDistributionPoolCap.spec (Phase 5b / Track B).
/// @notice Exposes Certora-only views over LaunchDistributionPool internal state and
/// mirrors the private cap clamp used at assignment. Verification target for
/// certora/specs/LaunchDistributionPool*.spec.
contract LaunchDistributionPoolHarness is LaunchDistributionPool {
constructor(address lrep, address registry, address governance)
LaunchDistributionPool(lrep, registry, governance)
Expand All @@ -17,12 +16,17 @@ contract LaunchDistributionPoolHarness is LaunchDistributionPool {
return uint256(launchRewardPolicy.unverifiedEarnedRaterCapBps);
}

/// @notice Exposes the internal cap-assignment so the spec can machine-check the
function verifiedBonusClaimedByAccount_(address account) external view returns (bool) {
return verifiedBonusClaimedByAccount[account];
}

/// @notice Mirrors the private cap-assignment clamp so the spec can machine-check the
/// clamp `activeCap <= fullCap` directly at the point it is computed
/// (`activeCap = (fullCap * bps) / BPS_DENOMINATOR`, or `fullCap` when the
/// full cap is unlocked). Uses the live policy, so `unverifiedCapBps_()`
/// is the bps the spec constrains <= 10000. See LaunchDistributionPoolCap.spec.
function assignLaunchCap_(address rater, uint256 fullCap) external returns (uint256 activeCap) {
(activeCap,) = _assignLaunchCap(rater, fullCap, launchRewardPolicy);
function assignLaunchCap_(address rater, uint256 fullCap) external view returns (uint256 activeCap) {
if (raterFullLaunchCapUnlocked[rater]) return fullCap;
return (fullCap * uint256(launchRewardPolicy.unverifiedEarnedRaterCapBps)) / BPS_DENOMINATOR;
}
}
4 changes: 4 additions & 0 deletions packages/foundry/certora/specs/FrontendRegistry.spec
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
* reduces the stake by exactly that amount (it can never confiscate more than is
* bonded).
*
* July 2026 storage-layout review: appending access-recorder mappings after the
* existing fee-accounting fields uses reserved gap slots only; these stake-conservation
* properties and their public state accessors are unchanged.
*
* All proved over public state with NONDET token summaries; the single-use gate keys off
* msg.sender, so no commit resolution is involved.
*/
Expand Down
6 changes: 3 additions & 3 deletions packages/foundry/certora/specs/LaunchDistributionPool.spec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* LaunchDistributionPool.spec — Phase 5.
*
* Verification target: contracts/LaunchDistributionPool.sol (verified directly).
* Verification target: certora/harnesses/LaunchDistributionPoolHarness.sol.
* Run with: certoraRun certora/confs/launch-distribution-pool.conf
*
* The launch pool pays a one-time "verified bonus" per account. This proves that bonus
Expand All @@ -18,7 +18,7 @@
*/

methods {
function verifiedBonusClaimedByAccount(address) external returns (bool) envfree;
function verifiedBonusClaimedByAccount_(address) external returns (bool) envfree;

// External token + registry calls in the reward/claim paths: NONDET so they cannot
// havoc this contract's accounting storage. The single-use property holds regardless
Expand All @@ -45,5 +45,5 @@ rule verifiedBonusSingleUsePerAccount(env e1, env e2, address referrer1, address
// And the mechanism behind that gate: a successful claim records the account flag.
rule verifiedBonusRecordsFlag(env e, address referrer) {
claimVerifiedBonus(e, referrer);
assert verifiedBonusClaimedByAccount(e.msg.sender);
assert verifiedBonusClaimedByAccount_(e.msg.sender);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* multiply-then-divide the *linear* SMT backend cannot discharge. This conf now enables
* the nonlinear-arithmetic backend (`-smt_useNIA`), under which the assignment clamp IS
* dischargeable. `assignedCapWithinFullCap` below machine-checks exactly that clamp
* at the point it is computed, via the harness wrapper `assignLaunchCap_`.
* through the same clamp expression, via the harness wrapper `assignLaunchCap_`.
*
* Still deferred (honest residual): the *global* invariant `raterLaunchPaid <= raterLaunchCap`
* over every method. Per the findings doc, even with NIA the catch-up paths
Expand Down Expand Up @@ -51,9 +51,9 @@ invariant policyBpsBounded()
invariant capAssignedWhenPaid(address r)
raterLaunchPaid(r) > 0 => raterLaunchCapAssigned(r);

// The cap-assignment clamp: the active cap computed at assignment never exceeds the full
// cap. This is the load-bearing mul-div step (activeCap = (fullCap * bps) / 10000 when the
// full cap is locked, else fullCap), the real-contract instance of MulDivLemma.spec's
// The cap-assignment clamp: the active cap expression never exceeds the full cap. This is
// the load-bearing mul-div step (activeCap = (fullCap * bps) / 10000 when the full cap is
// locked, else fullCap), the real-contract instance of MulDivLemma.spec's
// `(a*b)/c <= a`. Requires the nonlinear SMT backend enabled in this conf. The bps
// precondition is `policyBpsBounded`, proved as an invariant above.
rule assignedCapWithinFullCap(env e, address rater, uint256 fullCap) {
Expand Down
3 changes: 1 addition & 2 deletions packages/foundry/contracts/ConfidentialityEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -364,8 +364,7 @@ contract ConfidentialityEscrow is
address holder,
address recorder,
address registryAddress
) private
{
) private {
if (!_configs[contentId].gated) return;
_markNullifierBonded(holder, registryAddress);
emit ConfidentialityNexusRecorded(frontend, contentId, holder, recorder);
Expand Down
4 changes: 2 additions & 2 deletions packages/foundry/contracts/FrontendRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ contract FrontendRegistry is IFrontendRegistry, Initializable, AccessControlUpgr
mapping(address => uint256) public frontendExitAvailableAt;
mapping(address => address) public snapshotProposerForFrontend;
mapping(address => address) public frontendForSnapshotProposer;
mapping(address => address) public accessRecorderForFrontend;
mapping(address => address) public frontendForAccessRecorder;
bool public initialFeeCreditorConfigured;
address public feeCreditor;
mapping(address => bool) private authorizedFeeCreditors;
Expand All @@ -100,6 +98,8 @@ contract FrontendRegistry is IFrontendRegistry, Initializable, AccessControlUpgr
mapping(address => uint256) public pendingFeeWithdrawalAmount;
/// @notice Timestamp at which a pending fee withdrawal can be completed.
mapping(address => uint256) public pendingFeeWithdrawalReleaseAt;
mapping(address => address) public accessRecorderForFrontend;
mapping(address => address) public frontendForAccessRecorder;

/// @dev Reserved storage gap for future upgrades
uint256[37] private __gap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ test("contract size gate validates deploy-profile artifacts", () => {
assert.equal(packageJson.scripts["check:sizes"], "make check-contract-sizes");
assert.match(makefile, /forge build --force --skip script --skip test/);
assert.match(makefile, /forge build \$\(DEPLOYED_DEPENDENCY_SIZE_SOURCES\)/);
assert.doesNotMatch(makefile, /forge build --force \$\(DEPLOYED_DEPENDENCY_SIZE_SOURCES\)/);
assert.doesNotMatch(
makefile,
/forge build --force \$\(DEPLOYED_DEPENDENCY_SIZE_SOURCES\)/
);
assert.match(script, /Non-deploy-profile artifact/);
assert.match(script, /metadata\.settings\.optimizer\.runs/);
assert.match(script, /metadata\.settings\.metadata\.bytecodeHash/);
Expand Down
113 changes: 113 additions & 0 deletions packages/foundry/scripts-js/storageLayoutUpgradeSafety.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";

function frontendRegistrySlots() {
const layout = JSON.parse(
readFileSync(
join(
"packages",
"foundry",
"scripts",
"expected-storage-layouts",
"FrontendRegistry.json"
),
"utf8"
)
);

return new Map(layout.storage.map((entry) => [entry.label, entry]));
}

test("FrontendRegistry keeps fee accounting slots stable across upgrades", () => {
const slots = frontendRegistrySlots();

assert.deepEqual(
{
initialFeeCreditorConfigured: slots.get("initialFeeCreditorConfigured"),
feeCreditor: slots.get("feeCreditor"),
authorizedFeeCreditors: slots.get("authorizedFeeCreditors"),
feeCreditorForEngine: slots.get("feeCreditorForEngine"),
feeCreditorVotingEngine: slots.get("feeCreditorVotingEngine"),
pendingFeeWithdrawalAmount: slots.get("pendingFeeWithdrawalAmount"),
pendingFeeWithdrawalReleaseAt: slots.get("pendingFeeWithdrawalReleaseAt"),
},
{
initialFeeCreditorConfigured: {
slot: 9,
offset: 0,
label: "initialFeeCreditorConfigured",
type: "t_bool",
},
feeCreditor: {
slot: 9,
offset: 1,
label: "feeCreditor",
type: "t_address",
},
authorizedFeeCreditors: {
slot: 10,
offset: 0,
label: "authorizedFeeCreditors",
type: "t_mapping(t_address,t_bool)",
},
feeCreditorForEngine: {
slot: 11,
offset: 0,
label: "feeCreditorForEngine",
type: "t_mapping(t_address,t_address)",
},
feeCreditorVotingEngine: {
slot: 12,
offset: 0,
label: "feeCreditorVotingEngine",
type: "t_mapping(t_address,t_address)",
},
pendingFeeWithdrawalAmount: {
slot: 13,
offset: 0,
label: "pendingFeeWithdrawalAmount",
type: "t_mapping(t_address,t_uint256)",
},
pendingFeeWithdrawalReleaseAt: {
slot: 14,
offset: 0,
label: "pendingFeeWithdrawalReleaseAt",
type: "t_mapping(t_address,t_uint256)",
},
}
);
});

test("FrontendRegistry appends access-recorder state into the reserved gap", () => {
const slots = frontendRegistrySlots();

assert.deepEqual(
{
accessRecorderForFrontend: slots.get("accessRecorderForFrontend"),
frontendForAccessRecorder: slots.get("frontendForAccessRecorder"),
gap: slots.get("__gap"),
},
{
accessRecorderForFrontend: {
slot: 15,
offset: 0,
label: "accessRecorderForFrontend",
type: "t_mapping(t_address,t_address)",
},
frontendForAccessRecorder: {
slot: 16,
offset: 0,
label: "frontendForAccessRecorder",
type: "t_mapping(t_address,t_address)",
},
gap: {
slot: 17,
offset: 0,
label: "__gap",
type: "t_array(t_uint256)",
},
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,57 +57,57 @@
{
"slot": 9,
"offset": 0,
"label": "accessRecorderForFrontend",
"type": "t_mapping(t_address,t_address)"
},
{
"slot": 10,
"offset": 0,
"label": "frontendForAccessRecorder",
"type": "t_mapping(t_address,t_address)"
},
{
"slot": 11,
"offset": 0,
"label": "initialFeeCreditorConfigured",
"type": "t_bool"
},
{
"slot": 11,
"slot": 9,
"offset": 1,
"label": "feeCreditor",
"type": "t_address"
},
{
"slot": 12,
"slot": 10,
"offset": 0,
"label": "authorizedFeeCreditors",
"type": "t_mapping(t_address,t_bool)"
},
{
"slot": 13,
"slot": 11,
"offset": 0,
"label": "feeCreditorForEngine",
"type": "t_mapping(t_address,t_address)"
},
{
"slot": 14,
"slot": 12,
"offset": 0,
"label": "feeCreditorVotingEngine",
"type": "t_mapping(t_address,t_address)"
},
{
"slot": 15,
"slot": 13,
"offset": 0,
"label": "pendingFeeWithdrawalAmount",
"type": "t_mapping(t_address,t_uint256)"
},
{
"slot": 16,
"slot": 14,
"offset": 0,
"label": "pendingFeeWithdrawalReleaseAt",
"type": "t_mapping(t_address,t_uint256)"
},
{
"slot": 15,
"offset": 0,
"label": "accessRecorderForFrontend",
"type": "t_mapping(t_address,t_address)"
},
{
"slot": 16,
"offset": 0,
"label": "frontendForAccessRecorder",
"type": "t_mapping(t_address,t_address)"
},
{
"slot": 17,
"offset": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { after, before, beforeEach, test } from "node:test";
const env = process.env as Record<string, string | undefined>;
const originalCronSecret = env.CRON_SECRET;
const originalDatabaseUrl = env.DATABASE_URL;
const originalFrontendCode = env.NEXT_PUBLIC_FRONTEND_CODE;
const originalNodeEnv = env.NODE_ENV;

env.CRON_SECRET = "cron-secret";
env.DATABASE_URL = "memory:";
env.NEXT_PUBLIC_FRONTEND_CODE = "0x3333333333333333333333333333333333333333";
env.NODE_ENV = "test";

type DbModule = typeof import("~~/lib/db");
Expand Down Expand Up @@ -51,6 +53,7 @@ after(() => {
dbModule.__setDatabaseResourcesForTests(null);
restoreEnv("CRON_SECRET", originalCronSecret);
restoreEnv("DATABASE_URL", originalDatabaseUrl);
restoreEnv("NEXT_PUBLIC_FRONTEND_CODE", originalFrontendCode);
restoreEnv("NODE_ENV", originalNodeEnv);
});

Expand Down
Loading
Loading