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
2 changes: 1 addition & 1 deletion agent/flow-trace/02_TOKENS_AND_ACTIVATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions crates/keyshare/src/actors/threshold_keyshare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions packages/interfold-contracts/contracts/token/InterfoldToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(), ")");
Expand Down
2 changes: 1 addition & 1 deletion packages/interfold-contracts/scripts/deployInterfold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
46 changes: 24 additions & 22 deletions packages/interfold-contracts/test/Token/InterfoldToken.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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([
{
Expand Down Expand Up @@ -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);
Expand All @@ -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(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down
Loading