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);
| Parameter | Type | Description |
|---|---|---|
lockUntil | uint256 | Unix timestamp (seconds). The reveal is blocked until block.timestamp >= lockUntil. |
expiresAt | uint256 | Unix 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
lockUntilandexpiresAt: 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:
| Operation | Gas |
|---|---|
| ABI decode (2 uint256) | ~400 |
| Two comparisons | ~6 |
| Total | ~406 |
Well within the 100,000 gas cap.
Security Considerations
- Expiry is final: Once
expiresAtpasses, 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.timestampby a few seconds. For policies requiring precise timing, add a small buffer (e.g., setlockUntila few minutes after the desired time). - Parameter privacy: The
lockUntilandexpiresAtvalues are not stored on-chain in plaintext. Only thekeccak256hash is stored at commit time. The actual timestamps are revealed only when the withdrawal is executed.