Chainlink Virtual Hackathon prize winners Lukas Sexton, Luis Ramírez, Ahmad S, and Mike Robinson used a Chainlink External Adapter to connect a marine shipping water level API to a Chainlink oracle to create a parametric insurance product for marine shipping. Parametric insurance smart contracts are good examples of how developers can use Chainlink to connect smart contracts with off-chain APIs to automate legacy insurance models. In this post, the team explains the thinking and implementation for the project.


By Lukas Sexton, Mike Robinson, Luis Ramírez, and Ahmad S

Climate-related uncertainty is leading to more unpredictable seaway conditions such as low or high water levels, which can result in the temporary closing of major shipping channels. If the St. Lawrence Seaway temporarily closes, an estimated $193 million is lost per week in the U.S. economy. Last year, the channel was closed for 12 days, costing the U.S. economy an estimated $331 million.

The global marine insurance industry is approaching $30 billion and growing due to increased seaborne world trade. However, unpredictable closures of major shipping seaway channels present a significant financial risk to commercial shipping companies and downstream economies.

We want to make modern life for inland marine cargo transportation businesses more manageable during the times that matter most by providing a 21st-century insurance solution to help them adapt to the effects of a changing climate.

What Is Parametric Insurance?

Parametric insurance is a financial tool to mitigate risk by protecting oneself against potentially devastating events. Parametric insurance is a type of insurance whereby the policyholder receives a predetermined payout based on a specific predetermined event.

Traditionally, parametric insurance products are purchased globally. For example, the International Bank for Reconstruction and Development (IBRD) issued a $485 million catastrophe bond to Mexico, providing the country with a four-year source of parametric earthquake and hurricane insurance protection. The parametric triggers for these natural disaster risks consider the magnitude and location of occurrence, with payout rates of 25%, 50%, 75%, and 100% of principal determined by where a hurricane strikes or how powerful an earthquake is.

In the event of a successful policy claim, the insurance sends payments immediately because the conditions for a payout are clear and straightforward. Parametric insurance is a natural fit for blockchains and smart contracts because the smart contract automates the insurance process. Smart contracts remove the requirement for third party arbitration, and a blockchain’s distributed ledger creates an immutable, auditable trail. This combination provides an opportunity for an incredible number of use cases in parametric insurance.

The nature of parametric insurance products means they rely heavily on secure, trusted data—this is where Chainlink oracles come into play. Chainlink has paved the way for consumer-centric, self-executing, decentralized insurance products using real-time data and executing the agreement via smart contracts. By moving parametric insurance agreements to the blockchain and using Chainlink to connect data to the smart contract, parametric insurance products can be fully decentralized and automated to execute when agreed-upon data-driven conditions occur.

Decentralized Marine Shipping Insurance dApp

This dApp is an insurance product that protects against the risk of delayed shipping waterways. The dApp monitors water level data generated through the stormglass.io API, and triggers the payout when the water levels for a particular seaway reach predetermined levels. The smart contract calculates and determines the payout level according to the inoperable water level range for each waterway (conditions vary across North America). Finally, the smart contract sends a payment to the policyholder.

A diagram of a parametric shipping insurance smart contract referencing water level API data via Chainlink oracles.
The components of the parametric marine shipping insurance dApp using Chainlink to deliver off-chain data to smart contracts.
Diagram showing how smart contracts allocate parametric insurance contract payouts depending on St. Lawrence River system water levels.
An example use case for one of the most extensive shipping waterways in North America, the St. Lawrence Seaway System

Technical Description

In the hackathon, we implemented a proof-of-concept by creating a smart contract and web application that would allow any user to buy marine cargo insurance. The user would submit the days of insurance and the cargo ship’s water body location. After purchasing the insurance, the smart contract would automatically pay the policyholder every day the water level is out of the predefined range.

We first drafted an initial application flow that went through continuous iterations, resulting in a UI that performed the basic flow we were looking to implement for our proof-of-concept.  At the same time, we researched web APIs and data sources that will provide water level data. In the end, we chose stormglass.io as our provider since its web API returns sea level data relative to the mean sea level from many stations all around the world, approximating a decentralized data aggregator. After understanding how the API works and its responses, we built a Chainlink External Adapter to bridge the Storm Glass API to the blockchain.

Making decentralized calls to web APIs involves having a web of oracles and data sources to avoid the Oracle Problem. Unfortunately, we only used one data source due to time constraints, but we still wanted to add a degree of decentralization to our API calls. Therefore, we decided to host our External Adapter in two different services and register it in two Chainlink nodes. We deployed one instance of the External Adapter in the Google Cloud Platform's Cloud Functions and another instance in Microsoft Azure Cloud Functions. We then contacted two node operators who helped us register the External Adapter and create a job description in their nodes. Finally, we deployed a Chainlink PreCoordinator contract that consolidated the data from both nodes by taking the median of the values coming from both nodes. We deployed and configured this contract using the Remix IDE.

According to the business logic we designed, the platform should request each policy's water level daily and evaluate if the contract should pay the claim to the policyholder for that day. We implemented this using a Chainlink alarm integration, which would trigger the request and evaluation 24 hours after initiating. However, a better implementation that would fulfill this design is to use a Cron initiator. This initiator type schedules a job that the Chainlink node should run periodically.

Lastly, we created a set of administrative scripts that helped us test the project in the Kovan and Rinkeby test networks.

In parallel to the smart contract, we developed the web UI using React, Rimble UI, Material UI, and the Drizzle framework. On the frontend side, we implemented an ENS resolver.

Altogether, the following diagram shows an overview of the project architecture:

A diagram showing how the marine insurance smart contract interfaces with water level APIs via Chainlink oracles to determine insurance payouts.
The architecture of the marine shipping insurance smart contract and oracle logic.

We bootstrapped the smart contract code using the Chainlink Trufflebox and the External Adapter by using the codebase from this GitHub repository.

How to Build and Deploy the Project

Getting the Source Code

The source code of the repositories is located at this GitHub repository.

Clone both repositories using:

$ git clone https://github.com/chainlink-hackathon2020-insurance/insurance-product.git
$ git clone https://github.com/chainlink-hackathon2020-insurance/stormglass-sealevel-adapter 

Compiling and Deploying the Smart Contract

As a prerequisite, we need to make sure we have Truffle installed by running the command:

$ npm install -g truffle

We need first to install all dependencies by running:

$ npm install

To run the tests of the insurance-product codebase, we will run:

$ npm test

To deploy the contracts to a network, we will set the environment variables MNEMONIC and RPC_URL, used in the truffle-config.js file, with the appropriate values. The easiest way to do this is to create a .env file with those variables defined.

Then we're going to run the command:

$ npm migrate:live

Setting Up and Deploying the External Adapter

To test and deploy the External Adapter, we need to create an account and get our API key from Storm Glass: https://stormglass.io/.

To run the tests of the stormglass-sealevel-adapter we first need to define the environment variable API_KEY with the key we got from Storm Glass and then run the command:

$ npm test

We can also manually test the External Adapter by running:

$ npm start

This would run the Adapter in a local server. We can then call the Adapter by running the following curl command, which requests the water-level for specific coordinates:

$ curl -X POST -H "content-type:application/json" "http://localhost:8080/" --data '{ "id": 0, "data": { "lat": 43.38, "lng": -3.01 } }'

Finally, to make the adapter available to the public, we need to deploy it in a public environment. The easiest way to do this is by using serverless function services. The codebase we used was set up for GCP Functions and AWS Lambda. We also added support for Azure Cloud functions. The README describes in detail how to perform the deployment to these platforms. The critical thing to consider here is to set up the API_KEY variable in the deployment environment.

When we register our External Adapter we are going to use the deployment we did previously. We will set the bridge URL to the URL of the serverless function we deployed.

Next, we will create a job specification that makes use of the External Adapter:

{
  "initiators": [
    {
      "type": "runlog",
      "params": {
        "address": "<input Chainlink Oracle contract address>"
      }
    }
  ],
  "tasks": [
    {
      "type": "stormglass-sealevel-adapter"
    },
    {
      "type": "copy",
      "params": {
        "copyPath": "result"
      }
    },
    {
      "type": "multiply",
      "params": {
        "times": 100
      }
    },
    {
      "type": "ethint256"
    },
    {
      "type": "ethtx"
    }
  ]
}

Notice that in this specification we are using the multiply native adapter to multiply the External Adapter response by 100. The reason for this is that Solidity doesn't support floating-point numbers, and the Storm Glass API returns the result in meters with centimeters.

Deploying the PreCoordinator Contract

To make a genuinely decentralized API call to Storm Glass, we need to host the External Adapter in different platforms and register it in multiple Chainlink oracles. The more, the better. To consolidate the data returned by all the Chainlink oracles, we will deploy and configure a PreCoordinator contract. You can find the PreCoordinator contract in the contracts folder (MarineInsuranceEAPreCoordinator.sol) of the project. For convenience, we will deploy this contract using the Remix IDE.

First, add the file or create a new contract and copy and paste the PreCoordinator contract's contents. Next, compile the contract and deploy it using the "Injected Web3" environment with the MetaMask plug-in set to the appropriate network where the oracles exist. In the deployment tab, make sure you select PreCoordinator in the "Contract" dropdown.

After deploying the contract, create a service agreement using the createServiceAgreement function. The function accepts the following parameters:

_minResponses: Minimum amount of oracles we expect to respond before calculating the median of the values and calling the origin contract's callback function.
_oracles: Array of oracles that the service agreement will include.
_jobIds: Array of identifiers of the job specifications we created in the previous step.
_payments: Array of the LINK fees we defined to execute the job specification during its creation in the last step.

For each element of the _oracles array, there must be a job id and payment defined in the _jobIds and _payments arrays, respectively, in the same position. Example:

Marine insurance shipping oracle Remix-IDE array setup using Chainlink.
Service agreement array model for Remix IDE.

In the case of our project, this is what our values looked like:

Example screenshot of Remix IDE array for parametric shipping Chainlink oracle.
Snapshot of Remix IDE array with sample values.

After executing the function, we are going to take a look at the transaction submitted. The value returned in the topic at position one will be the Job ID we will specify in our marine insurance Chainlink-secured contract. We will also set the PreCoordinator's contract address as the oracle address.

Ethereum Kovan Network Etherscan transaction details showing verification of the submitted transaction.
Example of a submitted transaction on the Kovan Network Etherscan.

Truffle Administrative Scripts

The project comes with a set of Truffle scripts to fund, configure, and test the smart contract we have deployed in the previous section.

We can execute these scripts by using the command:

$ truffle exec scripts/<script_file_name> --network live

The scripts to do the initial configuration are numbered in an ideal execution sequence.

1_set_oracle_data.js: Sets the oracle data to fetch water level: oracle address, job id, and payment.
2_set_evaluation_period.js: Sets the water level evaluation period in seconds. By default, it's one day.
3_fund_contract_eth.js: Funds the contract with ETH.
4_fund_contract_link.js: Funds the contract with LINK.
5_create_insurance_policy.js: Creates an insurance policy with pre-defined data.
6_start_evaluation_period.js: Triggers Chainlink alarm to evaluate water levels after the configured period of time.

We need to review the values of each script to make sure they are adequate and correct.

The project also has another set of scripts that are useful for testing and demo purposes:

change_water_level_manually.js: Manually changes the water level of a given policy. This is used for demo purposes.
get_insurance_policies.js: Returns the policies and policy identifiers of the current user.
request_water_level_manually.js: Performs the request for water levels of all registered insurance policies manually.

Running the Frontend

When we execute the migrate command, the ABIs will be output to the client folder (client/src/abi) with the corresponding addresses where it was deployed. It's essential to do this before running the frontend.

To run the frontend, we're going to first go to the client folder by running the command:

$ cd client

Next, we will install the client dependencies by running the command:

$ npm install

Finally, we will run the frontend with:

$ npm start

The console will show the local URL where the frontend is available. Open the URL in a browser with the MetaMask plugin installed.

Smart Contract Highlights

Storage Structures

The main structures that store the insurance policy-related values are:

   InsurancePolicy[] public insurancePolicies;
   mapping(address => uint[]) public insurancePolicyOwnership;
   mapping(bytes32 => uint) requestToInsurancePolicyId;

The insurancePolicies array stores all the insurance policies with their cargo and location data, water-level value, and status. The array index serves as the insurance policy identifier.

The insurancePolicyOwnership mapping associates an Ethereum address to a set of policy identifiers.

The requestToInsurancePolicyId mapping associates a Chainlink request to a policy identifier.

Main Functions

Three main functions in the smart contract encapsulate the main business logic of our platform:

   function registerInsurancePolicy(ShipData memory _shipData,
       Coordinate memory _location,
       uint256 _startDate,
       uint256 _endDate)
   public payable returns(uint256) {
       uint256 premium = calculatePremium(_shipData);	
       require(premium == msg.value, "You need to pay the policy premium");
       uint256 claimPaymentAmount = calculateDailyClaimPayouts(_shipData);
 
       TrackingData memory initialTrackingData = TrackingData({
           location: _location,
           currentWaterLevel: -9999,
           currentRequestId: "0x0",
           requestStatus : RequestStatus.CREATED
       });
 
       CoverageData memory coverageData = CoverageData({
           startDate: _startDate,
           endDate: _endDate,
           dailyClaimAmount: claimPaymentAmount,
           waterLevelMin: -50,
           waterLevelMax: 50,
           beneficiary: msg.sender
       });
 
       InsurancePolicy memory policy = InsurancePolicy({
           coverageData: coverageData,
           shipData: _shipData,
           trackingData: initialTrackingData
       });
 
       insurancePolicies.push(policy);
       uint256 identifier = insurancePolicies.length - 1;
       insurancePolicyOwnership[msg.sender].push(identifier);
       emit InsurancePolicyCreation(msg.sender, identifier);
       return identifier;
   }

This function receives the cargo ship data, the location of the water body it's going to travel in, and the range of dates when the policy will be active. It checks that the sender has sent the exact quantity of the insurance premium (constant in this version). Then, it calculates the claim amount by calling another function and passing the ship data as an argument (the function returns a constant in this version). It then sets up all the insurance policy information with initial values for the status, request identifier, and water level. Following this, it adds the policy to the insurance policy array, gets the identifier, and associates the sender address to the policy identifier. Finally, it emits an event to signal the creation of the insurance policy.

function requestWaterLevels() private {
       for(uint i = 0; i < insurancePolicies.length; i++) {
           InsurancePolicy storage insurancePolicy = insurancePolicies[i];
           if(isPolicyActive(insurancePolicy)){
               insurancePolicy.trackingData.requestStatus = RequestStatus.INITIATED;
               Chainlink.Request memory request = buildChainlinkRequest(waterLevelJobId, address(this), this.receiveWaterLevel.selector);
               request.add("lat", insurancePolicy.trackingData.location.lat);
               request.add("lng", insurancePolicy.trackingData.location.lng);
               bytes32 requestId = sendChainlinkRequestTo(waterLevelOracle, request, waterLevelFee);
               requestToInsurancePolicyId[requestId] = i;
           }
       }
   }

This private function goes through all the policies in the insurancePolicy array. It requests the water level data using a Chainlink oracle or PreCoordinator for all policies that are active. Two other functions call this one: The function that requests water levels manually and the Chainlink alarm. This function specifies that once the oracle gets the data, this must be passed as an argument to the receiveWaterLevel function, which we will look at next.

function receiveWaterLevel(bytes32 _requestId, int256 _level) public recordChainlinkFulfillment(_requestId){
       InsurancePolicy storage insurancePolicy = insurancePolicies[requestToInsurancePolicyId[_requestId]];
       insurancePolicy.trackingData.currentWaterLevel = _level;
       insurancePolicy.trackingData.requestStatus = RequestStatus.COMPLETED;
       delete requestToInsurancePolicyId[_requestId];
       if (_level > insurancePolicy.coverageData.waterLevelMin &&
           _level < insurancePolicy.coverageData.waterLevelMax) {
           return;
       }
       //Create and pay claim
       else {
           uint amountToPayToday = insurancePolicy.coverageData.dailyClaimAmount;
           insurancePolicy.coverageData.beneficiary.transfer(amountToPayToday);
           emit ClaimPayout(insurancePolicy.coverageData.beneficiary,
               requestToInsurancePolicyId[_requestId],
               amountToPayToday);
       }
       if(!isPolicyActive(insurancePolicy)) {
           emit InsurancePolicyExpired(insurancePolicy.coverageData.beneficiary,
               requestToInsurancePolicyId[_requestId], now);
       }
       if(waterLevelEvaluationPeriodActive) {
           startWaterLevelEvaluationRequestPeriod();
       }
   }

In a broad sense, the receiveWaterLevel function performs the following operations. It gets the insurance policy by using the _requestId as an index to the requestToInsurancePolicyId mapping, which returns the policy identifier that is at the same time passed as an index of the insurance policy array. After this, it sets the water level to the insurance policy tracking data section. It then evaluates if the water level is outside of the pre-established range. If that's the case, then the smart contract transfers the claim amount to the insurance policy beneficiary, which in this particular contract is the address that registered the policy. Finally, it emits an event if the insurance policy has expired.

Building Out Decentralized Parametric Insurance

From here, the team is considering exploring more use cases for decentralized parametric insurance products that could operate on the same smart contract framework we have built. We chose to focus on one seaway for our proof-of-concept but the insurance product is scalable to all significant seaways in North America. We aim to start with those that handle the most marine transportation and experience the most significant water level fluctuations. Our vision is to provide fully decentralized parametric marine insurance across the globe to protect businesses and economies from the financial hardships resulting from fluctuating seaway conditions.

Taking the MVP we built in the hackathon, we have envisioned the following tasks to make it into a full-fledged product:

  • Request the user for ship and cargo data and use it to determine the claim and premium amount. Currently, both values are constant in the smart contract.
  • Review and add more checks to the input data, for example, making sure the _startDate parameter is always less than the _endDate parameter.
  • Register the External Adapter in Chainlink Nodes connected to mainnet.
  • Storm Glass returns data from a considerable amount of hydrometric stations worldwide, but it's not the entire universe of marine data. We will find other data sources that return the water level data from stations that Storm Glass doesn't have registered to increase insurance market reach.
  • Instead of tracking the coordinates of the water body the ship will use, track the ship's position coordinates.
  • Add insurance tiers or packages based on the amount of client data submitted. For example, we could start by putting accounts associated with an ENS address in a higher tier.

We are excited about the future opportunities in the parametric insurance industry unlocked by Chainlink and smart contracts. We welcome any invitations to connect with industry experts, other passionate developers or the Chainlink Community to discuss our ideas further. Hopefully this guide has provided guidance on how one could develop a parametric insurance product using Chainlink to enhance the functionality of their smart contract.


Learn More

Building your own decentralized insurance product? If you’re a developer and want to connect your smart contract to data outside the underlying blockchain, visit the developer documentation and join the technical discussion in Discord. If you want to schedule a call to discuss the integration more in-depth, reach out here. You should also visit chain.link, subscribe to the Chainlink newsletter, and follow @chainlink on Twitter.

Website | Twitter | Discord | Reddit | YouTube | Telegram | Events | GitHub | Price Feeds | DeFi