Introduction
In this tutorial, you’ll learn not only what an identity management system is but also how to build one on the Celo blockchain. Celo is a blockchain platform focused on inclusive finance and user empowerment. Using Solidity, we’ll create a system where individuals control their own identity information.
By the end, you’ll have the knowledge to build a decentralized identity management system on Celo. This will enable you to develop secure identity solutions for sectors like finance, healthcare, and e-commerce. Let’s get started on empowering individuals with control over their identities in the decentralized world of Celo!
Prerequisite
For the purpose of this tutorial, you will need to have some level of knowledge of Solidity and the Celo blockchain.
Requirements
For this tutorial, you will need
- A code editor. VScode is preferable.
- A crypto wallet: celo-wallet-extension, Celo wallet, MetaMask
- Fund your wallet using the Celo Faucet. To do this, copy the wallet address and visit the Celo Faucet . Paste your wallet address into the designated field and confirm.
- Nodejs is installed on your computer. If not, download from here
Check out this video on how to install and set up the celo-wallet-extension.
Decentralized Identity Management System (DID)
A decentralized identity management system (DID) is a type of identity management system that is designed to give individuals more control over their personal data and privacy. Unlike traditional identity management systems, which are often centralized and owned by a single entity (such as a government or corporation), decentralized identity management systems are based on a distributed architecture where users can store and manage their own identity information.
Each user in a DID system has a unique identifier, or “DID,” that serves as a representation of their digital identity. Depending on the situation, individuals can choose to divulge as much or as little information about themselves using this identification, which is not connected to any one organization or government.
Blockchain technology or other decentralized data storage systems are frequently used by decentralized identity management systems to store and manage user identification data. Since the user maintains control over their own data and there is no single point of failure that may be hacked or subject to data breaches, this enables improved security and privacy.
Examples of Decentralized Identity Management Systems
- uPort: uPort is a decentralized identity platform built on the Ethereum blockchain. It allows users to create and manage their own self-sovereign digital identities. uPort provides a wallet for storing and managing identity credentials and enables users to control the sharing of their personal information.
- SelfKey: SelfKey is a blockchain-based identity management system that allows individuals to manage their digital identities and control access to their personal data. It provides a digital wallet for storing identity documents, such as passports and driver’s licenses, and enables users to securely share their information with trusted parties.
- Civic: Civic is a decentralized identity verification platform that uses blockchain technology to provide secure and tamper-proof identity verification. Users can store their personal information in the Civic app and selectively share it with service providers when needed, eliminating the need for multiple username and password combinations.
These are just a few examples of decentralized identity management products available in the market. Each product has its own unique features and implementation, but they all share the common goal of providing individuals with control over their digital identities and enhancing privacy and security in the digital realm.
Building a Decentralized Identity Management System
We will be creating a decentralized identity management system using solidity that allows users to create and manage their own identity by storing their name, age, and email address and then deploy the smart contract to the Celo blockchain.
Project Setup
To set up the project, open a terminal and run these commands.
mkdir decentralized-identity
cd decentralized-identity
yarn init --yes
yarn add --dev hardhat @nomiclabs/hardhat-waffle hardhat-deploy
After running the commands above, in the same directory, run the command below
npx hardhat
Select Create a Javascript Project
and follow the steps. This sets up a new Hardhat project.
The Smart Contract
After setting up the hardhat project, create a new solidity file in the contracts directory and name it Identity.sol
Next, copy and paste the code below into the Identity.sol
file
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract IdentityManagement {
address public owner;
mapping(address => bool) public admins;
struct Identity {
address identityOwner;
string name;
uint256 age;
string email;
// You can add more identity attributes as needed
}
mapping(address => Identity) public identities;
event IdentityCreated(address indexed _identityOwner, string name);
event IdentityUpdated(address indexed _identityOwner, string name);
modifier onlyOwner() {
require(msg.sender == owner, "Only contract owner can perform this action");
_;
}
modifier onlyAdmin() {
require(admins[msg.sender], "Only administrators can perform this action");
_;
}
constructor() {
owner = msg.sender;
admins[msg.sender] = true;
}
function addAdmin(address _admin) public onlyOwner {
admins[_admin] = true;
}
function removeAdmin(address _admin) public onlyOwner {
require(_admin != owner, "Contract owner cannot be removed as admin");
admins[_admin] = false;
}
function createIdentity(string memory _name, uint256 _age, string memory _email) public {
require(identities[msg.sender].identityOwner == address(0), "Identity already exists");
Identity storage newIdentity = identities[msg.sender];
newIdentity.identityOwner = msg.sender;
newIdentity.name = _name;
newIdentity.age = _age;
newIdentity.email = _email;
emit IdentityCreated(msg.sender, _name);
}
function updateIdentity(string memory _name, uint256 _age, string memory _email) public onlyAdmin {
require(identities[msg.sender].identityOwner != address(0), "Identity does not exist");
Identity storage updatedIdentity = identities[msg.sender];
updatedIdentity.name = _name;
updatedIdentity.age = _age;
updatedIdentity.email = _email;
emit IdentityUpdated(msg.sender, _name);
}
function getIdentity(address _owner) public view returns (string memory, uint256, string memory) {
Identity storage identity = identities[_owner];
return (identity.name, identity.age, identity.email);
}
}
A brief breakdown of what the contract does:
pragma solidity ^0.8.0;
contract IdentityManagement {
address public owner;
mapping(address => bool) public admins;
struct Identity {
address identityOwner;
string name;
uint256 age;
string email;
// Add more identity attributes as needed
}
mapping(address => Identity) public identities;
event IdentityCreated(address indexed _identityOwner, string name);
event IdentityUpdated(address indexed _identityOwner, string name);
The contract begins with the declaration of the IdentityManagement
contract and its version pragma. The owner
variable is of type address
and represents the contract owner. The admins
mapping is used to store the addresses of administrators who have certain privileges within the contract.
The Identity
struct is defined to hold the identity information, including the identityOwner
address, name
, age
, email
, and any additional attributes that may be needed.
The identities
mapping is used to associate each address with its corresponding Identity
struct, allowing for easy lookup and retrieval of identity information.
The IdentityCreated
and IdentityUpdated
events are emitted to notify listeners when an identity is created or updated, respectively.
modifier onlyOwner() {
require(msg.sender == owner, "Only contract owner can perform this action");
_;
}
modifier onlyAdmin() {
require(admins[msg.sender], "Only administrators can perform this action");
_;
}
The onlyOwner
modifier is defined to restrict certain functions to being callable only by the contract owner. It checks whether the caller of the function (msg.sender
) matches the owner
address defined in the contract.
The onlyAdmin
modifier restricts certain functions to being callable only by administrators. It checks whether the msg.sender
address is present in the admins
mapping and sets it to true
.
constructor() {
owner = msg.sender;
admins[msg.sender] = true;
}
function addAdmin(address _admin) public onlyOwner {
admins[_admin] = true;
}
function removeAdmin(address _admin) public onlyOwner {
require(_admin != owner, "Contract owner cannot be removed as admin");
admins[_admin] = false;
}
The constructor function is executed only once during contract deployment. It sets the owner
address to the address that deployed the contract and adds the deployer as an admin by setting admins[msg.sender]
to true
.
The addAdmin
function allows the contract owner to add new administrators by specifying their address as an argument. It sets admins[_admin]
to true
, granting them admin privileges.
The removeAdmin
function allows the contract owner to remove administrators by specifying their address as an argument. It ensures that the contract owner cannot be removed as an admin and sets admins[_admin]
to false
, revoking their admin privileges.
function createIdentity(string memory _name, uint256 _age, string memory _email) public {
require(identities[msg.sender].identityOwner == address(0), "Identity already exists");
Identity storage newIdentity = identities[msg.sender];
newIdentity.identityOwner = msg.sender;
newIdentity.name = _name;
newIdentity.age = _age;
newIdentity.email = _email;
emit IdentityCreated(msg.sender, _name);
}
The function begins by checking if an identity already exists for the caller (msg.sender
). It does this by verifying if the owner
address in the identities
mapping is address(0)
, which indicates that no identity exists.
If the identity doesn’t already exist, a new Identity
struct is created and assigned to newIdentity
, which is a storage reference to the corresponding identity in the identities
mapping. The owner
field of the new identity is set to the caller’s address (msg.sender
), and the provided _name
, _age
, and _email
values are assigned to the respective fields.
Finally, the IdentityCreated
event is emitted, indicating the successful creation of the identity, with the caller’s address and the provided name as the event parameters.
function updateIdentity(string memory _name, uint256 _age, string memory _email) public onlyAdmin {
require(identities[msg.sender].identityOwner != address(0), "Identity does not exist");
Identity storage updatedIdentity = identities[msg.sender];
updatedIdentity.name = _name;
updatedIdentity.age = _age;
updatedIdentity.email = _email;
emit IdentityUpdated(msg.sender, _name);
}
The updateIdentity
function allows an admin to update their identity by providing the new _name
, _age
, and _email
values as arguments. It uses the onlyAdmin
modifier to ensure that only administrators can invoke this function.
Before updating the identity, the function checks if an identity exists for the caller (msg.sender
) by verifying if the owner
address in the identities
mapping is not address(0)
. If no identity exists, the function reverts with an error message.
If the identity exists, the function retrieves the storage reference to the corresponding identity in the identities
mapping and updates the name
, age
, and email
fields with the provided values.
Finally, the IdentityUpdated
event is emitted to indicate the successful update of the identity, with the caller’s address and the updated name as the event parameters.
function getIdentity(address _identityOwner) public view returns (string memory, uint256, string memory) {
Identity storage identity = identities[_owner];
return (identity.name, identity.age, identity.email);
}
}
The getIdentity
function allows anyone to retrieve the identity information associated with a specific address. It takes the _owner
address as an argument and returns the name
, age
, and email
fields of the corresponding identity.
The function retrieves the storage reference to the identity from the identities
mapping using the _owner
address. It then returns the requested identity attributes as a tuple.
That concludes the breakdown of the smart contract code for the identity management system.
Compiling The Smart Contract
Next, you will compile the smart contract
To compile the smart contract, run the command below in your terminal
npx hardhat compile
Once the compilation is successful, you should see this message on your terminal
Deploying The Smart Contract
We will deploy the smart contract to the celo test blockchain, as I already mentioned. To accomplish this, we must write a deployment script that connects to the Alfajores testnet through forno. First, we will substitute the configuration code for deployment made accessible by Celo for the content of the default hardhat.config.js
file provided to us by hardhat.
To do this, replace the content of the hardhat.config.js
file with the code below.
require("@nomiclabs/hardhat-waffle");
require('dotenv').config({path: '.env'});
require('hardhat-deploy');
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
defaultNetwork: "alfajores",
networks: {
localhost: {
url: "http://127.0.0.1:7545"
},
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0"
},
//chainId: 44787
},
celo: {
url: "https://forno.celo.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0"
},
chainId: 42220
},
},
solidity: "0.8.4",
};
In the hardhat.config.js
, add the gas price and gas to the alfajores object.
alfajores: {
gasPrice: 200000000000,
gas: 41000000000,
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0
}
Now we would have to install dotenv package in order to import the env file and use in the config file.
Run the command below in your terminal to install the dotenv package.
npm install dotenv
Now create an .env
file in your project root folder and add the following line
MNEMONIC=//Go to your celo wallet, copy and paste your 12 key phrase
Deploying To The Celo Blockchain
To deploy the smart contract, first, you will need to write a deployment script.
In the script folder, there is a deploy.js
file. Replace the content of the deploy.js
file with the code below.
const { ethers } = require("hardhat");
async function main() {
const IndentityContract = await ethers.getContractFactory("IdentityManagement");
const deployedIdentityContract = await IndentityContract.deploy();
await deployedIdentityContract.deployed();
console.log("Identity Management Contract Address:", deployedIdentityContract.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.log(error);
process.exit(1);
});
Run the following command in your project root directory to deploy your smart contract to the Celo Alfajores testnet.
npx hardhat run scripts/deploy.js --network alfajores
You can copy and save the smart contract address that will be printed on your terminal when deployment is successful somewhere.
View Contract Deployment
To view your deployed smart contract, take the smart contract address you copied from the terminal and visit the celo alfajores explorer to search for your deployed contract.
Conclusion
So far, we have been able to create an identity management smart contract that allows users to create and manage their own identities by storing their name, age, and email address.
To view your deployed smart contract, copy the smart contract address from the terminal and navigate to the block explorer to search for your deployed contract.
Please note that this is a simplified example for demonstration purposes, and in a real-world scenario, you would likely have additional functionality and security measures implemented.
About The Author
I am a React frontend developer with over 3 years of experience building for the web, a web3 developer, and a technical writer. Visit my GitHub profile to see some of the projects I have worked on and currently working on and also check out my profile on LinkedIn.
References
- Github Repo
- Check out the Celo doc for more details on what each part of the configuration code does.
- Follow the guidelines in this Celo doc to verify your smart contract on Celo explorer.