diff --git a/certora/confs/Vault.conf b/certora/confs/Vault.conf index c1aedc0..2c73a9a 100644 --- a/certora/confs/Vault.conf +++ b/certora/confs/Vault.conf @@ -1,16 +1,17 @@ { "files": [ - "contracts/Vault.sol", + "certora/harness/VaultHarness.sol", + //"contracts/Vault.sol", // "certora/harness/MarketplaceHarness.sol", // "contracts/Marketplace.sol", // "contracts/Vault.sol", // "contracts/Groth16Verifier.sol", "certora/helpers/ERC20A.sol", ], - "parametric_contracts": ["Vault"], + "parametric_contracts": ["VaultHarness"], "link" : [ - "Vault:_token=ERC20A", - // "MarketplaceHarness:_vault=Vault", + "VaultHarness:_token=ERC20A", + // "MarketplaceHarness:_vault=VaultHarness", // "MarketplaceHarness:_verifier=Groth16Verifier" ], "packages": [ @@ -18,7 +19,7 @@ ], "msg": "Verifying Vault", "rule_sanity": "basic", - "verify": "Vault:certora/specs/Vault.spec", + "verify": "VaultHarness:certora/specs/Vault.spec", // "optimistic_loop": true, "loop_iter": "3", // "optimistic_hashing": true, diff --git a/certora/harness/VaultHarness.sol b/certora/harness/VaultHarness.sol new file mode 100644 index 0000000..0756bac --- /dev/null +++ b/certora/harness/VaultHarness.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import "../../contracts/Vault.sol"; + +contract VaultHarness is Vault { + constructor(IERC20 token) Vault(token) {} + + function publicStatus( + Controller controller, + FundId fundId + ) public view returns (FundStatus) { + return _getFundStatus(controller, fundId); + } + +} diff --git a/certora/specs/Vault.spec b/certora/specs/Vault.spec index cc49017..227c879 100644 --- a/certora/specs/Vault.spec +++ b/certora/specs/Vault.spec @@ -49,6 +49,27 @@ hook Sstore _funds[KEY VaultBase.Controller Controller][KEY VaultBase.FundId Fun } +// frozenAtghost mirror +ghost mapping(VaultBase.Controller => mapping(VaultBase.FundId => VaultBase.Timestamp)) frozenAtMirror { + init_state axiom + forall VaultBase.Controller controller. + forall VaultBase.FundId fundId. + frozenAtMirror[controller][fundId] == 0; +} + +hook Sload VaultBase.Timestamp defaultValue + _funds[KEY VaultBase.Controller Controller][KEY VaultBase.FundId FundId].frozenAt +{ + require frozenAtMirror[Controller][FundId] == defaultValue; +} + +hook Sstore _funds[KEY VaultBase.Controller Controller][KEY VaultBase.FundId FundId].frozenAt + VaultBase.Timestamp defaultValue +{ + frozenAtMirror[Controller][FundId] = defaultValue; +} + + // (∀ controller ∈ Controller, fundId ∈ FundId: // fund.lockExpiry <= fund.lockMaximum // where fund = _funds[controller][fundId]) @@ -66,10 +87,13 @@ invariant lockExpiryLELockMaximum() ghost mapping(VaultBase.Controller => mapping(VaultBase.FundId => mapping(VaultBase.AccountId => VaultBase.TokensPerSecond))) outgoingMirror { init_state axiom - forall VaultBase.Controller controller. + (forall VaultBase.Controller controller. forall VaultBase.FundId fundId. forall VaultBase.AccountId accountId. - outgoingMirror[controller][fundId][accountId] == 0; + outgoingMirror[controller][fundId][accountId] == 0) && + (forall VaultBase.Controller controller. + forall VaultBase.FundId fundId. + (sum VaultBase.AccountId accountId. outgoingMirror[controller][fundId][accountId]) == 0); } hook Sload VaultBase.TokensPerSecond defaultValue @@ -162,10 +186,13 @@ invariant outgoingLEAvailableEasy(VaultBase.Controller controller, VaultBase.Fun ghost mapping(VaultBase.Controller => mapping(VaultBase.FundId => mapping(VaultBase.AccountId => VaultBase.TokensPerSecond))) incomingMirror { init_state axiom - forall VaultBase.Controller controller. + (forall VaultBase.Controller controller. forall VaultBase.FundId fundId. forall VaultBase.AccountId accountId. - incomingMirror[controller][fundId][accountId] == 0; + incomingMirror[controller][fundId][accountId] == 0) && + (forall VaultBase.Controller controller. + forall VaultBase.FundId fundId. + (sum VaultBase.AccountId accountId. incomingMirror[controller][fundId][accountId]) == 0); } hook Sload VaultBase.TokensPerSecond defaultValue @@ -185,14 +212,56 @@ hook Sstore _accounts[KEY VaultBase.Controller Controller][KEY VaultBase.FundId // (∑ accountId ∈ AccountId: accounts[accountId].flow.incoming) = // (∑ accountId ∈ AccountId: accounts[accountId].flow.outgoing) // where accounts = _accounts[controller][fundId]) -invariant incomingEqualsOutgoing() - (sum - VaultBase.Controller controller. - VaultBase.FundId fundId. - VaultBase.AccountId accountId. - outgoingMirror[controller][fundId][accountId]) - == (sum - VaultBase.Controller controller. - VaultBase.FundId fundId. - VaultBase.AccountId accountId. - incomingMirror[controller][fundId][accountId]); \ No newline at end of file +invariant incomingEqualsOutgoing(env e) + (forall VaultBase.Controller controller. + forall VaultBase.FundId fundId. + statusCVL(e, controller, fundId) == VaultBase.FundStatus.Withdrawing + => (sum + VaultBase.AccountId accountId. + outgoingMirror[controller][fundId][accountId]) + == (sum + VaultBase.AccountId accountId. + incomingMirror[controller][fundId][accountId])) + { + preserved with (env e2) { + require e.block.timestamp == e2.block.timestamp; + } + } + + +// copying status implementation from Funds.sol using CVL to use it in quantifier +/* +if (Timestamps.currentTime() < fund.lockExpiry) { + if (fund.frozenAt != Timestamp.wrap(0)) { + return FundStatus.Frozen; + } + return FundStatus.Locked; + } + if (fund.lockMaximum == Timestamp.wrap(0)) { + return FundStatus.Inactive; + } + return FundStatus.Withdrawing; +*/ + +ghost VaultBase.Timestamp zeroValue { + init_state axiom zeroValue == 0; + axiom zeroValue == 0; +} + +function statusCVL(env e, VaultBase.Controller controller, VaultBase.FundId fundId) returns VaultBase.FundStatus { + if (e.block.timestamp < lockExpiryMirror[controller][fundId]) { + if (frozenAtMirror != zeroValue) { + return VaultBase.FundStatus.Frozen; + } + return VaultBase.FundStatus.Locked; + } + if (lockMaximumMirror[controller][fundId] == 0) { + return VaultBase.FundStatus.Inactive; + } + return VaultBase.FundStatus.Withdrawing; +} + + + + +