Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Certora formally verified rewards checker #1656

Merged
merged 97 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 82 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
e9885c0
feat: verification of rewards, fresh start
QGarchery Mar 20, 2023
336fd3f
temp: prover error with axiom
QGarchery Mar 21, 2023
8b49960
temp: revert pover error with axiom
QGarchery Mar 21, 2023
6ed55d1
feat: ignore findAndClaimAt in the verification
QGarchery Mar 21, 2023
8759453
feat: enforce hashing to non-address in merkle
QGarchery Mar 23, 2023
bbc96f0
feat: factorizing invariants
QGarchery Mar 23, 2023
23d5e23
feat: add isCreatedWellFormed as an axiom
QGarchery Apr 13, 2023
43b4858
ci(docs): move to foundry-docs-aws
Rubilmax Mar 29, 2023
2a61fcd
Update .github/workflows/ci-docs-autogen.yml
Rubilmax Mar 29, 2023
390b4ee
refactor(ci): use of OpenID connect
julien-devatom May 10, 2023
ad356f8
fix: removing useless lines
Tristan22400 May 10, 2023
32f9f76
fix: useless lines
Tristan22400 May 10, 2023
8953d04
fix: indexes caching
MathisGD Jun 1, 2023
d49daf1
test: remove indexes update skip
MathisGD Jun 1, 2023
92f52c2
fix(ci): update foundry action
julien-devatom Jun 1, 2023
3662e72
test: indexes caching (ma2)
MathisGD Jun 3, 2023
8ef8205
docs: minor comments improvements
MathisGD Jun 3, 2023
fd7a798
fix: indexes caching
MathisGD Jun 2, 2023
78abecd
test: index caching issue (mc core + lens)
MathisGD Jun 3, 2023
1904ac6
fix: indexes caching (mc lens)
MathisGD Jun 3, 2023
2c174e0
chore: migrate to CVL2
QGarchery Jun 21, 2023
4f704d6
refactor: move spec to specs
QGarchery Jun 21, 2023
e222bf2
fix: don't use grounding branch
QGarchery Jun 21, 2023
dfc13de
chore: remove prover args
QGarchery Jul 12, 2023
578b906
refactor: remove munging and move to prover v5
QGarchery Nov 30, 2023
3b2df7c
refactor: remove initialized
QGarchery Nov 30, 2023
4435355
Merge remote-tracking branch 'origin/main' into certora-rewards
QGarchery Nov 30, 2023
8f41cc3
chore: remove unused applyHarness.patch
QGarchery Nov 30, 2023
d7bbb51
docs: small improvement of the description
QGarchery Nov 30, 2023
f885174
refactor: update Certora gitignore
QGarchery Nov 30, 2023
b030060
fix: remove unused initialized declaration
QGarchery Nov 30, 2023
477b3c1
feat: harness rewards distributor
QGarchery Nov 30, 2023
060f2d5
feat: add claimCorrectnessZero rule
QGarchery Nov 30, 2023
7275655
feat: add rule correctness one and refactor token
QGarchery Nov 30, 2023
81bc8ce
fix: correct hashing of leaves
QGarchery Dec 1, 2023
9c0b000
feat: link MORPHO
QGarchery Dec 1, 2023
a21f7eb
feat: add Certora CI
QGarchery Dec 1, 2023
2d4a5fa
fix: solc binary name for Certora
QGarchery Dec 1, 2023
5ba2124
fix: install yarn dependencies
QGarchery Dec 1, 2023
bfebc38
refactor: remove draft of completeness
QGarchery Dec 5, 2023
51af34b
fix: add created to fullyCreatedWellFormed
QGarchery Dec 5, 2023
d7106d6
fix: typos in created
QGarchery Dec 5, 2023
5de2c8a
feat: hardcode recursion depth
QGarchery Dec 5, 2023
6a8aaea
refactor: simplify root tree
QGarchery Dec 6, 2023
f306d45
refactor: internalize isEmpty
QGarchery Dec 6, 2023
2e4d40b
feat: remove summarization of SafeTransferLib
QGarchery Dec 6, 2023
91b872b
refactor: add back prevRoot
QGarchery Dec 6, 2023
53dec17
refactor: remove harness
QGarchery Dec 6, 2023
b83380a
feat: transferredTokens rule
QGarchery Dec 6, 2023
b319816
fix: workflow name RewardsDistributor
QGarchery Dec 6, 2023
482c728
fix: add required assumptions in noClaimAgain and transferredTokens
QGarchery Dec 6, 2023
1ee4190
fix: total supply requirement on solmate ERC20
QGarchery Dec 6, 2023
8cca093
docs: add comments and improve readability
QGarchery Dec 6, 2023
51b56be
fix: typo in variable name
QGarchery Dec 6, 2023
6ae9d24
feat: add production server flag to config
QGarchery Dec 6, 2023
6d8cdec
chore: add initial total supply to mocked Morpho token
QGarchery Dec 6, 2023
60be797
feat: fist checker prototype
QGarchery Dec 6, 2023
d45d7d5
feat: full certificate generation and checker
QGarchery Dec 7, 2023
d51e74c
chore: use project root directly
QGarchery Dec 7, 2023
9492c7f
chore: clean leftover files
QGarchery Dec 7, 2023
31688e7
refactor: use relative import for submodule
QGarchery Dec 7, 2023
234500e
refactor: profile to separate checker from other tests
QGarchery Dec 7, 2023
a9ea31f
Merge pull request #1668 from morpho-org/certora/rewards-checker
QGarchery Dec 7, 2023
2f409fc
refactor: renaming and verifying totals
QGarchery Dec 7, 2023
2153ffa
docs: add checker documentation
QGarchery Dec 8, 2023
e536b19
fix: value different from 0 in internal nodes
QGarchery Dec 8, 2023
42dbc54
refactor: remove created field
QGarchery Dec 8, 2023
75ca684
docs: add well-formed specification
QGarchery Dec 8, 2023
7aba314
fix: empty root check in checker
QGarchery Dec 8, 2023
652d15b
Merge branch 'fix/upgrade-tests' into certora-rewards
QGarchery Jan 5, 2024
8b8097f
feat: add an overview of the verification
QGarchery Jan 5, 2024
c792819
refactor: remove unnecessary hash condition
QGarchery Feb 1, 2024
f5e7dc1
refactor: remove unnecessary packages
QGarchery Feb 2, 2024
ac2af4c
feat: split no claim again rule
QGarchery Mar 6, 2024
48fb135
feat: add claim twice rule
QGarchery Mar 6, 2024
9f51cb2
feat: check sum of values for internal nodes
QGarchery Mar 6, 2024
1c33193
refactor: require well formed on the path only
QGarchery Mar 6, 2024
9736d1c
refactor: well formed path with a loop
QGarchery Mar 6, 2024
aff7755
refactor: require once in loop
QGarchery Mar 6, 2024
ed73f4b
chore: increase loop iteration to 4
QGarchery Mar 6, 2024
527a8c6
chore: loop iter to 4 in merkle tree
QGarchery Mar 6, 2024
adb56a4
Merge remote-tracking branch 'origin/main' into certora-rewards
QGarchery Mar 6, 2024
3b0b13f
refactor: remove unnecessary require invariant
QGarchery Mar 11, 2024
48e40e8
docs: add comments
QGarchery Mar 11, 2024
823f623
refactor: variable renaming
QGarchery Mar 11, 2024
38276f2
refactor: use structs in merkle tree lib
QGarchery Mar 11, 2024
f5daa10
fix: new interface for node creation
QGarchery Mar 11, 2024
b6f4f2a
feat: check increasing claim amounts
QGarchery Mar 11, 2024
07db98d
refactor: remove unnecessary root of the Merkle tree
QGarchery Mar 11, 2024
ef54f35
docs: example of list of proofs
QGarchery Mar 11, 2024
223f718
refactor: checker is now based on the contract instead of the lib
QGarchery Mar 11, 2024
0b41773
feat: check update root storage change
QGarchery Mar 11, 2024
8a3aed9
fix: update root is not envfree
QGarchery Mar 11, 2024
0ffd207
refactor: restrict read permission
QGarchery Mar 12, 2024
8ad06d2
docs: node is the root candidate
QGarchery Mar 12, 2024
226d508
refactor: clarify verification
QGarchery Mar 12, 2024
821c8ec
Merge remote-tracking branch 'origin/main' into certora-rewards
QGarchery Mar 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/ci-certora.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Certora

on:
push:
branches:
- main
pull_request:
paths:
- "certora/**"
- "lib/**"
- "src/common/**"
- "*.lock"
- "foundry.toml"
- "remappings.txt"
workflow_dispatch:

jobs:
verify:
runs-on: ubuntu-latest

strategy:
fail-fast: false

matrix:
conf:
- MerkleTrees
- RewardsDistributor

steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn

- name: Install dependencies
run: yarn install --frozen-lockfile
shell: bash

- name: Install python
uses: actions/setup-python@v4

- name: Install certora
run: pip install certora-cli

- name: Install solc
run: |
wget https://github.com/ethereum/solidity/releases/download/v0.8.13/solc-static-linux
chmod +x solc-static-linux
sudo mv solc-static-linux /usr/local/bin/solc8.13

- name: Verify ${{ matrix.conf }}
run: certoraRun certora/confs/${{ matrix.conf }}.conf
env:
CERTORAKEY: ${{ secrets.CERTORAKEY }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ lcov.*

# docs
/docs

# certora
.certora**
emv-*-certora*
60 changes: 60 additions & 0 deletions certora/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
This folder contains the verification of the rewards distribution mechanism of Morpho Optimizers using CVL, Certora's Verification Language.

# Folder and file structure

The [`certora/specs`](specs) folder contains the specification files.

The [`certora/confs`](confs) folder contains a configuration file for each corresponding specification file.

The [`certora/helpers`](helpers) folder contains files that enable the verification of Morpho Blue.

# Overview of the verification

This work aims at providing a formally verified rewards checker.
The rewards checker is composed of the [Checker.sol](checker/Checker.sol) file, which takes a certificate as an input.
The certificate is assumed to contain the submitted root to verify, a total amount of rewards distributed, and a Merkle tree, and it is checked that:

1. the Merkle tree is a well-formed Merkle tree
2. the total amount of rewards distributed matches the total rewards contained in the Merkle tree

Those checks are done by only using "trusted" functions, namely `newLeaf` and `newInternalNode`, that have been formally verified to preserve those invariants:

- it is checked in [MerkleTrees.spec](specs/MerkleTrees.spec) that those functions lead to creating well-formed trees.
Well-formed trees also verify that the value of their internal node is the sum of the rewards it contains.
- it is checked in [RewardsDistributor.spec](specs/RewardsDistributor.spec) that the rewards distributor is correct, meaning that claimed rewards correspond exactly to the rewards contained in the corresponding Merkle tree.

# Getting started

## Verifying the rewards checker

Install `certora-cli` package with `pip install certora-cli`.
To verify specification files, pass to `certoraRun` the corresponding configuration file in the [`certora/confs`](confs) folder.
It requires having set the `CERTORAKEY` environment variable to a valid Certora key.
You can also pass additional arguments, notably to verify a specific rule.
For example, at the root of the repository:

```
certoraRun certora/confs/MerkleTrees.conf --rule wellFormed
```

## Running the rewards checker

To verify that a given list of proofs corresponds to a valid Merkle tree, you must generate a certificate from it.
This assumes that the list of proofs is in the expected JSON format.
For example, at the root of the repository, given a `proofs.json` file:

```
python certora/checker/create_certificate.py proofs.json
```

This requires installing the corresponding libraries first:

```
pip install web3 eth-tester py-evm
```

Then, verify this certificate:
QGarchery marked this conversation as resolved.
Show resolved Hide resolved

```
FOUNDRY_PROFILE=checker forge test
```
59 changes: 59 additions & 0 deletions certora/checker/Checker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GNU AGPLv3
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
pragma solidity ^0.8.0;

import "../../lib/morpho-utils/lib/openzeppelin-contracts/contracts/utils/Strings.sol";
import "../helpers/MerkleTreeLib.sol";
import "../../lib/forge-std/src/Test.sol";
import "../../lib/forge-std/src/StdJson.sol";

contract Checker is Test {
using MerkleTreeLib for MerkleTreeLib.Tree;
using stdJson for string;

MerkleTreeLib.Tree public tree;

struct Leaf {
address addr;
uint256 value;
}

struct InternalNode {
address addr;
address left;
address right;
}
QGarchery marked this conversation as resolved.
Show resolved Hide resolved

function testVerifyCertificate() public {
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
string memory projectRoot = vm.projectRoot();
string memory path = string.concat(projectRoot, "/certificate.json");
string memory json = vm.readFile(path);

uint256 leafLength = abi.decode(json.parseRaw(".leafLength"), (uint256));
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
Leaf memory leaf;
for (uint256 i; i < leafLength; i++) {
leaf = abi.decode(
json.parseRaw(string.concat(".leaf[", Strings.toString(i), "]")),
(Leaf)
);
tree.newLeaf(leaf.addr, leaf.value);
}

uint256 nodeLength = abi.decode(json.parseRaw(".nodeLength"), (uint256));
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
InternalNode memory node;
for (uint256 i; i < nodeLength; i++) {
node = abi.decode(
json.parseRaw(string.concat(".node[", Strings.toString(i), "]")),
(InternalNode)
);
tree.newInternalNode(node.addr, node.left, node.right);
}

QGarchery marked this conversation as resolved.
Show resolved Hide resolved
assertTrue(!tree.isEmpty(node.addr), "empty root");

uint256 total = abi.decode(json.parseRaw(".total"), (uint256));
assertEq(tree.getValue(node.addr), total, "incorrect total rewards");

bytes32 root = abi.decode(json.parseRaw(".root"), (bytes32));
assertEq(tree.getHash(node.addr), root, "mismatched roots");
}
}
78 changes: 78 additions & 0 deletions certora/checker/create_certificate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import sys
import json
from web3 import Web3, EthereumTesterProvider

w3 = Web3(EthereumTesterProvider())


def keccak_node(left_hash, right_hash):
return w3.to_hex(
w3.solidity_keccak(["bytes32", "bytes32"], [left_hash, right_hash])
)


def keccak_leaf(address, amount):
address = w3.to_checksum_address(address)
return w3.to_hex(w3.solidity_keccak(["address", "uint256"], [address, amount]))


certificate = {}
hash_to_address = {}
hash_to_value = {}
left = {}
right = {}


def populate(address, amount, proof):
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
amount = int(amount)
computedHash = keccak_leaf(address, amount)
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
hash_to_address[computedHash] = address
hash_to_value[computedHash] = amount
for proofElement in proof:
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
[leftHash, rightHash] = (
[computedHash, proofElement]
if computedHash <= proofElement
else [proofElement, computedHash]
)
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
computedHash = keccak_node(leftHash, rightHash)
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
left[computedHash] = leftHash
right[computedHash] = rightHash
hash_to_address[computedHash] = keccak_node(computedHash, computedHash)[:42]


def walk(h):
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
if h in left:
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
walk(left[h])
walk(right[h])
certificate["node"].append(
{
"addr": hash_to_address[h],
"left": hash_to_address[left[h]],
"right": hash_to_address[right[h]],
}
)
else:
certificate["leaf"].append(
{"addr": hash_to_address[h], "value": hash_to_value[h]}
)


with open(sys.argv[1]) as input_file:
proofs = json.load(input_file)
certificate["root"] = proofs["root"]
certificate["total"] = int(proofs["total"])
certificate["leaf"] = []
certificate["node"] = []

for address, data in proofs["proofs"].items():
populate(address, data["amount"], data["proof"])

walk(proofs["root"])

certificate["leafLength"] = len(certificate["leaf"])
certificate["nodeLength"] = len(certificate["node"])

json_output = json.dumps(certificate)

with open("certificate.json", "w") as output_file:
output_file.write(json_output)
12 changes: 12 additions & 0 deletions certora/confs/MerkleTrees.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"files": [
"certora/helpers/MerkleTrees.sol"
],
"solc": "solc8.13",
"verify": "MerkleTrees:certora/specs/MerkleTrees.spec",
"loop_iter": "4",
"optimistic_loop": true,
"rule_sanity": "basic",
"server": "production",
"msg": "Merkle Tree",
}
21 changes: 21 additions & 0 deletions certora/confs/RewardsDistributor.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"files": [
"src/common/rewards-distribution/RewardsDistributor.sol",
"certora/helpers/MerkleTrees.sol",
"certora/helpers/MorphoToken.sol",
],
"solc": "solc8.13",
"verify": "RewardsDistributor:certora/specs/RewardsDistributor.spec",
"link": [
"RewardsDistributor:MORPHO=MorphoToken"
],
"packages": [
"@openzeppelin=node_modules/@openzeppelin",
"@rari-capital/solmate=lib/solmate",
],
"optimistic_loop": true,
"loop_iter": "4",
"rule_sanity": "basic",
"server": "production",
"msg": "Rewards Distributor",
}
Loading
Loading