This project is a prototype for cross-chain identity status verification. Using Chainlink CCIP and the Everest Identity Oracle, Cross-Chain Identity (CCID) is a demonstration of how the KYC status of an address can be securely transmitted across chains. Cross-Chain Identity leverages the security and sybil resistance of the underlying protocols to allow a new generation of interoperable and regulatory compliant Web3 applications.
Note: The Everest Identity Oracle is currently only available for testing on Ethereum Goerli, which unfortunately is not one of the test networks available on CCIP. Therefore I was unable to deploy and test this project, but the premise remains unchanged.
There are currently two versions of CCID.
V1 is the original implementation that builds on the Everest Consumer contract to send fulfilled identity requests from a CCIDSender
contract to a CCIDReceiver
contract on another chain.
V2 is the latest implementation that interacts with the Everest Consumer through an interface and allows bi-directional CCIP transactions between a CCIDRequest
contract and a CCIDFulfill
contract.
CCIDSender.sol
inherits the functionality from Everest's EverestConsumer.sol
, and Chainlink's IRouterClient.sol
and Client.sol
to send the KYC status of a queried address in the form of a string across chains to CCIDReceiver.sol
which inherits functionality from CCIPReceiver.sol
and Client.sol
.
The three possible results are:
NOT_FOUND
HUMAN_AND_UNIQUE
KYC_USER
These results are determined by an address' association/KYC status with Everest's biometric, digital identity wallet. Addresses that have been associated with an Everest Wallet and completed KYC will return KYC_USER
. Addresses that have been associated with an Everest Wallet, but have not completed KYC will return HUMAN_AND_UNIQUE
. And finally addresses that have not been associated with an Everest Wallet will return NOT_FOUND
.
When this project is able to be deployed, these are the steps that must be followed:
CCIDSender.sol
is deployed with the following constructor arguments:_router
- address of CCIP Router_link
- address of Chainlink (LINK) token_oracle
- address of Everest Identity Oracle_jobId
- string of JobID found in Everest docs_oraclePayment
- uint256 amount of Chainlink (LINK) to pay_signUpURL
- string of Everest Wallet URL
- send LINK to
CCIDSender
address CCIDReceiver.sol
is deployed to receiving blockchain with the following constructor arguments:_router
- address of CCIP Router
setCcidReceiver()
is called on first contract, passing address of receiver contract as parametersetCcidDestinationSelector()
is called on first contract, passing Chain Selector of receiving chain as parameter
Now when a request is made to the EverestConsumer/CCIDSender contract and fulfilled by the oracle, it will send the KYC status of the queried address to the CCIDReceiver contract on the other chain.
- imports EverestConsumer, IRouterClient, Client, Ownable
- takes same constructor arguments as EverestConsumer, as well as an address for
router
- has the following storage variables:
- address
router
- address
link
- address
ccidReceiver
- uint64
ccidDestinationSelector
- address
- has the following setter functions:
setCcidReceiver()
with onlyOwner modifiersetCcidDestinationSelector()
with onlyOwner modifier
- overrides EverestConsumer's
fulfill()
to callsendKycStatusToCcidReceiver()
sendKycStatusToCcidReceiver()
does the following:- takes kyc status of a fulfilled request as parameter
- converts to string with
statusToString()
- sends the status via IRouterClient to CCIDReceiver
- imports CCIPReceiver and Client
- takes same constructor argument as CCIPReceiver to set
router
address - overrides
_ccipReceive()
to receive and decode message to get identity status as a string - emits an event with the received identity status
This project inherits my own fork of the EverestConsumer which changed the visibility of the statusToString()
function from external
to public
to allow CCIDSender to call it. The fulfill()
was also made virtual
, and the _requests
mapping was changed from private
to internal
.
V2 is the latest implementation that interacts with the Everest Consumer through an interface and allows bi-directional CCIP transactions between a CCIDRequest
contract and a CCIDFulfill
contract.
A user interacts only with a CCIDRequest
contract on their chain of choice to make a request about an address' identity status, which is then sent to a CCIDFulfill
contract on the chain with the Everest Consumer. CCIDFulfill
interacts with the Everest Consumer through the IEverestConsumer
interface and uses Chainlink Log Trigger Automation to monitor fulfilled requests, sending the relevant one back to the CCIDRequest
contract on the user's chosen chain, all in a single transaction.
CCID V2 differs from V1 in the following ways:
- interacts with Everest Consumer through an interface, as opposed to importing it directly
- requests can be made from chains the Everest Consumer is not on (bi-directional request functionality)
- additional CCIP "best practices" implemented such as modifiers restricting interactions with unwanted chains
- Identity status is transmitted as an
IEverest.Status
enum, as opposed to a string - if a user has completed KYC, an epoch timestamp representing their KYC completion date is also transmitted
- if a user hasn't completed KYC, this value will return 0
When this project is able to be deployed, these are the steps that must be followed:
CCIDRequest.sol
is deployed on user's chain of choice (Chain A) with the following constructor arguments:_router
- address of CCIP Router_link
- address of Chainlink (LINK) token
CCIDFulfill.sol
is deployed on same chain as Everest Consumer (Chain B) with the following constructor arguments:_router
- address of CCIP Router_link
- address of Chainlink (LINK) token_consumer
- address of Everest Consumer_ccidRequest
- address ofCCIDRequest
deployed on Chain A_chainSelector
- uint64 of chain selector for Chain A
CCIDFulfill
contract owner must callallowlistSourceChain()
with uint64 of chain selector for Chain A and trueCCIDFulfill
contract owner must callallowlistSender()
with address ofCCIDRequest
deployed on Chain A and trueCCIDRequest
contract owner must callallowlistDestinationChain()
with uint64 of chain selector for Chain B and trueCCIDRequest
contract owner must callallowlistSourceChain()
with uint64 of chain selector for Chain B and trueCCIDRequest
contract owner must callallowlistSender()
with address ofCCIDFulfill
deployed on Chain B and true
Now anyone can call requestCcidStatus()
with the following parameters:
- address of
CCIDFulfill
on Chain B - address who's identity status is being requested
- uint64 of chain selector for Chain B
This will send the request via CCIP to CCIDFulfill
, which will interact with the Everest Consumer contract to request the identity status of the address. The requestedAddress
will be stored in an s_pendingRequests
mapping, evaluating to true. Chainlink's Automation nodes will monitor the Everest Consumer for fulfilled request events using Log Trigger Automation. When one of these events correspond to a true s_pendingRequests
address, Chainlink Automation will send the requestedAddress
, IEverestConsumer.Status
and kycTimestamp
back to the CCIDRequest
contract on Chain A. It will also set the s_pendingRequests
mapping of the requestedAddress
to false.
This entire process happens in a single transaction.
- imports
IEverestConsumer
,IRouterClient
andLinkTokenInterface
interfaces - imports
Client
,CCIPReceiver
and Openzeppelin'sOwnable
contracts - takes CCIP router address and LINK token address as constructor args
- stores these in immutable variables
- has storage mappings for allowlisted destination chain, source chain, and sender
requestCcidStatus()
is used for making requests, taking_ccidFulfill
address,_requestedAddress
, and_chainSelector
_ccipReceive()
receives fulfilled requests, decodes them, and emits aCCIDStatusReceived
event withrequestedAddress
,IEverest.Status status
, andkycTimestamp
- has the following setter functions with onlyOwner modifier:
allowlistDestinationChain()
allowlistSourceChain()
allowlistSender()
- imports
IEverestConsumer
,IRouterClient
,LinkTokenInterface
andILogAutomation
interfaces - imports
Client
,CCIPReceiver
, Openzeppelin'sOwnable
, andAutomationBase
contracts - imports
Log
struct - takes CCIP router address, LINK token address, Everest Consumer address, CCIDRequest address, CCIDRequest chain uint64 chain selector as constructor args
- stores these in immutable variables
- has storage mappings for allowlisted source chain and sender
- has storage mapping for pendingRequests of
requestedAddresses
_ccipReceive()
receives therequestedAddress
fromCCIDRequest
and:- maps the
requestedAddress
to true ins_pendingRequests
- requests the identity status of the
requestedAddress
from the Everest Consumer - emits CCIDStatusRequested event with the
requestedAddress
- maps the
checkLog()
is simulated continuously off chain by Chainlink Automation nodes and:- monitors fulfilled request events emitted by the Everest Consumer
- evaluates
upkeepNeeded
as true when one of these events correspond to a true address mapped ins_pendingRequests
- stores data from fulfilled request events about
requestedAddress
inperformData
which is used to callperformUpkeep()
performUpkeep()
is called by Chainlink Automation nodes whencheckLog()
evaulatesupkeepNeeded
to be true withperformData
and:- calls
fulfillCcidRequest()
withperformData
- calls
fulfillCcidRequest()
takes_fulfilledData
(performData
) and:- decodes it, to set the
requestedAddress
s_pendingRequests
mapping to false and emit aCCIDStatusFulfilled
event with - sends it to
CCIDRequest
via CCIP
- decodes it, to set the
- has the following setter functions with onlyOwner modifier:
allowlistSourceChain()
allowlistSender()
When requests are fulfilled by the Everest Consumer, the address of the "revealer" (the address making the request) is included in the Fulfilled event. Although I added the kycTimestamp
to V2, unlike V1, I left out the revealer address. This was done because the way the Everest Consumer is built, the revealer address is the msg.sender of the request, which in this case would not be the original user making the request on Chain A - it would be the CCIDFulfill contract.
I also left out the bytes32 requestId
. Adding this and allowing cross-chain queries of this parameter is something to consider.
The Chainlink Log Trigger Automation needs to be funded with LINK tokens. CCID V2 currently assumes the registration is done manually and tokens are deposited in a subscription. One of the next steps will be making the requestCcidStatus()
require a payment of LINK tokens that are also sent across chain and used to pay for the use of Log Trigger Automation.
Update: CCIDRequest currently takes a LINK payment for CCIP fees and an amount to send across chain. CCIDFulfill currently uses a storage variable for the Chainlink Automation payment address, this needs to be set with a setter function. When a request is received with LINK payment, some of that LINK is used to pay for the Everest request and the rest is used to pay for Automation.
The IEverestConsumer.Status
enum uses my pull request version because the checkLog()
interprets the enum value as a uint8
and I believe the order in the original interface is incorrect.
The following resources were used for developing CCID:
- CCIP Masterclass docs
- smartcontractkit - ccip-starter-kit-hardhat
- EverID - everest-chainlink-consumer
- palmcivet7 - hardhat-everest-chainlink-consumer
This project is licensed under the MIT License.