EOLink 0.1.5: Settling claim in new ERC20 currency

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

Code associated with this post available HERE

So far, the simplified flood insurance model has used LINK to pay oracle gas and also to settle the insurance claim. In reality, it would be much better to use a separate token for the oracle gas and the payment, first just for accounting the various pots of money relating to the contract and making sure there are sufficient funds for each transaction, but also because for most people payouts are best handled using a stablecoin that is easily and predictably swappable for fiat currency or useable as a real-world unit of exchange instead of a crypto asset that might be more volatile.

There are several stablecoins that could be used for this purpose including DAI, USDT and USDC, all of which peg to the US dollar. These are all ERC20 tokens which means they are Ethereum-compatible tokens that conform to a standard from which they inherit specific functionality. Each ERC20 token is its own smart contract. For this post, I will write a contract for a new ERC20 token to be used as the unit of exchange for the flood insurance payments. I could equally have chosen to use an existing ERC20 token, but building a new one is helpful for the purposes of understanding what an ERC20 token is and how it is handled by a Solidity contract. So, here I will create a new ERC20 token, deploy it to Kovan, then update the insurance smart contract so that it settle the claim in the new currency while paying transaction gas in ETH and oracle gas in LINK.

A new ERC20 token

The new token will be called “FloodToken” and have the symbol “FLOOD”. The first step is to start a new folder inside /contracts called “FloodToken.sol”. This is the contract where the new token is defined. The ERC20 standard is defined by OpenZeppelin. The full specification is defined in the OpenZeppelin ERC20 Solidity contract. It is important to read through this contract to see where the functions we rely upon for working with ERC20 tokens actually come from and how they are implemented. For our new ERC20 token we have the option of copying out the code from the OpenZeppelin contract, but we can also shortcut by importing it, and then inheriting the contents.

So, first navigate to the project directory, activate the relevant Python environment, and create a new file for the FLOOD token contract:

# in terminal

$ cd <path_to_project>

$ source activate BlockChain

$ touch ./contracts/FloodToken.sol

Now open FloodToken.sol, write the pragma statement and import the ERC20 contract from OpenZeppelin:

// in FloodToken.sol

pragma solidity ^0.6.6;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

For this import statement to be successful, we need to have defined a remapping in brownie-config.yaml because at the moment brownie does not know where exactly @openzeppelin directs to. If you have cloned my Github repository or built this project by following along my previous posts, this remapping is already done, but if not, then open brownie-config.yaml and add the following:

// in brownie-config.yaml

compiler:
    solc:
        remappings:

          - '@chainlink=smartcontractkit/chainlink-brownie-contracts@1.0.2'
          - '@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.4.0'

Those remappings link the patterns @chainlink and @openzeppelin to specific repositories where the relevant contracts can be imported from.

Back in FloodToken.sol, the ERC20 token standard has now been imported and we can define a new token that inherits ERC20 properties. Its name will be FloodToken and its symbol will be FLOOD. Since all the functionality we require is coming from the imported ERC20 contract, all this contract needs to achieve is the token minting, so that when deployed this contract has a fixed supply. Therefore, a constructor function alone does the job, because all other functions are inherited. The FloodToken.sol contract is therefore very short.

pragma solidity ^0.6.6;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract FloodToken is ERC20 {

    constructor(uint256 initialSupply) public ERC20("FloodToken","FLOOD"){

        _mint(msg.sender, initialSupply);
    }
}

This code first defines a new contract called FloodToken and inherits the ERC20 properties using the pattern “FloodToken is ERC20”. Solidity uses the keyword “is” for inheritance. Inside the contract definition there is a constructor that takes one argument: initialSupply. This variable will be defined when the contract is deployed and will set the token supply cap. The constructor then calls the function _mint() from the ERC20 contract to create the tokens. Once deployed, tokens are available by interacting with this contract using the functions in ERC20.sol.

Test token locally

With FloodToken.sol saved in the /contracts folder, it can be tested on a local blockchain. To do this, open brownie console either without specifying a network (it defaults to local) or specifying the development network.

$ brownie console

or

$ brownie console --network development

This will spin up a local Ganache blockchain. Assuming there were no compiler errors, the console will connect to Ganache and FloodToken.sol will be available by virtue of being in the /contracts folder. T deploy the FloodToken contract, first load some accounts into the console (I have two accounts, ‘main’ and ‘account2’ in brownie accounts), then call the deploy() function, providing a total supply. I have chosen to mint one million tokens. Remembering that one unit in Solidity is 1e18 (i.e. each unit can be subdivided into 18 subunits of decreasing order of magnitude) this is 1000000e18. As always we provided a {‘from’: account} statement to define the contract owner.

$ account1 = accounts.load('main')

$ account2 = accounts.load('account2')

$ floodToken = FloodToken.deploy(1000000e18,{'from':account1})

Now the contract is deployed at an address on the Ganache blockchain. By assigning the deployment to a named variable (“floodToken”) the contract is instantiated as an object in the console, enabling some quick test transactions:

# transfer half the available tokens to the contract owner's wallet

$ FloodToken.transfer(account1, 500000e18, {'from':account1})

# transfer the other half to the second wallet

$ FloodToken.transfer(account2, 500000e18, {'from': account1})

All of the tokens should now have been transferred out of the contract and split equally between the two wallets. To check, the balaceOf() function is used:

$ floodToken.balanceOf(floodToken.address)

>>> 0


$ floodToken.balanceOf(account1)

>>> 500000000000000000000000


$ floodToken.balanceOf(account1)

>>> 500000000000000000000000

The transfers seem to be behaving as expected, and now both wallets have a healthy balance of FLOOD tokens. So, once enough testing has been done that we have confidence in this contract and that there are no bugs or unexpected behaviours (formal testing coming in next post) this contract can be deployed to the Kovan testnet so that it has a permanent address that makes it available to the insurance contract. This can be achieved using a Python script as described here or brownie console. After closing down the previous console that was connected to Ganache, start a new on connected to Kovan:

$ brownie console --network kovan

Now deploy the contract as before, making a note of the deployment address.

$ account1 = accounts.load('main')


$ FloodToken.deploy(1000000e18,{'from':account1})


>>> Transaction sent: 0x50124a05cc5dd87205ab8563fe565c548afd433c789ab6dcaa0c8eccf5591970
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  FloodToken.constructor confirmed - Block: 1   Gas used: 721253 (6.01%)
  FloodToken deployed at: 0x4e5e0a5dAC9a0Ee733c3076ccE784d59DF458F13

Now the FloodToken contract has been deloyed to the Kovan testnet, the next step is to use it in the insurance contract.

USINg FLOOD TO SETTLE PAYMENTS

The original FloodInsurance.sol contract is the starting point – it can be downloaded from Github or built using the previous posts in this series. In the original contract the only import was the Chainlink client which enabled the contract to transact LINK and pay for a oracle to make a GET request. We still need to do that, as the flow of data from the DEFRA Flood warning API to the contract via a Chainlink oracle making a GET request is unchanged. However, now that the insurance contract is settled using FLOOD, the ERC20 token standard is also needed. Therefore, add the import statement to grab the token standard from openzeppelin:

pragma solidity ^0.6.6;

import "@chainlink/contracts/src/v0.6/ChainlinkClient.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Notice that this time I imported IERC20.sol not ERC20.sol. The additional “I” stands for “interface”. Interfaces make the functions associated with the ERC20 standard available inside the new contract (i.e. in FloodToken.sol we determined that the token was ERC20, now we can just import the ERC20 interface to make its functions available).

Now comes the contract definition. The contract still needs to inherit functions from the ChainlinkClient contract, but there are some additional variables to define:

IERC20 public floodToken;
uint256 private payout_amount;
bool public Funded;

The first of the new variables is an instance of the FloodToken. Then a private uint256 that will define the number of FLOOD tokens held in the contract ready to be paid out. I also defined a Boolean called “Funded” that will be used to determine whether the contract has sufficient FLOOD tokens. A Boolean is a True/False data type.

Next, the payout_amount and the address of the deployed FloodToken contract need to be added to the constructor definition as parameters (I have used a leading underscore to define parameters, removing the underscore when they are instantiated as variables). The token can then be instantiated inside the constructor using the address provided in the constructor definition. The constructor function therefore looks like this:

    constructor(address _oracle, string memory _jobId, uint256 _fee, address _link, address _customer, uint256 _warningThreshold, address _tokenAddress, uint256 _payout_amount) public {
        
        // set link token address depending on network
        if (_link == address(0)) {
            setPublicChainlinkToken();
        } else {
            setChainlinkToken(_link);
        }
        
        // set FLOOD token using address
        floodToken = IERC20(_tokenAddress);

        // instantiate variables with values provided at contract deployment
        oracle = _oracle;
        jobId = stringToBytes32(_jobId);
        fee = _fee;
        customer = _customer;
        warningThreshold = _warningThreshold;
        payout_amount = _payout_amount;
        owner = msg.sender;

    }

I decided to add a short function that allows the user to check that the contract has been funded with sufficient FLOOD to settle a payment. The rationale for this was that it is possible for a user to request data via the oracle and attempt to settle the claim without having funds in place, meaning they would waste LINK and ETH by paying gas for those transactions, even though the payment would ultimately fail. Therefore, checkFund() was defined as a public view function that returns the Boolean “Funded”. The value of Funded is true if the contract holds at least as much FLOOD as the payment amount defined in the contract deployment, or false otherwise. This can be called by the user before any other transactions to make sure the insurer has come good on their promise to hold the funds in escrow in the contract.

    function checkFund() public view returns (bool Funded) {
        
        if (floodToken.balanceOf(address(this)) >= payout_amount){
            return true;
        }

        else {return false;}
        
    }

No changes are required to the functions controlling the GET request, but the settleClaim() function does need to be updated to pay out in FLOOD instead of LINK. Most of the logic of the function remains the same – depending on the value of warningThreshold, the variable outAddress takes the address of the customer or the contract owner. outAddress is then passed to a payment function that sends tokens to that address. Previously, the tokens were LINK and they were sent using the LinkTokenInterface, now we are sending FLOOD using the IERC20 interface. A LINK transfer is still required later though, as the outstanding LINK held in the account that was not spent on oracle gas needs to be sent back to the contract owner. Therefore, the settleClaim() function looks like this:

    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;}

        // make payment in FLOOD
        require(floodToken.transfer(outAddress, payout_amount), "Transfer failed");
        
        // instantiate LINK token
        LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());

        // transfer remaining contract LINK balance to sender
        require(link.transfer(msg.sender, link.balanceOf(address(this))), "Unable to transfer");
        
    }

Those changes are now sufficient to execute the smart contract using the newly minted FLOOD token. The process would have been exactly the same to use any existing ERC20 token, except that the token address would just have to be found in advance rather than by deploying afresh.

Deploy the updated contract

The new contract can now be deployed to Kovan as before, except there are two new constructor variables to define. The deployment can be achieved by updating the Python scripts from the previous post, or interactively in brownie console. I’ll demo the latter here.

Open brownie console using the kovan network and load accounts.

$ brownie console --network kovan

$ account1 = accounts.load('main')

$ account2 = accounts.load('account2')

Now deploy the contract, providing values for all the constructor parameters. These are:

  • oracle address (in brownie-config.yaml)
  • jobID (in brownie-config.yaml)
  • fee (in brownie-config.yaml)
  • link address (in brownie-config.yaml)
  • customer address (loaded account2)
  • warningThreshold (user-defined values from 0-5: I set it to 3)
  • FLOOD token address (address where token was deployed earlier)
  • payout amount (number of FLOOD tokens used to settle payment: I used 500,000)
contract = floodInsurance.deploy('0x2f90A6D021db21e1B2A077c5a37B3C7E75D15b7e','29fa9aa13bf1468788b7cc4a500a45b8',100000000000000000,'0xa36085F69e2889c224210F60
3D836748e7dC0088',account2,3,'0x63585C9f4968658cB36C48fa33e34BE513c5e4D9',500000e18,{'from':account1})

>>> Transaction sent: 0x3f88366a5cbbbd052f027edc81ff8e82d5962b7f7bdd0ad42c25b39b84c197e5
  Gas price: 5.0 gwei   Gas limit: 1271047   Nonce: 60
  floodInsurance.constructor confirmed - Block: 25338858   Gas used: 1155498 (90.91%)
  floodInsurance deployed at: 0xE094A61c7e10b5ECbEE6006a1207239d515d1548

The updated contract is now deployed to its own address on Kovan.

TRANSACTIONS

With the contract deployed, we can set up an insurance scenario and settle it using brownie console. Since the contract object already exists, it makes sense to persist the same console session. The scenario we have instantiated is that a customer and insurer have agreed a contract that will cover the customer in case of flooding. If the DEFRA flood warning level exceeds 3, the contract pays out 500,000 FLOOD tokens that the user can then swap for another asset or hold in their wallet. The first requirement is that the insurer locks away enough FLOOD in the contract that the payment can actually happen. The checkFund() function allows to check that this has been done.

$ contract.checkFund()

>>> False

The function has returned False, indicating that insufficient funds are available in the contract. This makes sense as this is the first time we have interacted with the contract. The actual balance can be checked one an instance of the FLOOD token has been created:

$ floodToken = Contract(<deployed token address>)

$ floodToken.balanceOf(contract)

>>> 0

The FLOOD balance of the contract is 0. Acting as the insurer, we can send FLOOD into escrow in the insurance contract from the token contract as follows:

$ floodToken.transfer(contract,500000e18,{'from':account1})

>> Transaction sent: 0xfb67b0cb8150784efbf3c0985ffc06f750b35b1cd0546e6746cda45797f52c0c
  Gas price: 2.0 gwei   Gas limit: 56882   Nonce: 66
  Floodtoken.transfer confirmed - Block: 25341610   Gas used: 51711 (90.91%)

<Transaction '0xfb67b0cb8150784efbf3c0985ffc06f750b35b1cd0546e6746cda45797f52c0c'>

Now we cna use checkFund() again to be sure that enough FLOOD is held in the contract. Then we can check the actual balance again.

$ contract.checkFund()
>>> True

$ floodToken.balanceOf(contract)
>>> 500000000000000000000000

Now the contract holds 500,000 FLOOD, which is enough to make the payment if necessary. The contract owner’s account also holds enough ETH and LINK to pay for transaction and oracle gas (if this is not the case then top up the accounts from the Kovan faucets as explained here). Now, requestWarningLevel() can be used to make the oracle GET request. The flood warning level will then be available in the contract.

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

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

$ contract.warningLevel()
>>> 0

At the time that I made the GET request, there was no flood risk in the customer’s town. No further action needs to be taken unless this is the end of the insurance period, in which case settleClaim() can be called. Since the warning level was 0, which is below the warningThreshold, the expected behaviour is that the FLOOD tokens will be returned to the contract owner/insurer’s wallet. of course, had warningLevel been greater than the threshold, the FLOOD tokens would go to the customer’s wallet.

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

>>> Transaction sent: 0x186ed2d350b1f8e0bd7b48e673170c9466b168e31fca8bba9e0a4bcd4d8da9e7
  Gas price: 5.0 gwei   Gas limit: 74701   Nonce: 65
  floodInsurance.settleClaim confirmed - Block: 25339205   Gas used: 37821 (50.63%)

A quick check of the various account balances confirms the expected behaviour:

$ floodToken.balanceOf(contract)
>>> 0

$ floodToken.balanceOf(account1)
>>> 1000000000000000000000000

$ floodToken.balanceOf(account2)
>>> 0  

The customer has no FLOOD, the contract has no FLOOD, and the outstanding FLOOD tokens have been returned to the contract owner, who now holds the total FLOOD supply.

One thought on “EOLink 0.1.5: Settling claim in new ERC20 currency

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