Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
156 commits
Select commit Hold shift + click to select a range
21739ee
added rules inspired form URD
bhargavbh May 7, 2026
030c175
tuned
bhargavbh May 7, 2026
8e2e5ee
merged main
bhargavbh May 19, 2026
4898d72
take correctness passes; added some diagnostic tests
bhargavbh May 19, 2026
215698b
removed the diagnostic rules
bhargavbh May 19, 2026
b62eae7
cleaned up comments
bhargavbh May 19, 2026
b9d943e
add completeness rule
bhargavbh May 19, 2026
9b92e50
abstract properties instantiated to midnight
bhargavbh May 19, 2026
0cd4f3e
cvl fmt
bhargavbh May 19, 2026
07d95e3
fmt
bhargavbh May 19, 2026
bcc5198
renamed spec; minor changes in OfferTree definition
bhargavbh May 20, 2026
38c1875
tuned
bhargavbh May 20, 2026
8208a49
fmt
bhargavbh May 20, 2026
80d2ad2
forge fmt
bhargavbh May 20, 2026
7a3e019
Merge remote-tracking branch 'origin/main' into certora/offer-tree
bhargavbh May 20, 2026
03ba8cd
resolved errors from merge
bhargavbh May 20, 2026
dd415d0
updated callback summary signatures
bhargavbh May 20, 2026
24140f4
new rule in Ratification.spec: isRatified always calls isLeaf
bhargavbh May 20, 2026
cfb723f
rhashNode summarised similar to hashOffer
bhargavbh May 20, 2026
8187c24
revamped soundness: Offer included in the node
bhargavbh May 21, 2026
2d1eb87
tuned comment
bhargavbh May 21, 2026
d77d0b3
add checker
bhargavbh May 25, 2026
939a570
add checker
bhargavbh May 25, 2026
d9f174e
summarise isRatify
bhargavbh May 25, 2026
fc758a3
trimmed down to core properties
bhargavbh May 26, 2026
253e37e
removed completeness
bhargavbh May 29, 2026
89c39d6
comment style adapted to URD
bhargavbh Jun 1, 2026
a43f97d
retained only testVerifyCertificate in Checker; split offer into stat…
bhargavbh Jun 1, 2026
65e69d1
removed ratifier
bhargavbh Jun 1, 2026
7091ed9
added justification rules for the hashLeaf trick
bhargavbh Jun 1, 2026
9d0635b
removed assumptions from soundness rule
bhargavbh Jun 2, 2026
4a3f121
tuned
bhargavbh Jun 2, 2026
5dec124
tuned; retained market-level hashLeaf
bhargavbh Jun 2, 2026
956a11f
cleanup ratification spec
bhargavbh Jun 2, 2026
ea94696
varibale renaming in create_certificate.py; tuned checker
bhargavbh Jun 2, 2026
7d251ea
Merge origin/main into certora/offer-tree
bhargavbh Jun 2, 2026
fa88df5
added high-level verification approach in README
bhargavbh Jun 2, 2026
a1b4b8e
fmt
bhargavbh Jun 2, 2026
be2a563
acknowledge URD spec in README
bhargavbh Jun 2, 2026
6aaa420
asserts replaced with raising exceptions
bhargavbh Jun 2, 2026
bec4e8f
tuned
bhargavbh Jun 3, 2026
c5fb0c9
Merge branch 'main' into certora/offer-tree
MathisGD Jul 8, 2026
bf8c505
Merge branch 'main' into certora/offer-tree
bhargavbh Aug 7, 2026
6a5f897
updated to match latest midnight
bhargavbh Aug 9, 2026
758bfba
Merge branch 'main' into certora/offer-tree
MathisGD Aug 10, 2026
b8c5a24
Improved multiplication overflow check
jhoenicke Jul 21, 2026
86fd2d1
Remove investigation of all reverting mulDiv
jhoenicke Jul 21, 2026
a613bc8
Rewording of comments
jhoenicke Jul 21, 2026
8da8e32
More comment refinement
jhoenicke Jul 21, 2026
80eec16
blue fallback rolling first iteration
MathisGD Jul 24, 2026
2fe1897
nit
MathisGD Jul 24, 2026
58cf26e
lint
MathisGD Jul 24, 2026
f112dec
refactor: roll event
peyha Jul 29, 2026
99aefcc
refactor: undo
peyha Jul 29, 2026
7942e0d
doc
peyha Aug 5, 2026
9ba4fdb
feat: auction
peyha Aug 5, 2026
fc42bf1
feat: auction end
peyha Aug 7, 2026
059e09f
test: remove wrong test
peyha Aug 7, 2026
dff0e87
refactor: error name
peyha Aug 7, 2026
9b4a0cb
fix: require end time
peyha Aug 7, 2026
fed6519
refactor: inline incentive()
peyha Aug 7, 2026
01a91b3
test: lltv test
peyha Aug 10, 2026
eb46443
fix: remove increasing incentive requirement
peyha Aug 10, 2026
bf81ea9
doc
peyha Aug 10, 2026
098e1e3
fix: incentive at start max value check
peyha Aug 10, 2026
1a1f195
doc
peyha Aug 10, 2026
b5aa320
doc
peyha Aug 10, 2026
ffb0dca
Update src/periphery/blue-fallback-rolling/BlueFallbackRolling.sol
peyha Aug 10, 2026
a729282
Add files via upload
MathisGD Aug 10, 2026
367e97b
newest file
MathisGD Aug 10, 2026
9c8831f
Add files via upload
MathisGD Aug 10, 2026
cea79ab
Rename 2026-05-19-midnight-core-stermi-audit.pdf to 2026-05-19-stermi…
MathisGD Aug 10, 2026
73755a4
refactor: interface folder
peyha Aug 11, 2026
79a30b8
fix: decreasing incentive
peyha Aug 12, 2026
f89c785
test: coverage
peyha Aug 12, 2026
1e52be2
test: midnight coverage
peyha Aug 11, 2026
4b9d2e7
test: periphery coverage
peyha Aug 11, 2026
bac71f7
chore: fmt
peyha Aug 11, 2026
c02787f
feat: min rollable debt
peyha Aug 12, 2026
df175fe
fix: partial roll when below minrollableassets
peyha Aug 14, 2026
9c1031a
feat: revert to assets=debt condition
peyha Aug 14, 2026
642713a
doc: readme
peyha Aug 14, 2026
7670ea8
Remove lltv check
claude Aug 12, 2026
88344c5
Reword lltv warning comment
claude Aug 13, 2026
82ff177
Move lltv warning to contract natspec
claude Aug 13, 2026
38fa6f8
chore: lint
peyha Aug 14, 2026
77188c9
doc: readme
peyha Aug 14, 2026
b8508cd
[rolling fallback] append LTV note to rounding comment
claude Aug 16, 2026
aa0d660
test: cover authorized fallback config setters
prd-carapulse[bot] Aug 17, 2026
d4e32d3
feat: allow authorized fallback config setters
prd-carapulse[bot] Aug 17, 2026
8297909
test: require fallback config authorization first
prd-carapulse[bot] Aug 17, 2026
6a129c4
fix: check fallback config authorization first
prd-carapulse[bot] Aug 17, 2026
35143e5
feat: equality case for end start comp
peyha Aug 17, 2026
f65b91e
Revert "feat: equality case for end start comp"
peyha Aug 17, 2026
7d727f8
doc: periphery readme
peyha Aug 17, 2026
631c286
Update src/periphery/blue-fallback-rolling/BlueFallbackRolling.sol
peyha Aug 17, 2026
c614e85
refactor: inline interface
peyha Aug 17, 2026
db3f600
refactor: rolled assets too low
peyha Aug 17, 2026
b5d2d62
Add Certora spec: realizable bad debt cannot increase without price u…
claude Jul 24, 2026
9a50fa9
Apply suggestion from @MathisGD
MathisGD Jul 24, 2026
e003af3
Fix RealizableBadDebt rules: health bridge for take/withdrawCollatera…
claude Jul 24, 2026
9446625
Tighten mulDiv ghosts in liquidateRealizesBadDebt to close seize-path…
claude Jul 24, 2026
d0051d2
Prove mulDivUp >= mulDivDown in MulDiv.spec
claude Jul 24, 2026
bfe5d11
Merge take/withdrawCollateral into parametric rule; assert liquidate …
claude Jul 24, 2026
eed485c
Help prover on liquidateRealizesBadDebt: import LIF seize bound + spl…
claude Jul 24, 2026
7f77776
Isolate hard liquidate realizable-bad-debt rule with getter-form seiz…
claude Jul 24, 2026
024a893
Split RealizableBadDebtLiquidate by liquidate input branch; lighten s…
claude Jul 24, 2026
bcdccd1
Bound realizable bad debt after liquidation by 2 (mulDivUp rounding d…
claude Jul 25, 2026
aad45e1
Bound realizable bad debt after liquidation by 3; enable case-splitti…
claude Jul 25, 2026
c3f8d3a
Pin collateral invariant for full bitmap loop in RealizableBadDebtLiq…
claude Jul 25, 2026
a1f624f
Use deterministic mulDiv ghost in RealizableBadDebtLiquidate to kill …
claude Jul 25, 2026
8e0a58f
Prove double-mulDivUp subadditivity lemma; apply in RealizableBadDebt…
claude Jul 26, 2026
94b10cc
Materialize seize-bound term in RealizableBadDebtLiquidate repaid bra…
claude Jul 26, 2026
28406dd
Restrict RealizableBadDebtLiquidate repaid branch to single collatera…
claude Jul 26, 2026
9c246c1
Try -depth 0 (match passing peers) + restore 2 collaterals on Realiza…
claude Jul 26, 2026
1e21778
Try cvc5-heavy portfolio + collateralIndex/single-collateral pins on …
claude Jul 27, 2026
aca534c
Drop forall quantifiers from RealizableBadDebtLiquidate repaid branch…
claude Jul 27, 2026
b105ad9
Spec improvements
jochencertora Jul 29, 2026
58f86a2
Fix formatting
jochencertora Jul 29, 2026
085246a
Apply suggestions from code review
jhoenicke Jul 30, 2026
bb98ee9
Some more review issues:
jochencertora Jul 30, 2026
5c05e7f
Fix summary
jochencertora Jul 30, 2026
c2d82fd
remove comment
jhoenicke Jul 30, 2026
b3c76af
nit
MathisGD Jul 30, 2026
5754a84
nit
MathisGD Jul 30, 2026
1940aeb
Remove some require
jochencertora Jul 30, 2026
7360f87
Remove the rules no longer needed
jochencertora Jul 31, 2026
dc3a82a
Apply suggestions from code review
MathisGD Jul 31, 2026
6eba1a5
Apply suggestions from code review
MathisGD Jul 31, 2026
88ec1d4
Inline axioms into rules and reference their MulDiv lemmas
claude Aug 1, 2026
4a86303
Prove liquidate does not increase another position's realizable bad debt
claude Aug 1, 2026
c60091c
Fix inlined axiomAddUpUp antecedent to match original
claude Aug 1, 2026
f10a7bd
Update certora/confs/RealizableBadDebt.conf
MathisGD Aug 3, 2026
8300f7c
Update certora/confs/RealizableBadDebtLiquidate.conf
MathisGD Aug 3, 2026
aeac0d0
Use general forall form for inlined axiom requires
claude Aug 3, 2026
18f8179
Pin AddUpUp/InverseUpDown/UpZero axioms to call args (forall form tim…
claude Aug 3, 2026
8ea0155
Test: make ERC20NoReturn.approve genuinely return nothing
claude Aug 17, 2026
7997fc5
Apply suggestion from @MathisGD
MathisGD Aug 17, 2026
314b913
[tests] BlueFallbackRolling: safe approve on loan token
claude Aug 18, 2026
86551f6
Port NONDET summaries for tickToPrice and mulDiv to ReentrancyView spec
claude Aug 10, 2026
aef0c2a
Separate MulDiv axiom spec file
jochencertora Aug 5, 2026
42ad512
fix assertion
jochencertora Aug 6, 2026
29b71a3
Make ghost persistent (they never change)
jochencertora Aug 13, 2026
e113fc0
[blue fallback rolling] require non-decreasing incentive
claude Aug 19, 2026
5277f60
[blue fallback rolling] remove redundant incentive check
claude Aug 19, 2026
56f27bf
[blue fallback rolling] add configId to Roll event
claude Aug 20, 2026
7f9a1e1
Apply suggestion from @peyha
peyha Aug 21, 2026
34b1ce0
ci: pin foundry-toolchain to v1.9.1
prd-carapulse[bot] Aug 25, 2026
cbdbe1d
Merge branch 'main' into certora/offer-tree
bhargavbh Aug 27, 2026
008b787
reduced scope to soundness and reference implementation generating roo
bhargavbh Aug 31, 2026
a52e7ac
trimmed comments
bhargavbh Aug 31, 2026
a258ed6
removed leafHashDisjointFromNodeHash and nodeHashInjective
bhargavbh Aug 31, 2026
f6cbe2f
fmt
bhargavbh Aug 31, 2026
c0e2d92
tuned
bhargavbh Aug 31, 2026
a539e66
removed hashLeafReproducesHashOffer
bhargavbh Aug 31, 2026
b5870a9
factored out generate root; restored empty leaf condition
bhargavbh Sep 1, 2026
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
21 changes: 21 additions & 0 deletions certora/confs/OfferTreeMembership.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"files": [
"certora/helpers/OfferTree.sol"
],
"verify": "OfferTree:certora/specs/OfferTreeMembership.spec",
"solc": "solc-0.8.34",
"solc_via_ir": true,
"solc_optimize": "200",
"solc_evm_version": "osaka",
"optimistic_loop": true,
"loop_iter": 3,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Raise the proof loop bound to cover supported trees

With optimistic_loop enabled, this loop_iter only unrolls the proof.length loops in OfferTree.wellFormedPath and HashLib.isLeaf for three levels, while the ratifiers accept signed offer trees up to height 20 via offerTreeTypeHash. For any concrete root/proof of height 4–20, the headline membership soundness proof is therefore only checking a truncated Merkle path, so it does not justify the README claim for the supported tree sizes. Set the loop bound to 20 (or otherwise require proof.length <= 3 in the property and docs).

Useful? React with 👍 / 👎.

"optimistic_hashing": true,
"hashing_length_bound": 1024,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cover callback data beyond the hashing bound

For an otherwise valid offer whose callbackData exceeds 1024 bytes, HashLib.hashOffer hashes that dynamic byte array but this optimistic hashing bound prunes the full execution, so membershipSoundness does not cover all offers accepted by take. The same bound appears in OfferTreeWellFormed.conf; either constrain and document callback-data length in both properties or model its hash without this bound.

Useful? React with 👍 / 👎.

"rule_sanity": "basic",
"smt_timeout": 7200,
"prover_args": [
"-splitParallel true",
"-s [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5}]"
],
"msg": "Offer Tree Membership"
}
21 changes: 21 additions & 0 deletions certora/confs/OfferTreeWellFormed.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"files": [
"certora/helpers/OfferTree.sol"
],
"verify": "OfferTree:certora/specs/OfferTreeWellFormed.spec",
"solc": "solc-0.8.34",
"solc_via_ir": true,
"solc_optimize": "200",
"solc_evm_version": "osaka",
"optimistic_loop": true,
"loop_iter": 3,
Comment on lines +10 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cover full collateral arrays in the well-formedness proof

With optimistic_loop enabled, this bound also truncates the HashLib.hashMarket loops executed by OfferTree.newLeaf; an offer containing more than three collateral parameters is therefore not checked with the real leaf hash during invariant preservation. This contradicts the stated guarantee that the proof remains valid regardless of offer size. Summarize the dynamic hash soundly or raise/constrain the bound to cover every supported collateral array.

Useful? React with 👍 / 👎.

"optimistic_hashing": true,
"hashing_length_bound": 1024,
"rule_sanity": "basic",
"smt_timeout": 7200,
"prover_args": [
"-splitParallel true",
"-s [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5}]"
],
"msg": "OfferTreeWellFormed"
}
44 changes: 44 additions & 0 deletions certora/helpers/GenerateRoot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Morpho Association
pragma solidity ^0.8.0;

import {OfferTree} from "./OfferTree.sol";
import {Offer} from "../../src/interfaces/IMidnight.sol";
import {HashLib} from "../../src/ratifiers/libraries/HashLib.sol";

contract GenerateRoot is OfferTree {
// Build a perfect tree from a non-empty power-of-two list. Duplicate hashes share nodes.
function generateRoot(Offer[] memory leaves) public returns (bytes32) {
require(leaves.length > 0 && (leaves.length & (leaves.length - 1)) == 0, "invalid leaves length");

bytes32[] memory level = new bytes32[](leaves.length);
for (uint256 i = 0; i < leaves.length; i++) {
bytes32 leafHash = HashLib.hashOffer(leaves[i]);
if (isEmpty(tree[leafHash])) {
newLeaf(leaves[i]);
} else {
require(isLeafNode(leafHash), "leaf id collision");
}
level[i] = leafHash;
}

uint256 levelLength = level.length;
while (levelLength > 1) {
levelLength /= 2;
for (uint256 i = 0; i < levelLength; i++) {
bytes32 left = level[2 * i];
bytes32 right = level[2 * i + 1];
bytes32 nodeHash = HashLib.hashNode(left, right);
Node storage n = tree[nodeHash];
if (isEmpty(n)) {
newInternalNode(nodeHash, left, right);
} else {
require(n.left == left && n.right == right && n.hash == nodeHash, "internal node id collision");
}
level[i] = nodeHash;
}
}

return level[0];
}
}
159 changes: 159 additions & 0 deletions certora/helpers/OfferTree.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Morpho Association
pragma solidity ^0.8.0;

import {Offer} from "../../src/interfaces/IMidnight.sol";
import {HashLib, OFFER_TYPEHASH} from "../../src/ratifiers/libraries/HashLib.sol";

// Fixed-size pre-image of HashLib.hashOffer. Dynamic fields are stored as hashes so CVL can re-hash a leaf
// without iterating over dynamic data.
struct Leaf {
bytes32 marketHash; // = HashLib.hashMarket(offer.market)
bool buy;
address maker;
uint256 start;
uint256 expiry;
uint256 tick;
bytes32 group;
address callback;
bytes32 callbackDataHash; // = keccak256(offer.callbackData)
address receiverIfMakerIsSeller;
address ratifier;
bool reduceOnly;
uint256 maxUnits;
uint256 maxAssets;
uint256 continuousFeeCap;
}

contract OfferTree {
struct Node {
bytes32 left;
bytes32 right;
Leaf leaf;
// Offer hash for leaves and hash of the children for internal nodes.
bytes32 hash;
}

// Leaf ids are offer hashes. Internal node ids may be arbitrary.
mapping(bytes32 => Node) internal tree;

function newLeaf(Offer memory offer) public {
bytes32 id = HashLib.hashOffer(offer);
require(id != 0, "id is the zero bytes");
Node storage n = tree[id];
require(isEmpty(n), "leaf is not empty");
Leaf storage l = n.leaf;
l.marketHash = HashLib.hashMarket(offer.market);
l.buy = offer.buy;
l.maker = offer.maker;
l.start = offer.start;
l.expiry = offer.expiry;
l.tick = offer.tick;
l.group = offer.group;
l.callback = offer.callback;
l.callbackDataHash = keccak256(offer.callbackData);
l.receiverIfMakerIsSeller = offer.receiverIfMakerIsSeller;
l.ratifier = offer.ratifier;
l.reduceOnly = offer.reduceOnly;
l.maxUnits = offer.maxUnits;
l.maxAssets = offer.maxAssets;
l.continuousFeeCap = offer.continuousFeeCap;
n.hash = id;
}

function newInternalNode(bytes32 id, bytes32 left, bytes32 right) public {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a leafnode, the id equals the hash (tree[i].hashNode == id). But for internal nodes you allow the id to be different from the hashNode. Is there a reason for it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially this constraint was there to prevent having the same address multiple times in the tree.

We don't have this requirement, but it's weirdly useful here. Assume that the id of the leaves was not necessarily the same as the hash, and you remove the corresponding check in isWellFormed (the check id == expected). Now you could have a function that would change marketHash and adapt hashNode accordingly (as the hashLeaf of the new params with the changed marketHash). This would not break the invariant isWellFormed, but it would allow to forge another root.

Another more straightforward way to avoid the above scenario, is to check that the tree is immutable.

require(id != 0, "zero id");
Node storage n = tree[id];
require(isEmpty(n), "node already populated");
bytes32 leftHash = tree[left].hash;
bytes32 rightHash = tree[right].hash;
require(leftHash != 0, "left empty");
require(rightHash != 0, "right empty");
bytes32 hash = HashLib.hashNode(leftHash, rightHash);
require(hash != 0, "zero hash");
n.left = left;
n.right = right;
n.hash = hash;
}

function isEmpty(Node storage n) internal view returns (bool) {
return n.hash == 0;
}

function isEmpty(bytes32 id) public view returns (bool) {
return isEmpty(tree[id]);
}

function getHash(bytes32 id) public view returns (bytes32) {
return tree[id].hash;
}

function isLeafNode(bytes32 id) public view returns (bool) {
return tree[id].left == 0 && tree[id].right == 0 && tree[id].hash != 0;
}

function hashOffer(Offer memory offer) public pure returns (bytes32) {
return HashLib.hashOffer(offer);
}

function isLeaf(bytes32 root, bytes32 leafHash, uint256 leafIndex, bytes32[] memory proof)
public
pure
returns (bool)
{
return HashLib.isLeaf(root, leafHash, leafIndex, proof);
}

// Reconstruct HashLib.hashOffer from the stored pre-image.
function hashLeaf(Leaf storage l) internal view returns (bytes32) {
return keccak256(
abi.encode(
OFFER_TYPEHASH,
l.marketHash,
l.buy,
l.maker,
l.start,
l.expiry,
l.tick,
l.group,
l.callback,
l.callbackDataHash,
l.receiverIfMakerIsSeller,
l.ratifier,
l.reduceOnly,
l.maxUnits,
l.maxAssets,
l.continuousFeeCap
)
Comment on lines +123 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include continuousFeeCap in leaf hashing

HashLib.hashOffer includes offer.continuousFeeCap after maxAssets, but _hashLeaf stops at maxAssets. Since newLeaf stores n.hashNode = HashLib.hashOffer(offer), any inserted leaf has an id/hashNode from a different ABI preimage than _hashLeaf recomputes, even when the cap is zero, so isWellFormed returns false for real leaves and the well-formedness proof/checker cannot certify the offer tree. Store continuousFeeCap in Leaf and include it in this abi.encode.

Useful? React with 👍 / 👎.

);
}

// A node is empty, a correctly hashed leaf, or a correctly hashed internal node with two children.
function isWellFormed(bytes32 id) public view returns (bool) {
Node storage n = tree[id];
if (isEmpty(n)) return n.left == 0 && n.right == 0;
if (n.left == 0 && n.right == 0) {
bytes32 expected = hashLeaf(n.leaf);
return n.hash == expected && id == expected;
}
if (n.left != 0 && n.right != 0) {
bytes32 leftHash = tree[n.left].hash;
bytes32 rightHash = tree[n.right].hash;
return leftHash != 0 && rightHash != 0 && n.hash == HashLib.hashNode(leftHash, rightHash);
}
return false;
}

// Check the path selected by leafIndex.
function wellFormedPath(bytes32 id, uint256 leafIndex, uint256 depth) public view returns (bool) {
for (uint256 i = depth;;) {
require(isWellFormed(id));

if (i == 0) break;

--i;
id = ((leafIndex >> i) & 1 == 0) ? tree[id].left : tree[id].right;
}
return true;
}
}
20 changes: 20 additions & 0 deletions certora/specs/OfferTreeMembership.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later

methods {
function getHash(bytes32) external returns (bytes32) envfree;
function hashOffer(OfferTree.Offer) external returns (bytes32) envfree;
function isLeaf(bytes32, bytes32, uint256, bytes32[]) external returns (bool) envfree;
function isLeafNode(bytes32) external returns (bool) envfree;
function wellFormedPath(bytes32, uint256, uint256) external returns (bool) envfree;
}

// Soundness: if a Merkle proof verifies along a well-formed path, hashOffer(offer) must be a leaf node.
rule membershipSoundness(OfferTree.Offer offer, bytes32 root, uint256 leafIndex, bytes32[] proof) {
bytes32 node;
require getHash(node) == root, "root is the hash of node";
require wellFormedPath(node, leafIndex, proof.length), "the path from the root to the leaf is well-formed";
bytes32 leafId = hashOffer(offer);
require isLeaf(root, leafId, leafIndex, proof), "Merkle proof verifies the offer";

assert isLeafNode(leafId);
}
21 changes: 21 additions & 0 deletions certora/specs/OfferTreeWellFormed.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-2.0-or-later

// Leaves store a fixed-size pre-image so isWellFormed can re-hash them without dynamic loops.

methods {
function isEmpty(bytes32) external returns (bool) envfree;
function isWellFormed(bytes32) external returns (bool) envfree;
}

// The zero node remains empty.
strong invariant zeroIsEmpty()
isEmpty(to_bytes32(0));

// Every node remains well-formed.
strong invariant wellFormed(bytes32 id)
isWellFormed(id)
{
preserved {
requireInvariant zeroIsEmpty();
}
}
Loading