Gating smart contracts
On this page
This guide gates Solidity functions on Self Agent ID, so only agents backed by a human with a fresh proof can call them. The agent sends the transaction from its own EVM key, so msg.sender is the agent.
1. Declare the registry interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ISelfAgentRegistry {
struct AgentCredentials {
string issuingState;
string[] name;
string idNumber;
string nationality;
string dateOfBirth;
string gender;
string expiryDate;
uint256 olderThan;
bool[3] ofac;
}
function isVerifiedAgent(bytes32 agentKey) external view returns (bool);
function getAgentId(bytes32 agentKey) external view returns (uint256);
function isProofFresh(uint256 agentId) external view returns (bool);
function proofExpiresAt(uint256 agentId) external view returns (uint256);
function getProofProvider(uint256 agentId) external view returns (address);
function selfProofProvider() external view returns (address);
function getAgentCredentials(uint256 agentId) external view returns (AgentCredentials memory);
function getHumanNullifier(uint256 agentId) external view returns (uint256);
function getAgentCountForHuman(uint256 nullifier) external view returns (uint256);
function sameHuman(uint256 agentIdA, uint256 agentIdB) external view returns (bool);
}
The full interfaces are in the repo: contracts/src/interfaces.
2. Add an onlyVerifiedAgent modifier
contract MyContract {
ISelfAgentRegistry public immutable registry;
constructor(address registry_) {
registry = ISelfAgentRegistry(registry_);
}
modifier onlyVerifiedAgent() {
bytes32 agentKey = bytes32(uint256(uint160(msg.sender)));
require(registry.isVerifiedAgent(agentKey), "Agent not registered");
require(registry.isProofFresh(registry.getAgentId(agentKey)), "Human proof expired");
_;
}
function protectedAction() external onlyVerifiedAgent {
// only verified agents get here
}
}
The agent key is the address left-padded to 32 bytes: bytes32(uint256(uint160(addr))).
3. Require Self as the provider
uint256 agentId = registry.getAgentId(agentKey);
require(registry.getProofProvider(agentId) == registry.selfProofProvider(), "Not a Self proof");
4. Check credentials
Credentials hold what the human disclosed at registration. Undisclosed fields are empty.
function gatedByAge(uint256 minAge) external onlyVerifiedAgent {
uint256 agentId = registry.getAgentId(bytes32(uint256(uint160(msg.sender))));
ISelfAgentRegistry.AgentCredentials memory creds = registry.getAgentCredentials(agentId);
require(creds.olderThan >= minAge, "Age requirement not met");
}
5. Add sybil checks
bool same = registry.sameHuman(agentIdA, agentIdB);
uint256 count = registry.getAgentCountForHuman(registry.getHumanNullifier(agentId));
require(count <= maxAllowed, "Too many agents for this human");
6. Deploy
Pass the SelfAgentRegistry address to the constructor: 0xaC3DF9ABf80d0F5c020C06B04Cced27763355944 on Celo Mainnet, or 0x043DaCac8b0771DD5b444bCC88f2f8BBDBEdd379 on Celo Sepolia. Other contract addresses are in Smart contracts.
7. Foundry setup
Hub V2 uses the PUSH0 opcode, so build and test with --evm-version cancun:
forge build --evm-version cancun
forge test --evm-version cancun
Or set it in foundry.toml:
[profile.default]
evm_version = "cancun"
Next steps
- Build an agent that calls your contract
- Smart contracts: the full on-chain API
- Troubleshooting
Thanks. What went wrong?
Thanks for your feedback!