Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
101 changes: 78 additions & 23 deletions contracts/contracts/abstract/SelfVerificationRoot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ abstract contract SelfVerificationRoot is ISelfVerificationRoot {

/// @notice The attestation ID that proofs must match
/// @dev Used to validate that submitted proofs is generated with allowed attestation IDs
mapping(uint256 => bool) internal _attestationIds;
mapping(uint256 attestationId => bool attestationIdEnabled) internal _attestationIdToEnabled;

/// @notice Configuration settings for the verification process
/// @dev Contains settings for age verification, country restrictions, and OFAC checks
Expand Down Expand Up @@ -69,27 +69,53 @@ abstract contract SelfVerificationRoot is ISelfVerificationRoot {
/// @dev Triggered in verifySelfProof when attestation ID validation fails
error InvalidAttestationId();

// ====================================================
// Events
// ====================================================

/// @notice Emitted when the verification configuration is updated
event VerificationConfigUpdated(ISelfVerificationRoot.VerificationConfig indexed verificationConfig);

/// @notice Emitted when the verification is successful
event VerificationSuccess(uint256[3] revealedDataPacked, uint256 indexed userIdentifier, uint256 indexed nullifier);

/// @notice Emitted when the scope is updated
event ScopeUpdated(uint256 indexed newScope);

/// @notice Emitted when a new attestation ID is added
event AttestationIdAdded(uint256 indexed attestationId);

/// @notice Emitted when an attestation ID is removed
event AttestationIdRemoved(uint256 indexed attestationId);

/**
* @notice Initializes the SelfVerificationRoot contract.
* @param identityVerificationHub The address of the Identity Verification Hub.
* @param scope The expected proof scope for user registration.
* @param identityVerificationHubAddress The address of the Identity Verification Hub.
* @param scopeValue The expected proof scope for user registration.
* @param attestationIds The expected attestation identifiers required in proofs.
*/
constructor(address identityVerificationHub, uint256 scope, uint256[] memory attestationIds) {
_identityVerificationHub = IIdentityVerificationHubV1(identityVerificationHub);
_scope = scope;
for (uint256 i = 0; i < attestationIds.length; i++) {
_attestationIds[attestationIds[i]] = true;
constructor(address identityVerificationHubAddress, uint256 scopeValue, uint256[] memory attestationIds) {
_identityVerificationHub = IIdentityVerificationHubV1(identityVerificationHubAddress);
_scope = scopeValue;

// Cache array length for gas optimization
uint256 length = attestationIds.length;
for (uint256 i; i < length; ) {
_attestationIdToEnabled[attestationIds[i]] = true;
unchecked {
++i;
}
}
}

/**
* @notice Updates the verification configuration
* @dev Used to set or update verification parameters after contract deployment
* @param verificationConfig The new verification configuration to apply
* @param newVerificationConfig The new verification configuration to apply
*/
function _setVerificationConfig(ISelfVerificationRoot.VerificationConfig memory verificationConfig) internal {
_verificationConfig = verificationConfig;
function _setVerificationConfig(ISelfVerificationRoot.VerificationConfig memory newVerificationConfig) internal {
_verificationConfig = newVerificationConfig;
emit VerificationConfigUpdated(newVerificationConfig);
}

/**
Expand All @@ -108,6 +134,7 @@ abstract contract SelfVerificationRoot is ISelfVerificationRoot {
*/
function _setScope(uint256 newScope) internal {
_scope = newScope;
emit ScopeUpdated(newScope);
}

/**
Expand All @@ -116,7 +143,8 @@ abstract contract SelfVerificationRoot is ISelfVerificationRoot {
* @param attestationId The attestation ID to add
*/
function _addAttestationId(uint256 attestationId) internal {
_attestationIds[attestationId] = true;
_attestationIdToEnabled[attestationId] = true;
emit AttestationIdAdded(attestationId);
}

/**
Expand All @@ -125,7 +153,8 @@ abstract contract SelfVerificationRoot is ISelfVerificationRoot {
* @param attestationId The attestation ID to remove
*/
function _removeAttestationId(uint256 attestationId) internal {
_attestationIds[attestationId] = false;
_attestationIdToEnabled[attestationId] = false;
emit AttestationIdRemoved(attestationId);
}

/**
Expand All @@ -134,8 +163,8 @@ abstract contract SelfVerificationRoot is ISelfVerificationRoot {
* @param pubSignals The proof's public signals
* @return revealedDataPacked Array of the three packed revealed data values
*/
function getRevealedDataPacked(
uint256[21] memory pubSignals
function _getRevealedDataPacked(
uint256[21] calldata pubSignals
) internal pure returns (uint256[3] memory revealedDataPacked) {
revealedDataPacked[0] = pubSignals[REVEALED_DATA_PACKED_INDEX];
revealedDataPacked[1] = pubSignals[REVEALED_DATA_PACKED_INDEX + 1];
Expand All @@ -148,22 +177,28 @@ abstract contract SelfVerificationRoot is ISelfVerificationRoot {
* @dev Validates scope and attestation ID before performing verification through the identity hub
* @param proof The proof data for verification and disclosure
*/
function verifySelfProof(ISelfVerificationRoot.DiscloseCircuitProof memory proof) public virtual {
if (_scope != proof.pubSignals[CircuitConstants.VC_AND_DISCLOSE_SCOPE_INDEX]) {
function verifySelfProof(ISelfVerificationRoot.DiscloseCircuitProof calldata proof) public {
// Cache storage reads for gas optimization
uint256 cachedScope = _scope;

if (cachedScope != proof.pubSignals[CircuitConstants.VC_AND_DISCLOSE_SCOPE_INDEX]) {
revert InvalidScope();
}

if (!_attestationIds[proof.pubSignals[CircuitConstants.VC_AND_DISCLOSE_ATTESTATION_ID_INDEX]]) {
if (!_attestationIdToEnabled[proof.pubSignals[CircuitConstants.VC_AND_DISCLOSE_ATTESTATION_ID_INDEX]]) {
revert InvalidAttestationId();
}

// Cache verification config to avoid multiple storage reads
ISelfVerificationRoot.VerificationConfig memory config = _verificationConfig;

_identityVerificationHub.verifyVcAndDisclose(
IIdentityVerificationHubV1.VcAndDiscloseHubProof({
olderThanEnabled: _verificationConfig.olderThanEnabled,
olderThan: _verificationConfig.olderThan,
forbiddenCountriesEnabled: _verificationConfig.forbiddenCountriesEnabled,
forbiddenCountriesListPacked: _verificationConfig.forbiddenCountriesListPacked,
ofacEnabled: _verificationConfig.ofacEnabled,
olderThanEnabled: config.olderThanEnabled,
olderThan: config.olderThan,
forbiddenCountriesEnabled: config.forbiddenCountriesEnabled,
forbiddenCountriesListPacked: config.forbiddenCountriesListPacked,
ofacEnabled: config.ofacEnabled,
vcAndDiscloseProof: IVcAndDiscloseCircuitVerifier.VcAndDiscloseProof({
a: proof.a,
b: proof.b,
Expand All @@ -172,5 +207,25 @@ abstract contract SelfVerificationRoot is ISelfVerificationRoot {
})
})
);

uint256[3] memory revealedDataPacked = _getRevealedDataPacked(proof.pubSignals);
uint256 userIdentifier = proof.pubSignals[USER_IDENTIFIER_INDEX];
uint256 nullifier = proof.pubSignals[NULLIFIER_INDEX];

emit VerificationSuccess(revealedDataPacked, userIdentifier, nullifier);
onBasicVerificationSuccess(revealedDataPacked, userIdentifier, nullifier);
}

/**
* @notice Hook called after successful verification
* @dev Virtual function to be overridden by derived contracts for custom business logic
* @param revealedDataPacked The packed revealed data from the proof
* @param userIdentifier The user identifier from the proof
* @param nullifier The nullifier from the proof
*/
function onBasicVerificationSuccess(
uint256[3] memory revealedDataPacked,
uint256 userIdentifier,
uint256 nullifier
) internal virtual;
}
130 changes: 72 additions & 58 deletions contracts/contracts/example/Airdrop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ pragma solidity 0.8.28;

import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {SelfVerificationRoot} from "../abstract/SelfVerificationRoot.sol";
import {ISelfVerificationRoot} from "../interfaces/ISelfVerificationRoot.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

import {ISelfVerificationRoot} from "../interfaces/ISelfVerificationRoot.sol";

import {SelfVerificationRoot} from "../abstract/SelfVerificationRoot.sol";

/**
* @title Airdrop (Experimental)
* @notice This contract manages an airdrop campaign by verifying user registrations with zero‐knowledge proofs
* and distributing ERC20 tokens. It is provided for testing and demonstration purposes only.
* **WARNING:** This contract has not been audited and is NOT intended for production use.
* @dev Inherits from PassportAirdropRoot for registration logic and Ownable for administrative control.
* @dev Inherits from SelfVerificationRoot for registration logic and Ownable for administrative control.
*/
contract Airdrop is SelfVerificationRoot, Ownable {
using SafeERC20 for IERC20;
Expand All @@ -32,8 +34,11 @@ contract Airdrop is SelfVerificationRoot, Ownable {
/// @notice Indicates whether the claim phase is active.
bool public isClaimOpen;

mapping(uint256 => uint256) internal _nullifiers;
mapping(uint256 => bool) internal _registeredUserIdentifiers;
/// @notice Maps nullifiers to user identifiers for registration tracking
mapping(uint256 nullifier => uint256 userIdentifier) internal _nullifierToUserIdentifier;

/// @notice Maps user identifiers to registration status
mapping(uint256 userIdentifier => bool registered) internal _registeredUserIdentifiers;

// ====================================================
// Errors
Expand All @@ -51,9 +56,12 @@ contract Airdrop is SelfVerificationRoot, Ownable {
error RegistrationNotClosed();
/// @notice Reverts when a claim is attempted while claiming is not enabled.
error ClaimNotOpen();

error RegisteredNullifier();
/// @notice Reverts when an invalid user identifier is provided.
error InvalidUserIdentifier();
/// @notice Reverts when a user identifier has already been registered
error UserIdentifierAlreadyRegistered();
/// @notice Reverts when a nullifier has already been registered
error RegisteredNullifier();

// ====================================================
// Events
Expand All @@ -73,13 +81,11 @@ contract Airdrop is SelfVerificationRoot, Ownable {
/// @notice Emitted when the claim phase is closed.
event ClaimClose();

/// @notice Emitted when a user identifier is registered.
event UserIdentifierRegistered(uint256 indexed registeredUserIdentifier, uint256 indexed nullifier);
/// @notice Emitted when the scope is updated.
event ScopeUpdated(uint256 newScope);
/// @notice Emitted when a new attestation ID is added.
event AttestationIdAdded(uint256 attestationId);
/// @notice Emitted when an attestation ID is removed.
event AttestationIdRemoved(uint256 attestationId);

/// @notice Emitted when the Merkle root is updated.
event MerkleRootUpdated(bytes32 newMerkleRoot);

// ====================================================
// Constructor
Expand All @@ -89,17 +95,17 @@ contract Airdrop is SelfVerificationRoot, Ownable {
* @notice Constructor for the experimental Airdrop contract.
* @dev Initializes the airdrop parameters, zero-knowledge verification configuration,
* and sets the ERC20 token to be distributed.
* @param _identityVerificationHub The address of the Identity Verification Hub.
* @param _scope The expected proof scope for user registration.
* @param _identityVerificationHubAddress The address of the Identity Verification Hub.
* @param _scopeValue The expected proof scope for user registration.
* @param _attestationIds The expected attestation identifiers required in proofs.
* @param _token The address of the ERC20 token for airdrop.
*/
constructor(
address _identityVerificationHub,
uint256 _scope,
address _identityVerificationHubAddress,
uint256 _scopeValue,
uint256[] memory _attestationIds,
address _token
) SelfVerificationRoot(_identityVerificationHub, _scope, _attestationIds) Ownable(_msgSender()) {
) SelfVerificationRoot(_identityVerificationHubAddress, _scopeValue, _attestationIds) Ownable(_msgSender()) {
token = IERC20(_token);
}

Expand All @@ -110,10 +116,11 @@ contract Airdrop is SelfVerificationRoot, Ownable {
/**
* @notice Sets the Merkle root for claim validation.
* @dev Only callable by the contract owner.
* @param _merkleRoot The new Merkle root.
* @param newMerkleRoot The new Merkle root.
*/
function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner {
merkleRoot = _merkleRoot;
function setMerkleRoot(bytes32 newMerkleRoot) external onlyOwner {
merkleRoot = newMerkleRoot;
emit MerkleRootUpdated(newMerkleRoot);
}

/**
Expand Down Expand Up @@ -193,32 +200,6 @@ contract Airdrop is SelfVerificationRoot, Ownable {
emit ClaimClose();
}

/**
* @notice Registers a user's address by verifying a provided zero-knowledge proof.
* @dev Reverts if the registration phase is not open.
* @param proof The VC and Disclose proof data used to verify and register the user.
*/
function verifySelfProof(ISelfVerificationRoot.DiscloseCircuitProof memory proof) public override {
if (!isRegistrationOpen) {
revert RegistrationNotOpen();
}

if (_nullifiers[proof.pubSignals[NULLIFIER_INDEX]] != 0) {
revert RegisteredNullifier();
}

if (proof.pubSignals[USER_IDENTIFIER_INDEX] == 0) {
revert InvalidUserIdentifier();
}

super.verifySelfProof(proof);

_nullifiers[proof.pubSignals[NULLIFIER_INDEX]] = proof.pubSignals[USER_IDENTIFIER_INDEX];
_registeredUserIdentifiers[proof.pubSignals[USER_IDENTIFIER_INDEX]] = true;

emit UserIdentifierRegistered(proof.pubSignals[USER_IDENTIFIER_INDEX], proof.pubSignals[NULLIFIER_INDEX]);
}

/**
* @notice Retrieves the expected proof scope.
* @return The scope value used for registration verification.
Expand All @@ -233,16 +214,7 @@ contract Airdrop is SelfVerificationRoot, Ownable {
* @return True if the attestation ID is allowed, false otherwise.
*/
function isAttestationIdAllowed(uint256 attestationId) external view returns (bool) {
return _attestationIds[attestationId];
}

/**
* @notice Retrieves the stored nullifier for a given key.
* @param nullifier The nullifier to query.
* @return The user identifier associated with the nullifier.
*/
function getNullifier(uint256 nullifier) external view returns (uint256) {
return _nullifiers[nullifier];
return _attestationIdToEnabled[attestationId];
}

/**
Expand Down Expand Up @@ -290,11 +262,53 @@ contract Airdrop is SelfVerificationRoot, Ownable {

// Mark as claimed and transfer tokens.
_setClaimed();
IERC20(token).safeTransfer(msg.sender, amount);
token.safeTransfer(msg.sender, amount);

emit Claimed(index, msg.sender, amount);
}

// ====================================================
// Override Functions from SelfVerificationRoot
// ====================================================

/**
* @notice Hook called after successful verification - handles user registration
* @dev Validates registration conditions and registers the user
* @param userIdentifier The user identifier from the proof
* @param nullifier The nullifier from the proof
*/
function onBasicVerificationSuccess(
uint256[3] memory /* revealedDataPacked */,
uint256 userIdentifier,
uint256 nullifier
) internal override {
// Check if registration is open
if (!isRegistrationOpen) {
revert RegistrationNotOpen();
}

// Check if nullifier has already been registered
if (_nullifierToUserIdentifier[nullifier] != 0) {
revert RegisteredNullifier();
}

// Check if user identifier is valid
if (userIdentifier == 0) {
revert InvalidUserIdentifier();
}

// Check if user identifier has already been registered
if (_registeredUserIdentifiers[userIdentifier]) {
revert UserIdentifierAlreadyRegistered();
}

_nullifierToUserIdentifier[nullifier] = userIdentifier;
_registeredUserIdentifiers[userIdentifier] = true;

// Emit registration event
emit UserIdentifierRegistered(userIdentifier, nullifier);
}

// ====================================================
// Internal Functions
// ====================================================
Expand Down
Loading