diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index f3489d6..d520cd5 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -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 @@ -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"); @@ -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 '"}' ); diff --git a/test/GenesisOneShot.t.sol b/test/GenesisOneShot.t.sol index 4b55a84..e7ff666 100644 --- a/test/GenesisOneShot.t.sol +++ b/test/GenesisOneShot.t.sol @@ -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); @@ -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); @@ -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); @@ -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));