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
4 changes: 4 additions & 0 deletions contracts/interfaces/Events.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ event AttestationValidityDurationSet(uint256 duration);
/// @param status The new credential group status.
event CredentialGroupStatusChanged(uint256 indexed credentialGroupId, ICredentialRegistry.CredentialGroupStatus status);

/// @notice Emitted when the future attestation buffer is updated.
/// @param buffer The new buffer duration in seconds.
event FutureAttestationBufferSet(uint256 buffer);

/// @notice Emitted when the registry-level default Merkle tree duration is updated.
/// @param duration The new default duration in seconds.
event DefaultMerkleTreeDurationSet(uint256 indexed duration);
Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces/ICredentialRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ interface ICredentialRegistry {
/// @param verifier_ The verifier address to remove.
function removeTrustedVerifier(address verifier_) external;

/// @notice Updates the forward-tolerance buffer for future attestation timestamps.
/// @param buffer_ New buffer in seconds (0 to disable tolerance).
function setFutureAttestationBuffer(uint256 buffer_) external;

/// @notice Updates the registry-level default Merkle tree duration for new Semaphore groups.
/// @param duration_ New default Merkle tree duration in seconds.
function setDefaultMerkleTreeDuration(uint256 duration_) external;
Expand Down
2 changes: 1 addition & 1 deletion contracts/registry/base/AttestationVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ abstract contract AttestationVerifier is RegistryStorage {
if (apps[attestation_.appId].status != AppStatus.ACTIVE) revert AppNotActive();
if (attestation_.registry != address(this)) revert WrongRegistryAddress();
if (attestation_.chainId != block.chainid) revert WrongChain();
if (attestation_.issuedAt > block.timestamp) revert FutureAttestation();
if (attestation_.issuedAt > block.timestamp + futureAttestationBuffer) revert FutureAttestation();
if (block.timestamp > attestation_.issuedAt + attestationValidityDuration) revert AttestationExpired();

signer = keccak256(abi.encode(attestation_)).toEthSignedMessageHash().recover(v, r, s);
Expand Down
7 changes: 7 additions & 0 deletions contracts/registry/base/RegistryAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ abstract contract RegistryAdmin is RegistryStorage {
emit AttestationValidityDurationSet(duration_);
}

/// @notice Updates the forward-tolerance buffer for future attestation timestamps.
/// @param buffer_ New buffer in seconds (0 to disable tolerance).
function setFutureAttestationBuffer(uint256 buffer_) public onlyOwner {
futureAttestationBuffer = buffer_;
emit FutureAttestationBufferSet(buffer_);
}

/// @notice Updates the registry-level default Merkle tree duration for new Semaphore groups.
/// @dev Does not propagate to existing groups. Only affects groups created after this call.
/// @param duration_ New duration in seconds (must be > 0).
Expand Down
5 changes: 5 additions & 0 deletions contracts/registry/base/RegistryStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ abstract contract RegistryStorage is ICredentialRegistry, Ownable2Step, Pausable
/// @notice Maximum age (in seconds) an attestation is accepted. Default 30 minutes.
uint256 public attestationValidityDuration = 30 minutes;

/// @notice Forward-tolerance buffer (in seconds) for attestation issuedAt timestamps.
/// On L2s a sequencer's block.timestamp can lag behind real-world time,
/// causing valid attestations to be rejected as "future". Default 10 minutes.
uint256 public futureAttestationBuffer = 10 minutes;

/// @notice Array of all registered credential group IDs (for enumeration).
uint256[] public credentialGroupIds;

Expand Down
66 changes: 66 additions & 0 deletions test/CredentialRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1938,6 +1938,7 @@ contract CredentialRegistryTest is Test {
bytes32 credentialId = keccak256("blinded-id");
uint256 commitment = COMMITMENT_12345;

// issuedAt exceeds the 10-minute buffer
ICredentialRegistry.Attestation memory att = ICredentialRegistry.Attestation({
registry: address(registry),
chainId: block.chainid,
Expand All @@ -1953,6 +1954,71 @@ contract CredentialRegistryTest is Test {
registry.registerCredential(att, v, r, s);
}

function testRegisterCredentialFutureAttestationWithinBuffer() public {
uint256 credentialGroupId = 1;
registry.createCredentialGroup(credentialGroupId, 0, 0);

bytes32 credentialId = keccak256("blinded-id");
uint256 commitment = COMMITMENT_12345;

// issuedAt is 5 minutes ahead, within the 10-minute default buffer
ICredentialRegistry.Attestation memory att = ICredentialRegistry.Attestation({
registry: address(registry),
chainId: block.chainid,
credentialGroupId: credentialGroupId,
credentialId: credentialId,
appId: DEFAULT_APP_ID,
semaphoreIdentityCommitment: commitment,
issuedAt: block.timestamp + 5 minutes
});
(uint8 v, bytes32 r, bytes32 s) = _signAttestation(att);

registry.registerCredential(att, v, r, s);
}

function testRegisterCredentialFutureAttestationBufferZero() public {
// Disable the buffer
registry.setFutureAttestationBuffer(0);

uint256 credentialGroupId = 1;
registry.createCredentialGroup(credentialGroupId, 0, 0);

bytes32 credentialId = keccak256("blinded-id");
uint256 commitment = COMMITMENT_12345;

// Even 1 second ahead should revert with buffer=0
ICredentialRegistry.Attestation memory att = ICredentialRegistry.Attestation({
registry: address(registry),
chainId: block.chainid,
credentialGroupId: credentialGroupId,
credentialId: credentialId,
appId: DEFAULT_APP_ID,
semaphoreIdentityCommitment: commitment,
issuedAt: block.timestamp + 1
});
(uint8 v, bytes32 r, bytes32 s) = _signAttestation(att);

vm.expectRevert(FutureAttestation.selector);
registry.registerCredential(att, v, r, s);
}

function testSetFutureAttestationBuffer() public {
assertEq(registry.futureAttestationBuffer(), 10 minutes);

vm.expectEmit(false, false, false, true);
emit FutureAttestationBufferSet(5 minutes);

registry.setFutureAttestationBuffer(5 minutes);
assertEq(registry.futureAttestationBuffer(), 5 minutes);
}

function testSetFutureAttestationBufferOnlyOwner() public {
address notOwner = makeAddr("not-owner");
vm.prank(notOwner);
vm.expectRevert("Ownable: caller is not the owner");
registry.setFutureAttestationBuffer(5 minutes);
}

function testRegisterCredentialExpiredAttestation() public {
uint256 credentialGroupId = 1;
registry.createCredentialGroup(credentialGroupId, 0, 0);
Expand Down