Self is a privacy-first, open-source identity protocol that uses zero-knowledge proofs for secure identity verification.
It enables Sybil resistance and selective disclosure using real-world attestations like passports.
[TOC]
Self Protocol allows digital identity verification with zero-knowledge proofs in three steps:
- Scan its Passport: the user scan its passport using the NFC reader of its phone.
- Generate a Proof: the Self application generates a zk proof over the user's passport, selecting only what the user wants to disclose.
- Share The Proof: the user can now share its zk proof with the selected application.
Self can be relevant to apply transfers restrictions on a token representing a financial instrument such as stock, equities or debt.
- Nationality
- Why: Different countries have regulations on who can hold shares (e.g., restrictions on foreign investors).
- Restriction Use: Block or allow share ownership based on nationality.
- Age Check Result (older_than)
- Why: Legal minimum age requirements to own or trade shares (e.g., must be 18+).
- Restriction Use: Prevent minors from holding or transacting tokenized shares.
- OFAC Checks (passport_no_ofac, name_and_dob_ofac, name_and_yob_ofac)
- Why: Must comply with sanctions and anti-money laundering (AML) regulations to avoid transferring shares to blacklisted or restricted individuals/entities.
- Restriction Use: Block shares transfers or ownership if the user is on a sanctions or watchlist (OFAC).
- User ID / Application (contextual)
- Why: To ensure each tokenized share is linked to a verified identity and application scope, preventing double registration or fraud.
- Restriction Use: Prevent double ownership
- CMTAT is a security token framework that includes various compliance features such as conditional transfer, account freeze, and token pause. By the past, CMTAT has been used by several different companies such as Taurus SA, UBS (Project Guardian), obligate (on-chain bond and dividend) and many others.
- RuleEngine is an external contract used to apply transfer restrictions to another contract, initially the CMTAT. Acting as a controller, it can call different contract rules and apply these rules on each transfer.
These two contracts use ERC-1404 (draft ERC) to provide human-readable message and error code if a transfer is restricted
The project aims to provide a rule called RuleSelf to allow an issuer to restrict transfers of a CMTAT token deployed by using some propreties of a passport.
Typically, it will be possible to forbid transfer to address present on OFAC sanction list.
During this hackaton, unfortunataly, only the Rule was deployed and no integration testing has been done with CMTAT and RuleEngine.
Schema showing the interaction between a token holder and a CMTAT token with a configured RuleEngine as well as a RuleSelf.
A same RuleSelfand RuleEnginecan be used for several different tokens.
- Schema showing the interaction between a Self user and the smart contract
RuleSelf.
Self uses zero-knowledge proofs to verify identity document information without exposing the actual document data. Here's the complete flow:
- SelfAppBuilder creates a configuration object defining wha the user wants to verify
- SelfQRcodeWrapper displays a QR code that users scan with the Self mobile app
- The QR code contains the verification requirements and a unique session ID
- User scans QR code with Self mobile app
- App reads the identity document's NFC chip and generates a zero-knowledge proof
- Proof validates requirements (age, nationality, etc.) without revealing actual data
- Mobile app sends proof to the smart contract via the Self relayer service
- The smart contract entrypoint is the public function
verifySelfProof
- The smart contract entrypoint is the public function
- The smart contract checks the proof validity inside the function
verfySelfProof - Then it calls the hub contract
identityVerificationHubV2 - This contract will perform a callback to the the function
onVerificationSuccess - Then the smart contract can perform custom logic inside the customizable verification hook
customVerificationHook
Reference: https://docs.self.xyz/sdk-reference/selfappbuilder
Disclosures allow the users to reveal information about their passport. For example, if an application (smart contract or backend server) want to check if a user is above the age of 18 then at the very least the application will end up disclosing the lower bound of the age range of the user.
The disclosures object allows the application to specify what information the application wants to verify and request from the user's passport:
disclosures: {
// Identity fields (optional)
issuing_state?: boolean, // Country that issued the passport
name?: boolean, // Full name
passport_number?: boolean, // Passport number
nationality?: boolean, // Nationality
date_of_birth?: boolean, // Date of birth
gender?: boolean, // Gender
expiry_date?: boolean, // Passport expiry date
// Verification requirements (optional)
minimumAge?: number, // Minimum age requirement (e.g., 18, 21)
excludedCountries?: string[], //
ofac?: boolean, // Enable OFAC sanctions checking
}- Issuing Country – The country that issued the passport.
- Full Name – The name as shown on the passport.
- Passport Number – The user's passport number.
- Nationality – The user's nationality according to the passport.
- Date of Birth – Full date of birth.
- Gender – Gender information from the passport.
- Passport Expiry Date – When the passport will expire.
- Age Check Result – Whether the user is older than a specific age (e.g., 18).
- Countries excluded - ISO 3-letter codes (e.g., ['IRN', 'PRK'])
- OFAC Check (Passport Number) – Result of a sanctions list check using the passport number.
- OFAC Check (Name and DOB) – Result of a sanctions list check using the name and full date of birth.
- OFAC Check (Name and YOB) – Result of a sanctions list check using the name and year of birth.
For our use case, we have performed the following ferification:
- The token holder must be major (18 years old minimum)
- IRAN and PRK are excluded
- OFAC sanctions are checked
disclosures: {
minimumAge: 18,
ofac: true,
excludedCountries: [countries.IRAN, countries.AFGHANISTAN],
expiry_date: true,
}Reference: https://docs.self.xyz/use-self/use-deeplinking
This section explains how the contracts are build
The constructor set the initial config identifiant and scope value.
It also sets the identity hub verification address.
constructor(
address identityVerificationHubAddress,
bytes32 configId_,
uint256 scopeValue
) SelfVerificationRoot(identityVerificationHubAddress, scopeValue) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setConfigId(configId_);
}//App-specific configuration ID
bytes32 public configId;
Override to provide configId for verification
function getConfigId(
bytes32 destinationChainId,
bytes32 userIdentifier,
bytes memory userDefinedData // Custom data from the qr code configuration
) public view override returns (bytes32) {
// Return your app's configuration ID
return configId;
}function setConfigId(bytes32 _configId) public onlyRole(DEFAULT_ADMIN_ROLE) {
_setConfigId(_configId);
}
function _setConfigId(bytes32 _configId) internal {
configId = _configId;
}The Self Configuration Tools allows to create a verification configuration and generates a config ID. This tool allows you to configure age requirements, country restrictions, and OFAC checks with a user-friendly interface. A contract representing the configuration will be deployed by the tool
Reference:
Override to handle successful verification.
This hook is called if the proof is considered as valid.
It is responsible to register the user and to store the nullifier.
function customVerificationHook(
ISelfVerificationRoot.GenericDiscloseOutputV2 memory output,
bytes memory /* userData */
) internal override {
// Check if registration is open
if (!isRegistrationOpen) {
revert RegistrationNotOpen();
}
// Check if nullifier has already been registered
if (_nullifierToUserIdentifier[output.nullifier] != 0) {
revert RegisteredNullifier();
}
// Check if user identifier is valid
if (output.userIdentifier == 0) {
revert InvalidUserIdentifier();
}
// Check if user identifier has already been registered
if (_registeredUserIdentifiers[output.userIdentifier]) {
revert UserIdentifierAlreadyRegistered();
}
_nullifierToUserIdentifier[output.nullifier] = output.userIdentifier;
_registeredUserIdentifiers[output.userIdentifier] = true;
// Emit registration event
emit UserIdentifierRegistered(output.userIdentifier, output.nullifier);
}The scope is the final value you set in your Self Verification contract. It's generated by hashing the scope seed 🌱 with the address or DNS, creating a unique identifier for the verification requirements.
Your contract needs a proper scope for verification. You have two approaches:
Your contract needs a proper scope for verification. You have two approaches:
// Calculate scope before deployment using predicted address
// Use tools.self.xyz to calculate scope with your predicted contract address
Option 2: Update scope after deployment (easier)
uint256 public scope;
function setScope(uint256 _scope) external onlyOwner {
scope = _scope;
// Update the scope in the parent contract
_setScope(_scope);
}
After deployment, use the Self Configuration Tools to calculate the actual scope with your deployed contract address and update it using the setter function.
Once we have the scope, we set the value inside the smart contract
These functions allows to open and close the registration, as well as returned a boolean to indicate if a target address is registered inside the contract.
Taken from the Aidrop contract example
/**
* @notice Opens the registration phase for users.
* @dev Only callable by the contract owner.
*/
function openRegistration() public onlyRole(DEFAULT_ADMIN_ROLE) {
isRegistrationOpen = true;
emit RegistrationOpen();
}
/**
* @notice Closes the registration phase.
* @dev Only callable by the contract owner.
*/
function closeRegistration() public onlyRole(DEFAULT_ADMIN_ROLE) {
isRegistrationOpen = false;
emit RegistrationClose();
}
/**
* @notice Checks if a given address is registered.
* @param registeredAddress The address to check.
* @return True if the address is registered, false otherwise.
*/
function isRegistered(address registeredAddress) public view returns (bool) {
return _registeredUserIdentifiers[uint256(uint160(registeredAddress))];
}The contract RuleSelf is deployed on Celo testnet at the following address: 0xcadbe20e16d68c7abbb3a109a18fc3709ed49fdc
First Verify Self Proof function calls: https://alfajores.celoscan.io/tx/0xac3ca7de3bd89a5223b6fb55a318942aa5e5576b9afc73c12ff3c635edcfa7f6
We can see that the transfer is denied because the recipient tois not registered.
- It may happen that some tokens can only be sold to citizens of a certain country, e.g. the country where the shares are issued. Self's solution does not allow for determining these restrictions based on residency but only on nationality.
- It is not possible for a token holder to transfer his tokens to another address because this other address will not be registered in the contracts.
Possible solutions are as follows:
-
A "recoveryWallet/burnAndMint" function accessible only to the issuer to perform the transfer
-
Allow through a dedicated function a token holder to:
- Delete their previous address
- Add their new address through a function
-
A malicious person could steal the passport of a person and use it to create a valid identifiant and to register inside the
RuleSelfcontract.










