Introducing the Pact property checker

July 18, 2018

Together with Kadena, Monic has open-sourced a language-integrated static analysis tool for the Pact smart contract language that we’re calling the property checker. In this post we’ll talk about the purpose of the tool, what it can do today, and what we have planned for the future.

Pact: some background

As a smart contract language, Pact is designed to be run on a blockchain. If you’re not completely familiar with how smart contracts work, it’s helpful to understand them as autonomous agents with which users interact. A user submits a transaction to the network, and if accepted into the system, this will either create a new contract, or interact with an existing one already deployed to the system. In Pact, each contract maintains state across these interactions via a SQL-like table model for data.

Like most smart contract languages, Pact is deterministic (so that the same code produces the same result when executing on each node), but additionally it’s much more computationally constrained than languages like Ethereum’s Solidity (or the EVM generally). In Pact, there are no loops, recursion, null values, or exceptions; and authorization patterns are encoded as builtins which either successfully execute or abort (and roll back) the transaction:

(defun read-user:user (name:string)
  "Read the user indexed by `name` if the current tx was signed by an admin. Aborts otherwise."
  (enforce-keyset 'admins)
  (read users name))

Aside: keysets

Here you can think of the admins keyset as a pre-published list of administrators of the smart contract.

In general, a keyset is a set of users (public keys) authorized to perform some action. enforce-keyset checks that the current user (from the transaction’s metadata) matches a keyset. In this case we’re checking that the user is an admin. One interesting feature of keysets in Pact is that they’re first-class: they can be (e.g.) passed as arguments to a function or stored in the database.

The state of smart contract security

As we’ve seen from the string of successful attacks on contracts in the Ethereum world, it’s clear that the current approaches to smart contract security aren’t working. Most exploited high-profile Ethereum contracts were written by Solidity experts. Existing smart contract platforms (not just Ethereum) make security exceptionally hard.

Though Pact was designed to make programmer errors less likely, between the combination of conditionals, DB access, and authorization concerns, programs can become non-trivial very quickly. Pact’s (optional) type system goes some way toward building confidence in programs, but in the adversarial world of smart contracts, type systems and unit tests aren’t sufficient for building secure systems.

The Pact property checker

To address the current state of affairs, we’ve built our property checking system that allows programmers to decorate both

  • table schemas with invariants, and
  • functions with properties (think: theorems)

that must hold for all possible inputs and database states.

The Pact property checker shares some similarities with contracts (note: not smart contracts!) from e.g. Dafny and Liquid Haskell-style refinement types.

It is implemented in Haskell and powered by the sbv library for SMT solving.

Some simple examples

As an example, we can decorate an absolute value function with the property that the function’s return value must always be non-negative:

(defun abs:integer (x:integer)
  (meta "Returns the absolute value of an integer"
    (property (>= result 0)))

  (if (< x 0)
    (negate x)
    x))

and the property checker verifies that this property holds for all possible values of x.

Similarly we can place a schema invariant on a database table to ensure that an account balance must always be positive:

(defschema account
  (meta "A user account"
    (invariant (>= balance 0)))

  balance:integer
  ks:keyset)

For this invariant, the system ensures that every function in the contract maintains the invariant on any write to a table with that schema.

We’ve also built editor integration for Atom that verifies these invariants and properties whenever a smart contract is modified during development:

screenshot of atom plugin

To see how the property checking system would be used in the real world, let’s go through a longer example.

A real-world example: transferring funds

We’ll write a simple contract for tracking user balances of a fictional currency. If you’re familiar with Ethereum, you can think of this as simplified version of an ERC20 contract. For this example we’re going to ignore concerns like issuance of the currency, and demonstrate only a transfer function to send funds between accounts.

Step 1: initial contract

In the initial version of our smart contract, the transfer function checks that the sender authorized the transfer (the transaction sent to the blockchain was signed by the sender) and that they have sufficient funds. It then updates both the sending (from) and receiving (to) account balances.

Note that we start out with one property, using row-enforced. This states that the row indexed by name must have the keyset it contains (in its ks column) "enforced" in every possible code path. If the function's implementation enforces the keyset, the transaction will abort if it was not signed by the owner of that row.

@@ -0,0 +1,26 @@
(enforce-pact-version "2.4.1")
(define-keyset 'accounts-admin-keyset
(read-keyset "accounts-admin-keyset"))
(module accounts 'accounts-admin-keyset
"Demo module"
A schema describes the shape of a database row:
(defschema account
"account"
balance:integer
ks:keyset)
We create a table which contains rows of accounts:
(deftable accounts:{account})
(defun transfer (from:string to:string amount:integer)
(meta "Transfer money between accounts"
(properties [
Our first property on the function:
(row-enforced 'accounts 'ks from)
]))
(with-read accounts from { 'balance := from-bal, 'ks := from-ks }
(with-read accounts to { 'balance := to-bal }
(enforce-keyset from-ks)
(enforce (>= from-bal amount) "Insufficient Funds")
(update accounts from { "balance": (- from-bal amount) })
(update accounts to { "balance": (+ to-bal amount) })))))

Step 2: positive balance invariant

Let's add an invariant that account balances must be non-negative, to prevent overdrawn accounts.

After we add the invariant, our tool reports back with example input to the function that it claims invalidates this invariant! And indeed, we forgot to check for a negative amount. It's good we checked this, because it turns out this bug would have allowed any user to drain (and even overdraw!) other users' accounts.

@@ -1,26 +1,27 @@
(enforce-pact-version "2.4.1")
(define-keyset 'accounts-admin-keyset
(read-keyset "accounts-admin-keyset"))
(module accounts 'accounts-admin-keyset
"Demo module"
(defschema account
"account"
(meta "account"
(invariant (>= balance 0)))
balance:integer
ks:keyset)
Invalidating model found:
Arguments:
    from := ""
    to := ""
    amount := -2

  Variables:
    from-bal := 1
    from-ks := KeySet 2
    to-bal := 1

  Reads:
    "" => { balance: 1, ks: KeySet 2 }
    "" => { balance: 1, ks: KeySet 2 }
    "" => { balance: 1, ks: KeySet 2 }

  Writes:
    "" => { balance: 3 }
    "" => { balance: -1 }

  Keysets:
    authorized:   database keyset at (accounts, 'ks, "")

  Result:
    "Write succeeded"
(deftable accounts:{account})
(defun transfer (from:string to:string amount:integer)
(meta "Transfer money between accounts"
(properties [
(row-enforced 'accounts 'ks from)
]))
(with-read accounts from { 'balance := from-bal, 'ks := from-ks }
(with-read accounts to { 'balance := to-bal }
(enforce-keyset from-ks)
(enforce (>= from-bal amount) "Insufficient Funds")
(update accounts from { "balance": (- from-bal amount) })
(update accounts to { "balance": (+ to-bal amount) })))))

Step 3: enforcing a positive transfer amount

The fix to this bug is simply to enforce that the amount we're transferring is positive:

@@ -1,27 +1,28 @@
(enforce-pact-version "2.4.1")
(define-keyset 'accounts-admin-keyset
(read-keyset "accounts-admin-keyset"))
(module accounts 'accounts-admin-keyset
"Demo module"
(defschema account
(meta "account"
(invariant (>= balance 0)))
balance:integer
ks:keyset)
(deftable accounts:{account})
(defun transfer (from:string to:string amount:integer)
(meta "Transfer money between accounts"
(properties [
(row-enforced 'accounts 'ks from)
]))
(with-read accounts from { 'balance := from-bal, 'ks := from-ks }
(with-read accounts to { 'balance := to-bal }
(enforce-keyset from-ks)
(enforce (>= from-bal amount) "Insufficient Funds")
(enforce (> amount 0) "Amount must be positive")
(update accounts from { "balance": (- from-bal amount) })
(update accounts to { "balance": (+ to-bal amount) })))))

Step 4: column conservation

Now we add a property to ensure that a transfer could never possibly destroy money, or create some out of thin air. This column-conserves property states that "the sum of all values in the balance column in the accounts table is preserved" across any possible transaction.

The checker again reports back with an input to the function that it claims to invalidate this new mass conservation property. This time the balance (1) looks fine, but it's a bit suspicious that from and to are the same string.

@@ -1,28 +1,29 @@
(enforce-pact-version "2.4.1")
(define-keyset 'accounts-admin-keyset
(read-keyset "accounts-admin-keyset"))
(module accounts 'accounts-admin-keyset
"Demo module"
(defschema account
(meta "account"
(invariant (>= balance 0)))
balance:integer
ks:keyset)
(deftable accounts:{account})
(defun transfer (from:string to:string amount:integer)
(meta "Transfer money between accounts"
Invalidating model found:
Arguments:
    from := ""
    to := ""
    amount := 1

  Variables:
    from-bal := 2
    from-ks := KeySet 4
    to-bal := 2

  Reads:
    "" => { balance: 2, ks: KeySet 4 }
    "" => { balance: 2, ks: KeySet 4 }
    "" => { balance: 2, ks: KeySet 4 }

  Writes:
    "" => { balance: 1 }
    "" => { balance: 3 }

  Keysets:
    authorized:   database keyset at (accounts, 'ks, "")

  Result:
    "Write succeeded"
(properties [
(row-enforced 'accounts 'ks from)
(= (column-delta 'accounts 'balance) 0)
]))
(with-read accounts from { 'balance := from-bal, 'ks := from-ks }
(with-read accounts to { 'balance := to-bal }
(enforce-keyset from-ks)
(enforce (>= from-bal amount) "Insufficient Funds")
(enforce (> amount 0) "Amount must be positive")
(update accounts from { "balance": (- from-bal amount) })
(update accounts to { "balance": (+ to-bal amount) })))))

If we look closely at the last two lines of the function, we see that, given the provided inputs (amount set to 1 and a sender and receiver of the same account, "") we end up performing the following two writes:

(update accounts "" { "balance": (- from-bal 1) })
(update accounts "" { "balance": (+ to-bal 1)   })

Comparing to the Writes-section of the invalidating model report:


        Writes:
          "" => { balance: 1 }
          "" => { balance: 3 }
        

This shows that, starting from a balance of 2, we first update the balance to be 1, and immediately then overwrite that value with 3. The net effect is that this set of inputs lets an attacker create money out of thin air!

Step 5: another fix

To address this bug, we can simply enforce that the sender and recipient are not the same account. At this point, the property checker reports that all properties and invariants validate for all possible inputs!

@@ -1,29 +1,30 @@
(enforce-pact-version "2.4.1")
(define-keyset 'accounts-admin-keyset
(read-keyset "accounts-admin-keyset"))
(module accounts 'accounts-admin-keyset
"Demo module"
(defschema account
(meta "account"
(invariant (>= balance 0)))
balance:integer
ks:keyset)
(deftable accounts:{account})
(defun transfer (from:string to:string amount:integer)
(meta "Transfer money between accounts"
(properties [
(row-enforced 'accounts 'ks from)
(= (column-delta 'accounts 'balance) 0)
]))
(with-read accounts from { 'balance := from-bal, 'ks := from-ks }
(with-read accounts to { 'balance := to-bal }
(enforce-keyset from-ks)
(enforce (>= from-bal amount) "Insufficient Funds")
(enforce (> amount 0) "Amount must be positive")
(enforce (!= from to) "Sender and recipient can't be the same")
(update accounts from { "balance": (- from-bal amount) })
(update accounts to { "balance": (+ to-bal amount) })))))

Future directions

We’re actively developing this tool and very excited for these upcoming projects.

Abstracting properties

Currently there’s no way to define a new property by name. We have to write (= (column-delta table 'column) 0) where we’d rather write conserves-mass. Soon you’ll be able to define new properties with (defproperty conserves-mass (= (column-delta table 'column) 0).

Improved UX

Currently, for falsifying models, we show concrete arguments, DB accesses, and whether keysets were authorized; but due to the fact that symbolic programs don’t execute linearly from inputs to output, these models can be slightly confusing. Consider the following code:

(if (x < 10)
  (read accounts "a")
  (read accounts "b"))

A falsifying model for this expression will actually contain values for both reads, even though a concrete execution would only perform one.

We plan to improve this experience by synthesizing a linear execution trace from the model, similar to what something like the Chrome DevTools debugger might show. This would contain a full walk through a single concrete execution path, without any superfluous DB accesses or variable bindings from paths not taken. This work is currently in-progress, and we’ll share more about this in an upcoming post.

Covering all of the language

For the initial release, we punted on some parts of the language that are less-frequently used and harder to model in Z3. In particular, we’re actively working on adding support for sequences to sbv to model lists in Pact.

Stronger defaults

Once we have defproperty, we’ll have all of the necessary building blocks to write a pure property, asserting that a function doesn’t read or write the database, or abort the current transaction. Our plan is to make something like this the default, so that all non-pure code must be explicitly marked.

More to come

These are just a few of the projects on the Pact formal verification roadmap, as we work towards a great environment for writing correct contracts. Of course, there’s a lot to do — Pact is always looking for more contributors, and Monic is hiring, so if you made it this far and you’re interested in our work, definitely get in touch!

Thanks to Libby Kent and Stuart Popejoy for reading a draft of this post.


Joel Burget

Joel Burgetcofounder

Brian Schroeder

Brian Schroedercofounder