Decentralized KYC on Blockchain: A Case Study

Decentralized KYC on Blockchain: A Case Study

A case study on how blockchain helps improve KYC management for financial service providers and institutions.

Play this article

The KYC chain in financial sector is essential and requires strict compliance and costly manual tasks. Know Your Customer processes are the backbones of a financial institution’s anti-money laundering efforts. As per the estimate the spending on KYC processes has risen to $1.5 billion on a global level. Despite the whooping amount of money being spent on the process, it is estimated that a majority of efforts go into gathering information. The amount of efforts put into assessing data and monitoring them closely is quite low.

Several financial service providers and institutions are finally coming together to solve this issue by leveraging technologies such as Artificial Intelligence (AI) and blockchain. In this article, we will discuss how blockchain acts as a bridge to remove manual repetitive work, improves transparency, and reduces the massive costs for financial institutions. However, before we start with the solution, it is important to understand the problem areas of the present system.

Centralised KYC systems

In the current centralized system, every financial service provider comes up with their own set of specifications for KYC documentation. As a result, users have to perform KYC requirement with every institution and service provider they use. Even when any of the user details has to be updated later, it has to be notified separately to all the financial institutions and service providers.

2-87.png

Case Study: KYC on blockchain

The KYC on blockchain would be a 3 step process :

Step 1: The users register themselves on the KYC chain.

User completes profile registration as a one-time setup using their identity verification documents. Once uploaded, the data becomes accessible to, say, Financial Institution number 1 (let's call them as FI1) for verification purpose.

Step 2: User provides access rights of his profile to FI1.

FI1 requests for access of user profile, the user provides it. FI1 then verifies the KYC data and saves a copy of the hash-value of the uploaded data on the DLT as well as on their private servers.

Now, if the KYC data is altered, the Hash Function of the KYC data under user profile will not match the one posted on the platform against FI1's, alerting the other financial institutions on the blockchain of such change.

Step 3: User performs a transaction with FI2 (Financial Institution 2).

Later when FI2 asks the user to perform KYC, the user simply grants access to their user profile to FI2. FI2 then reviews the KYC data (and its Hash Function) with the Hash function uploaded by FI1. If the two matches, FI2 would know that the KYC is the same as the one received by FI1.

In case the Hash Functions don’t match, FI2 would have to manually validate KYC documents.

0_tX95mc08c_ceiGqN.png

What happens when user updates their profile such as adding a new driving license?

In such cases, smart contract will be leveraged for automatically updating the system when the user provides new documents. The user submits the new document to FI1 who then broadcasts the change across the blockchain (through the new Hash Function), which then becomes accessible to other FI participants on the network.

Assumptions:

  • Admins will onboard Financial Institutions on the network.

  • FIs can be active or inactive based on network protocols.

  • FIs can add Customers and request for KYC from Customers.

  • Customers can approve/reject the KYC request from FIs.

  • If a Customer approves the KYC request, a notification will be sent to FI. The FI can access the Customer's KYC documents such as Social security number, Photo ID, Signature, etc. for verification.

  • FIs can approve/reject the Customer's data after verifying.

  • If FI rejects Customer's KYC verification, a notification will be sent to Customer with the reason.

  • Customers can update the KYC documents again and the update notification gets broadcasted to all the associated FIs.

  • All type of user roles must have mandatory metamask address on the main network.

Business Flow

In the proposed KYC chain we have 3 key entities Admins, Financial Institutions, and Customers.

kyc-small.jpg

Smart Contract for KYC chain

Now that our business flow is ready, let's start with the smart contract.

2.png

Type library for user and roles mapping, KYC status, and status of FI's i.e., active or inactive.

/**
* @title Types
 * @author Pushkar Kumar
 * @dev Library for managing all custom types that were used in KYC process
 */
library Types {
    enum Role {
        Admin, // 0
        FI, // 1
        Customer // 2
    }

    enum FIStatus {
        Active, // 0
        Inactive // 1
    }

    enum KycStatus {
        Pending, // 0
        KYCVerified, // 1
        KYCFailed // 2
    }

    enum DataHashStatus {
        Pending, // 0
        Approved, // 1
        Rejected // 2
    }

Key Entities

User is a generic entity for all user types with a role attribute. Customer and FI consist of attributes for customers and financial institutions respectively.

struct User {
        string name;
        string email;
        address id_;
        Role role;
        FIStatus status;
    }

    struct Customer {
        string name;
        string email;
        uint256 mobileNumber;
        address id_;
        address kycVerifiedBy; // Address of the bank only if KYC gets verified
        string dataHash; // Documents will be stored in decentralised storage & a hash will be created for the same
        uint256 dataUpdatedOn;
    }

    struct FI {
        string name;
        string email;
        address id_;
        string ifscCode;
        uint16 kycCount; // How many KCY's did this bank completed so far
        BankStatus status; // RBI, we call "admin" here can disable the bank at any instance
    }

    struct KycRequest {
        string id_; // Combination of customer Id & bank is going to be unique
        address userId_;
        string customerName;
        address bankId_;
        string fiName;
        string dataHash;
        uint256 updatedOn;
        KycStatus status;
        DataHashStatus dataRequest; // Get approval from user to access the data
        string additionalNotes; // Notes that can be added if KYC verification fails  OR
        // if customer rejects the access & bank wants to re-request with some message
    }

Customer-specific contract

The contract Customer consist of all the cruds associated to customer such as adding a customer, updating their profiles, search customer by address, as well as updating the KYC status.

Screenshot 2022-04-29 at 10.51.46 AM.png

// SPDX-License-Identifier: GPL-3.0
pragma experimental ABIEncoderV2;
pragma solidity >=0.4.25 <0.9.0;

import "./Types.sol";
import "./Helpers.sol";

/**
 * @title Customers
 * @author Pushkar Kumar
 * @dev Library for managing all customers, who are involved in the KYC process
 */
contract Customers {
    address[] internal customerList;
    mapping(address => Types.Customer) internal customers;

    // Events

    event CustomerAdded(address id_, string name, string email);
    event CustomerDataUpdated(address id_, string name, string email);
    event DataHashUpdated(address id_, string customerName, string dataHash);

    // Modifiers

    /**
     * @notice Checks whether customer already exists
     * @param id_ Metamask address of the customer
     */
    modifier isValidCustomer(address id_) {
        require(id_ != address(0), "Id is Empty");
        require(customers[id_].id_ != address(0), "User Id Empty");
        require(
            !Helpers.compareStrings(customers[id_].email, ""),
            "User Email Empty"
        );
        _;
    }

    // Support Functions

    /**
     * @notice Checks whether customer already exists
     * @param id_ Metamask address of the customer
     * @return exists_ boolean value to say if customer exists or not
     */
    function customerExists(address id_) internal view returns (bool exists_) {
        require(id_ != address(0), "Id is empty");
        if (
            customers[id_].id_ != address(0) &&
            !Helpers.compareStrings(customers[id_].email, "")
        ) {
            exists_ = true;
        }
    }

    // Contract Functions

    /**
     * @dev To get details of the customer
     * @param id_ Customer's metamask address
     * @return Customer object which will have complete details of the customer
     */
    function getcustomerdetails(address id_)
        internal
        view
        returns (Types.Customer memory)
    {
        return customers[id_];
    }

    /**
     * @dev Updates the user profile
     * @param name_ Customer name
     * @param email_ Email that need to be updated
     * @param mobile_ Mobile number that need to be updated
     */
    function updateprofile(
        string memory name_,
        string memory email_,
        uint256 mobile_
    ) internal {
        customers[msg.sender].name = name_;
        customers[msg.sender].email = email_;
        customers[msg.sender].mobileNumber = mobile_;
        emit CustomerDataUpdated(msg.sender, name_, email_);
    }

    /**
     * @dev Add new customer
     * @param customer_ Customer object
     */
    function addcustomer(Types.Customer memory customer_) internal {
        customers[customer_.id_] = customer_;
        customerList.push(customer_.id_);
        emit CustomerAdded(customer_.id_, customer_.name, customer_.email);
    }

    /**
     * @dev To Update KYC verification bank
     * @param id_ Customer's metamask ID
     */
    function updatekycdoneby(address id_) internal {
        require(id_ != address(0), "Customer Id Empty");
        customers[id_].kycVerifiedBy = msg.sender;
    }

    /**
     * @dev Updates the Datahash of the documents
     * @param hash_ Data hash value that need to be updated
     * @param currentTime_ Current Date Time in unix epoch timestamp
     */
    function updatedatahash(string memory hash_, uint256 currentTime_)
        internal
    {
        customers[msg.sender].dataHash = hash_;
        customers[msg.sender].dataUpdatedOn = currentTime_;
        emit DataHashUpdated(msg.sender, customers[msg.sender].name, hash_);
    }

    /**
     * @dev Search for customer details in the list that the bank is directly linked to
     * @param id_ Customer's metamask Id
     * @param customers_ Customer metamask Id's
     * @return boolean to say if customer exists or not
     * @return Customer object to get the complete details of the customer
     * Costly operation if we had more customers linked to this single bank
     */
    function searchcustomers(address id_, address[] memory customers_)
        internal
        view
        returns (bool, Types.Customer memory)
    {
        bool found_;
        Types.Customer memory customer_;

        for (uint256 i = 0; i < customers_.length; i++) {
            if (customers_[i] == id_) {
                found_ = true;
                customer_ = customers[id_];
                break;
            }
        }
        return (found_, customer_);
    }
}

Bank-specific contracts

Contract FI includes methods such as adding a financial institution, updating the FI, activating and deactivating the status, and so on.

Screenshot 2022-04-29 at 10.55.55 AM.png

// SPDX-License-Identifier: GPL-3.0
pragma experimental ABIEncoderV2;
pragma solidity >=0.4.25 <0.9.0;

import "./Types.sol";
import "./Helpers.sol";

/**
 * @title Banks
 * @author Suresh Konakanchi
 * @dev Library for managing all finanicial institutions thata were involved in the KYC process
 */
contract FI {
    address[] internal bankList;
    mapping(address => Types.Bank) internal banks;

    // Events

    event BankAdded(address id_, string name, string email, string ifscCode);
    event BankUpdated(address id_, string name, string email);
    event BankActivated(address id_, string name);
    event BankDeactivated(address id_, string name);

    // Modifiers

    /**
     * @notice Checks whether the requestor is bank & is active
     * @param id_ Metamask address of the bank
     */
    modifier isValidBank(address id_) {
        require(banks[id_].id_ != address(0), "Bank not found");
        require(banks[id_].id_ == id_, "Bank not found");
        require(
            banks[id_].status == Types.BankStatus.Active,
            "Bank is not active"
        );
        _;
    }

    // Contract Methods

    /**
     * @dev All the banks list. Data will be sent in pages to avoid the more gas fee
     * @param pageNumber page number for which data is needed (1..2..3....n)
     * @return totalPages Total pages available
     * @return Bank[] List of banks in the current page
     */
    function getallbanks(uint256 pageNumber)
        internal
        view
        returns (uint256 totalPages, Types.Bank[] memory)
    {
        require(pageNumber > 0, "PN should be > 0");
        uint256 reminder_ = bankList.length % 25;
        uint256 pages = bankList.length / 25;
        if (reminder_ > 0) pages++;

        uint256 pageLength_ = 25;
        uint256 startIndex_ = 25 * (pageNumber - 1);
        uint256 endIndex_ = 25 * pageNumber;

        if (pageNumber > pages) {
            // Page requested is not existing
            pageLength_ = 0;
            endIndex_ = 0;
        } else if (pageNumber == pages && reminder_ > 0) {
            // Last page where we don't had 25 records
            pageLength_ = reminder_;
            endIndex_ = bankList.length;
        }

        Types.Bank[] memory banksList_ = new Types.Bank[](pageLength_);
        for (uint256 i = startIndex_; i < endIndex_; i++)
            banksList_[i] = banks[bankList[i]];
        return (pages, banksList_);
    }

    /**
     * @dev To get details of the single bank
     * @param id_ metamask address of the requested bank
     * @return Bank Details of the bank
     */
    function getsinglebank(address id_)
        internal
        view
        returns (Types.Bank memory)
    {
        require(id_ != address(0), "Bank Id Empty");
        return banks[id_];
    }

    /**
     * @dev To add new bank account
     * @param bank_ Bank details, which need to be added to the system
     */
    function addbank(Types.Bank memory bank_) internal {
        require(banks[bank_.id_].id_ == address(0), "Bank exists");

        banks[bank_.id_] = bank_;
        bankList.push(bank_.id_);
        emit BankAdded(bank_.id_, bank_.name, bank_.email, bank_.ifscCode);
    }

    /**
     * @dev To add new bank account
     * @param id_ Bank's metamask address
     * @param email_ Bank's email address that need to be updated
     * @param name_ Bank's name which need to be updated
     */
    function updatebank(
        address id_,
        string memory email_,
        string memory name_
    ) internal {
        require(banks[id_].id_ != address(0), "Bank not found");

        banks[id_].name = name_;
        banks[id_].email = email_;
        emit BankUpdated(id_, name_, email_);
    }

    /**
     * @dev To add new bank account
     * @param id_ Bank's metamask address
     * @param makeActive_ If true, bank will be marked as active, else, it will be marked as deactivateds
     * @return BankStatus current status of the bank to update in common list
     */
    function activatedeactivatebank(address id_, bool makeActive_)
        internal
        returns (Types.BankStatus)
    {
        require(banks[id_].id_ != address(0), "Bank not found");

        if (makeActive_ && banks[id_].status == Types.BankStatus.Inactive) {
            banks[id_].status = Types.BankStatus.Active;
            emit BankActivated(id_, banks[id_].name);

            // Updating in common list
            return Types.BankStatus.Active;
        } else if (
            !makeActive_ && banks[id_].status == Types.BankStatus.Active
        ) {
            banks[id_].status = Types.BankStatus.Inactive;
            emit BankDeactivated(id_, banks[id_].name);

            // Updating in common list
            return Types.BankStatus.Inactive;
        } else {
            // Already upto date
            return banks[id_].status;
        }
    }

    /**
     * @dev To update the kyc count that bank did
     * @param id_ Bank's metamask address
     */
    function updatekyccount(address id_) internal {
        require(id_ != address(0), "Bank not found");
        banks[id_].kycCount++;
    }
}

KYC contract

The KYC contract inherits Customers and FI contract and combines cruds across entities including Admin, such as adding and updating a financial institutions and so on. 3.png

contract KYC is FI, Banks {
    address admin;
    address[] internal userList;

    mapping(address => Types.User) internal users;
    mapping(string => Types.KycRequest) internal kycRequests;
    mapping(address => address[]) internal bankCustomers; // All customers associated to a Bank
    mapping(address => address[]) internal customerbanks; // All banks associated to a Customer

Advantages of KYC chain through blockchain.

Quick response time: On KYC chain, financial institutions get direct access to data which saves them data gathering and processing time.

Removes manual work: With KYC chain, financial institutions do not need manual work as it eliminates paper work.

Data Quality: Any counterfeit in the process can be traced to its source using the blockchain trail.

Disadvantages of KYC chain management through blockchain.

  • Even though blockchain record is secure, there is still a risk of contaminated or counterfeit information being verified on the network.

  • Transactions on a blockchain network are accepted by the majority of participants (proof of work), but unfortunately, it also limits the speed at which new blocks can be added. Consequently, it might be too slow to handle the volume of transactions required on scale.

Conclusion

KYC chain reduces the time it takes to process and gather information with very few resources required for monitoring and assessing user behaviour for anomalies. The time and cost saved in turn can be used to find solutions to more complex KYC challenges.

However, blockchain cannot solve all the issues faced in KYC management. After the data is acquired, financial institutions still require to validate the information.

KYC chain when used in combination with other technologies such as AI can showcase high potential to help institutions reduce the cost and time linked with the KYC process.

Please find the full source code here

This article is part of Research & Development work being done by Pushkar Kumar, Suresh konakanchi, and Ruchika Gupta. We will be covering a series of articles along with open source projects around blockchain, smart contracts, and web3 in general. Here is the list of all the articles that you can follow to start with Blockchain and write your first contract.