Tezos applications: automated insurance
January 12, 2018
cryptocurrency tezos michelson blockchain smart contracts applications oracles golangJuly 30th 2018: Tezos betanet has now launched, and this article doesn’t reflect the current state of the project. You can still read through it, but the code examples won’t work as is, and will need various changes. Up to date documentation for the betanet is available here. You can change the address to access docs for other branches like zeronet.
A word about different testnets: there is usually a zeronet running, which follows new developments closely and an alphanet which is more stable. The real network is currently called betanet (transactions persist to the main network). The alphanet is currently obsolete.
Let’s consider the most basic reason for the demand for insurance - loss aversion. People hate disappointment and losing something that they had even more than not having it in the first place. Many people face disappointment on a daily basis, e.g. when they have to cancel a trip or a barbecue because of unsuitable weather.
In the previous article, we have used the Tezos smart contract language called Michelson, to build an oracle that provides weather information to other contracts. Today, we are going to create an automated micro insurance provider, that will insure against rainy weather.
What are the properties that we want the system to have? We would like the resolution to be done on the blockchain, so that our customers can be sure that their policies will be evaluated fairly and instantly. However we would also like our pricing logic to be hidden, so it doesn’t get immediately copied by competition.
To accomplish this, we are going to have one server that gives out quotes through a REST API. A provider contract on the blockchain is going to accept the quotes to register a new policy. After the moment specified in the policy has passed, the contract can be called again to request weather information for that moment from a weather oracle. Then it will evaluate the policy and pay out the agreed amount by creating a new account with the money, for which the user owns the private key.
Among other benefits, this allows us to easily swap out the pricing logic without any changes to the contract. This is critical, because changes to an existing smart contract are impossible, whereas the pricing logic needs to be constantly improved and tweaked, to respond to new events and market pressures.
- changed the coordinates precision to 4 decimals, to get location within 10 meters.
- changed the parameter type to a contract instead of a string, this ensures that valid contract address is supplied for callback.
- added a string to the parameter type, to differentiate similar requests from identical source
Note that in a real world scenario, the oracle would likely be operated by a different party, and making such changes would be much harder.
type PolicyQuote struct {
Arg string `json:"arg"`
PolicyParams
}
type PolicyParams struct {
Key string `json:"key"`
Payout float64 `json:"payout"`
Price float64 `json:"price"`
Expiration int64 `json:"expiration"`
T int64 `json:"t"`
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
req darksky.ForecastRequest
}
The main function simply starts the server:
func main() {
flag.Parse()
client = darksky.New(*apiKey)
router := httprouter.New()
router.GET("/quote", getQuote)
fmt.Println("Listening for policy requests.")
log.Fatal(http.ListenAndServe(":11337", router))
}
First, we have to parse the request:
func parseRequest(values url.Values) (PolicyParams, error) {
var p PolicyParams
for param, value := range values {
if len(value) > 1 {
//expected a single value
return p, fmt.Errorf("invalid query params")
}
switch param {
case "lat", "lon":
l, err := strconv.ParseFloat(value[0], 64)
if err != nil {
return p, fmt.Errorf("converting location: %v", err)
}
l = roundPlaces(l, 4)
if param == "lat" {
p.Latitude = l
p.req.Latitude = darksky.Measurement(l)
} else if param == "lon" {
p.Longitude = l
p.req.Longitude = darksky.Measurement(l)
}
case "time":
ts, err := strconv.ParseInt(value[0], 10, 64)
if err != nil {
return p, fmt.Errorf("converting time: %v", err)
}
t := time.Unix(ts, 0)
cutoff := time.Now().Add(time.Minute * 30)
if cutoff.After(t) {
//can't insure less then 30 minutes before requested policy time
return p, fmt.Errorf("requested time too soon")
}
p.T = ts
p.req.Time = darksky.Timestamp(ts)
case "key":
p.Key = value[0]
case "payout":
payout, err := strconv.ParseFloat(value[0], 64)
if err != nil {
return p, fmt.Errorf("converting payout: %v", err)
}
p.Payout = ceilTwoPlaces(payout)
}
}
return p, nil
}
Then we get weather information from Dark Sky:
func getProbability(client darksky.DarkSky, request darksky.ForecastRequest) (float64, error) {
//use metric units
request.Options.Units = "si"
resp, err := client.Forecast(request)
if err != nil {
return 0, err
}
hourly := resp.Hourly.Data
var d darksky.DataPoint
for _, h := range hourly {
if request.Time < h.Time {
break
}
d = h
}
prob := float64(d.PrecipProbability)
return prob, nil
}
Everything comes together in the handler function:
func getQuote(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
var p PolicyParams
p, err := parseRequest(r.URL.Query())
if err != nil {
http.Error(w, fmt.Sprintf("400 %v", err), http.StatusBadRequest)
return
}
prob, err := getProbability(client, p.req)
if err != nil {
http.Error(w, fmt.Sprintf("400 %v", err), http.StatusBadRequest)
return
}
//tez values can only have 2 decimal places at the moment
//always round up so that we don't accidentaly lose money
p.Price = ceilTwoPlaces(float64((1 + margin) * p.Payout * prob))
//quote is valid for 15 minutes
p.Expiration = time.Now().Add(time.Minute * 15).Unix()
data := prepData(p)
sig, err := signData(data, *identity)
if err != nil {
http.Error(w, "500 internal server error", http.StatusInternalServerError)
return
}
var q PolicyQuote
q.PolicyParams = p
q.Arg = fmt.Sprintf(`(Right (Pair %s %s))`, sig, data)
resp, err := json.Marshal(&q)
if err != nil {
http.Error(w, "500 internal server error", http.StatusInternalServerError)
return
}
w.Header().Set(contentType, appJSON)
w.Write(resp)
}
After calculating the price, we need to format the data so the client can just pass it as an argument to a transaction.
func prepData(p PolicyParams) string {
tsExp := time.Unix(p.Expiration, 0).UTC().Format(time.RFC3339)
tsT := time.Unix(p.T, 0).UTC().Format(time.RFC3339)
tzPayout := formatTez(p.Payout)
tzPrice := formatTez(p.Price)
return fmt.Sprintf(`(Pair "%s" (Pair (Pair (Pair "%s" "%s") (Pair (Pair %d %d) "%s")) "%s"))`, tsExp, tzPayout, tzPrice, int(p.Latitude*10000), int(p.Longitude*10000), tsT, p.Key)
}
The UTC()
method changes the timezone to UTC, so that the output of Format()
doesn’t depend on the timezone of the server. The smart contract converts the time to UTC and signatures wouldn’t match if we used the time of a local timezone here instead.
We sign the resulting quote:
func signData(data string, identity string) (string, error) {
c := exec.Command("./alphanet.sh", "client", "hash", "and", "sign", "data", data, "for", identity)
c.Env = append(c.Env, "ALPHANET_EMACS=true")
b, err := c.CombinedOutput()
if err != nil {
fmt.Println("error:", string(b))
return "", fmt.Errorf("running commands to sign: %v", err)
}
lines := strings.Split(string(b), "\n")
for _, l := range lines {
l := strings.TrimSpace(l)
if strings.HasPrefix(l, "Signature:") {
words := strings.Split(l, " ")
signature := strings.Trim(words[1], "")
return signature, nil
}
}
return "", fmt.Errorf("parsing output: signature not found")
}
Finally the handler sends the signed data back to the client, who can then accept the offer by calling the smart contract with the signed data as an argument.
Most of the logic will be inside the main contract, its parameter looks like this:
parameter (or
(or
(pair @resolve string int)
(or
(string @trigger)
(pair @init signature (contract (pair signature (pair string int)) unit))))
(pair @register
signature
(pair
timestamp
(pair
(pair
(pair @terms
(tez @payout)
(tez @price))
(pair @params
(pair @location
int
int)
timestamp))
key))));
As can be gleaned from the parameter type, the contract has 4 different paths through the code. One of them is just for initialization and will be explained later. After the contract is initialized, there are three different kinds of transactions it can accept. The words starting with @
are annotations, and don’t have any effect – they are just comments that make reading the code more convenient for us. If you use emacs with the Michelson mode, you’ll also see the annotations in the stack state visualizations.
A diagram to help you think about what each part of the input is used for:
Left Right: register
/ \
/ \
Left: resolve Right
/ \
/ \
Left: trigger evaluation Right: init
The rest of the contract header:
storage (pair
(pair
(pair
(key @provider_key)
(bool @initialized))
(pair @contracts
(contract @oracle
(or
(pair
signature
string)
(pair
(pair
string
(contract (pair
signature
(pair
string
int))
unit))
(pair
(pair
int
int)
timestamp)))
(option string))
(contract @redirector
(pair
signature
(pair
string
int))
unit)))
(map
string
(pair
(pair
(pair @terms
(tez @payout)
(tez @price))
(pair @params
(pair
int
int)
timestamp))
(bool key))));
return (option string);
The contract stores a map of insurance policies (a hash table), the public key of the provider identity (to verify authenticity of requests) and a flag that indicates whether the contract has been properly initialized. It also stores addresses of the oracle contract and the redirector contract. A string hash wrapped inside an option is returned when a new request is accepted, so that the user can trigger evaluation later on. The other paths return a NONE
, indicating the option type is empty.
Let’s start with handling new requests. Policies are stored in a hash table, so that we can access, create or remove them efficiently. If a request with a valid quote is received, a policy is created and added to the table. In case the policy already exists, the transaction fails. The policy details are stored in a format that facilitates easy communication with the oracle contract. This requires some manipulation of the data at the beginning, but it helps keep the evaluation code simple.
{ #register for a policy
DUP;
DUP;
CAR;
DIP{CDR;H;};
PAIR;
DUUUP;
CDAAAR;
CHECK_SIGNATURE;
ASSERT; # fail if not authentic
...
Using the instructions H
(stands for hash) and CHECK_SIGNATURE
, we can make sure that the request was generated by our quote server (assuming no one else has the private key). CHECK_SIGNATURE
outputs a boolean and the ASSERT
instruction is a shorthand for IF{}{FAIL};
, which simply fails if the boolean at the top of the stack is false.
CDR;
DUP;
CAR;
NOW;
ASSERT_CMPLE; # fail if terms are out of date
...
In a similar fashion, CMPLE
is a shorthand for COMPARE
, which produces an integer from two numbers, and LE
(“less or even”), which takes and integer and produces a boolean. There are other variations for “greater or even”, “greater than”, “less than” and so on. NOW
lets us use the timestamp of the block that contains the current transaction.
DUP;
CDAADR;
AMOUNT;
ASSERT_CMPGE; # fail if incoming tx amount is lower than quoted price
...
AMOUNT
will give us the amount of tez of the current transaction.
DIP{CDR;};
CDR; # generate hash the same way the oracle does, use as a key
DUP;
CDR;
H;
DUUUP;
CADDR;
SWAP;
PAIR;
DIP{ DUP;
CADR;
};
PAIR;
H;
DUP;
DIIP{ MAP_CDR{ PUSH bool False;
PAIR;
};
SOME;
DIP{ DUP;
CDR;
};
};
DUUUUP;DUUP;MEM; # check if policy exists
NOT;ASSERT; # fail if policy already exists
DIP{ UPDATE;
SWAP;
MAP_CDR{ DROP;
SWAP;
};
};
SOME; # return the resulting hash, so user can trigger evaluation
};
For convenient manipulation of deeper elements of the stack, we can use DIIP
, where each I
means we are working deeper in the stack – DIP
means we dip into the stack 1 level, and DIIP
means we dip 2 levels (so we can work, while the top two elements remain unchanged). Similarly, DUUP
duplicates the element below the top element. CAR
and CDR
can work with nested pairs by including more As or Ds in the middle.
The contract relies on users to call and trigger evaluation of their policy at the appropriate time. The evaluation branch uses a policy hash to retrieve the details, and calls the oracle contract.
IF_LEFT{ #trigger evaluation of policy specified by hash
DIP{CDR};
DUUP;
CDR;
DUUP;
GET;
ASSERT_SOME;
DUP;
DUP;
CDAR;
NOT;
ASSERT; # fail if already triggered
CADR;
DUUP;
CDDR;
H;
DIP{ DUUUUP;
CADDR;
};
PAIR;
PAIR;
RIGHT (pair signature string);
DIP{ #flip triggered flag for this policy
MAP_CDAR{NOT;};
SOME;
SWAP;
DIIP{ DUP;
CDR;
};
UPDATE;
SWAP;
MAP_CDR{ DROP;
SWAP;
};
# call oracle
DUP;
CADAR;
PUSH tez "1.00";
};
TRANSFER_TOKENS;
}
When retrieving items from a map, they are wrapped in an option. If no item is found, it’s NONE
, otherwise it’s SOME
. We can use ASSERT_SOME
to automatically fail in case there is no policy with the specified hash. We pack the value into an appropriate or type using the RIGHT
instruction (since the compiler knows the type of the data on stack, we only have to specify the type of the other part).
The oracle contract then retrieves information about the state of the outside world using Dark Sky, as explained in the previous article. Then the oracle server sends the information to the address specified in the request. Usually, different users will be using the oracle, and their parameter types will differ as well. We can solve this by using a redirector contract that simply takes the information, wraps it in appropriate types and sends it to the main contract.
parameter (pair signature (pair string int));
storage (pair (key @oracle_key) (contract @provider (or (or (pair string int) (or (string @trigger_by_hash) (pair @init signature (contract (pair signature (pair string int)) unit)))) (pair signature (pair timestamp (pair (pair (pair @terms tez tez) (pair @params (pair @location int int) timestamp)) key)))) (option string)));
return unit;
code { DUP;
CAR;
DUP;
CAR;
DIP{ CDR;
H;
};
PAIR;
DUUP;
CDAR;
CHECK_SIGNATURE;
ASSERT;
DUP;
CDDR;
PUSH tez "0.00";
DUUUP;
CADR;
LEFT (or (string @trigger) (pair @init signature (contract (pair signature (pair string int)) unit)));
LEFT (pair @register signature (pair timestamp (pair (pair (pair @terms tez tez) (pair @params (pair @location int int) timestamp)) key)));
DIIIP{CDR};
TRANSFER_TOKENS;
DROP;
UNIT;
PAIR;
}
The redirector contract is very straightforward and just checks whether the transaction is signed by the oracle’s key. Then it wraps the data and calls the resolve path of the provider contract. Any other transactions are dropped.
The resolve path takes a hash and rain probability from the oracle and retrieves the details of a policy associated with the hash. The probability is used to calculate the appropriate payout. Finally, a new default account (a contract with no code) is created and the amount to pay out (if any) is transferred to it. The user controls the private key paired with the public key that’s used to create the account, and can use the paid out tez however they like. By using a default account we make sure that it’s safe to transfer to. If we just transferred to a contract supplied by the user, we would have to take special care to avoid reentrancy vulnerabilities.
IF_LEFT{ #(pair string int)
SOURCE (pair string int) unit;
H;
DUUUP;
CDADDR;
H;
ASSERT_CMPEQ; # fail if not coming from redirector
...
The SOURCE
instruction is used to work with the contract from which the current transaction was sent. Here, we are using it to make sure the transaction comes from our redirector contract.
#retrieve terms from map, multiply, create default account based on specified key, with appropriate balance
DUUP;
CDDR;
DUUP;
CAR;
GET;
ASSERT_SOME;
DUP;
DUP;
CDAR;
ASSERT; # fail if not triggered
...
GET
lets us retrieve elements from a map.
DIP{ CAAR;
CAR;
DIP{ DUP;
CDR;
DUP;
PUSH int 0;
ASSERT_CMPLE;
ABS;
};
MUL;
PUSH nat 100; # result value is multiplied by 100 so we can have 2 decimal places, now we divide by 100
SWAP;
EDIV;
ASSERT_SOME; # EDIV returns NONE in case of division by zero
CAR; # throw away the remainder
};
CDDR;
HASH_KEY;
DEFAULT_ACCOUNT;
SWAP;
UNIT;
DIIIP{ DUUP;
CDDR;
SWAP;
CAR;
DIP{NONE (pair (pair (pair @terms tez tez) (pair @params (pair int int) timestamp)) (pair bool key));};
UPDATE;
SWAP;
MAP_CDDR{ DROP;
SWAP;
};
CDR;
};
TRANSFER_TOKENS;
DROP;
NONE string;
}
HASH_KEY
and DEFAULT_ACCOUNT
let us create a contract with no code, so that we can send the payout to it safely (note that we can reuse the same address and the money will simply be added to the balance). After calculating the payout, we remove the resolved policy from the contract’s storage with UPDATE
. We could first transfer the payout and remove the policy afterwards, but remember that we need to clear the stack and save anything important to storage before calling TRANSFER_TOKENS
, so it would be tedious.
The initialization path is used when setting up the provider contract. The provider needs to contain the address of a redirector contract, while the redirector needs to contain the address of the provider contract. Obviously, one of them needs to be originated first, so we use a placeholder redirector when originating the provider. Then we can originate the redirector, and call the provider to change the address. To make sure only we can make these changes, the transaction is signed. We don’t need to use a counter, because the initialization can only be done once and the effects are not reversible. Subsequent initialization attemps, such as would result from a replay attack are simply dropped.
code { DUP;
CDAADR;
IF{}
{ DUP;
CAR;
ASSERT_LEFT;
ASSERT_RIGHT;
ASSERT_RIGHT;
DUP;
CAR;
DIP{ DUP;
CDR;
H;
};
PAIR;
DUUUP;
CDAAAR;
CHECK_SIGNATURE;
ASSERT;
# initialize the redirector contract address
CDR;
SWAP;
MAP_CDADDR{ DROP;
SWAP;
};
# the initialized flag is flipped below
};
MAP_CAR
lets us change an element of a pair conveniently, without having to break up the pair and compose it again. We can use it on nested pairs too. I’m explaining this piece of code last, but it’s actually executed before the contract does anything else. By checking the initialization flag first, we don’t have to worry about it later, and the rest of the code can be simpler.
...
{ IF_LEFT{ #trigger evaluation of policy specified by hash
...
}
{ # init done above already, here we just flip the initialization flag
# contract can only be initialized once, fail if already initialized
DROP;
CDR;
MAP_CAADR{NOT;ASSERT;PUSH bool True;};
NONE string;
};
In the if branch belonging to init, we change the initialization flag if it’s false, or fail if the contract has already been initialized.
Take at a look at the whole contract here.
Let’s see what replay attacks do to the other paths. Attacking the registration path would simply make the transaction fail, as we don’t allow multiple policies for same combination of location, time and payout address. Attacking the trigger path wouldn’t do anything, since there is a boolean flag indicating that the policy has already been triggered. A duplicate transaction from the redirector on the resolve path would simply fail to retrieve any policy, while a transaction from anyone else would be dropped right away.
A good primer to thinking about smart contract security in Tezos is the list of Michelson anti-patterns by Milo Davis.
Once cryptocurrencies are widely adopted, one could imagine that micro-insurance products would be offered by a number of providers, and the end-user would have an application select the best offer automatically, along with restaurant reservations (when planning a barbecue outside) or train tickets (when planning a trip). Everyone would simply set their preferred degree of risk exposure, and their virtual assistent would buy a corresponding insurance policy any time they plan an activity. Then, come sudden rain, the disappointed would-be hiker would have a back-up plan ready, in the form of movie tickets or another indoor activity and a nice pile of money to pay for them.
If you are interested in building things with Tezos, check out Tezos.help for a list of resources and come to the developer chat room on matrix.