区块链技术具备独特的属性,能够用来创立创新性的去中心化保险产品,为保险供应商和客户带来诸多益处。在本技术教程中,咱们将向您展现:
- 去中心化参数化保险合约的次要特点
- 为什么 Chainlink 预言机在这些新的保险产品中起着无足轻重的作用
- 在去中心化保险合约中应用 Chainlink Price Feed 的劣势
- 如何把所有的货色放在一起,创立一个可用的参数化作物保险合约
- 如何应用 Chainlink 节点来自动更新保险合约
上面例子的残缺代码能够在 Remix 或 GitHub 上查看,包含上面提到的所有性能以及所有须要的帮忙函数。
去中心化保险
去中心化保险利用区块链技术和智能合约来取代传统的保险协定。去中心化保险产品次要有三大特点。
数据驱动的自动化
去中心化保险合约最重要的一点是,它是数据驱动和主动执行的。这意味着保险合约在不须要人工干预的状况下主动执行逻辑,依附从内部获取的平安精确的数据来决定合约逻辑的执行。这些保险智能合约还能够与内部输入连贯,如领取处理器或企业财务零碎,以不便触发领取。
智能合约
智能合约代表了保险人与客户之间的保险合同,它本质上是保险人对客户指定类型的损失、毁坏或责任进行抵偿的承诺,如果是参数保险,则是对冲特定事件产生的危险。它蕴含了保险合同的所有细节,如指数(例如农作物保险合同中的降雨量)、客户领取的细节(如钱包地址,或内部领取零碎的客户 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 密钥在每个申请中应用。
- WeatherBit Weather API
- OpenWeather API
- LinkPool GET>Uint256 Job
- Steelblock GET>Uint256 Job
一旦咱们记下了 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
函数参数中。
- World Weather Online API response format
- Weatherbit API response format
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);
}
接下来咱们创立 payoutContract
函数。这个函数作为理赔解决步骤,执行保险人向客户主动领取约定的价值。咱们在这里分外小心,确保它只能在合约仍处于激活状态(即未完结)时被调用,并且只能被其余合约函数在外部调用。它还将任何残余的 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 节点都解决了作业申请并返回一个后果后,咱们就能够调用 getContractRainfall
和getContractRequestCount
函数来查看均匀降雨量的更新,以及数据申请数的减少。一个数据申请意味着两个节点都返回了一个后果,取平均值之后存储在合约中。在本例中,爱荷华州目前两个数据源的均匀降雨量为 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 上的技术探讨。如果您想安顿电话探讨更深刻的集成,请在这里分割。