区块链技术具有独特的属性,可以用来创建创新性的去中心化保险产品,为保险供应商和客户带来诸多好处。在本技术教程中,我们将向您展示:

  • 去中心化参数化保险合约的主要特点
  • 为什么Chainlink预言机在这些新的保险产品中起着举足轻重的作用
  • 在去中心化保险合约中使用Chainlink Price Feed的优势
  • 如何把所有的东西放在一起,创建一个可用的参数化作物保险合约
  • 如何使用Chainlink节点来自动更新保险合约

下面例子的完整代码可以在RemixGitHub上查看,包括下面提到的所有功能以及所有需要的帮助函数。

去中心化保险

去中心化保险利用区块链技术和智能合约来取代传统的保险协议。去中心化保险产品主要有三大特点。

数据驱动的自动化

去中心化保险合约最重要的一点是,它是数据驱动和自动执行的。这意味着保险合约在不需要人工干预的情况下自动执行逻辑,依靠从外部获取的安全准确的数据来决定合约逻辑的执行。这些保险智能合约还可以与外部输出连接,如支付处理器或企业财务系统,以方便触发支付。

智能合约

智能合约代表了保险人与客户之间的保险合同,它实质上是保险人对客户指定类型的损失、破坏或责任进行赔偿的承诺,如果是参数保险,则是对冲特定事件发生的风险。它包含了保险合同的所有细节,如指数(例如农作物保险合同中的降雨量)、客户支付的细节(如钱包地址,或外部支付系统的客户ID)、合同日期或期限、指数的测量地点、阈值和商定的赔付值。由于保险合约存储和执行在通常运行在大量节点上的区块链上,因此它具有高度确定性,不容易被黑客攻击或篡改。

理赔流程

与传统的保险合同不同,去中心化保险合约中,理赔过程是作为合约执行的一部分自动处理的。客户不需要提交理赔,不需要提供任何证据,也不需要与保险公司或智能合约有任何互动。当智能合约认为应该发生赔付时,赔付将作为合约执行的一部分自动触发。这可以通过直接向客户进行链上支付,也可以通过智能合约连接的外部支付通道或金融系统来完成。

创建数据驱动的参数化保险合同

现在,我们已经了解了什么构成了一个去中心化的参数化保险合约,我们将通过构建一个简单的例子来展示上述三个概念。在这个场景中,我们将创建一个具有以下属性的参数化农作物保险合约。

  • 如果在指定时间内没有降雨,合同将向客户支付约定的价值,目前设置为三天,以便于演示。合同将从两个不同的数据源获取降雨数据,以缓解任何数据完整性问题,然后对结果进行平均。
  • 该合约将以相当于美元价值的ETH全额出资,用于约定的赔付金额,以确保在触发索赔时的完全确定性。它将使用Chainlink ETH/USD Price Feed来确定合约所需的ETH数量。
去中心化保险架构

建立保险合约工厂

首先,我们需要创建一个主 "合约工厂 "合约,它将生成多个保险协议,并允许我们与它们进行交互。这个合约将由保险公司拥有,并为每个生成的保险合约提供足够的ETH和LINK资金,以确保保险合约一旦生成,就能在其整个存续期内执行所有需要的操作,包括赔付。

首先,我们的Solidity代码包含两个合约,一个是InsuranceProvider合约,一个是InsuranceContract合约。InsuranceProvider合约会生成很多保险合约。

InsuranceProvider合约的构造函数初始化了Kovan网络上的Chainlink ETH/USD Price Feed 。InsuranceContract合约的构造函数定义如下,后面会进一步充实。

pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;

//Truffle Imports
import "chainlink/contracts/ChainlinkClient.sol";
import "chainlink/contracts/vendor/Ownable.sol";
import "chainlink/contracts/interfaces/LinkTokenInterface.sol";

contract InsuranceProvider {
    
    constructor()   public payable {
        priceFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);
    }

}

contract InsuranceContract is ChainlinkClient, Ownable  {
    
    constructor(address _client, uint _duration, uint _premium, uint _payoutValue, string _cropLocation, address _link, uint256 _oraclePaymentAmount)  payable Ownable() public {
        
    }
}

InsuranceProvider合约的一般结构如下:

  • 每个生成的合约都存储在保险合同的contracts map中 以生成的合同的Ethereum地址作为键。值是一个实例化的 InsuranceContract Solidity 智能合约。
//here is where all the insurance contracts are stored.
mapping (address => InsuranceContract) contracts;

  • newContract函数接收所需的输入并生成一个新的保险合约,按照之前定义的构造函数定义传递所有所需的参数。它还会发送与支付金额相等的ETH,从而使生成的合约资金充足。它使用Chainlink ETH/USD Price Feed来完成这个转换。然后,它将生成的合约存储在contracts map中,并将足够的LINK传输到生成的合约中,这样它就有足够的数据请求每天两次,并且有一个余量。这个余量是为了考虑到合约到期后可能需要额外调用的时间问题。当合约结束时,任何剩余的LINK都会被返还给保险提供商。
function newContract(address _client, uint _duration, uint _premium, uint _payoutValue, string _cropLocation) public payable onlyOwner() returns(address) {
        

        //create contract, send payout amount so contract is fully funded plus a small buffer
        InsuranceContract i = (new InsuranceContract).value((_payoutValue * 1 ether).div(uint(getLatestPrice())))(_client, _duration, _premium, _payoutValue, _cropLocation, LINK_KOVAN,ORACLE_PAYMENT);
         
        contracts[address(i)] = i;  //store insurance contract in contracts Map
        
        //emit an event to say the contract has been created and funded
        emit contractCreated(address(i), msg.value, _payoutValue);
        
        //now that contract has been created, we need to fund it with enough LINK tokens to fulfil 1 Oracle request per day, with a small buffer added
        LinkTokenInterface link = LinkTokenInterface(i.getChainlinkToken());
        link.transfer(address(i), ((_duration.div(DAY_IN_SECONDS)) + 2) * ORACLE_PAYMENT.mul(2));
        
        
        return address(i);
        
    }

  • updateContract函数用于更新保险合约的数据,并检查是否达到了触发付款的阈值,或者合约是否已经到了结束日期。
function updateContract(address _contract) external {
        InsuranceContract i = InsuranceContract(_contract);
        i.updateContract();
    }

  • 最后,getContractRainfall函数用于返回给定保险合同的降雨量(以毫米为单位),getContractRequestCount函数用于查看有多少数据请求成功地传回保险合约。
function getContractRainfall(address _contract) external view returns(uint) {
        InsuranceContract i = InsuranceContract(_contract);
        return i.getCurrentRainfall();
    }

    function getContractRequestCount(address _contract) external view returns(uint) {
        InsuranceContract i = InsuranceContract(_contract);
        return i.getRequestCount();
    }


获取外部数据

生成的保险合约需要获得外部数据才能正常执行。这就是Chainlink网络发挥作用的地方,因为你可以使用它将保险合约连接到多个降雨数据源。在这个例子中,我们将在两个不同的Chainlink节点上使用Job Specification,从两个不同的天气API中获取数据,然后将在链上取平均值来得出最终结果。这两个天气API都需要注册获得一个免费的API密钥在每个请求中使用。

一旦我们记下了Weather API key以及上面的Job Specification Id和oracle合约,我们现在就可以创建 InsuranceContract合约,填写所需的常量字段。在生产场景中,这些常量字段会被私有存储在Chainlink节点上,在链上是不可见的,但为了方便跟随演示,它们被留在了合约中。我们还存储了所需的JSON路径,当Chainlink节点从每个API中获取天气数据时,我们要遍历这些路径来找到每日总降雨量(以毫米为单位)。

   string constant WORLD_WEATHER_ONLINE_URL = "http://api.worldweatheronline.com/premium/v1/weather.ashx?";
    string constant WORLD_WEATHER_ONLINE_KEY = "insert API key here";
    string constant WORLD_WEATHER_ONLINE_PATH = "data.current_condition.0.precipMM";
      
    string constant WEATHERBIT_URL = "https://api.weatherbit.io/v2.0/current?";
    string constant WEATHERBIT_KEY = "insert API key here";
    string constant WEATHERBIT_PATH = "data.0.precip";


完成保险合约

下一步是完成保险合约(InsuranceContract),它代表客户和保险公司之间的作物保险合同。

该合约被实例化,所有所需的值都传递到构造函数中。它还做了以下工作:

  • 使用Chainlink ETH/USD Price Feed来检查是否有足够的ETH被发送,以确保在触发支付时有足够的资金。
  • 设置合约执行所需的一些变量
  • 将JobId和oracle数组设置为包含从上面“获取外部数据”部分的两个Job Specification中获取的值。然而,如果你想运行你自己的Chainlink节点,将两个请求都设置为使用你的Job Specification和oracle合约,这样就可以看到每个Job的输出。这样做需要在market.link上创建一个新的Job Specification,和这个例子一样,只需要修改runlog initiator中的地址为你的oracle contract。
constructor(address _client, uint _duration, uint _premium, uint _payoutValue, string _cropLocation, 
                address _link, uint256 _oraclePaymentAmount)  payable Ownable() public {
        
        priceFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);
        
        //initialize variables required for Chainlink Node interaction
        setChainlinkToken(_link);
        oraclePaymentAmount = _oraclePaymentAmount;
        
        //first ensure insurer has fully funded the contract
        require(msg.value >= _payoutValue.div(uint(getLatestPrice())), "Not enough funds sent to contract");
        
        //now initialize values for the contract
        insurer= msg.sender;
        client = _client;
        startDate = now + DAY_IN_SECONDS; //contract will be effective from the next day
        duration = _duration;
        premium = _premium;
        payoutValue = _payoutValue;
        daysWithoutRain = 0;
        contractActive = true;
        cropLocation = _cropLocation;
        
        //if you have your own node and job setup you can use it for both requests
        oracles[0] = 0x05c8fadf1798437c143683e665800d58a42b6e19;
        oracles[1] = 0x05c8fadf1798437c143683e665800d58a42b6e19;
        jobIds[0] = 'a17e8fbf4cbf46eeb79e04b3eb864a4e';
        jobIds[1] = 'a17e8fbf4cbf46eeb79e04b3eb864a4e';

        emit contractCreated(insurer,
                             client,
                             duration,
                             premium,
                             payoutValue);
    }


然后,我们创建一个函数来调用,以从每个Chainlink节点和天气API中请求降雨数据。这个函数被主保险提供者合约所调用。它为每个请求建立了所需的URL,然后为每个请求调用checkRainfall函数。但在这之前,它调用了一个checkEndContract函数来检查合约结束日期是否已经到了,并且只有在合约仍然有效的情况下才会继续。这个checkEndContract函数定义如下。

function updateContract() public onContractActive() returns (bytes32 requestId)   {
        //first call end contract in case of insurance contract duration expiring, if it hasn't then this function execution will resume
        checkEndContract();
        
        //contract may have been marked inactive above, only do request if needed
        if (contractActive) {
            dataRequestsSent = 0;
            //First build up a request to World Weather Online to get the current rainfall
            string memory url = string(abi.encodePacked(WORLD_WEATHER_ONLINE_URL, "key=",WORLD_WEATHER_ONLINE_KEY,"&q=",cropLocation,"&format=json&num_of_days=1"));
            checkRainfall(oracles[0], jobIds[0], url, WORLD_WEATHER_ONLINE_PATH);

            
            // Now build up the second request to WeatherBit
            url = string(abi.encodePacked(WEATHERBIT_URL, "city=",cropLocation,"&key=",WEATHERBIT_KEY));
            checkRainfall(oracles[1], jobIds[1], url, WEATHERBIT_PATH);    
        }
    }


现在我们可以创建checkRainfall函数。这是实际执行外部数据请求的函数。它接收所有需要的参数,建立一个请求,然后将其发送到指定的Chainlink节点oracle合约。

在我们的演示中,传递到checkRainfall函数中的_path变量的值用来遍历请求返回的JSON的路径,找到当前的降雨量。这些值取决于调用哪一个天气API,这两个选项都存储在我们的合约中的静态变量中,并根据需要传递到_path函数参数中。

string constant WORLD_WEATHER_ONLINE_PATH = "data.current_condition.0.precipMM";
    string constant WEATHERBIT_PATH = "data.0.precip";

function checkRainfall(address _oracle, bytes32 _jobId, string _url, string _path) private onContractActive() returns (bytes32 requestId)   {

        //First build up a request to get the current rainfall
        Chainlink.Request memory req = buildChainlinkRequest(_jobId, address(this), this.checkRainfallCallBack.selector);
           
        req.add("get", _url); //sends the GET request to the oracle
        req.add("path", _path);
        req.addInt("times", 100);
        
        requestId = sendChainlinkRequestTo(_oracle, req, oraclePaymentAmount); 
            
        emit dataRequestSent(requestId);
    }


然后,我们创建一个回调函数,当Chainlink节点发回响应时调用。这个函数接收指定位置的更新雨量数据,如果是第二次数据更新(即两个请求的都得到了响应),则执行取平均计算,然后用最新的雨量数据更新合约。

回调函数还根据当前合约的降雨量数据检查是否实现了参数化损失。本例中,它根据给定的阈值检查连续无雨天数。如果满足赔付条件,则调用payoutContract函数。

function checkRainfallCallBack(bytes32 _requestId, uint256 _rainfall) public recordChainlinkFulfillment(_requestId) onContractActive() callFrequencyOncePerDay()  {
        //set current temperature to value returned from Oracle, and store date this was retrieved (to avoid spam and gaming the contract)
       currentRainfallList[dataRequestsSent] = _rainfall; 
       dataRequestsSent = dataRequestsSent + 1;
       
       //set current rainfall to average of both values
       if (dataRequestsSent > 1) {
          currentRainfall = (currentRainfallList[0].add(currentRainfallList[1]).div(2));
          currentRainfallDateChecked = now;
          requestCount +=1;
        
          //check if payout conditions have been met, if so call payoutcontract, which should also end/kill contract at the end
          if (currentRainfall == 0 ) { //temp threshold has been  met, add a day of over threshold
              daysWithoutRain += 1;
          } else {
              //there was rain today, so reset daysWithoutRain parameter 
              daysWithoutRain = 0;
              emit ranfallThresholdReset(currentRainfall);
          }
       
          if (daysWithoutRain >= DROUGHT_DAYS_THRESDHOLD) {  // day threshold has been met
              //need to pay client out insurance amount
              payOutContract();
          }
       }
       
       emit dataReceived(_rainfall);
        
    }


执行保险人向客户自动支付约定的价值。我们在这里格外小心,确保它只能在合约仍处于激活状态(即未结束)时被调用,并且只能被其他合约函数在内部调用。它还将任何剩余的LINK返回到保险提供商主合同,并将合约设置为已完成状态,以防止对其进行任何进一步的操作。

function payOutContract() private onContractActive()  {
        
        //Transfer agreed amount to client
        client.transfer(address(this).balance);
        
        //Transfer any remaining funds (premium) back to Insurer
        LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());
        require(link.transfer(insurer, link.balanceOf(address(this))), "Unable to transfer");
        
        emit contractPaidOut(now, payoutValue, currentRainfall);
        
        //now that amount has been transferred, can end the contract 
        //mark contract as ended, so no future calls can be done
        contractActive = false;
        contractPaid = true;
    
    }


最后,我们创建一个函数来处理这样的场景:合约结束日期已经到了,还没有触发支付,我们需要归还合约中的资金,然后标记为结束。该函数执行检查整个合约是否收到足够的数据请求。每天需要收到一个数据请求,总计只允许漏掉一个请求。因此,如果合约的持续时间为30天,则必须有至少29个成功的数据请求。如果合约在其生命周期内收到足够的请求,所有资金将被退回给保险供应商。否则,如果在整个合约的存续期内没有足够的数据请求,客户就会自动收到作为退款的保费,而保险商则会拿回任何剩余的资金。

这个方案还利用Chainlink ETH/USD Price Feed来确定正确的ETH数量,将其返还给客户。这个检查给客户一定程度的保证,保险提供商不会试图通过不更新降雨量数据来玩弄合约,直到达到结束日期。该函数还将返回任何剩余的LINK回保险提供商合约。

function checkEndContract() private onContractEnded()   {
        //Insurer needs to have performed at least 1 weather call per day to be eligible to retrieve funds back.
        //We will allow for 1 missed weather call to account for unexpected issues on a given day.
        if (requestCount >= (duration.div(DAY_IN_SECONDS) - 1)) {
            //return funds back to insurance provider then end/kill the contract
            insurer.transfer(address(this).balance);
        } else { //insurer hasn't done the minimum number of data requests, client is eligible to receive his premium back
            client.transfer(premium.div(uint(getLatestPrice())));
            insurer.transfer(address(this).balance);
        }
        
        //transfer any remaining LINK tokens back to the insurer
        LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());
        require(link.transfer(insurer, link.balanceOf(address(this))), "Unable to transfer remaining LINK tokens");
        
        //mark contract as ended, so no future state changes can occur on the contract
        contractActive = false;
        emit contractEnded(now, address(this).balance);
    }


部署和测试合同

首先,我们需要部署InsuranceProvider合约,并用一些ETH和LINK为其提供资金,以便在生成的InsuranceContract合约中使用。

完成这些工作后,我们就可以创建一个新的InsuranceContract,传递所需的值。请注意以下几点。

  • 持续时间的单位是秒 在这个演示中,1天被缩短为60秒(如在DAY_IN_SECONDS常量中所指定的),所以合同期限300秒代表5天。
  • premium和payoutValue参数以美元为单位,乘以100000000,例如100美元就是10000000000。
创建一个新的保险合同

当保险合同生成后,我们可以通过Etherscan中的交易或通过交易输出获取其地址。

在Etherscan上查看生成的合同

然后,我们可以将生成的合同地址,传入updateContract函数,开始将降雨量数据传入合同中。

更新保险合约

当两个Chainlink节点都处理了作业请求并返回一个结果后,我们就可以调用getContractRainfallgetContractRequestCount函数来查看平均降雨量的更新,以及数据请求数的增加。一个数据请求意味着两个节点都返回了一个结果,取平均值之后存储在合约中。在本例中,爱荷华州目前两个数据源的平均降雨量为0.6mm。我们还可以调用帮助函数getContractStatus来验证合同是否还处于活动状态。

获取保险合同的状态

在合约有效期内(本演示为5次/300秒),每天(本例为1分钟)应重复此步骤,结束合约。如果无雨天数达到DROUGHT_DAYS_THRESHOLD中设置的阈值,合约将向客户支付约定的金额,合约状态结束。

为了达到演示的目的,我们又为一个没有降雨的地点创建了一个保险合约,并在三分钟内重复上述步骤三次,以演示在赔付时发生的情况。在这个案例中,我们可以看到最新的降雨量为0,请求次数为3,合同不再处于活动状态。

获取保险合同的状态

如果我们再去以太坊上检查该合约,就会发现约定的美元ETH赔付值已经转回了上面创建合约时指定的客户钱包地址,而保险合约已经不再持有任何ETH或LINK。由于保险合约现在处于完成状态,所以后续对保险合约的任何操作都会被拒绝。

验证保险合约的支付情况

自动更新数据

在当前版本的合同中,必须有人手动调用updateContract函数来让合约与Chainlink节点通信并获取降雨量数据。这并不理想,因为它需要在整个合约期限内多次调用。一个好的自动化方法是利用Chainlink节点的cron initiator

cron initiator是一种使用简单的cron语法在Chainlink节点上调度循环Job的方法。在这种情况下,我们可以做的是在Chainlink节点上创建一个新的Job Specification,使用cron initiator每天触发一次Job Specification。但为了本演示的目的,我们将根据前面提到的常量SECONDS_IN_DAY,将其设置为每分钟触发一次。

Job Specification的剩余部分将简单地在每次Cron job触发执行Job Specification时,调用部署的智能合约updateContract函数。这个想法是,保险前端将拥有所有相关的细节(合约地址,开始日期,结束日期),并可以将它们传递进来。

{
   "initiators": [
      {
         "type": "cron",
         "params": {
            "schedule": "CRON_TZ=UTC 0/' + 6 + ' * * * * *"
         }
      }
   ],
   "tasks": [
      {
         "type": "ethtx",
         "confirmations": 0,
         "params": {
            "address": "' + address + '",
            "functionSelector": "checkContract()"
         }
      }
   ],
   "startAt": "' + startDate + '",
   "endAt": "' + endDate + '"
}

我们的想法是,去中心化的保险应用前端将向Chainlink节点API发送请求,动态生成新的Job Specification,并提供节点自动开始定期更新保险合同所需的所有正确细节,而不必通过Chainlink节点前端接口手动创建这个工作规范。

要做到这一点,首先我们需要Chainlink节点的IP地址和端口,以及登录节点的用户名和密码。这些都是用来生成下一次请求的cookiefile。

curl -c cookiefile -X POST -H 'Content-Type: application/json' -d '{"email":"user@email.com", "password":"password"}' http://35.189.58.211:6688/sessions

完成了这些工作后,我们会得到一个响应,以显示认证成功。

{"data":{"type":"session","id":"sessionID","attributes":{"authenticated":true}}}

然后我们可以向Chainlink节点API发送另一个POST请求,这次是向/v2/specs端点发送。请求中的JSON是定期更新的生成的保险合约的地址,以及开始和结束的日期/时间(如果需要的话,还有指定的时间偏移),这样节点就知道什么时候停止定期更新保险合约。

curl  -b cookiefile -X POST -H 'Content-Type: application/json' -d '{"initiators":[{"type":"cron","params":{"schedule":"CRON_TZ=UTC 0/60 * * * * *"}}],"tasks":[{"type":"ethtx","confirmations":0,"params":{"address":"0xdC71C577A67058fE1fF4Df8654291e00deC28Fbf","functionSelector":"updateContract()"}}],"startAt": "2020-11-10T15:37:00+10:30","endAt": "2020-11-10T15:42:00+10:30"}' http://35.189.58.211:6688/v2/specs

这个命令会在返回一个成功的消息,其中包含了生成的Job Specification的细节。在这之后,你就可以登录到Chainlink节点前端,并看到新创建的Job Specification。

Cron启动器工作规范语法

Job Specification创建后,它就会按照cron initiator中设置的参数开始执行请求。我们可以在Chainlink节点前端监控到这一点。

节点成功地完成了请求后可以回到智能合约,看到它的状态已经成功更新。

获取保险合约的状态

总结

在这篇技术文章中,我们已经演示了如何建立一个去中心化的作物保险产品,以补偿农民的干旱期。我们已经展示了保险合约拥有准确和去中心化数据的重要性,以及Chainlink oracles在安全提供这些数据方面的作用。

我们还演示了如何利用连接到外部数据和事件的确定性智能合约来彻底降低处理保险理赔的开销和管理成本,以及在合约条款以美元为基础却以加密货币支付的情况下,如何利用Chainlink去中心化喂价送来准确确定正确的赔付金额。最后,我们还演示了Chainlink节点cron initiator如何与Chainlink节点API结合使用,以自动安排和执行智能合约更新。

虽然这个演示包含了许多功能,但它可以作为一个基本模板来构建一个完整的、功能丰富的去中心化保险产品。开发者可以在这个模板的基础上以各种方式进行构建,比如去掉人工数据聚合,利用Chainlink的聚合器PreCoordinator合约。另一种选择是将保险合约证券化,并将其作为DeFi生态系统或其他市场的抵押品。

如果您是开发人员,并希望将您的智能合约连接到链外数据和系统,请访问开发文档并加入Discord上的技术讨论。如果您想安排电话讨论更深入的集成,请在这里联系。

如果你是开发者,想要快速为你的应用连接到Chainlink的价格参考数据,访问开发者文档,并接入Discord技术讨论。如果你想要安排一个电话来更深入讨论Matic/Chainlink的集成,请在此处联系。

English channels

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

中文渠道

中文官网 | 知乎 | SegmentFault | CSDN |