Skip to content
Open
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
44 changes: 29 additions & 15 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ contract Deploy is Script {
// wrongly-granted future minter cannot mint. StakeEngine (exempt) is
// the only address that can move supply, per protocol staking mechanics.
vm.envOr("VSP_INCEPTION_TIMESTAMP", uint256(1778544000)),
vm.envOr("VSP_INCEPTION_SUPPLY", uint256(1_000_002_000 * 1e18)),
// 2026-09-01 (patch_genesis_1b): genesis is 1,000,000,000 VSP EXACTLY
// (founder decision). The former +2,000 was an LP-seed allowance stacked
// on the liquid tranche; the LP seed is now drawn FROM liquid instead.
vm.envOr("VSP_INCEPTION_SUPPLY", uint256(1_000_000_000 * 1e18)),
vm.envOr("VSP_GROWTH_BASE_PER_YEAR", uint256(1e18)),
predictedStakeProxy // patch_bundle10_5_part2a_stakeengine_exempt
); // patch_bundle10_5_part2a_timecap: 4-arg constructor
Expand Down Expand Up @@ -206,31 +209,42 @@ contract Deploy is Script {
)
);

// ── patch_oneshot_genesis: ONE-SHOT GENESIS MINT + LOCK ──
// The full supply is minted here, once, split liquid/locked:
// liquid -> GENESIS_TREASURY (LP seed + board-authorized ops/grants)
// locked -> OZ VestingWallet (linear release to GENESIS_TREASURY)
// ── patch_oneshot_genesis: ONE-SHOT GENESIS MINT ──
// The full supply is minted here, once. Default (patch_genesis_nolock,
// founder ruling 2026-09-01): ALL of it to GENESIS_TREASURY, no lock at
// genesis. A lock is one-way (can be added later by transferring into a
// VestingWallet, never removed), so it is deferred until a concrete
// trigger exists. Setting VSP_GENESIS_LOCKED_SUPPLY > 0 re-enables the
// locked tranche -> OZ VestingWallet (linear release to the treasury);
// when enabled, set VSP_VESTING_START_TS explicitly (default is
// INCEPTION_TIMESTAMP, which may be in the past at deploy time).
// The deployer can mint ONLY inside this script (Authority constructor
// auto-grant, revoked at the end). Liquid+locked must equal
// VSP_INCEPTION_SUPPLY so the flat cap is filled exactly: any later
// capped mint of even 1 wei reverts MintExceedsTimeWindowCap.
address genesisTreasury = vm.envOr("VSP_GENESIS_TREASURY", deployer);
uint256 genesisLiquid = vm.envOr("VSP_GENESIS_LIQUID_SUPPLY", uint256(100_002_000 * 1e18));
uint256 genesisLocked = vm.envOr("VSP_GENESIS_LOCKED_SUPPLY", uint256(900_000_000 * 1e18));
uint256 genesisLocked = vm.envOr("VSP_GENESIS_LOCKED_SUPPLY", uint256(0)); // patch_genesis_nolock
uint256 genesisLiquid = vm.envOr("VSP_GENESIS_LIQUID_SUPPLY", token.INCEPTION_SUPPLY() - genesisLocked);
require(
genesisLiquid + genesisLocked == token.INCEPTION_SUPPLY(),
"Deploy: genesis liquid+locked must equal VSP_INCEPTION_SUPPLY (fill the flat cap exactly)"
);
uint64 vestStart = uint64(vm.envOr("VSP_VESTING_START_TS", uint256(token.INCEPTION_TIMESTAMP())));
uint64 vestDuration = uint64(vm.envOr("VSP_VESTING_DURATION_SECONDS", uint256(4 * 365 days)));
VestingWallet vestingWallet = new VestingWallet(genesisTreasury, vestStart, vestDuration);
token.mint(genesisTreasury, genesisLiquid);
token.mint(address(vestingWallet), genesisLocked);
console.log("GENESIS: liquid -> treasury:", genesisTreasury);
address vestingWalletAddr = address(0); // stays zero when no lock (addresses.json sentinel)
if (genesisLocked > 0) {
uint64 vestStart = uint64(vm.envOr("VSP_VESTING_START_TS", uint256(token.INCEPTION_TIMESTAMP())));
uint64 vestDuration = uint64(vm.envOr("VSP_VESTING_DURATION_SECONDS", uint256(4 * 365 days)));
VestingWallet vestingWallet = new VestingWallet(genesisTreasury, vestStart, vestDuration);
vestingWalletAddr = address(vestingWallet);
token.mint(vestingWalletAddr, genesisLocked);
console.log("GENESIS: locked -> VestingWallet:", vestingWalletAddr);
console.log("GENESIS: vesting start / duration (s):", vestStart, vestDuration);
} else {
console.log("GENESIS: no locked tranche (VSP_GENESIS_LOCKED_SUPPLY=0)");
}
require(token.totalSupply() == token.INCEPTION_SUPPLY(), "Deploy: genesis mint != inception supply");
console.log("GENESIS: total supply (wei):", token.totalSupply());
console.log("GENESIS: liquid -> treasury:", genesisTreasury);
console.log("GENESIS: locked -> VestingWallet:", address(vestingWallet));
console.log("GENESIS: vesting start / duration (s):", vestStart, vestDuration);

vm.label(address(registry), "PostRegistry");
vm.label(address(graph), "LinkGraph");
Expand Down Expand Up @@ -287,7 +301,7 @@ contract Deploy is Script {
'","ProtocolPolicy":"',
vm.toString(address(protocolPolicy)),
'","VestingWallet":"',
vm.toString(address(vestingWallet)),
vm.toString(vestingWalletAddr), // address(0) when no lock at genesis
'"}'
);

Expand Down
29 changes: 21 additions & 8 deletions test/GenesisOneShot.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import {MockCPAMM} from "../src/mock/MockCPAMM.sol";
/// everything at end; release() pays the beneficiary
/// G6 after revoking the deployer, the minter set is exactly {stakeEngine}
contract GenesisOneShotTest is Test {
uint256 constant GENESIS = 1_000_002_000 * 1e18;
uint256 constant LIQUID = 100_002_000 * 1e18;
uint256 constant GENESIS = 1_000_000_000 * 1e18; // patch_genesis_1b: 1B exactly
// patch_genesis_nolock: default genesis mints ALL of GENESIS to the treasury.
// LOCKED below is only used by G5, which exercises the OPTIONAL lock path
// (a lock added later by transferring treasury supply into a VestingWallet).
uint256 constant LOCKED = 900_000_000 * 1e18;
uint256 constant INCEPTION_TS = 1_778_544_000;
uint64 constant VEST_DURATION = uint64(4 * 365 days);
Expand All @@ -46,10 +48,8 @@ contract GenesisOneShotTest is Test {
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), abi.encodeCall(VSPToken.initialize, (address(authority))));
token = VSPToken(address(proxy));

// genesis: mint liquid to treasury, locked to the vesting wallet
vest = new VestingWallet(treasury, uint64(INCEPTION_TS), VEST_DURATION);
token.mint(treasury, LIQUID);
token.mint(address(vest), LOCKED);
// genesis (no lock, patch_genesis_nolock): the whole supply to the treasury
token.mint(treasury, GENESIS);

// mirror the deploy script's end state: StakeEngine granted, deployer revoked
authority.setMinter(stakeEngine, true);
Expand Down Expand Up @@ -90,9 +90,16 @@ contract GenesisOneShotTest is Test {
assertEq(token.maxAllowedSupply(), GENESIS);
}

// G5
// G5 — OPTIONAL lock path: the lock is not part of genesis; it can be added
// later by moving treasury supply into a VestingWallet. Verifies the schedule
// math and that the deferred path works unchanged.
function test_vesting_schedule() public {
vest = new VestingWallet(treasury, uint64(INCEPTION_TS), VEST_DURATION);
vm.prank(treasury);
token.transfer(address(vest), LOCKED);
uint256 LIQUID = GENESIS - LOCKED;
assertEq(token.balanceOf(address(vest)), LOCKED);
assertEq(token.balanceOf(treasury), LIQUID);

// nothing before start (warp back to just before inception)
vm.warp(INCEPTION_TS - 1);
Expand All @@ -109,10 +116,16 @@ contract GenesisOneShotTest is Test {
// everything at end
vm.warp(INCEPTION_TS + VEST_DURATION + 1);
vest.release(address(token));
assertEq(token.balanceOf(treasury), LIQUID + LOCKED);
assertEq(token.balanceOf(treasury), GENESIS);
assertEq(token.balanceOf(address(vest)), 0);
}

// G5b — the no-lock genesis itself: whole supply sits with the treasury
function test_genesis_no_lock_all_to_treasury() public view {
assertEq(token.balanceOf(treasury), GENESIS);
assertEq(token.totalSupply(), GENESIS);
}

// G6
function test_minter_set_is_exactly_stake_engine() public view {
assertTrue(authority.isMinter(stakeEngine));
Expand Down