Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
793778d
new rule: canForceDeallocate
bhargavbh Feb 11, 2026
a714c93
tuned
bhargavbh Feb 11, 2026
fe30416
added assumtpions on penaltyAssets
bhargavbh Feb 16, 2026
cce2d4c
can forceDeallocate on zero
bhargavbh Feb 16, 2026
e3dc39b
superset of preconditions
bhargavbh Feb 18, 2026
bbd341f
superset of preconditions
bhargavbh Feb 18, 2026
33bd54b
reduced assumptions
bhargavbh Feb 18, 2026
c8a22d7
reduced assumptions
bhargavbh Feb 18, 2026
aa79259
summarised accrueInterestView
bhargavbh Feb 19, 2026
ad57c49
realAsset summary approach
bhargavbh Feb 19, 2026
cd6242b
created a new spec for forceDeallocate; summaries of deallocate were …
bhargavbh Feb 19, 2026
1988971
tuned
bhargavbh Feb 19, 2026
047169e
reset RemoveMarketLiveness
bhargavbh Feb 19, 2026
f860168
cleaned up ForceDeallocate spec
bhargavbh Feb 19, 2026
cd93c55
restored realAssetSummary
bhargavbh Feb 19, 2026
5234c3f
cleaned up unnecessary assumptions and formatted
bhargavbh Feb 20, 2026
18eab79
tuned
bhargavbh Feb 20, 2026
16b3517
tuned
bhargavbh Feb 20, 2026
8b54670
improved comments
bhargavbh Feb 20, 2026
e6120fc
updated workflow
bhargavbh Feb 20, 2026
9ef2587
transfer and transferFrom are now dispatched
bhargavbh Feb 25, 2026
88abc10
cleanup
bhargavbh Feb 26, 2026
3314589
cleanup
bhargavbh Feb 26, 2026
8010514
added comments and formatted
bhargavbh Feb 26, 2026
ba37122
added reference to accrueInterestView revert conditions
bhargavbh Mar 2, 2026
dfa29e3
tuned
bhargavbh Mar 2, 2026
445f8ba
tightened totalSupply
bhargavbh Mar 2, 2026
db23457
matched the post conditions from reverts spec
bhargavbh Mar 2, 2026
99ca1f1
tuned
bhargavbh Mar 2, 2026
30e5f1c
cleaned up config
bhargavbh Mar 3, 2026
8bd2ae8
need to justify the mismatch in post conditions of newTotalAssets fro…
bhargavbh Mar 3, 2026
6a0c689
updated comments
bhargavbh Mar 3, 2026
19513af
rearranged assumptions
bhargavbh Mar 4, 2026
6395c2c
reomved realAsset summary, was unused
bhargavbh Mar 4, 2026
0369340
added asset link
bhargavbh Mar 4, 2026
9912302
tuned
bhargavbh Mar 4, 2026
5596660
replaced with requireInvariant for the feeRecipient non-zero assumption
bhargavbh Mar 6, 2026
88c4bfd
summarise multicall
bhargavbh Mar 9, 2026
11e9f60
updated comments
bhargavbh Mar 10, 2026
deb799f
categorised assumptions in canForceDallocateZero
bhargavbh Mar 11, 2026
91d4067
formatted
bhargavbh Mar 11, 2026
ef6e053
tuned
bhargavbh Mar 11, 2026
492b37f
tuned
bhargavbh Mar 11, 2026
ac0dbc4
added reference to AccrueInterestReverts.spec
bhargavbh Mar 11, 2026
7779bdb
tuned comment
bhargavbh Mar 11, 2026
98940a5
removed MorphoHarness from scope
bhargavbh Mar 12, 2026
d963556
jochen and quentin's comments
bhargavbh Mar 12, 2026
743f494
Merge remote-tracking branch 'origin/main' into certora/forceDeallocate
bhargavbh Mar 12, 2026
6676cd4
minor changes to AccrueInterestReverts.spec; spill over from 897
bhargavbh Mar 12, 2026
6d3f6c1
used invariants on fee!=0 => feeRecipient!=0
bhargavbh Mar 12, 2026
336a964
tuned: minor
bhargavbh Mar 12, 2026
79a0fdb
accrueInterestReview: summarised multicall
bhargavbh Mar 12, 2026
0f96469
used requiredInvariants instead of linking with comments
bhargavbh Mar 13, 2026
1c6335f
Merge remote-tracking branch 'origin/main' into certora/forceDeallocate
QGarchery Mar 23, 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
1 change: 1 addition & 0 deletions .github/workflows/certora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
- EarliestTime
- EntrypointEquivalence
- ExchangeRate
- ForceDeallocate
- Gates
- IdsMorphoMarketV1AdapterV2
- IdsMorphoVaultV1Adapter
Expand Down
25 changes: 25 additions & 0 deletions certora/confs/ForceDeallocate.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"files": [
"src/VaultV2.sol",
"lib/morpho-blue/certora/helpers/MorphoHarness.sol"
],
"parametric_contracts": [
"VaultV2"
],
"verify": "VaultV2:certora/specs/ForceDeallocate.spec",
"loop_iter": "3",
"optimistic_loop": true,
"optimistic_hashing": true,
"compiler_map": {
"VaultV2": "solc-0.8.28",
"MorphoMarketV1AdapterV2": "solc-0.8.28",
"MorphoHarness": "solc-0.8.19",
"Utils": "solc-0.8.28"
},
"prover_args": [
"-depth 5",
"-mediumTimeout 20",
"-timeout 3600"
],
"msg": "Vault V2 ForceDeallocate"
}
115 changes: 115 additions & 0 deletions certora/specs/ForceDeallocate.spec
Comment thread
QGarchery marked this conversation as resolved.
Comment thread
bhargavbh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Morpho Association

import "../helpers/UtilityVault.spec";

methods {
function virtualShares() external returns (uint256) envfree;
function lastUpdate() external returns (uint64) envfree;

// assume safeTransfer and safeTransferFrom do not revert.
Comment thread
bhargavbh marked this conversation as resolved.
Outdated
function SafeERC20Lib.safeTransferFrom(address, address, address, uint256) internal => NONDET;
function SafeERC20Lib.safeTransfer(address, address, uint256) internal => NONDET;
Comment thread
QGarchery marked this conversation as resolved.
Outdated

// `balanceOf` is summarized to a bounded value.
Comment thread
QGarchery marked this conversation as resolved.
Outdated
function _.balanceOf(address account) external => summaryBalanceOf() expect(uint256);

// `deallocate` is the core adapter callback. It is summarized with structural constraints from the MorphoMarketV1AdapterV2 implementation;
Comment thread
bhargavbh marked this conversation as resolved.
Outdated
function _.deallocate(bytes data, uint256 assets, bytes4 selector, address sender) external with(env e) => summaryDeallocate(e, data, assets, selector, sender) expect(bytes32[], int256);

// `realAssets` is summarized to a bounded value; see summaryRealAssets.
Comment thread
bhargavbh marked this conversation as resolved.
Outdated
function _.realAssets() external => summaryRealAssets() expect(uint256);

// Trick to be able to retrieve the value returned by the corresponding contract before it is called, without the value changing between the retrieval and the call.
function _.canSendShares(address account) external => ghostCanSendShares(calledContract, account) expect(bool);
function _.canReceiveAssets(address account) external => ghostCanReceiveAssets(calledContract, account) expect(bool);
function _.canReceiveShares(address account) external => ghostCanReceiveShares(calledContract, account) expect(bool);
}

ghost ghostCanSendShares(address, address) returns bool;

ghost ghostCanReceiveAssets(address, address) returns bool;

ghost ghostCanReceiveShares(address, address) returns bool;

// Maximum signed 256-bit integer, used to bound int256 return values.
definition max_int256() returns int256 = (2 ^ 255) - 1;

// Returns a value bounded by 2^128.
function summaryBalanceOf() returns uint256 {
uint256 balance;
require balance < 2 ^ 128, "totalAssets is bounded by 2 ^ 128; vault balance is less than totalAssets";
return balance;
}

// Returns a value bounded by 2^128
function summaryRealAssets() returns uint256 {
uint256 realAssets;
require realAssets < 2 ^ 128, "totalAssets is bounded by 2 ^ 128; realAssets from each adater is less than totalAssets";
return realAssets;
}

// This summary models the post-conditions of the adapter's deallocate which are required for the canForceDallocateZero rule to hold.
function summaryDeallocate(env e, bytes data, uint256 assets, bytes4 selector, address sender) returns (bytes32[], int256) {
bytes32[] ids;
int256 change;

// the adapter returns exactly 3 market ids.
require ids.length == 3, "see IdsMorphoMarketV1Adapter";
Comment thread
bhargavbh marked this conversation as resolved.
Outdated

// the 3 returned market ids must be pairwise distinct.
require ids[0] != ids[1], "ids must be unique";
require ids[0] != ids[2], "ids must be unique";
require ids[1] != ids[2], "ids must be unique";

// each returned id must correspond to a market with a positive allocation in the vault.
require forall uint256 i. i < ids.length => currentContract.caps[ids[i]].allocation > 0, "assume that the allocation is positive";

// allocation values are bounded.
require forall uint256 i. i < ids.length => currentContract.caps[ids[i]].allocation < 2 ^ 20 * 2 ^ 128, "market v1 fits total supply assets on 128 bits, and assume at most 2^20 markets";

// the net change is bounded by 2^128 (Morpho V1 supply assets fit in 128 bits).
require change < 2 ^ 128, "market v1 fits total supply assets on 128 bits";

Comment thread
bhargavbh marked this conversation as resolved.
Outdated
// after applying `change`, no allocation may go negative.
require forall uint256 i. i < ids.length => currentContract.caps[ids[i]].allocation + change >= 0, "see changeForAllocateOrDeallocateIsBoundedByAllocation";

return (ids, change);
}

// forceDeallocate with assets=0 triggers the adapter to update the allocation tracking in caps.
// This rule verifies the liveness property that `forceDeallocate()` can be called with assets=0 with the following pre-conditions:
// 1. The adapter is registered in the vault.
// 2. No ETH is sent with the call.
Comment thread
bhargavbh marked this conversation as resolved.
Outdated
// 3. The `onBehalf` address passes the sendShares gate check.
// 4. The vault itself passes the receiveAssets gate check.
// 5. Total shares do not overflow uint256 when virtual shares are included.
// 6. `onBehalf` is not the zero address.
// 7. Interest has already been accrued at the vault level for this block (i.e. lastUpdate == block.timestamp).
rule canForceDeallocateZero(env e, address adapter, bytes data, address onBehalf) {

// the adapter must be registered in the vault.
require isAdapter(adapter);

// forceDeallocate is non-payable.
require e.msg.value == 0;

// gate checks that withdraw within forceDeallocate will not revert.
require canSendShares(onBehalf);
require canReceiveAssets(currentContract);

// prevent totalSupply + virtualShares from overflowing, which would cause an arithmetic revert.
require totalSupply() + virtualShares() <= max_uint256;

// vault's exit logic requires onBehalf to be non-zero address.
require(onBehalf != 0, "onBehalf can't be the zero address");

// interest must have been accrued for this block so that the vault's internal lastUpdate matches the current timestamp.
// this is a fair assumption because accrueInterest() can be called permissionlessly.
Comment thread
QGarchery marked this conversation as resolved.
Outdated
require(currentContract.lastUpdate() == e.block.timestamp, "assume interest has been accrued at the Vault level");

// call forceDeallocate with zero requested assets.
forceDeallocate@withrevert(e, adapter, data, 0, onBehalf);

assert !lastReverted;
}
Loading