From 01aa27bbd2c7b15d7ecce0def96a14835ec23965 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 24 Jun 2026 12:31:59 +0500 Subject: [PATCH 1/3] feat: stop minting at tge --- .../contracts/token/InterfoldToken.sol | 12 ++--- .../test/Token/InterfoldToken.spec.ts | 46 ++++++++++--------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/packages/interfold-contracts/contracts/token/InterfoldToken.sol b/packages/interfold-contracts/contracts/token/InterfoldToken.sol index 0d5e255c5..6d005d4ff 100644 --- a/packages/interfold-contracts/contracts/token/InterfoldToken.sol +++ b/packages/interfold-contracts/contracts/token/InterfoldToken.sol @@ -184,7 +184,7 @@ contract InterfoldToken is bytes32 public constant LOCK_MANAGER_ROLE = keccak256("LOCK_MANAGER_ROLE"); /// @notice Minimum time between {CCA_END} and {tge}. - uint64 public constant TGE_COOLDOWN = 45 days; + uint64 public constant TGE_COOLDOWN = 40 days; bytes32 public constant PENDING_LOCK_POLICY_ID = "PENDING"; @@ -338,27 +338,27 @@ contract InterfoldToken is // ───────────────────────────────────────────────────────────────────────── /// @notice Plain vanilla admin mint: FOLD with no lock attached. Only - /// allowed during the Virtual phase. + /// allowed before TGE (Virtual, CCA, and Cooldown phases). function mint( address recipient, uint256 amount, bytes32 label ) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (phase() != Phase.Virtual) revert MintingClosed(); + if (phase() == Phase.Live) revert MintingClosed(); _mintTokens(recipient, amount); emit AllocationMinted(recipient, amount, bytes32(0), label); } /// @notice Mints allocations locked under their policies; the path the /// minter role uses to distribute vested supply. Only allowed - /// during the Virtual phase. + /// before TGE (Virtual, CCA, and Cooldown phases). /// @dev Team / GG "vested as of TGE" amounts must be calculated /// off-chain using the expected TGE date, since {tgeTimestamp} is - /// not known when Virtual minting closes at {CCA_START}. + /// not known at the time allocations are minted. function mintAllocations( MintAllocation[] calldata allocations ) external onlyRole(MINTER_ROLE) { - if (phase() != Phase.Virtual) revert MintingClosed(); + if (phase() == Phase.Live) revert MintingClosed(); uint256 len = allocations.length; for (uint256 i = 0; i < len; i++) { _mintAllocation(allocations[i]); diff --git a/packages/interfold-contracts/test/Token/InterfoldToken.spec.ts b/packages/interfold-contracts/test/Token/InterfoldToken.spec.ts index daba82907..c944b3adf 100644 --- a/packages/interfold-contracts/test/Token/InterfoldToken.spec.ts +++ b/packages/interfold-contracts/test/Token/InterfoldToken.spec.ts @@ -16,7 +16,7 @@ const { loadFixture, time } = networkHelpers; const DAY = 24n * 60n * 60n; const YEAR = 365n * DAY; const NO_MORE_LOCKS_DELAY = 4n * YEAR; -const TGE_COOLDOWN = 45n * DAY; +const TGE_COOLDOWN = 40n * DAY; function noMoreLocksFor(ccaEnd: bigint) { return ccaEnd + TGE_COOLDOWN + NO_MORE_LOCKS_DELAY; @@ -114,7 +114,7 @@ describe("InterfoldToken", function () { ]); // Fire TGE. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); const tgeTx = await token.tge(); const receipt = await tgeTx.wait(); @@ -134,7 +134,7 @@ describe("InterfoldToken", function () { .connect(admin) .mint(await alice.getAddress(), amount, ethers.ZeroHash); - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); @@ -382,7 +382,7 @@ describe("InterfoldToken", function () { it("enters Live phase after TGE", async function () { const { token, ccaEnd } = await loadFixture(deploy); - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); expect(await token.phase()).to.equal(3); // Phase.Live @@ -416,9 +416,10 @@ describe("InterfoldToken", function () { expect(await token.balanceOf(await alice.getAddress())).to.equal(amount); }); - it("mint reverts after Virtual phase", async function () { - const { token, admin, alice, ccaStart } = await loadFixture(deploy); - await time.increaseTo(ccaStart); + it("mint reverts after TGE (Live phase)", async function () { + const { token, admin, alice, ccaEnd } = await loadFixture(deploy); + await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); + await token.tge(); await expect( token .connect(admin) @@ -513,10 +514,11 @@ describe("InterfoldToken", function () { ).to.be.revertedWithCustomError(token, "PolicyNotDefined"); }); - it("reverts after Virtual phase", async function () { - const { token, admin, alice, ccaStart } = await loadFixture(deploy); + it("mintAllocations reverts after TGE (Live phase)", async function () { + const { token, admin, alice, ccaEnd } = await loadFixture(deploy); const policyId = await createLinearPolicy(token, admin, "TEST_POLICY"); - await time.increaseTo(ccaStart); + await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); + await token.tge(); await expect( token.connect(admin).mintAllocations([ { @@ -614,7 +616,7 @@ describe("InterfoldToken", function () { it("anyone can trigger TGE after cooldown", async function () { const { token, ccaEnd, alice } = await loadFixture(deploy); - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await expect(token.connect(alice).tge()).to.emit(token, "TgeTriggered"); expect(await token.tgeTimestamp()).to.be.gt(0); @@ -623,7 +625,7 @@ describe("InterfoldToken", function () { it("reverts if already live", async function () { const { token, ccaEnd } = await loadFixture(deploy); - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); await expect(token.tge()).to.be.revertedWithCustomError( @@ -1006,7 +1008,7 @@ describe("InterfoldToken", function () { ]); // Fire TGE. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); const tgeTx = await token.tge(); const receipt = await tgeTx.wait(); @@ -1103,7 +1105,7 @@ describe("InterfoldToken", function () { ]); // Fire TGE. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); const tgeTx = await token.tge(); const receipt = await tgeTx.wait(); @@ -1161,7 +1163,7 @@ describe("InterfoldToken", function () { const aliceAddress = await alice.getAddress(); // Compute the intended TGE timestamp before firing it. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; const intendedTge = ccaEnd + TGE_COOLDOWN + 1n; // Policy: 1-year linear vest, holdUntil = intended TGE + 2 years. @@ -1685,7 +1687,7 @@ describe("InterfoldToken", function () { .mint(await claimSource.getAddress(), linkAmount, ethers.ZeroHash); // Fire TGE so transfers are unrestricted. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); @@ -1748,7 +1750,7 @@ describe("InterfoldToken", function () { .connect(admin) .mint(await claimSource.getAddress(), linkAmount, ethers.ZeroHash); - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); @@ -1778,7 +1780,7 @@ describe("InterfoldToken", function () { .connect(admin) .mint(await claimSource.getAddress(), claimAmount, ethers.ZeroHash); - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); @@ -1857,7 +1859,7 @@ describe("InterfoldToken", function () { .mint(await claimSource.getAddress(), totalClaim, ethers.ZeroHash); // Fire TGE. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); @@ -1910,7 +1912,7 @@ describe("InterfoldToken", function () { ]); // Fire TGE. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); @@ -1986,7 +1988,7 @@ describe("InterfoldToken", function () { .mint(await claimSource.getAddress(), ccaAmount, ethers.ZeroHash); // Fire TGE. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); @@ -2091,7 +2093,7 @@ describe("InterfoldToken", function () { ]); // Fire TGE. - const TGE_COOLDOWN = 45n * DAY; + const TGE_COOLDOWN = 40n * DAY; await time.increaseTo(ccaEnd + TGE_COOLDOWN + 1n); await token.tge(); From 6baf4174b546847fee731c480fa9f5599adefe72 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 25 Jun 2026 15:36:01 +0500 Subject: [PATCH 2/3] fix: tge timing --- agent/flow-trace/02_TOKENS_AND_ACTIVATION.md | 2 +- .../interfold-contracts/scripts/deployAndSave/interfoldToken.ts | 2 +- packages/interfold-contracts/scripts/deployInterfold.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/agent/flow-trace/02_TOKENS_AND_ACTIVATION.md b/agent/flow-trace/02_TOKENS_AND_ACTIVATION.md index e7b96d5d2..7d231c9b1 100644 --- a/agent/flow-trace/02_TOKENS_AND_ACTIVATION.md +++ b/agent/flow-trace/02_TOKENS_AND_ACTIVATION.md @@ -441,7 +441,7 @@ enforcement based on immutable policy curves. Key changes: - **Phase-based lifecycle.** The token derives its phase from immutable `CCA_START` / `CCA_END` and the one-way `tge()` call: Virtual → PublicSale → Cooldown → Live. Minting is gated to Virtual - phase only; TGE is permissionless after `CCA_END + TGE_COOLDOWN` (45 days). The pre-TGE transfer + phase only; TGE is permissionless after `CCA_END + TGE_COOLDOWN` (40 days). The pre-TGE transfer gate automatically lifts at TGE — no `disableTransferRestrictions` / `transfersRestricted` flag. - **Pre-TGE transfer gate.** Before TGE, only bonding-registry transfers, claim-source distributions, and whitelisted addresses can transfer. Bonding is always allowed so operators can diff --git a/packages/interfold-contracts/scripts/deployAndSave/interfoldToken.ts b/packages/interfold-contracts/scripts/deployAndSave/interfoldToken.ts index d30492b45..6266011f8 100644 --- a/packages/interfold-contracts/scripts/deployAndSave/interfoldToken.ts +++ b/packages/interfold-contracts/scripts/deployAndSave/interfoldToken.ts @@ -44,7 +44,7 @@ async function disableTransferRestrictionsForLocal( const tgeTs = await contract.tgeTimestamp(); if (tgeTs === 0n) { console.warn( - "TGE not yet fired — call tge() after advancing time past CCA_END + 45 days.", + "TGE not yet fired — call tge() after advancing time past CCA_END + 40 days.", ); } else { console.log("Token is already Live (TGE timestamp:", tgeTs.toString(), ")"); diff --git a/packages/interfold-contracts/scripts/deployInterfold.ts b/packages/interfold-contracts/scripts/deployInterfold.ts index 950127e1a..440931920 100644 --- a/packages/interfold-contracts/scripts/deployInterfold.ts +++ b/packages/interfold-contracts/scripts/deployInterfold.ts @@ -115,7 +115,7 @@ export const deployInterfold = async ( const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; const SEVEN_DAYS_IN_SECONDS = 60 * 60 * 24 * 7; - const TGE_COOLDOWN_SECONDS = 60 * 60 * 24 * 45; + const TGE_COOLDOWN_SECONDS = 60 * 60 * 24 * 40; const FOUR_YEARS_IN_SECONDS = 60 * 60 * 24 * 365 * 4; const SORTITION_SUBMISSION_WINDOW = 10; const addressOne = "0x0000000000000000000000000000000000000001"; From 1e7e848a3f2619961e65e7778f71716b77f3d3d8 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 25 Jun 2026 21:39:22 +0500 Subject: [PATCH 3/3] fix: handle stale c4 verificaton replay --- .../keyshare/src/actors/threshold_keyshare.rs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/keyshare/src/actors/threshold_keyshare.rs b/crates/keyshare/src/actors/threshold_keyshare.rs index 757b93893..cbb04abf9 100644 --- a/crates/keyshare/src/actors/threshold_keyshare.rs +++ b/crates/keyshare/src/actors/threshold_keyshare.rs @@ -1557,6 +1557,42 @@ impl ThresholdKeyshare { let (msg, ec) = msg.into_components(); let ciphertext_output = msg.ciphertext_output; + // If we are already in Decrypting (or beyond), this is a duplicate + // event — e.g. replayed from the EventStore during crash recovery. + // Re-issue the compute request idempotently (downstream aggregation + // deduplicates by party_id) and skip the state transition. + { + let state = self.state.try_get()?; + match &state.state { + KeyshareState::Decrypting(_) => { + info!( + e3_id = %state.e3_id, + "CiphertextOutputPublished received while already Decrypting — re-issuing decryption-share request" + ); + return self.issue_decryption_share_request(ec); + } + KeyshareState::GeneratingDecryptionProof(_) | KeyshareState::Completed => { + info!( + e3_id = %state.e3_id, + state = %state.variant_name(), + "CiphertextOutputPublished received after decryption already in progress — ignoring" + ); + return Ok(()); + } + KeyshareState::ReadyForDecryption(_) => { + // Normal path — proceed with transition below. + } + other => { + warn!( + e3_id = %state.e3_id, + state = %other.variant_name(), + "CiphertextOutputPublished received in unexpected state — cannot process decryption" + ); + return Ok(()); + } + } + } + // Set state to decrypting, storing ciphertext for later C6 proof generation self.state.try_mutate(&ec, |s| { use KeyshareState as K;