Skip to content

[certora] allocation at groupId is greater than equal leafId#905

Open
bhargavbh wants to merge 14 commits intomainfrom
certora/allocationsSumOfMarketIdAllocations
Open

[certora] allocation at groupId is greater than equal leafId#905
bhargavbh wants to merge 14 commits intomainfrom
certora/allocationsSumOfMarketIdAllocations

Conversation

@bhargavbh
Copy link
Copy Markdown
Contributor

@bhargavbh bhargavbh commented Mar 8, 2026

adds invariants that allocation at AdapterId and CollateralId is sum of MarketId allocations.
as a result, proves:
- adapter allocation >= any individual market allocation
- collateral allocation >= any individual market allocation

For a generic adapter following a hierarchical model of leafId (e.g. marketId) and groupId (e.g. collateralId), proves that the allocation at the group-level >= allocation at leaf-level.

@bhargavbh bhargavbh changed the title adds invariants that allocation at AdapterId and CollateralId is sum … [certora] allocation at AdapterId and CollateralId is greater than marketId Mar 16, 2026
@bhargavbh bhargavbh changed the title [certora] allocation at AdapterId and CollateralId is greater than marketId [certora] allocation at groupId is greater than leafId Mar 17, 2026
bhargavbh and others added 4 commits March 17, 2026 16:54
renamed

Signed-off-by: Bhargav Bhatt <40268131+bhargavbh@users.noreply.github.com>
renamed

Signed-off-by: Bhargav Bhatt <40268131+bhargavbh@users.noreply.github.com>
@bhargavbh bhargavbh self-assigned this Mar 17, 2026
@bhargavbh bhargavbh marked this pull request as ready for review March 17, 2026 21:27
@bhargavbh bhargavbh changed the title [certora] allocation at groupId is greater than leafId [certora] allocation at groupId is greater than equal leafId Mar 17, 2026

// Require change values to be bounded, else the sum exceeds max_uint256.
// Note that the implicit cast from uint256 to int256 is saf here, see allocationIsInt256 in Invariants.spec
require allocation(ids[0]) + change >= 0, "group level allocation + change is non-negative";
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.

maybe worth redeclaring :

strong invariant allocationIsInt256(bytes32 id)
    allocation(id) <= max_int256();

like in AllocationMorphoMarketV1AdapterV2 and require them directly ?

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.

maybe not finally , these lines are stronger than these invariants

"-destructiveOptimizations twostage",
"-s [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5},z3:def{randomSeed=6},z3:def{randomSeed=7},z3:def{randomSeed=8},z3:def{randomSeed=9},z3:def{randomSeed=10}]"
],
"global_timeout": "7200",
Copy link
Copy Markdown
Collaborator

@lilCertora lilCertora Mar 18, 2026

Choose a reason for hiding this comment

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

just curious , is it intentional to have both global and smt timeout set to 7200 ?
default smt is 300 but maybe this is wanted

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

just used the same timeout as a similar spec, but i can try reducing it. here is the prover link.
In general, it is not a very heavy spec.


// assume the following functions return arbitrary value but do not modify the relevant storage for this rule.
function _.balanceOf(address) external => NONDET;
function _.realAssets() external => NONDET;
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.

Suggested change
function _.realAssets() external => NONDET;

it's unused because accrueInterestView is summarized

Comment on lines +12 to +13
function _.transfer(address, uint256) external => NONDET;
function _.transferFrom(address, address, uint256) external => NONDET;
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.

The comment above assume the following functions return arbitrary value but do not modify the relevant storage for this rule. only really applies to the summary of the transfer functions (and accrueInterest), because the other functions are view so NONDET is an over-approximation since we are not looking at reverts

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.

Btw, wondering if we cannot remove those 2 summaries, since we are proving strong invariants, which models reentrancy

Suggested change
function _.transfer(address, uint256) external => NONDET;
function _.transferFrom(address, address, uint256) external => NONDET;

function _.canSendShares(address) external => NONDET;
function _.canReceiveAssets(address) external => NONDET;
function _.canSendAssets(address) external => NONDET;
function accrueInterest() internal => NONDET;
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.

Suggested change
function accrueInterest() internal => NONDET;

would this be possible, since we are already summarizing accrueInterestView ? It would be really nice, because then it removes part of the assumption "the following functions return arbitrary value but do not modify the relevant storage for this rule" which is not trivial to check

// - groupId and leafId are distinct
// - groupId is never itself a leaf in any other adapter
// - leafId is never itself a group in any other adapter
// - a leaf's parent group never changes across calls
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.

Suggested change
// - a leaf's parent group never changes across calls
// - a leaf has a unique parent group, and that group never changes across calls

because the existence of the group is already an assumption in itself

Comment on lines +76 to +81
// Require change values to be bounded, else the sum exceeds max_uint256.
// Note that the implicit cast from uint256 to int256 is saf here, see allocationIsInt256 in Invariants.spec
require allocation(ids[0]) + change >= 0, "group level allocation + change is non-negative";
require allocation(ids[0]) + change <= max_uint256, "group level allocation + change is bounded";
require allocation(ids[1]) + change >= 0, "leaf level allocation + change is non-negative";
require allocation(ids[1]) + change <= max_uint256, "leaf level allocation + change is bounded";
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.

Not clear to me why we need those. If an adapter doesn't have the property then it might revert, but this isn't a path that the prover would consider.
Also note that the property allocation(ids[0]) + change <= max_uint256 is precisely the reason why we are trying to prove the whole spec, so that we "prove" why the requires of this form in the other specs are sound to make

Comment on lines +115 to +117
// Every ghost cell stays within uint256 bounds.
strong invariant ghostAllocationBounded(bytes32 g, bytes32 l)
ghostAllocationByGroupId[g][l] >= 0 && ghostAllocationByGroupId[g][l] <= max_uint256;
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.

Suggested change
// Every ghost cell stays within uint256 bounds.
strong invariant ghostAllocationBounded(bytes32 g, bytes32 l)
ghostAllocationByGroupId[g][l] >= 0 && ghostAllocationByGroupId[g][l] <= max_uint256;

let's just define ghostAllocationByGroupId as a uint256 instead of as a mathint

!(ghostIsLeafId[id] && ghostIsGroupId[id]);

// A ghost cell is non-zero only if its leaf is registered and maps to that group.
strong invariant ghostGroupConsistency(bytes32 groupId, bytes32 leafId)
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.

Suggested change
strong invariant ghostGroupConsistency(bytes32 groupId, bytes32 leafId)
strong invariant nonMatchingLeafAndGroupHasZeroGhostAllocation(bytes32 groupId, bytes32 leafId)

Comment on lines +5 to +7
"compiler_map": {
"VaultV2": "solc-0.8.28"
},
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.

Suggested change
"compiler_map": {
"VaultV2": "solc-0.8.28"
},
"solc": "solc-0.8.28",

ghostIsGroupId[groupId] => to_mathint(allocation(groupId)) == (usum bytes32 leafId. ghostAllocationByGroupId[groupId][leafId])
{
preserved with (env e) {
bytes32 anyLeafId;
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.

I don't think that this does anything, the anyLeafId can be chosen arbitrarily by the prover, so it could chose nonsensical value for it which would add a useless property to the context. Here is a run that goes through without it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants