Skip to main content

TimelockExpiry

The TimelockExpiry policy enforces a time window during which a reveal (withdrawal) is permitted. Funds can only be withdrawn after a lock period has elapsed and before an expiration deadline. This enables use cases like vesting schedules, delayed withdrawals, and time-limited redemption windows.

Deployed address: 0xd84D534E94f1eacE9BC5e9Bd90338d574d02B95c

Interface

contract TimelockExpiry is IRevealPolicy {
function validateReveal(
bytes32 commitment,
bytes32 nullifier,
address recipient,
uint256 amount,
address token,
bytes calldata policyParams
) external view override returns (bool valid);
}

Policy Parameters

The policyParams must be ABI-encoded as:

bytes memory policyParams = abi.encode(lockUntil, expiresAt);
ParameterTypeDescription
lockUntiluint256Unix timestamp (seconds). The reveal is blocked until block.timestamp >= lockUntil.
expiresAtuint256Unix timestamp (seconds). The reveal is blocked after block.timestamp > expiresAt.

Validation Logic

function validateReveal(
bytes32, // commitment (unused)
bytes32, // nullifier (unused)
address, // recipient (unused)
uint256, // amount (unused)
address, // token (unused)
bytes calldata policyParams
) external view override returns (bool valid) {
(uint256 lockUntil, uint256 expiresAt) = abi.decode(
policyParams,
(uint256, uint256)
);

return block.timestamp >= lockUntil && block.timestamp <= expiresAt;
}

The function returns true if and only if:

lockUntil <= block.timestamp <= expiresAt

Time Window Visualization

Time ──────────────────────────────────────────────────────►

◄── LOCKED ──►◄──── VALID WINDOW ────►◄── EXPIRED ──►
│ │
lockUntil expiresAt
  • Before lockUntil: Reveal is rejected. Funds remain locked.
  • Between lockUntil and expiresAt: Reveal is permitted.
  • After expiresAt: Reveal is rejected. Funds are effectively frozen unless another recovery mechanism exists.

Usage Examples

Commit with a 7-Day Lock and 30-Day Expiry

uint256 lockUntil = block.timestamp + 7 days;
uint256 expiresAt = block.timestamp + 30 days;

bytes memory params = abi.encode(lockUntil, expiresAt);
bytes32 paramsHash = keccak256(params);

vault.commitWithPolicy(
tokenAddress,
amount,
commitment,
quantumCommitment,
0xd84D534E94f1eacE9BC5e9Bd90338d574d02B95c, // TimelockExpiry address
paramsHash
);

Reveal After Lock Period

// Must be called when: lockUntil <= block.timestamp <= expiresAt
bytes memory params = abi.encode(lockUntil, expiresAt);

vault.reveal(
tokenAddress,
proof,
publicInputs,
commitment,
quantumProof,
changeQuantumCommitment,
params // policyParams
);

Immediate-Available with Expiry Only

// Set lockUntil to 0 (or any past timestamp) for immediate availability
uint256 lockUntil = 0;
uint256 expiresAt = block.timestamp + 90 days;

bytes memory params = abi.encode(lockUntil, expiresAt);

Permanent Lock (No Expiry)

// Set expiresAt to type(uint256).max for no practical expiry
uint256 lockUntil = block.timestamp + 365 days;
uint256 expiresAt = type(uint256).max;

bytes memory params = abi.encode(lockUntil, expiresAt);

Gas Usage

This policy is extremely gas-efficient:

OperationGas
ABI decode (2 uint256)~400
Two comparisons~6
Total~406

Well within the 100,000 gas cap.

Security Considerations

  • Expiry is final: Once expiresAt passes, funds committed with this policy cannot be revealed. There is no admin override. Ensure the expiry window is sufficiently generous.
  • Block timestamp manipulation: Validators can manipulate block.timestamp by a few seconds. For policies requiring precise timing, add a small buffer (e.g., set lockUntil a few minutes after the desired time).
  • Parameter privacy: The lockUntil and expiresAt values are not stored on-chain in plaintext. Only the keccak256 hash is stored at commit time. The actual timestamps are revealed only when the withdrawal is executed.