EOLink 0.1.3: Simplified flood insurance

If you like this content consider tipping in ETH, LINK or an ERC20 stablecoin at this address: 0xf9f6849230cBe10200B43dB103511b778898e71C

Note, in this series I am using linux (Ubuntu 20.04) and Python 3.8 – some instructions will be specific to this OS/env. The code associated with this post is here.

In the last posts, I used a simple GET request routed through a Chainlink oracle to get financial information about ETH and LINK. This data was ingested into a smart contract where it could be viewed by interacting with the contract via brownie console or Python scripts.

Those previous iterations of the code were useful for learning some basic concepts, but useless as contracts in the wild. For one thing, the contract did not actually do anything other than retrieve some price data – I did not need a smart contract to do that. Instead of providing an API endpoint to a chainlink oracle sitting on Ethereum and getting the resulting data by sending a transaction, I could have just pasted the url into my browser. The smart contracts developed so far have done nothing more useful and have not provided any real value that could not be accessed more easily by other means. Furthermore, I overfunded the contracts with LINK to make sure there was sufficient funds to cover the oracle as, but the remainder is now locked away forever in those deployed contracts. Thankfully, these are just test LINK!

In this post, I will start to address some of these shortcomings. Firstly, I will move away from price feed data and start to incorporate real-world environmental data. Second, I will use the data to trigger a transaction using the LINK token. This moves the project much closer to having some real world utility, for example an insurance contract. This will fall short of a real-world deployable insurance contract, but it will be a significant step in the right direction and will introduce some important concepts. Later on I will expand the functionality, account for the customer’s premium payments, add security features, formalize the escrow and use an ERC20 stablecoin for the funds rather than LINK.

To start this project, either clone my code from Github, start a new project by running “brownie init” or bake the brownie chainlink-mix and discard or ignore the unnecessary files. As usual, credit to Patrick Collins and the chainlink Github who created the boilerplate code that underpinned this post.

git clone https://github.com/....


#or


brownie init


# or

brownie bake chainlink-mix

A super-simplified insurance model

In this iteration of the project I will use a Chainlink oracle to grab data from the DEFRA Data Services platform which exposes a json API containing flood risk warning levels for towns in the UK. I will substantially overfund the contract with LINK, first to fund the oracle gas, but also to effectively park funds in the contract ready to pay out if the flood warning level is above a threshold. If there is a severe flood warning, funds will be released to a customer’s account. Otherwise, the funds come back to me (acting as the insurer and contract owner).

Create dummy customer account

For this update two accounts are needed. One is the existing account that was previously funded with Kovan ETH and LINK. This account will be the contract owner, which can be thought of as the insurer in this insurance contract scenario. The other account can be empty of assets but able to receive LINK tokens as an insurance payout. To create this account, open MetaMask and choose “create account”:

In the menu that appear define a name for the new account – I named mine “KovanTestAccount_2”. Then the account needs to be able to see Kovan LINK tokens. To configure this, click “add tokens” then provide the LINK token address from the brownie-config.yaml file in the project repository. Once this is done the account will appear in MetaMask as shown in the image below – empty of assets but able to receive LINK and ETH.

I also decided to add this new account to my brownie environment. This requires the account’s private key which can be grabbed from MetaMask by opening the account, opening the three-dot menu on the right hand side >> account details >> export private key. Copy this to the clipboard.

In the terminal, navigate into the project folder and activate the development environment, then add the account, naming it “account2” as follows:

% brownie accounts new account2

>>> Enter the private key you wish to add: <paste private key here>

>>> Enter the password to encrypt this account with: <type password here>

Now the account can be loaded into brownie console using the usual syntax. So at this point a main account acting as insurer and secondary account acting as customer both exist and are available in brownie.

start new contract

I started a new contract and named it FloodInsurance.sol. As before, this contract starts with a pragma statement for solidity ^0.6.6. and instruction to import the ChainlinkClient.sol contract.

Then the contract “floodInsurance” is defined, inheriting from ChainlinkClient. Three additional variables are defined in this contract compared to those in the previous posts, both relating to the flood warning that will be ingested by the Chainlink oracle later. First, “warningThreshold” is defined as a uint256. This represents the user-defined threshold flood warning level above which the contract will pay out to the customer. If the retrieved flood warning level is below this threshold, the funds will return to the insurer’s wallet. Then we also need to define a variable for the retrieved flood warning level itself, which will also be an integer. The address of the customer is also required in order to transfer a payout to the correct wallet.

Also, I want to make sure that only the contract owner can access the functions that distribute funds, so I added a variable “owner” that is instantiated to the address of the account deploying the contract in the constructor. This means that later, when the fund functions are written, some degree of security can be added by requiring that the function caller is the same address as “owner”.

Of these new variables, two are defined by the contract owner and can be instantiated in the contract’s constructor function.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.6;

import "@chainlink/contracts/src/v0.6/ChainlinkClient.sol";

contract floodInsurance is ChainlinkClient {

    // define new vars: customer, warningThreshold and warningLevel
    uint256 private warningThreshold;
    uint256 public warningLevel;
    address private oracle;
    address private customer;
    address private owner;
    bytes32 private jobId;
    uint256 private fee;

    

    // read user defined values and instantiate vars in constructor
    constructor(address _oracle, string memory _jobId, uint256 _fee, address _link, address _customer, uint256 _warningThreshold) public {
        
        if (_link == address(0)) {
            setPublicChainlinkToken();
        } else {
            setChainlinkToken(_link);
        }

        
        oracle = _oracle;
        jobId = stringToBytes32(_jobId);
        fee = _fee;
        customer = _customer;
        warningThreshold = _warningThreshold;
        owner = msg.sender;
        
    }

The next task is to define the API call via a Chainlink oracle. The desired data lives at the following API endpoint: https://environment.data.gov.uk/flood-monitoring/id/floods.

This AP exposes a json file containing flood information for towns/regions in the UK. The full response looks like this:

The data we want to access is the severityLevel. This gives a measure of the risk of flooding for a particular location as a level from 0-5. For this example I will select the first location: Devon, Cornwall and the Isles of Scilly. The required data is an integer, and there is a distinct path through the json that ends at our desired value. This all needs to be included in the API call in the contract.

Since the required data is an integer accessible using a GET request, it is straightforward to find a suitable oracle from the Chainlink marketplace. The oracle address, jobID and fee are displayed in the marketplace too. The path to the specific value in the full json response is defined using request.add(). To correct the data to the right order of magnitude the response is multiplied by

// define API request function    

function requestWarningLevel() public returns (bytes32 requestId) 
   {
     Chainlink.Request memory request = buildChainlinkRequest(jobId, 
                                                              address(this), 
                                                              this.fulfill.selector
                                                              );
        
     // Set the URL to perform the GET request on

     request.add("get", "https://environment.data.gov.uk/flood-monitoring/id/floods");
     request.add("path", "items.0.severityLevel");
        
     // Sends the request
     return sendChainlinkRequestTo(oracle, request, fee);
    }

This function instructs the oracle to make a request, but it does not pull the result into the contract. For this the fulfill() function is required, which uses the request ID to retrieve the requested value. The returned value is assigned to the defined var “warningLevel”.

    function fulfill(bytes32 _requestId, uint256 _warningLevel) public recordChainlinkFulfillment(_requestId)
    
    {
        warningLevel = _warningLevel;
    }

Finally, the warningLevel value that has now been ingested into the contract can be used to trigger a transaction of LINK. If the retrieved value exceeds the user-defined threshold, the LINK remaining in the contract after the oracle gas has been paid will be transferred to the customer. If the warning level is below the threshold, the remaining LINK returns to the contract owner’s wallet.

    function settleClaim() public {
        require (msg.sender==owner);

        address outAddress;
        
        if (warningLevel>warningThreshold){outAddress = customer;}
        else{outAddress = msg.sender;}
        
        LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());
        
        require(link.transfer(outAddress, link.balanceOf(address(this))), "Unable to transfer");
        
    }

There is also a “stringToBytes32” function defined in the contract which allows the string delivered to the contract in deployment to be converted to bytes32 type which is what is expected by the oracle (not shown here, see Github version).

deploy contract

I will deploy this contract using the brownie console. First, in the terminal, navigate to the project folder, activate the development environment and compile the contracts. Then open brownie console, defining the network as “kovan”.

$ source activate BlockChainEnv
$ brownie compile
$ brownie console --network kovan

In the brownie console, load the main account (the one that will act as contract owner/insurer) and deploy the new contract to the kovan network. Deploying the contract also requires the customer’s account address, so load this too. Consistent with the constructor function in the contract code, the deployment requires the following values: oracle address, jobID, fee, link address, customer address, and warningThreshold. For now, I will choose to set the threshold warning level to 3.

$ account1 = accounts.load('main')
$ account2 = accounts.load('account2')
$ warningThreshold = 3

$  floodInsurance.deploy( '0x2f90A6D021db21e1B2A077c5a37B3C7E75D15b7e','29fa9aa13bf1468788b7cc4a500a45b8',100000000000000000,'0xa36085F69e2889c224210F603D836748e7dC0088',account2,warningThreshold,{'from':account1})

The contract is now deployed on the Kovan blockchain, but it currently holds no assets.

overfund contract with LINK

In MetaMask, open the main account and send 10 LINK to the contract address. The address of the deployed contract is displayed in the brownie console in response to the .deploy() function being called.

My account stared with 78.6 LINK, and after sending, the LINK balances of the contract and the two accounts can be queried in the brownie console. The syntax is:

$ interface.LinkTokenInterface(link_token_address).balanceOf(account_address)

The expected values after the LINK transfer are:

main account/insurer: 68.6 LINK

customer: 0 LINK

contract: 10 LINK

$ interface.LinkTokenInterface( '0xa36085F69e2889c224210F603D836748e7dC0088').balanceOf(account1)

>>> 68600000000000000000

$ interface.LinkTokenInterface( '0xa36085F69e2889c224210F603D836748e7dC0088').balanceOf(account2)

>>> 0

$ interface.LinkTokenInterface( '0xa36085F69e2889c224210F603D836748e7dC0088').balanceOf(contract)

>>> 10000000000000000000

These values require multiplying by 1e-18 to place the decimal point in the correct location, but these results confirm that the account balances meet our expectations.

Request data and settle payment

The deployed contract is now (over)funded with LINK and ready to grab the flood warning data and, depending on its value, send the unspent LINK to either the customer or the insurer’s account. This requires instantiation of the contract in the brownie console and calling the contract functions.

First, instantiate the contract with its deployment address:

$ contract = Contract('0x3bC5f7A40A3cD1C28bDeC639993bB53A8060C08D')

Now, request the flood warning level data:

$ transactionID = contract.requestWarningLevel({'from':account1})

>>> Transaction sent: 0x8e48ba230ecbea379dc539d997a2f573def7768367d01bad845ca2fe9323a1c6
  Gas price: 5.0 gwei   Gas limit: 142809   Nonce: 33
  floodInsurance.requestWarningLevel confirmed - Block: 25078170   Gas used: 129827 (90.91%)

Now the warning level is available in the contract:

$ warningLevel = contract.warningLevel()
$ warningLevel
>>> 4

Now the warning level has been returned with the value 4, which exceeds the threshold defined earlier, the settleClaim() function should return 9.9 LINK to the customer account (the original 10 LINK sent to the contract less the oracle gas fee of 0.1 LINK):

$ contract.settleClaim({'from':account1})

>>> Transaction sent: 0x2e64848eaf0b8a5381088699de4a0ffad438f101fed45c6681d0b10b4eaab09c
  Gas price: 5.0 gwei   Gas limit: 50079   Nonce: 34
  floodInsurance.settleClaim confirmed - Block: 25078217   Gas used: 30438 (60.78%)

<Transaction '0x2e64848eaf0b8a5381088699de4a0ffad438f101fed45c6681d0b10b4eaab09c'>

To confirm that this has worked, either check the account balances in Metamask or query them in brownie console:

$ interface.LinkTokenInterface( '0xa36085F69e2889c224210F603D836748e7dC0088').balanceOf(account)

>>> 68600000000000000000

$ interface.LinkTokenInterface( '0xa36085F69e2889c224210F603D836748e7dC0088').balanceOf(account2)

>>> 99000000000000000000

$ interface.LinkTokenInterface( '0xa36085F69e2889c224210F603D836748e7dC0088').balanceOf(contract)

>>> 0

The warning level was above the warning threshold, and the customer got paid out a hearty 9.9 LINK tokens as an insurance settlement. The model system functioned as expected, and for the first time in this series a financial outcome from a smart contract was settled across 3 accounts (owner, contract, customer) depending on a value delivered by a Chainlink oracle.

Modifiers for simplified security

The settleClaim() function should only be called by the contract owner, otherwise a malicious actor could trigger a payment at some opportune moment to force a payment. In the contract above, this was prevented by including a require() statement inside the function. However, better practice is to define a custom modifier. A modifier changes the behaviour of a function in some specific way, often being used to determine who has access some function or information in a contract (the permission definitions public, private, internal.. etc are modifiers).

In this case, the modifier should force the condition that only the contract owner (the address that deployed the contract) can trigger a payment using the settleClaim() function. Perhaps in a real system this is too much power to give to one party – maybe both parties or maybe a independent 3rd party should have to give permission before a payment is released? Anyway, for now responsility rests with the insurer and this is achieved in the contract using a modifier. The modifier is defined in the contract as follows:

$ modifier onlyOwner(){
    require(owner==msg.sender);
    _;
    }

The modifier includes a require() statement that checks that variable “owner” (which is set to the contract owner’s address in the contract’s constructor) matches the address that has called the function (in brownie, this is the account passed in the {‘from’: account} part of the function call). If these addresses do not match then the function call ill fail because the equality n the require() statement evaluates to False. The modifier syntax also includes a lone underscore. This underscore represents the function body such that when the underscore is after the require() statement, the require statement is evaluated before the rest of the function can run.

Then, this modifier can be added to the function definition, and the original require() statements removed. The final contract therefore looks like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.6;

import "@chainlink/contracts/src/v0.6/ChainlinkClient.sol";

contract floodInsurance is ChainlinkClient {
  
    uint256 public warningThreshold; //user-defined threshold for paying out funds
    uint256 public warningLevel; // will be the value returned by oracle
    address private oracle; // oracle address
    address private customer; // wallet address for customer/insuree
    bytes32 private jobId; // oracle jobID
    uint256 private fee; // oracle fee
    address private owner; // stores contract owner address
    

    constructor(address _oracle, string memory _jobId, uint256 _fee, address _link, address _customer, uint256 _warningThreshold) public {
        
        // set link token address depending on network
        if (_link == address(0)) {
            setPublicChainlinkToken();
        } else {
            setChainlinkToken(_link);
        }
        
        // instantiate variables with values provided at contract deployment
        oracle = _oracle;
        jobId = stringToBytes32(_jobId);
        fee = _fee;
        customer = _customer;
        warningThreshold = _warningThreshold;
        owner = msg.sender;
        
    }
    

    function requestWarningLevel() public returns (bytes32 requestId) 
    {
        Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
        
        // Set the URL to perform the GET request on
        request.add("get", "https://environment.data.gov.uk/flood-monitoring/id/floods");
        request.add("path", "items.0.severityLevel");
        
        // Sends the request
        return sendChainlinkRequestTo(oracle, request, fee);
    }




    function fulfill(bytes32 _requestId, uint256 _warningLevel) public recordChainlinkFulfillment(_requestId)
    
    {
        // fulfill request and instantiate warningLevel var with retrieved value
        warningLevel = _warningLevel;
    }




    function settleClaim() public onlyOwner{
        // settleClaim() can only be called by contract owner

        // address to make payment to: function level scope
        address outAddress;
        
        // condition: is warning level above threshold for payment
        // if so, set payment address to customer, else payment address is contract owner
        if (warningLevel>warningThreshold){outAddress = customer;}
        else{outAddress = msg.sender;}
        
        // instantiate LINK token
        LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());

        // transfer contract LINK balance to payment address
        require(link.transfer(outAddress, link.balanceOf(address(this))), "Unable to transfer");
        
    }




    /// ACCESSORY FUNCS ///


    modifier onlyOwner(){
        require(owner==msg.sender);
        _;
    }


    function stringToBytes32(string memory source) public pure returns (bytes32 result) {
        // accessory function for converting string to bytes32 - required 
        // for passing jobID to oracle
    
        bytes memory tempEmptyStringTest = bytes(source);
        if (tempEmptyStringTest.length == 0) {
            return 0x0;
        }

        assembly {
            result := mload(add(source, 32))
        }
    }



}

5 thoughts on “EOLink 0.1.3: Simplified flood insurance

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s