diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..2337bc34
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,5 @@
+# Specify the owners of the code in the src directory
+src/ @dglowinski @kasperpawlowski @hoytech
+
+# Specify the owners of the code in the certora directory
+certora/ @dglowinski @kasperpawlowski @hoytech
diff --git a/.gitmodules b/.gitmodules
index a074a2c2..3acf41a0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,12 +1,12 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
-[submodule "lib/ethereum-vault-connector"]
- path = lib/ethereum-vault-connector
- url = https://github.com/euler-xyz/ethereum-vault-connector
[submodule "lib/permit2"]
path = lib/permit2
url = https://github.com/Uniswap/permit2
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
+[submodule "lib/ethereum-vault-connector"]
+ path = lib/ethereum-vault-connector
+ url = https://github.com/euler-xyz/ethereum-vault-connector
diff --git a/README.md b/README.md
index 9d187f62..b18168d3 100644
--- a/README.md
+++ b/README.md
@@ -61,8 +61,6 @@ This software is **experimental** and is provided "as is" and "as available".
Always include thorough tests when using the Euler Vault Kit to ensure it interacts correctly with your code.
-The Euler Vault Kit is currently undergoing security audits and should not be used in production.
-
## Known limitations
Refer to the [whitepaper](https://docs.euler.finance/euler-vault-kit-white-paper/) for a list of known limitations and security considerations.
diff --git a/audits/Certora_EVK_report.pdf b/audits/Certora_EVK_report.pdf
index 71f9a90f..205ed49c 100644
Binary files a/audits/Certora_EVK_report.pdf and b/audits/Certora_EVK_report.pdf differ
diff --git a/audits/ChainSecurity_EVK_report.pdf b/audits/ChainSecurity_EVK_report.pdf
new file mode 100644
index 00000000..52b5ae1e
Binary files /dev/null and b/audits/ChainSecurity_EVK_report.pdf differ
diff --git a/audits/ChainSecurity_preliminary_findings.txt b/audits/ChainSecurity_preliminary_findings.txt
deleted file mode 100644
index dc7254b6..00000000
--- a/audits/ChainSecurity_preliminary_findings.txt
+++ /dev/null
@@ -1,63 +0,0 @@
-ChainSecurity preliminary findings in EVK audit
-
-THIS IS WORK IN PROGRESS
-
-1. We think that the liquidation mechanism can be missused in cases where the LTV for a collateral is high. Instead of performing a single liquidation, the liquidator can perform many partial liquidations which gives him more reward then the single liquidation. Thus, more bad debt than expected is created in the vault which is equal to the additional reward earned by the liquidator when liquidating a position partially multiple times instead of all at once.
-For example:
-If the violator has 85 units of collateral with 0.8 LTV and 80 units of debt. A liquidator can liquidate the position all at once with a discount factor of 0.85. The liquidator will receive 72.25 units of debt and 85 units of collateral thus making 12.75 in the process. The violator's position will end up with 7.75 (bad) debt. However, the liquidator can decide to perform the liquidation in two steps, liquidating half of the position first and then the other half. The liquidator will end up with 36.125 + 34 debt and 42.5 + 42.5 collateral, resulting in a profit of 14.875. The violator's position will at the end contain 9.875 debt which is more debt than if the liquidation would have been performed at once. The difference was earned by the liquidator.
-
-Preliminary response from Euler:
-
-We have made a set of 3 changes to the liquidation system in order to mitigate the issues discovered by our auditors.
-The first issue raised is related to the "Counterproductive Incentives" issue described by OpenZeppelin in their [2019 Compound audit](https://blog.openzeppelin.com/compound-audit). Liquidation systems that incentivise liquidators with extra collateral value as a bonus (or discount) can, in some circumstances, leave violators *more* unhealthy than they were pre-liquidation. In the Euler system, the discount is proportional to how unhealthy the user is, which means that in these cases, a liquidator may improve their total yield by performing many small liquidations, rather than one large liquidation. Each smaller liquidation will decrease the user's health and therefore increase their discount for subsequent liquidations, up until the maximum liquidation discount is reached. As described in our [Dutch Liquidation Analysis](https://docs.euler.finance/Dutch_Liquidation_Analysis.pdf) research paper, this scenario can be avoided by selecting an appropriately low maximum discount factor.
-Change 1: With this in mind, we have added EVK functionality that allows governors to configure the vault's maximum discount factor. In many cases, governors will compute an appropriate maximum discount based on the highest configured LTV for the vault, although there may be other considerations involved. A governor must specify a value for this parameter, otherwise the liquidation system will not function properly.
-The second issue raised is a general observation that price manipulation can be used to attack lending markets, and that some of the oracles we would like to support have special challenges. In particular, pull-based oracles like Pyth and Redstone provide more flexibility to attackers because they can typically choose to use any published prices within an N-minute window. For example, an attacker may be monitoring prices off-chain, waiting for a large decline in the price of a vault's collateral asset (or, equivalently, a large increase in the price of the liability asset). If the decline is sufficiently large, the attacker will search the previous N-minutes of prices and select the pair with the largest difference. The attacker will then submit a transaction that performs the following attack:
-* Updates the oracle with the old price
-* Deposits collateral and borrows as much as possible
-* Updates the oracle with the new price, causing the position to become very unhealthy
-* Liquidates the position from another separate account, leaving bad debt. This bad debt corresponds to profit from the attack at the expense of the vault's depositors
-Although impossible to solve in the general case, to reduce the impact of this issue we have have made two modifications to the EVK:
-Change 2: We now allow the governor to configure separate borrowing and liquidation LTVs. This requires the attacker to find correspondingly larger price jumps.
-Change 3: We have added a "cool-off period" where an account cannot be liquidated. Cool-off periods begin once an account has successfully passed an account status check, and last a governor-configurable number of seconds. By setting a non-zero cool-off period, accounts cannot be liquidated inside a block that they were previously healthy.
-The consequence of this is that the attack described above can no longer be done in an entirely risk-free manner. The position needs to be setup in one block but liquidated in a following block, potentially opening up the opportunity for other unrelated parties to perform the liquidation instead of the attacker. Additionally, such attacks cannot be financed with flash loans. As well as price-oracle related attacks, this protection may also reduce the impact of future unknown protocol attacks.
-More generally, the cool-off period allows a vault creator to express a minimum-expected liveness period for a particular chain. If the maximum possible censorship time can be estimated, the cool-off period can be configured larger than this, with the trade-off being that legitimate liquidations of new positions may be delayed by this period of time.
-
-2. In Governance.sol we noted that if convertFees has not been called in a while and then the protocolFee is modified that the new fee distribution between the governor and the protocol will be applied retroactively to the unclaimed fees.
-
-Comment from Euler: Acknowledged.
-
-3. We found that deposit() and redeem() in Vault are not ERC-4626 compatible as they replace amount by the maximum available amount if it is equal to type(uint256).max. We noticed that a list of incompatibilities between the EVK and ERC-4626 vaults is available in the EVK white paper but the deposit() and redeem() incompatibility is not mentionned there.
-
-Comment from Euler: Acknowledged, white paper updated
-
-4. The specification for Synths stipulates that : "Euler synthetic vaults are a special configuration of vaults which use hooks to disable deposit, mint and loop for all addresses except the synth address itself, disallowing deposits from normal users". However, it would still be possible for any user to invoke skim(), allowing the user to deposit funds into the synthetic vault and receive shares.
-
-Comment from Euler: Acknowledged, white paper update
-
-
-5. We noticed that in initVaultCache, on line 100 in Cache that the computation of newTotalShares does not make use of VIRTUAL_DEPOSIT_AMOUNT used for every conversion between shares and assets. We calculated that the current computation results in less shares created for fees. We are wondering if this is a voluntary decision and if yes we would be interested to know the motivation behind it.
-
-Comment from Euler: Acknowledged.
-
-6. In LiquidityUtils.sol, in checkLiquidity a violator can enable on purpose the maximum amount of collaterals (10) to increase the amount of gas a liquidator has to spend if he wants to liquidate the violator.
-
-Comment from Euler: For this reason we are limiting the amount of collaterals to a relatively small number of 10. Note that the vault's creator/governor should be take the gas costs into account when setting LTVs. Some collaterals might have expensive balanceOf functions (in the future, with multiple vault implementations) or be expensive to price. Just setting a list of collaterals to 10 without any LTVs configured would cost 20k to cold read LTV config slots. With overhead it's still very acceptable. The max number of collaterals could be reduced further per controller vault by using hooks.
-
-7. There is a rounding issue in Liquidations.sol line 75 :
-liqCache.liability = getCurrentOwed(vaultCache, violator).toAssetsUp()
-The liability amount is used to compute how much collateral the liquidator will get and it is rounded up. Therefore, if the liability is 1.01 wei (1 wei + dust), it will round to 2. However, when the liability is transfered (line 177) it will be 1.01. But the liquidator's reward will be computed as if he took over 2 debt
-
-Comment from Euler: Even though the internal debt would be 1.01, it's not possible to repay it other than by providing 2 whole wei of assets (or equivalent shares in case of deloop). The liquidator could maybe bundle multiple such accounts, in this case up to 99 and repay 100 wei for an actual 99.99 debt. In theory it's a seizable saving, but probably unrealistic in practice.
-
-Comment from CS: While we understand your explanation, we would like to point out the fact that a liquidator will be able to pay back 0.99 of his own debt for free.
-
-Preliminary resolution from Euler: Acknowledged. The security and business impact is negligible.
-
-
-8. In conversionTotals, partial interest is always rounded up using toAssetsUp() to compute totalAssets. This can producde a rounding in the wrong direction in Shares.toAssetsDown() and Assets.toSharesUp. For example, if there are 10^6 (virtual assets) + 1.1 (partial interest accrued), and shares amount is 10^6 (virtual shares) + 2 then toAssetsDown(1) will return 1 instead of 0.
-
-Comment from Euler: A similar reasoning can be applied as in the previous point. Although the actual debt is 1.1, the last borrower (in general case) to repay their debt would need to provide whole 2 wei. This time the totalBorrows are a single value so there is no way to get rid of 1.1 borrows for less than 2 wei.
-
-Comment from CS: We understand your explanation but would like to point out that this will allow a user to redeem() 1 wei more in certain cases. For example, amount to redeem is 1e6, shares is 2e6 and assets is (2e6-0.99). The user should receive 1e6 - 1 but will get 1e6.
-
-Preliminary resolution from Euler: Acknowledged. The security and business impact is negligible.
\ No newline at end of file
diff --git a/audits/EnigmaDark_EVK_report.pdf b/audits/EnigmaDark_EVK_report.pdf
new file mode 100644
index 00000000..94ca5f94
Binary files /dev/null and b/audits/EnigmaDark_EVK_report.pdf differ
diff --git a/audits/Omniscia_EVK_report.pdf b/audits/Omniscia_EVK_report.pdf
new file mode 100644
index 00000000..82508d2b
Binary files /dev/null and b/audits/Omniscia_EVK_report.pdf differ
diff --git a/audits/OpenZeppelin_Synths_report.pdf b/audits/OpenZeppelin_Synths_report.pdf
new file mode 100644
index 00000000..2b2509c4
Binary files /dev/null and b/audits/OpenZeppelin_Synths_report.pdf differ
diff --git a/certora/conf/Cache.conf b/certora/conf/Cache.conf
new file mode 100644
index 00000000..04c06c80
--- /dev/null
+++ b/certora/conf/Cache.conf
@@ -0,0 +1,27 @@
+{
+ "files": [
+ "certora/harness/CacheHarness.sol"
+ ],
+ "verify": "CacheHarness:certora/specs/Cache.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "prover_version": "master",
+ "server" : "production",
+ "parametric_contracts": ["CacheHarness"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-smt_nonLinearArithmetic true",
+ "-adaptiveSolverConfig false",
+ "-solvers [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}]"
+ ],
+ "smt_timeout": "7000"
+}
\ No newline at end of file
diff --git a/certora/conf/ERC4626Split/README.md b/certora/conf/ERC4626Split/README.md
new file mode 100644
index 00000000..7878fab4
--- /dev/null
+++ b/certora/conf/ERC4626Split/README.md
@@ -0,0 +1 @@
+To run all of these conf files easily, use the certora/scripts/runERC4626RulesSplitConfs.py
\ No newline at end of file
diff --git a/certora/conf/ERC4626Split/VaultERC4626-assetsMoreThanSupply.conf b/certora/conf/ERC4626Split/VaultERC4626-assetsMoreThanSupply.conf
new file mode 100644
index 00000000..b49687ff
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-assetsMoreThanSupply.conf
@@ -0,0 +1,34 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["assetsMoreThanSupply",],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_args": [
+ "-maxConcurrentTransforms INLINED_HOOKS:8",
+ "-smt_nonLinearArithmetic true -adaptiveSolverConfig false -splitParallel true -splitParallelInitialDepth 2 -depth 15 -s [z3:arith1,yices:def] -mediumTimeout 2 -splitParallelTimelimit 7200"
+ ],
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-convertToAssetsWeakAdditivity.conf b/certora/conf/ERC4626Split/VaultERC4626-convertToAssetsWeakAdditivity.conf
new file mode 100644
index 00000000..4c4f32b1
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-convertToAssetsWeakAdditivity.conf
@@ -0,0 +1,31 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["convertToAssetsWeakAdditivity"],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "disable_auto_cache_key_gen": true,
+ "fe_version": "latest",
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-convertToSharesWeakAdditivity.conf b/certora/conf/ERC4626Split/VaultERC4626-convertToSharesWeakAdditivity.conf
new file mode 100644
index 00000000..5b966620
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-convertToSharesWeakAdditivity.conf
@@ -0,0 +1,31 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["convertToSharesWeakAdditivity"],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "disable_auto_cache_key_gen": true,
+ "fe_version": "latest",
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-depositMonotonicity.conf b/certora/conf/ERC4626Split/VaultERC4626-depositMonotonicity.conf
new file mode 100644
index 00000000..b3133b21
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-depositMonotonicity.conf
@@ -0,0 +1,32 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["depositMonotonicity",],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "disable_auto_cache_key_gen": true,
+ "fe_version": "latest",
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-dustFavorsTheHouse.conf b/certora/conf/ERC4626Split/VaultERC4626-dustFavorsTheHouse.conf
new file mode 100644
index 00000000..3786ca25
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-dustFavorsTheHouse.conf
@@ -0,0 +1,32 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["dustFavorsTheHouse",],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "disable_auto_cache_key_gen": true,
+ "fe_version": "latest",
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-dustFavorsTheHouseAssets.conf b/certora/conf/ERC4626Split/VaultERC4626-dustFavorsTheHouseAssets.conf
new file mode 100644
index 00000000..11e17588
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-dustFavorsTheHouseAssets.conf
@@ -0,0 +1,32 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["dustFavorsTheHouseAssets",],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "disable_auto_cache_key_gen": true,
+ "fe_version": "latest",
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-noAssetsIfNoSupply.conf b/certora/conf/ERC4626Split/VaultERC4626-noAssetsIfNoSupply.conf
new file mode 100644
index 00000000..2ca02811
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-noAssetsIfNoSupply.conf
@@ -0,0 +1,36 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["noAssetsIfNoSupply",],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "disable_auto_cache_key_gen": true,
+ "fe_version": "latest",
+ "prover_args": [
+ "-maxConcurrentTransforms INLINED_HOOKS:6",
+ "-smt_nonLinearArithmetic true -adaptiveSolverConfig false -depth 0 "
+ ],
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-noSupplyIfNoAssets.conf b/certora/conf/ERC4626Split/VaultERC4626-noSupplyIfNoAssets.conf
new file mode 100644
index 00000000..e454884c
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-noSupplyIfNoAssets.conf
@@ -0,0 +1,32 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["noSupplyIfNoAssets",],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "disable_auto_cache_key_gen": true,
+ "fe_version": "latest",
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-onlyContributionMethodsReduce.conf b/certora/conf/ERC4626Split/VaultERC4626-onlyContributionMethodsReduce.conf
new file mode 100644
index 00000000..3f77397c
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-onlyContributionMethodsReduce.conf
@@ -0,0 +1,33 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "prover_args": [
+ "-maxConcurrentTransforms INLINED_HOOKS:8"
+ ],
+ "rule": ["onlyContributionMethodsReduceAssets"],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-totalsMonotonicity.conf b/certora/conf/ERC4626Split/VaultERC4626-totalsMonotonicity.conf
new file mode 100644
index 00000000..f82c6085
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-totalsMonotonicity.conf
@@ -0,0 +1,33 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "prover_args": [
+ "-maxConcurrentTransforms INLINED_HOOKS:8"
+ ],
+ "rule": ["totalsMonotonicity"],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-most.conf b/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-most.conf
new file mode 100644
index 00000000..f347bc3a
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-most.conf
@@ -0,0 +1,35 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["vaultSolvency"],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "server": "staging", // 10 hour queue
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "prover_args" : [
+ "-smt_nonLinearArithmetic false",
+ ],
+ // "smt_timeout": "7200",
+ "smt_timeout": "28800",
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-redeem.conf b/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-redeem.conf
new file mode 100644
index 00000000..a65cac9d
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-redeem.conf
@@ -0,0 +1,36 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["vaultSolvency"],
+ "method": "redeem(uint256,address,address)",
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "server": "staging", // 10 hour queue
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "prover_args" : [
+ "-smt_nonLinearArithmetic false",
+ ],
+ // "smt_timeout": "7200",
+ "smt_timeout": "28800",
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-withdraw-as-rule.conf b/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-withdraw-as-rule.conf
new file mode 100644
index 00000000..a64f4ac6
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-withdraw-as-rule.conf
@@ -0,0 +1,41 @@
+{
+ "build_cache": true,
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ // "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol"
+ ],
+ "loop_iter": "2",
+ "msg": " -vaultSolvency-withdraw : rerun ERC4626 for wrap up",
+ "optimistic_loop": true,
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "parametric_contracts": [
+ "ERC4626Harness"
+ ],
+ "process": "emv",
+ "prover_args": [
+ "-smt_nonLinearArithmetic true",
+ "-adaptiveSolverConfig false",
+ "-deleteSMTFile false",
+ "-depth 20",
+ ],
+ "prover_version" : "master",
+ "rule": [
+ "vaultSolvencyWithdraw_totals",
+ "vaultSolvencyWithdraw_underlying",
+ ],
+ "rule_sanity": "basic",
+ "server": "production",
+ "smt_timeout": "7800",
+ "solc": "solc8.23",
+ "solc_optimize": "10000",
+ "solc_via_ir": true,
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec"
+}
\ No newline at end of file
diff --git a/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-withdraw.conf b/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-withdraw.conf
new file mode 100644
index 00000000..f21e139f
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626-vaultSolvency-withdraw.conf
@@ -0,0 +1,36 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule": ["vaultSolvency"],
+ "method": "withdraw(uint256,address,address)",
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "server": "staging", // 10 hour queue
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "prover_args": [
+ "-smt_nonLinearArithmetic true -adaptiveSolverConfig false -depth 0"
+ ],
+ // "smt_timeout": "7200",
+ "smt_timeout": "28800",
+}
+
diff --git a/certora/conf/ERC4626Split/VaultERC4626.conf b/certora/conf/ERC4626Split/VaultERC4626.conf
new file mode 100644
index 00000000..cf5951d2
--- /dev/null
+++ b/certora/conf/ERC4626Split/VaultERC4626.conf
@@ -0,0 +1,31 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/EthereumVaultConnector.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyERC20B.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/ERC4626Harness.sol",
+ ],
+ "verify": "ERC4626Harness:certora/specs/VaultERC4626.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault ERC4626",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "parametric_contracts": ["ERC4626Harness"],
+ "build_cache": true,
+ "prover_version" : "master",
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "disable_auto_cache_key_gen": true,
+ "fe_version": "latest",
+ "smt_timeout": "7200"
+}
+
diff --git a/certora/conf/EVault/modules/BalanceForwarder.conf b/certora/conf/EVault/modules/BalanceForwarder.conf
new file mode 100644
index 00000000..a9ef9f78
--- /dev/null
+++ b/certora/conf/EVault/modules/BalanceForwarder.conf
@@ -0,0 +1,31 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/harness/EVCHarness.sol",
+ "src/EVault/modules/BalanceForwarder.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/modules/BalanceForwarderHarness.sol",
+ ],
+ "link": [
+ "BalanceForwarderHarness:evc=EVCHarness",
+ ],
+ "verify": "BalanceForwarderHarness:certora/specs/BalanceForwarder.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "prover_version": "master",
+ "server" : "production",
+ "parametric_contracts": ["BalanceForwarderHarness"],
+ "build_cache": true,
+ "prover_args": ["-smt_bitVectorTheory", "true"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+}
\ No newline at end of file
diff --git a/certora/conf/EVault/modules/Governance.conf b/certora/conf/EVault/modules/Governance.conf
new file mode 100644
index 00000000..283a5ee7
--- /dev/null
+++ b/certora/conf/EVault/modules/Governance.conf
@@ -0,0 +1,32 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "src/ProtocolConfig/ProtocolConfig.sol",
+ "src/EVault/modules/Governance.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/modules/GovernanceHarness.sol",
+ ],
+ "link": [
+ "GovernanceHarness:protocolConfig=ProtocolConfig",
+ "GovernanceHarness:evc=EVCHarness"
+ ],
+ "verify": "GovernanceHarness:certora/specs/Governance.spec",
+ "parametric_contracts": ["GovernanceHarness"],
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Governance benchmarking",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "prover_version": "master",
+ "build_cache": true,
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "smt_timeout":"7200",
+}
\ No newline at end of file
diff --git a/certora/conf/EVault/modules/Liquidation.conf b/certora/conf/EVault/modules/Liquidation.conf
new file mode 100644
index 00000000..08edecf5
--- /dev/null
+++ b/certora/conf/EVault/modules/Liquidation.conf
@@ -0,0 +1,38 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "src/EVault/modules/Liquidation.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/modules/LiquidationHarness.sol"
+ ],
+ "link": [
+ "LiquidationHarness:evc=EVCHarness",
+ ],
+ "verify": "LiquidationHarness:certora/specs/Liquidation.spec",
+ "solc": "solc8.23",
+ "msg": "Liquidation benchmarking",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "parametric_contracts": ["LiquidationHarness"],
+ "rule_sanity": "basic",
+ "prover_version": "master",
+ "server" : "production",
+ // "coverage_info" : "advanced",
+ "build_cache": true,
+ // Performance tuing options below this line
+ "prover_args": [
+ "-depth 10",
+ "-smt_nonLinearArithmetic true",
+ "-adaptiveSolverConfig false"
+ ],
+ "function_finder_mode": "relaxed",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "smt_timeout": "7000"
+}
\ No newline at end of file
diff --git a/certora/conf/EVault/modules/RiskManager.conf b/certora/conf/EVault/modules/RiskManager.conf
new file mode 100644
index 00000000..bef8006e
--- /dev/null
+++ b/certora/conf/EVault/modules/RiskManager.conf
@@ -0,0 +1,37 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "src/EVault/modules/RiskManager.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/modules/RiskManagerHarness.sol",
+ ],
+ "link": [
+ "RiskManagerHarness:evc=EVCHarness",
+ ],
+ "verify": "RiskManagerHarness:certora/specs/RiskManager.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "parametric_contracts": ["RiskManagerHarness"],
+ "rule_sanity": "basic",
+ "server": "production",
+ "build_cache": true,
+ // "coverage_info" : "advanced",
+ // Performance tuning options below this line
+ "prover_args": [
+ "-depth 0",
+ "-smt_nonLinearArithmetic true",
+ "-adaptiveSolverConfig false"
+ ],
+ "function_finder_mode": "relaxed",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "smt_timeout": "7000",
+ "prover_version": "master"
+}
\ No newline at end of file
diff --git a/certora/conf/EVault/modules/Vault.conf b/certora/conf/EVault/modules/Vault.conf
new file mode 100644
index 00000000..9b20d1cb
--- /dev/null
+++ b/certora/conf/EVault/modules/Vault.conf
@@ -0,0 +1,29 @@
+{
+ "files": [
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/EVCHarness.sol",
+ "certora/harness/modules/VaultHarness.sol",
+ ],
+ "link" : [
+ "VaultHarness:evc=EVCHarness",
+ ],
+ "verify": "VaultHarness:certora/specs/Vault.spec",
+ "solc": "solc8.23",
+ "rule_sanity": "basic",
+ "msg": "Vault",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "parametric_contracts": ["VaultHarness"],
+ "prover_version": "master",
+ "build_cache" : true,
+ // Performance tuning options below this line
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "optimistic_loop": true,
+ "loop_iter": "2",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/BalanceForwarderHealthStatus.conf b/certora/conf/healthStatus/BalanceForwarderHealthStatus.conf
new file mode 100644
index 00000000..acdd8250
--- /dev/null
+++ b/certora/conf/healthStatus/BalanceForwarderHealthStatus.conf
@@ -0,0 +1,41 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/BalanceForwarder.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/BalanceForwarderHSHarness.sol",
+ ],
+ "link": [
+ "BalanceForwarderHSHarness:evc=EVCHarness",
+ ],
+ "verify": "BalanceForwarderHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy"
+ ],
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["BalanceForwarderHSHarness"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-splitParallel true",
+ "-deleteSMTFile false",
+ "-smt_easy_LIA true"
+ ],
+ "smt_timeout": "4800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/BorrowingHealthStatus.conf b/certora/conf/healthStatus/BorrowingHealthStatus.conf
new file mode 100644
index 00000000..2892eb49
--- /dev/null
+++ b/certora/conf/healthStatus/BorrowingHealthStatus.conf
@@ -0,0 +1,43 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/BorrowingHSHarness.sol",
+ ],
+ "link": [
+ "BorrowingHSHarness:evc=EVCHarness"
+ ],
+ "verify": "BorrowingHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy"
+ ],
+ // "method" : "borrow(uint256,address)",
+ // "method" : "pullDebt(uint256,address)",
+ // "method" : "repayWithShares(uint256,address)",
+ // "method" : "repay(uint256,address)",
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["BorrowingHSHarness"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-smt_easy_LIA true"
+ ],
+ "smt_timeout": "28800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/ETokenCollateralHealthStatus.conf b/certora/conf/healthStatus/ETokenCollateralHealthStatus.conf
new file mode 100644
index 00000000..9e18f729
--- /dev/null
+++ b/certora/conf/healthStatus/ETokenCollateralHealthStatus.conf
@@ -0,0 +1,42 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/BalanceForwarder.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/BalanceForwarderHSHarness.sol",
+ ],
+ "link": [
+ "BalanceForwarderHSHarness:evc=EVCHarness",
+ "DummyETokenA:evc=EVCHarness",
+ ],
+ "verify": "BalanceForwarderHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy"
+ ],
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["DummyETokenA"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ // "prover_args": [
+ // "-splitParallel true",
+ // "-deleteSMTFile false",
+ // "-smt_easy_LIA true"
+ // ],
+ "smt_timeout": "28800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/GovernanceHealthStatus.conf b/certora/conf/healthStatus/GovernanceHealthStatus.conf
new file mode 100644
index 00000000..7d342fe3
--- /dev/null
+++ b/certora/conf/healthStatus/GovernanceHealthStatus.conf
@@ -0,0 +1,42 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/GovernanceHSHarness.sol",
+ ],
+ "link": [
+ "GovernanceHSHarness:evc=EVCHarness",
+ ],
+ "verify": "GovernanceHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy"
+ ],
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["GovernanceHSHarness"],
+ // "prover_args": ["-smt_bitVectorTheory", "true"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-splitParallel true",
+ "-deleteSMTFile false",
+ "-smt_easy_LIA true"
+ ],
+ "smt_timeout": "28800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/InitializeHealthStatus.conf b/certora/conf/healthStatus/InitializeHealthStatus.conf
new file mode 100644
index 00000000..8e70b6bf
--- /dev/null
+++ b/certora/conf/healthStatus/InitializeHealthStatus.conf
@@ -0,0 +1,41 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/InitializeHSHarness.sol",
+ ],
+ "link": [
+ "InitializeHSHarness:evc=EVCHarness",
+ ],
+ "verify": "InitializeHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy"
+ ],
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["InitializeHSHarness"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-splitParallel true",
+ "-deleteSMTFile false",
+ "-smt_easy_LIA true"
+ ],
+ "smt_timeout": "28800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/LiquidateHealthStatus.conf b/certora/conf/healthStatus/LiquidateHealthStatus.conf
new file mode 100644
index 00000000..12d41059
--- /dev/null
+++ b/certora/conf/healthStatus/LiquidateHealthStatus.conf
@@ -0,0 +1,42 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/LiquidationHSHarness.sol",
+ ],
+ "link": [
+ "LiquidationHSHarness:evc=EVCHarness",
+ ],
+ "verify": "LiquidationHSHarness:certora/specs/LiquidateHealthStatus.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "liquidateAccountsStayHealthy_liquidator_no_debt_socialization",
+ "liquidateAccountsStayHealthy_liquidator_with_debt_socialization",
+ "liquidateAccountsStayHealthy_not_violator"
+ ],
+ "method":"liquidate(address,address,uint256,uint256)",
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["LiquidationHSHarness"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-smt_easy_LIA true"
+ ],
+ "smt_timeout": "28800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/LiquidationHealthStatus.conf b/certora/conf/healthStatus/LiquidationHealthStatus.conf
new file mode 100644
index 00000000..62ba115d
--- /dev/null
+++ b/certora/conf/healthStatus/LiquidationHealthStatus.conf
@@ -0,0 +1,39 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/LiquidationHSHarness.sol",
+ ],
+ "link": [
+ "LiquidationHSHarness:evc=EVCHarness",
+ ],
+ "verify": "LiquidationHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy"
+ ],
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["LiquidationHSHarness"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ " -smt_easy_LIA true"
+ ],
+ "smt_timeout": "28800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/README.md b/certora/conf/healthStatus/README.md
new file mode 100644
index 00000000..caf79ad0
--- /dev/null
+++ b/certora/conf/healthStatus/README.md
@@ -0,0 +1,8 @@
+Note that for most modules the spec HealthStatusInvariant.spec is used, but for Liquidation,
+the spec needs to be split into more cases for performance reasons so it uses LiquidateHealthStatus.spec
+
+Also note that for ETokenCollateralHealthStatus is used to verify functions called on the collateral
+EToken contract rather than the vault under test, and UnderlyingTokenHealthStatus is used to verify
+functions called on the underlying asset.
+
+To run all of these configurations easily, use certora/scripts/runHealthStatusAllModules.py
\ No newline at end of file
diff --git a/certora/conf/healthStatus/TokenHealthStatus.conf b/certora/conf/healthStatus/TokenHealthStatus.conf
new file mode 100644
index 00000000..d9261854
--- /dev/null
+++ b/certora/conf/healthStatus/TokenHealthStatus.conf
@@ -0,0 +1,41 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/Vault.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/TokenHSHarness.sol",
+ ],
+ "link": [
+ "TokenHSHarness:evc=EVCHarness",
+ ],
+ "verify": "TokenHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy"
+ ],
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["TokenHSHarness"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-splitParallel true",
+ "-deleteSMTFile false",
+ "-smt_easy_LIA true"
+ ],
+ "smt_timeout": "28800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/UnderlyingTokenHealthStatus.conf b/certora/conf/healthStatus/UnderlyingTokenHealthStatus.conf
new file mode 100644
index 00000000..e84a7b49
--- /dev/null
+++ b/certora/conf/healthStatus/UnderlyingTokenHealthStatus.conf
@@ -0,0 +1,41 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/BalanceForwarder.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/BalanceForwarderHSHarness.sol",
+ ],
+ "link": [
+ "BalanceForwarderHSHarness:evc=EVCHarness",
+ ],
+ "verify": "BalanceForwarderHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy"
+ ],
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["DummyERC20A"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-splitParallel true",
+ "-deleteSMTFile false",
+ "-smt_easy_LIA true"
+ ],
+ "smt_timeout": "4800",
+}
\ No newline at end of file
diff --git a/certora/conf/healthStatus/VaultHealthStatus.conf b/certora/conf/healthStatus/VaultHealthStatus.conf
new file mode 100644
index 00000000..ac2c3d5b
--- /dev/null
+++ b/certora/conf/healthStatus/VaultHealthStatus.conf
@@ -0,0 +1,43 @@
+{
+ "files": [
+ "certora/harness/EVCHarness.sol",
+ "lib/ethereum-vault-connector/src/ExecutionContext.sol",
+ "certora/helpers/DummyERC20A.sol",
+ "certora/helpers/DummyETokenA.sol",
+ "certora/helpers/DummyETokenB.sol",
+ "src/EVault/modules/Vault.sol",
+ "src/EVault/modules/Token.sol",
+ "certora/harness/BaseHarness.sol",
+ "certora/harness/healthStatus/VaultHSHarness.sol",
+ ],
+ "link": [
+ "VaultHSHarness:evc=EVCHarness",
+ ],
+ "verify": "VaultHSHarness:certora/specs/HealthStatusInvariant.spec",
+ "solc": "solc8.23",
+ "packages": [
+ "ethereum-vault-connector=lib/ethereum-vault-connector/src",
+ "forge-std=lib/forge-std/src"
+ ],
+ "rule" : [
+ "accountsStayHealthy_strategy",
+ ],
+ "build_cache": true,
+ "prover_version": "master",
+ "server" : "staging",
+ "parametric_contracts": ["VaultHSHarness"],
+ "optimistic_loop": true,
+ "loop_iter": "2",
+ "solc_via_ir": true,
+ "solc_optimize": "10000",
+ "rule_sanity": "basic",
+ "function_finder_mode" : "relaxed",
+ "finder_friendly_optimizer" : false,
+ "prover_args": [
+ "-maxConcurrentTransforms INLINED_HOOKS:8",
+ "-splitParallel true",
+ "-deleteSMTFile false",
+ "-smt_easy_LIA true"
+ ],
+ "smt_timeout": "28800",
+}
\ No newline at end of file
diff --git a/certora/harness/AbstractBaseHarness.sol b/certora/harness/AbstractBaseHarness.sol
new file mode 100644
index 00000000..5dfdf2c6
--- /dev/null
+++ b/certora/harness/AbstractBaseHarness.sol
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.0;
+
+import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector.sol";
+import "../../src/EVault/shared/Base.sol";
+// Needed for checkLiquidityReturning:
+import {LiquidityUtils} from "../../src/EVault/shared/LiquidityUtils.sol";
+
+// This exists so that Base.LTVConfig and other type declarations
+// are available in CVL and can be used across specs for different modules.
+// We need to split this into a concrete contract and an Abstract contract
+// so that we can refer to Base.LTVConfig as a type in shared CVL functions
+// while also making function definitions sharable among harnesses via
+// AbstractBase. AbstractBaseHarness includes the shared function definitions.
+abstract contract AbstractBaseHarness is Base, LiquidityUtils {
+
+ function getLTVConfig(address collateral) external view returns (LTVConfig memory) {
+ return vaultStorage.ltvLookup[collateral];
+ }
+
+ function vaultCacheOracleConfigured() external returns (bool) {
+ return address(loadVault().oracle) != address(0);
+ }
+
+ function isAccountStatusCheckDeferredExt(address account) external view returns (bool) {
+ return isAccountStatusCheckDeferred(account);
+ }
+
+ function getBalanceAndForwarderExt(address account) public returns (Shares, bool) {
+ return vaultStorage.users[account].getBalanceAndBalanceForwarder();
+ }
+
+ function checkAccountMagicValue() public view returns (bytes4) {
+ return IEVCVault.checkAccountStatus.selector;
+ }
+
+ function checkAccountMagicValueMemory() public view returns (bytes memory) {
+ return abi.encodeWithSelector(IEVCVault.checkAccountStatus.selector);
+ }
+
+ function checkVaultMagicValueMemory() public view returns (bytes memory) {
+ return abi.encodeWithSelector(IEVCVault.checkVaultStatus.selector);
+ }
+
+ function getUserInterestAccumulator(address account) public view returns (uint256) {
+ return vaultStorage.users[account].interestAccumulator;
+ }
+
+ // This mirrors LiquidityUtils.checkLiquidity except that it returns
+ // a bool rather than reverting.
+ function checkLiquidityReturning(address account, address[] memory collaterals) public returns (bool) {
+ VaultCache memory vaultCache = loadVault();
+
+ Owed owed = vaultStorage.users[account].getOwed();
+ if (owed.isZero()) return true;
+
+ uint256 liabilityValue = getLiabilityValue(vaultCache, account, owed, false);
+
+ uint256 collateralValue;
+ for (uint256 i; i < collaterals.length; ++i) {
+ collateralValue += getCollateralValue(vaultCache, account, collaterals[i], false);
+ if (collateralValue > liabilityValue) return true;
+ }
+
+ return false;
+ }
+
+
+ //--------------------------------------------------------------------------
+ // Controllers
+ //--------------------------------------------------------------------------
+ function vaultIsOnlyController(address account) external view returns (bool) {
+ address[] memory controllers = IEVC(evc).getControllers(account);
+ return controllers.length == 1 && controllers[0] == address(this);
+ }
+
+ function vaultIsController(address account) external view returns (bool) {
+ return IEVC(evc).isControllerEnabled(account, address(this));
+ }
+
+ //--------------------------------------------------------------------------
+ // Collaterals
+ //--------------------------------------------------------------------------
+ function getCollateralsExt(address account) public view returns (address[] memory) {
+ return getCollaterals(account);
+ }
+
+ function isCollateralEnabledExt(address account, address market) external view returns (bool) {
+ return isCollateralEnabled(account, market);
+ }
+
+
+ //--------------------------------------------------------------------------
+ // Operation disable checks
+ //--------------------------------------------------------------------------
+ function isOperationDisabledExt(uint32 operation) public returns (bool) {
+ VaultCache memory vaultCache = updateVault();
+ return isOperationDisabled(vaultCache.hookedOps, operation);
+ }
+
+ function isDepositDisabled() public returns (bool) {
+ return isOperationDisabledExt(OP_DEPOSIT);
+ }
+
+ function isMintDisabled() public returns (bool) {
+ return isOperationDisabledExt(OP_MINT);
+ }
+
+ function isWithdrawDisabled() public returns (bool) {
+ return isOperationDisabledExt(OP_WITHDRAW);
+ }
+
+ function isRedeemDisabled() public returns (bool) {
+ return isOperationDisabledExt(OP_REDEEM);
+ }
+
+ function isSkimDisabled() public returns (bool) {
+ return isOperationDisabledExt(OP_SKIM);
+ }
+
+ //--------------------------------------------------------------------------
+ // VaultStorage Accessors:
+ //--------------------------------------------------------------------------
+ function storage_lastInterestAccumulatorUpdate() public view returns (uint48) {
+ return vaultStorage.lastInterestAccumulatorUpdate;
+ }
+ function storage_cash() public view returns (Assets) {
+ return vaultStorage.cash;
+ }
+ function storage_supplyCap() public view returns (uint256) {
+ return vaultStorage.supplyCap.resolve();
+ }
+ function storage_borrowCap() public view returns (uint256) {
+ return vaultStorage.borrowCap.resolve();
+ }
+ // reentrancyLocked seems not direclty used in loadVault
+ function storage_hookedOps() public view returns (Flags) {
+ return vaultStorage.hookedOps;
+ }
+ function storage_snapshotInitialized() public view returns (bool) {
+ return vaultStorage.snapshotInitialized;
+ }
+ function storage_totalShares() public view returns (Shares) {
+ return vaultStorage.totalShares;
+ }
+ function storage_totalBorrows() public view returns (Owed) {
+ return vaultStorage.totalBorrows;
+ }
+ function storage_accumulatedFees() public view returns (Shares) {
+ return vaultStorage.accumulatedFees;
+ }
+ function storage_interestAccumulator() public view returns (uint256) {
+ return vaultStorage.interestAccumulator;
+ }
+ function storage_configFlags() public view returns (Flags) {
+ return vaultStorage.configFlags;
+ }
+
+
+}
\ No newline at end of file
diff --git a/certora/harness/BaseHarness.sol b/certora/harness/BaseHarness.sol
new file mode 100644
index 00000000..304eeeca
--- /dev/null
+++ b/certora/harness/BaseHarness.sol
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.0;
+
+import "../../src/EVault/shared/Base.sol";
+import "../../certora/harness/AbstractBaseHarness.sol";
+
+// This exists so that Base.LTVConfig and other type declarations
+// are available in CVL and can be used across specs for different modules.
+// We need to split this into a concrete contract and an Abstract contract
+// so that we can refer to Base.LTVConfig as a type in shared CVL functions
+// while also making function definitions sharable among harnesses via
+// AbstractBase.
+contract BaseHarness is Base, AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/CacheHarness.sol b/certora/harness/CacheHarness.sol
new file mode 100644
index 00000000..1676ba0c
--- /dev/null
+++ b/certora/harness/CacheHarness.sol
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.0;
+
+import "../../src/EVault/shared/Cache.sol";
+
+contract CacheHarness is Cache {
+ function updateVaultExt() external virtual returns (VaultCache memory vaultCache) {
+ updateVault();
+ }
+ function initVaultCacheExt(VaultCache memory vaultCache) external view returns (bool dirty) {
+ return initVaultCache(vaultCache);
+ }
+ function getlastInterestAccumulatorUpdate() external view returns (uint256) {
+ return vaultStorage.lastInterestAccumulatorUpdate;
+ }
+ function getTotalBorrows() external view returns (Owed) {
+ return vaultStorage.totalBorrows;
+ }
+ function getInterestAcc() external view returns (uint256) {
+ return vaultStorage.interestAccumulator;
+ }
+ function getAccumulatedFees() external view returns (Shares) {
+ return vaultStorage.accumulatedFees;
+ }
+ function getTotalShares() external view returns (Shares) {
+ return vaultStorage.totalShares;
+ }
+
+}
\ No newline at end of file
diff --git a/certora/harness/ERC4626Harness.sol b/certora/harness/ERC4626Harness.sol
new file mode 100644
index 00000000..50874401
--- /dev/null
+++ b/certora/harness/ERC4626Harness.sol
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.0;
+import "../../certora/harness/AbstractBaseHarness.sol";
+import "../../src/EVault/modules/Vault.sol";
+import "../../src/EVault/modules/Token.sol";
+
+contract ERC4626Harness is VaultModule, TokenModule, AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+
+ // Linked against DummyERC20A in verification config
+ IERC20 underlying_asset;
+
+ function userAssets(address user) public view returns (uint256) { // harnessed
+ // The assets in the underlying asset contract (not in the vault)
+ return IERC20(asset()).balanceOf(user);
+ }
+
+ function updateVault() internal override returns (VaultCache memory vaultCache) {
+ // initVaultCache is difficult to summarize because we can't
+ // reason about the pass-by-value VaultCache at the start and
+ // end of the call as separate values. So this harness
+ // gives us a way to keep the loadVault summary when updateVault
+ // is called
+ vaultCache = loadVault();
+ if(block.timestamp - vaultCache.lastInterestAccumulatorUpdate > 0) {
+ vaultStorage.lastInterestAccumulatorUpdate = vaultCache.lastInterestAccumulatorUpdate;
+ vaultStorage.accumulatedFees = vaultCache.accumulatedFees;
+
+ vaultStorage.totalShares = vaultCache.totalShares;
+ vaultStorage.totalBorrows = vaultCache.totalBorrows;
+
+ vaultStorage.interestAccumulator = vaultCache.interestAccumulator;
+ }
+ return vaultCache;
+ }
+
+ function toSharesExt(uint256 amount) external view returns (uint256) {
+ require(amount < MAX_SANE_AMOUNT, "Assets are really uint112");
+ VaultCache memory vaultCache = loadVault();
+ return Assets.wrap(uint112(amount)).toSharesDownUint(vaultCache);
+ }
+
+ function cache_cash() public view returns (Assets) {
+ return loadVault().cash;
+ }
+
+}
diff --git a/certora/harness/EVCHarness.sol b/certora/harness/EVCHarness.sol
new file mode 100644
index 00000000..1e2ab8f4
--- /dev/null
+++ b/certora/harness/EVCHarness.sol
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.19;
+
+import "../../lib/ethereum-vault-connector/src/EthereumVaultConnector.sol";
+contract EVCHarness is EthereumVaultConnector {
+ using ExecutionContext for EC;
+ using Set for SetStorage;
+
+ // Trigger the (deferred) status checks in restoreExecutionContext
+ // explicitly.
+ function checkStatusAllExt() external {
+ checkStatusAll(SetType.Account);
+ }
+}
\ No newline at end of file
diff --git a/certora/harness/EVaultHarness.sol b/certora/harness/EVaultHarness.sol
new file mode 100644
index 00000000..041185f2
--- /dev/null
+++ b/certora/harness/EVaultHarness.sol
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.0;
+
+import {Base} from "../../src/EVault/shared/Base.sol";
+
+import {TokenModule} from "../../src/EVault/modules/Token.sol";
+import {VaultModule} from "../../src/EVault/modules/Vault.sol";
+import {BorrowingModule} from "../../src/EVault/modules/Borrowing.sol";
+import {LiquidationModule} from "../../src/EVault/modules/Liquidation.sol";
+import {InitializeModule} from "../../src/EVault/modules/Initialize.sol";
+import {BalanceForwarderModule} from "../../src/EVault/modules/BalanceForwarder.sol";
+import {GovernanceModule} from "../../src/EVault/modules/Governance.sol";
+import {RiskManagerModule} from "../../src/EVault/modules/RiskManager.sol";
+
+import "../../certora/harness/AbstractBaseHarness.sol";
+
+contract EVaultHarness is
+ Base,
+ InitializeModule,
+ TokenModule,
+ VaultModule,
+ BorrowingModule,
+ LiquidationModule,
+ RiskManagerModule,
+ BalanceForwarderModule,
+ GovernanceModule,
+ AbstractBaseHarness
+{
+
+ constructor(
+ Integrations memory integrations
+ ) Base(integrations) {}
+
+ // Unlike EVault.sol, does not override methods with the useView pattern
+}
\ No newline at end of file
diff --git a/certora/harness/ModuleDispatchHarness.sol b/certora/harness/ModuleDispatchHarness.sol
new file mode 100644
index 00000000..f1000bf1
--- /dev/null
+++ b/certora/harness/ModuleDispatchHarness.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+
+pragma solidity ^0.8.0;
+
+import "../../src/EVault/modules/ModuleDispatch.sol";
+
+contract ModuleDispatchHarness is ModuleDispatch {
+ constructor(Integrations memory integrations) Base(integrations) {}
+}
diff --git a/certora/harness/TokenHarness.sol b/certora/harness/TokenHarness.sol
new file mode 100644
index 00000000..f3d5dbae
--- /dev/null
+++ b/certora/harness/TokenHarness.sol
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+
+import "../../src/EVault/modules/Token.sol";
+import "../../src/EVault/shared/types/Types.sol";
+
+contract TokenHarness is TokenModule {
+ // for amount.toShares()
+ using TypesLib for uint256;
+
+ constructor(Integrations memory integrations) Base(integrations) {}
+
+ function transferFromInternalHarnessed(address from, address to, uint256 amount) public returns (bool) {
+ // This is similar to the body of Token.transferFromInternal
+ // when it gets its arguments from Token.transfer.
+ // It is not harnessed directly since Token.transferFromInternal is private
+ // and we want to avoid munging.
+ // This is used for the enforceCollateralTransfer function
+ Shares shares = amount.toShares();
+ if (from == to) revert E_SelfTransfer();
+ decreaseAllowance(from, from, shares);
+ transferBalance(from, to, shares);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/certora/harness/healthStatus/BalanceForwarderHSHarness.sol b/certora/harness/healthStatus/BalanceForwarderHSHarness.sol
new file mode 100644
index 00000000..0dee46fc
--- /dev/null
+++ b/certora/harness/healthStatus/BalanceForwarderHSHarness.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+import "../../../src/EVault/modules/BalanceForwarder.sol";
+
+// To prove the Health Status rule we need to include the RiskManager module
+// which implemeants the status check
+contract BalanceForwarderHSHarness is BalanceForwarderModule, RiskManagerModule,
+ AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/healthStatus/BorrowingHSHarness.sol b/certora/harness/healthStatus/BorrowingHSHarness.sol
new file mode 100644
index 00000000..2b9329ec
--- /dev/null
+++ b/certora/harness/healthStatus/BorrowingHSHarness.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+import "../../../src/EVault/modules/Borrowing.sol";
+
+// To prove the Health Status rule we need to include the RiskManager module
+// which implemeants the status check
+contract BorrowingHSHarness is BorrowingModule, RiskManagerModule,
+ AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/healthStatus/GovernanceHSHarness.sol b/certora/harness/healthStatus/GovernanceHSHarness.sol
new file mode 100644
index 00000000..5a9645f6
--- /dev/null
+++ b/certora/harness/healthStatus/GovernanceHSHarness.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+import "../../../src/EVault/modules/Governance.sol";
+
+// To prove the Health Status rule we need to include the RiskManager module
+// which implemeants the status check
+contract GovernanceHSHarness is GovernanceModule, RiskManagerModule,
+ AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/healthStatus/InitializeHSHarness.sol b/certora/harness/healthStatus/InitializeHSHarness.sol
new file mode 100644
index 00000000..691cbfea
--- /dev/null
+++ b/certora/harness/healthStatus/InitializeHSHarness.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+import "../../../src/EVault/modules/Initialize.sol";
+
+// To prove the Health Status rule we need to include the RiskManager module
+// which implemeants the status check
+contract InitializeHSHarness is InitializeModule, RiskManagerModule,
+ AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/healthStatus/LiquidationHSHarness.sol b/certora/harness/healthStatus/LiquidationHSHarness.sol
new file mode 100644
index 00000000..e6479576
--- /dev/null
+++ b/certora/harness/healthStatus/LiquidationHSHarness.sol
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+import "../../../src/EVault/modules/Liquidation.sol";
+
+// To prove the Health Status rule we need to include the RiskManager module
+// which implemeants the status check
+contract LiquidationHSHarness is LiquidationModule, RiskManagerModule,
+ AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+
+ function hasDebtSocialization() external returns (bool) {
+ VaultCache memory vaultCache = loadVault();
+ return vaultCache.configFlags.isNotSet(CFG_DONT_SOCIALIZE_DEBT);
+ }
+}
\ No newline at end of file
diff --git a/certora/harness/healthStatus/TokenHSHarness.sol b/certora/harness/healthStatus/TokenHSHarness.sol
new file mode 100644
index 00000000..935ab6c7
--- /dev/null
+++ b/certora/harness/healthStatus/TokenHSHarness.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+import "../../../src/EVault/modules/Token.sol";
+
+// To prove the Health Status rule we need to include the RiskManager module
+// which implemeants the status check
+contract TokenHSHarness is TokenModule, RiskManagerModule,
+ AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/healthStatus/VaultHSHarness.sol b/certora/harness/healthStatus/VaultHSHarness.sol
new file mode 100644
index 00000000..990f39e1
--- /dev/null
+++ b/certora/harness/healthStatus/VaultHSHarness.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+import "../../../src/EVault/modules/Vault.sol";
+
+// To prove the Health Status rule we need to include the RiskManager module
+// which implemeants the status check
+contract VaultHSHarness is VaultModule, RiskManagerModule,
+ AbstractBaseHarness {
+ constructor(Integrations memory integrations) Base(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/modules/BalanceForwarderHarness.sol b/certora/harness/modules/BalanceForwarderHarness.sol
new file mode 100644
index 00000000..53709dbe
--- /dev/null
+++ b/certora/harness/modules/BalanceForwarderHarness.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/BalanceForwarder.sol";
+
+contract BalanceForwarderHarness is BalanceForwarder, AbstractBaseHarness {
+ constructor(Integrations memory integrations) BalanceForwarder(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/modules/BorrowingHarness.sol b/certora/harness/modules/BorrowingHarness.sol
new file mode 100644
index 00000000..0fde5614
--- /dev/null
+++ b/certora/harness/modules/BorrowingHarness.sol
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/Borrowing.sol";
+
+contract BorrowingHarness is Borrowing, AbstractBaseHarness {
+ constructor(Integrations memory integrations) Borrowing (integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/modules/GovernanceHarness.sol b/certora/harness/modules/GovernanceHarness.sol
new file mode 100644
index 00000000..e5d2c08d
--- /dev/null
+++ b/certora/harness/modules/GovernanceHarness.sol
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+import "../../../src/EVault/modules/Governance.sol";
+
+contract GovernanceHarness is Governance, AbstractBaseHarness, RiskManagerModule{
+ constructor(Integrations memory integrations) Governance (integrations) {}
+
+ function getAccountBalance(address account) external view returns (Shares balance){
+ UserStorage storage user = vaultStorage.users[account];
+ (balance, ) = user.getBalanceAndBalanceForwarder();
+ }
+
+ function getGovernorReceiver() external view returns (address governorReceiver){
+ governorReceiver = vaultStorage.feeReceiver;
+ }
+
+ function getProtocolFeeConfig(address vault) external view returns (address protocolReceiver, uint16 protocolFee){
+ (protocolReceiver, protocolFee) = protocolConfig.protocolFeeConfig(address(this));
+ }
+
+ function getTotalShares() external view returns (Shares){
+ return vaultStorage.totalShares;
+ }
+
+ function getAccumulatedFees() external view returns (Shares){
+ VaultCache memory vaultCache;
+ initVaultCache(vaultCache);
+ return vaultCache.accumulatedFees;
+ }
+
+ function getLastAccumulated() external view returns (uint256){
+ return uint256(vaultStorage.lastInterestAccumulatorUpdate);
+ }
+
+ function getLTVHarness(address collateral, bool liquidation) public view virtual returns (ConfigAmount) {
+ return getLTV(collateral, liquidation);
+ }
+
+}
\ No newline at end of file
diff --git a/certora/harness/modules/InitializeHarness.sol b/certora/harness/modules/InitializeHarness.sol
new file mode 100644
index 00000000..f71b9ce7
--- /dev/null
+++ b/certora/harness/modules/InitializeHarness.sol
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/Initialize.sol";
+
+contract InitializeHarness is Initialize, AbstractBaseHarness {
+ constructor(Integrations memory integrations) Initialize(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/modules/LiquidationHarness.sol b/certora/harness/modules/LiquidationHarness.sol
new file mode 100644
index 00000000..baa0908e
--- /dev/null
+++ b/certora/harness/modules/LiquidationHarness.sol
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/Liquidation.sol";
+
+contract LiquidationHarness is AbstractBaseHarness, Liquidation {
+
+ constructor(Integrations memory integrations) Liquidation(integrations) {}
+
+ function calculateLiquidityExternal(
+ address account
+ ) public view returns (uint256 collateralValue, uint256 liabilityValue) {
+ return calculateLiquidity(loadVault(), account, getCollaterals(account), true);
+ }
+
+ function calculateLiquidityLiquidation(
+ address account
+ ) public view returns (uint256 collateralValue, uint256 liabilityValue) {
+ return calculateLiquidity(loadVault(), account, getCollaterals(account), false);
+ }
+
+
+ function calculateLiquidationExt(
+ VaultCache memory vaultCache,
+ address liquidator,
+ address violator,
+ address collateral,
+ uint256 desiredRepay
+ ) external view returns (LiquidationCache memory liqCache) {
+ return calculateLiquidation(vaultCache, liquidator, violator, collateral, desiredRepay);
+ }
+
+ function isRecognizedCollateralExt(address collateral) external view virtual returns (bool) {
+ return isRecognizedCollateral(collateral);
+ }
+
+ function getLiquidator() external returns (address liquidator) {
+ (, liquidator) = initOperation(OP_LIQUIDATE, CHECKACCOUNT_CALLER);
+ }
+
+ function getCurrentOwedExt(VaultCache memory vaultCache, address violator) external view returns (Assets) {
+ return getCurrentOwed(vaultCache, violator).toAssetsUp();
+ }
+
+ function getCollateralValueExt(VaultCache memory vaultCache, address account, address collateral, bool liquidation)
+ external
+ view
+ returns (uint256 value) {
+ return getCollateralValue(vaultCache, account, collateral, liquidation);
+ }
+
+}
\ No newline at end of file
diff --git a/certora/harness/modules/RiskManagerHarness.sol b/certora/harness/modules/RiskManagerHarness.sol
new file mode 100644
index 00000000..568dc9bf
--- /dev/null
+++ b/certora/harness/modules/RiskManagerHarness.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import "../../../src/interfaces/IPriceOracle.sol";
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/RiskManager.sol";
+
+contract RiskManagerHarness is RiskManager, AbstractBaseHarness {
+ constructor(Integrations memory integrations) RiskManager(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/modules/TokenHarness.sol b/certora/harness/modules/TokenHarness.sol
new file mode 100644
index 00000000..9c9fa98e
--- /dev/null
+++ b/certora/harness/modules/TokenHarness.sol
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/Token.sol";
+
+contract TokenHarness is Token, AbstractBaseHarness {
+ constructor(Integrations memory integrations) Token(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/harness/modules/VaultHarness.sol b/certora/harness/modules/VaultHarness.sol
new file mode 100644
index 00000000..46a6403b
--- /dev/null
+++ b/certora/harness/modules/VaultHarness.sol
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity ^0.8.0;
+import {ERC20} from "../../../lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+import "../../../certora/harness/AbstractBaseHarness.sol";
+import "../../../src/EVault/modules/Vault.sol";
+
+contract VaultHarness is Vault, AbstractBaseHarness {
+ constructor(Integrations memory integrations) Vault(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/helpers/DummyERC20A.sol b/certora/helpers/DummyERC20A.sol
new file mode 100644
index 00000000..188b9260
--- /dev/null
+++ b/certora/helpers/DummyERC20A.sol
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+import "./DummyERC20Impl.sol";
+
+contract DummyERC20A is DummyERC20Impl {}
\ No newline at end of file
diff --git a/certora/helpers/DummyERC20B.sol b/certora/helpers/DummyERC20B.sol
new file mode 100644
index 00000000..0f97f1ef
--- /dev/null
+++ b/certora/helpers/DummyERC20B.sol
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+import "./DummyERC20Impl.sol";
+
+contract DummyERC20B is DummyERC20Impl {}
\ No newline at end of file
diff --git a/certora/helpers/DummyERC20Impl.sol b/certora/helpers/DummyERC20Impl.sol
new file mode 100644
index 00000000..afcb8bee
--- /dev/null
+++ b/certora/helpers/DummyERC20Impl.sol
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+
+// with mint
+contract DummyERC20Impl {
+ uint256 t;
+ mapping (address => uint256) b;
+ mapping (address => mapping (address => uint256)) a;
+
+ // string private name; // change public to private
+ // string private symbol; // change public to private
+ uint public decimals;
+
+ function name() public returns (string memory) { // added for testing
+ return "";
+ }
+
+ function symbol() public returns (string memory) { // added for testing
+ return "";
+ }
+
+ function myAddress() public returns (address) {
+ return address(this);
+ }
+
+ function add(uint a, uint b) internal pure returns (uint256) {
+ uint c = a +b;
+ require (c >= a);
+ return c;
+ }
+ function sub(uint a, uint b) internal pure returns (uint256) {
+ require (a>=b);
+ return a-b;
+ }
+
+ function totalSupply() external view returns (uint256) {
+ return t;
+ }
+ function balanceOf(address account) external view returns (uint256) {
+ return b[account];
+ }
+ function transfer(address recipient, uint256 amount) external returns (bool) {
+ b[msg.sender] = sub(b[msg.sender], amount);
+ b[recipient] = add(b[recipient], amount);
+ return true;
+ }
+ function allowance(address owner, address spender) external view returns (uint256) {
+ return a[owner][spender];
+ }
+ function approve(address spender, uint256 amount) external returns (bool) {
+ a[msg.sender][spender] = amount;
+ return true;
+ }
+
+ function transferFrom(
+ address sender,
+ address recipient,
+ uint256 amount
+ ) external returns (bool) {
+ b[sender] = sub(b[sender], amount);
+ b[recipient] = add(b[recipient], amount);
+ a[sender][msg.sender] = sub(a[sender][msg.sender], amount);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/certora/helpers/DummyETokenA.sol b/certora/helpers/DummyETokenA.sol
new file mode 100644
index 00000000..b669c001
--- /dev/null
+++ b/certora/helpers/DummyETokenA.sol
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+
+import "certora/harness/TokenHarness.sol";
+
+contract DummyETokenA is TokenHarness {
+ constructor(Integrations memory integrations) TokenHarness(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/helpers/DummyETokenB.sol b/certora/helpers/DummyETokenB.sol
new file mode 100644
index 00000000..e72c7059
--- /dev/null
+++ b/certora/helpers/DummyETokenB.sol
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+
+import "certora/harness/TokenHarness.sol";
+
+contract DummyETokenB is TokenHarness {
+ constructor(Integrations memory integrations) TokenHarness(integrations) {}
+}
\ No newline at end of file
diff --git a/certora/scripts/runERC4626RulesSeparatelyBaseConf.py b/certora/scripts/runERC4626RulesSeparatelyBaseConf.py
new file mode 100644
index 00000000..82886034
--- /dev/null
+++ b/certora/scripts/runERC4626RulesSeparatelyBaseConf.py
@@ -0,0 +1,39 @@
+import argparse
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-M', '--batchMsg', metavar='M', type=str, nargs='?',
+ default='',
+ help='a message for all the jobs')
+
+# The commented out ones are the ones that need a special config file.
+# Those can be run easily by running runERC4626RulesSplitConfs.py
+rule_names = [
+ # "assetsMoreThanSupply",
+ "contributingProducesShares",
+ "conversionOfZero",
+ "conversionWeakIntegrity",
+ "conversionWeakMonotonicity",
+ # "convertToAssetsWeakAdditivity",
+ # "convertToSharesWeakAdditivity",
+ # "depositMonotonicity",
+ # "dustFavorsTheHouse",
+ # "dustFavorsTheHouseAssets",
+ # "noAssetsIfNoSupply",
+ # "noSupplyIfNoAssets",
+ # "onlyContributionMethodsReduceAssets",
+ "reclaimingProducesAssets",
+ "redeemingAllValidity",
+ "totalSupplyIsSumOfBalances",
+ # "totalsMonotonicity",
+ "zeroDepositZeroShares",
+ "underlyingCannotChange",
+ # "vaultSolvency",
+]
+
+for name in rule_names:
+ args = parser.parse_args()
+ script = "certora/conf/ERC4626Split/VaultERC4626.conf"
+ command = f"certoraRun {script} --rule \"{name}\" --msg \"{name} : {args.batchMsg}\""
+ print(f"runing {command}")
+ subprocess.run(command, shell=True)
\ No newline at end of file
diff --git a/certora/scripts/runERC4626RulesSplitConfs.py b/certora/scripts/runERC4626RulesSplitConfs.py
new file mode 100644
index 00000000..0f665aac
--- /dev/null
+++ b/certora/scripts/runERC4626RulesSplitConfs.py
@@ -0,0 +1,34 @@
+import argparse
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-M', '--batchMsg', metavar='M', type=str, nargs='?',
+ default='',
+ help='a message for all the jobs')
+
+erc4626_confs = {
+ "",
+ "-assetsMoreThanSupply",
+ "-convertToAssetsWeakAdditivity",
+ "-convertToSharesWeakAdditivity",
+ "-depositMonotonicity",
+ "-dustFavorsTheHouse",
+ "-dustFavorsTheHouseAssets",
+ "-noAssetsIfNoSupply",
+ "-noSupplyIfNoAssets",
+ "-onlyContributionMethodsReduce",
+ "-totalsMonotonicity",
+ "-vaultSolvency-most",
+ "-vaultSolvency-redeem",
+ "-vaultSolvency-withdraw"
+ # In case the invariant times out for withdraw
+ "-vaultSolvency-withdraw-as-rule"
+}
+
+for name in erc4626_confs:
+ args = parser.parse_args()
+ script = f"certora/conf/ERC4626Split/VaultERC4626{name}.conf"
+ command = f"certoraRun {script} --msg \"{name} : {args.batchMsg}\""
+ print(f"runing {command}")
+ subprocess.run(command, shell=True)
+
diff --git a/certora/scripts/runHealthStatusAllModules.py b/certora/scripts/runHealthStatusAllModules.py
new file mode 100644
index 00000000..78a7c9ad
--- /dev/null
+++ b/certora/scripts/runHealthStatusAllModules.py
@@ -0,0 +1,119 @@
+import argparse
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-M', '--batchMsg', metavar='M', type=str, nargs='?',
+ default='',
+ help='a message for all the jobs')
+args = parser.parse_args()
+
+hs_confs = [
+ "BalanceForwarder",
+ "Borrowing",
+ "Governance",
+ "Initialize",
+ "Liquidation",
+ "Token",
+ "Vault",
+ "ETokenCollateral",
+ "UnderlyingToken"
+]
+
+def runModules():
+ for conf in hs_confs:
+ script = f"certora/conf/healthStatus/{conf}HealthStatus.conf"
+ command = f"certoraRun {script} --msg \"{conf} : {args.batchMsg}\" --rule \"accountsStayHealthy_strategy\""
+ print(f"runing {command}")
+ subprocess.run(command, shell=True)
+
+# List includes all public non-view methods aside from setLTV, clearLTV which
+# are out of scope for the holy grail rule.
+gov_separate_methods = [
+ "convertFees()",
+ "setGovernorAdmin(address)",
+ "setFeeReceiver(address)",
+ "setMaxLiquidationDiscount(uint16)",
+ "setLiquidationCoolOffTime(uint16)",
+ "setInterestRateModel(address)",
+ "setHookConfig(address,uint32)",
+ "setConfigFlags(uint32)",
+ "setCaps(uint16,uint16)",
+ "setInterestFee(uint16)"
+]
+
+def runGovSeparately():
+ for method in gov_separate_methods:
+ script = f"certora/conf/healthStatus/GovernanceHealthStatus.conf"
+ command = f"certoraRun {script} --msg \"Governance.{method} : {args.batchMsg}\" --rule \"accountsStayHealthy_strategy\" --method \"{method}\""
+ print(f"runing {command}")
+ subprocess.run(command, shell=True)
+
+# List includes all public non-view methods
+token_separate_methods = [
+ "transfer(address,uint256)",
+ "transferFromMax(address,address)",
+ "transferFrom(address,address,uint256)",
+ "approve(address,uint256)"
+]
+
+def runTokenSeparately():
+ for method in token_separate_methods:
+ script = f"certora/conf/healthStatus/TokenHealthStatus.conf"
+ command = f"certoraRun {script} --msg \"Token.{method} : {args.batchMsg}\" --rule \"accountsStayHealthy_strategy\" --method \"{method}\""
+ print(f"runing {command}")
+ subprocess.run(command, shell=True)
+
+borrow_separate_methods = [
+ "borrow(uint256,address)",
+ "pullDebt(uint256,address)",
+ "repayWithShares(uint256,address)",
+ "repay(uint256,address)",
+]
+
+def runBorrowSeparately():
+ for method in borrow_separate_methods:
+ script = f"certora/conf/healthStatus/BorrowingHealthStatus.conf"
+ command = f"certoraRun {script} --msg \"Borrow.{method} : {args.batchMsg}\" --rule \"accountsStayHealthy_strategy\" --method \"{method}\""
+ print(f"runing {command}")
+ subprocess.run(command, shell=True)
+
+# Includes all public non-view methods of the vault
+vault_separate_methods = [
+ # Initially just the first two timed out unless run separately,
+ # but now all will run into memory issues unless we run separately
+ # "redeem(uint256,address,address)",
+ # "withdraw(uint256,address,address)",
+ "deposit(uint256,address)",
+ "mint(uint256,address)",
+ "withdraw(uint256,address,address)",
+ "redeem(uint256,address,address)",
+ "skim(uint256,address)",
+]
+
+def runVaultSeparately():
+ for method in vault_separate_methods:
+ script = f"certora/conf/healthStatus/VaultHealthStatus.conf"
+ command = f"certoraRun {script} --msg \"Vault.{method} : {args.batchMsg}\" --rule \"accountsStayHealthy_strategy\" --method \"{method}\""
+ print(f"runing {command}")
+ subprocess.run(command, shell=True)
+
+liquidate_cases = [
+ "liquidateAccountsStayHealthy_liquidator_no_debt_socialization",
+ "liquidateAccountsStayHealthy_liquidator_with_debt_socialization",
+ "liquidateAccountsStayHealthy_not_violator",
+ "liquidateAccountsStayHealthy_account_cur_contract"
+]
+
+def runLiquidateCases():
+ for rule in liquidate_cases:
+ script = f"certora/conf/healthStatus/LiquidateHealthStatus.conf"
+ command = f"certoraRun {script} --msg \"Liquidate case: {rule} : {args.batchMsg}\" --rule \"{rule}\""
+ print(f"runing {command}")
+ subprocess.run(command, shell=True)
+
+runModules()
+runGovSeparately()
+runTokenSeparately()
+runBorrowSeparately()
+runVaultSeparately()
+runLiquidateCases()
\ No newline at end of file
diff --git a/certora/specs/BalanceForwarder.spec b/certora/specs/BalanceForwarder.spec
new file mode 100644
index 00000000..8c0d2b73
--- /dev/null
+++ b/certora/specs/BalanceForwarder.spec
@@ -0,0 +1,27 @@
+import "Base.spec";
+
+methods {
+ function _.balanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) external => NONDET;
+}
+
+//passing:
+// https://prover.certora.com/output/65266/ee7d7eb1364e4589a271b46267aa4742?anonymousKey=82512541ee02ddf9a6830a9555878361946ac19a
+rule enableBalanceForwarder {
+ address account;
+ env e1;
+ env e2;
+ require actualCaller(e1) == account;
+ enableBalanceForwarder(e1);
+ assert balanceForwarderEnabled(e2, account);
+}
+
+// passing:
+// https://prover.certora.com/output/65266/ee7d7eb1364e4589a271b46267aa4742?anonymousKey=82512541ee02ddf9a6830a9555878361946ac19a
+rule disableBalanceForwarder {
+ address account;
+ env e1;
+ env e2;
+ require actualCaller(e1) == account;
+ disableBalanceForwarder(e1);
+ assert !balanceForwarderEnabled(e2, account);
+}
\ No newline at end of file
diff --git a/certora/specs/Base.spec b/certora/specs/Base.spec
new file mode 100644
index 00000000..6b327e8d
--- /dev/null
+++ b/certora/specs/Base.spec
@@ -0,0 +1,108 @@
+using DummyERC20A as erc20;
+using EVCHarness as evc;
+
+methods {
+ // envfree
+ function getLTVConfig(address collateral) external returns (BaseHarness.LTVConfig memory) envfree;
+ function getCollateralsExt(address account) external returns (address[] memory) envfree;
+ function isCollateralEnabledExt(address account, address market) external returns (bool) envfree;
+ function vaultIsOnlyController(address account) external returns (bool) envfree;
+ function isAccountStatusCheckDeferredExt(address account) external returns (bool) envfree;
+ function vaultIsController(address account) external returns (bool) envfree;
+
+ // Inline assembly here gives the tool problems
+ function _.calculateDTokenAddress() internal => NONDET;
+
+ // IPriceOracle
+ function _.getQuote(uint256 amount, address base, address quote) external => CVLGetQuote(amount, base, quote) expect (uint256);
+ function _.getQuotes(uint256 amount, address base, address quote) external => CVLGetQuotes(amount, base, quote) expect (uint256, uint256);
+
+ // ProxyUtils
+ function ProxyUtils.metadata() internal returns (address, address, address)=> CVLProxyMetadata();
+ function ProxyUtils.useViewCaller() internal returns (address) => CVLUseViewCaller();
+
+ // IERC20
+ function _.name() external => DISPATCHER(true);
+ function _.symbol() external => DISPATCHER(true);
+ function _.decimals() external => DISPATCHER(true);
+ function _.totalSupply() external => DISPATCHER(true);
+ function _.balanceOf(address) external => DISPATCHER(true);
+ function _.allowance(address,address) external => DISPATCHER(true);
+ function _.approve(address,uint256) external => DISPATCHER(true);
+ function _.transfer(address,uint256) external => DISPATCHER(true);
+ function _.transferFrom(address,address,uint256) external => DISPATCHER(true);
+}
+
+ghost CVLGetQuote(uint256, address, address) returns uint256 {
+ // The total value returned by the oracle is assumed < 2**230-1.
+ // There will be overflows without an upper bound on this number.
+ // (For example, it must be less than 2**242-1 to avoid overflow in
+ // LTVConfig.mul)
+ axiom forall uint256 x. forall address y. forall address z.
+ CVLGetQuote(x, y, z) < 1725436586697640946858688965569256363112777243042596638790631055949823;
+ // monotonicity of amount
+ axiom forall uint256 x1. forall uint256 x2. forall address y. forall address z.
+ x1 > x2 => CVLGetQuote(x1, y, z) > CVLGetQuote(x2, y, z);
+}
+
+function CVLGetQuotes(uint256 amount, address base, address quote) returns (uint256, uint256) {
+ return (
+ CVLGetQuote(amount, base, quote),
+ CVLGetQuote(amount, base, quote)
+ );
+}
+
+ghost address oracleAddress {
+ init_state axiom oracleAddress != 0;
+}
+ghost address unitOfAccount {
+ init_state axiom unitOfAccount != 0;
+}
+function CVLProxyMetadata() returns (address, address, address) {
+ require oracleAddress != 0;
+ require unitOfAccount != 0;
+ return (erc20, oracleAddress, unitOfAccount);
+}
+persistent ghost address viewCallerGhost {
+ init_state axiom viewCallerGhost != 0;
+}
+function CVLUseViewCaller() returns address {
+ // require not zero?
+ return viewCallerGhost;
+}
+
+function LTVConfigAssumptions(env e, BaseHarness.LTVConfig ltvConfig) returns bool {
+ bool targetLTVLessOne = ltvConfig.liquidationLTV < 10000;
+ bool originalLTVLessOne = ltvConfig.initialLiquidationLTV < 10000;
+ bool liquidationLTVHigher = ltvConfig.liquidationLTV > ltvConfig.borrowLTV;
+ bool initialLTVHigherTarget = ltvConfig.initialLiquidationLTV > ltvConfig.liquidationLTV;
+ mathint timeRemaining = ltvConfig.targetTimestamp - e.block.timestamp;
+ return targetLTVLessOne &&
+ originalLTVLessOne &&
+ liquidationLTVHigher &&
+ initialLTVHigherTarget &&
+ (require_uint32(timeRemaining) < ltvConfig.rampDuration);
+}
+
+function actualCaller(env e) returns address {
+ if(e.msg.sender == evc) {
+ address onBehalf;
+ bool unused;
+ onBehalf, unused = evc.getCurrentOnBehalfOfAccount(e, 0);
+ return onBehalf;
+ } else {
+ return e.msg.sender;
+ }
+}
+
+function actualCallerCheckController(env e) returns address {
+ if(e.msg.sender == evc) {
+ address onBehalf;
+ bool unused;
+ // Similar to EVCAuthenticateDeferred when checkController is true.
+ onBehalf, unused = evc.getCurrentOnBehalfOfAccount(e, currentContract);
+ return onBehalf;
+ } else {
+ return e.msg.sender;
+ }
+}
\ No newline at end of file
diff --git a/certora/specs/Cache.spec b/certora/specs/Cache.spec
new file mode 100644
index 00000000..8183fdc2
--- /dev/null
+++ b/certora/specs/Cache.spec
@@ -0,0 +1,22 @@
+// passing
+// run: https://prover.certora.com/output/65266/7c027fe6b03f4ead8d1fc08b876c8e75?anonymousKey=475104b4504c765772a29b9124ee15355a4cf2c9
+rule updateVault_no_unexpected_reverts {
+ env e;
+
+ // revert case run:
+ // https://prover.certora.com/output/65266/8d688972399441b6baaca896f085402a?anonymousKey=0e79c1406bd82a2ad2672404bb8b62d184cd537b
+ require e.msg.value == 0;
+ uint256 lastInterestAccUpd = getlastInterestAccumulatorUpdate(e);
+
+ // assignment to deltaT
+ require lastInterestAccUpd <= e.block.timestamp;
+
+ // newTotalBorrows assigment, prevent divide by zero
+ require getInterestAcc(e) > 0;
+
+ // typecast of newAccumulatedFees
+ require getAccumulatedFees(e) < getTotalShares(e);
+
+ updateVaultExt@withrevert(e);
+ assert !lastReverted;
+}
\ No newline at end of file
diff --git a/certora/specs/GhostPow.spec b/certora/specs/GhostPow.spec
new file mode 100644
index 00000000..5dc05bc7
--- /dev/null
+++ b/certora/specs/GhostPow.spec
@@ -0,0 +1,56 @@
+/// @doc Ghost power function that incorporates mathematical pure x^y axioms.
+/// @warning Some of these axioms might be false, depending on the Solidity implementation
+/// The user must bear in mind that equality-like axioms can be violated because of rounding errors.
+// _ghostPow summarizes RPow.rpow, or:
+// _ghostPow(x, y, scalar) = scalar * x^y
+ghost _ghostPow(uint256, uint256, uint256) returns mathint {
+ /// x^0 = 1
+ axiom forall uint256 x. forall uint256 base. _ghostPow(x, 0, base) == to_mathint(base);
+ /// 0^x = 0
+ axiom forall uint256 y. forall uint256 base. _ghostPow(0, y, base) == 0;
+ /// x^1 = x
+ axiom forall uint256 x. forall uint256 base. _ghostPow(x, base, base) == to_mathint(x);
+ /// 1^y = 1
+ axiom forall uint256 y. forall uint256 base. _ghostPow(base, y, base) == to_mathint(base);
+
+ /// I. x > 1 && y1 > y2 => x^y1 > x^y2
+ /// II. x < 1 && y1 > y2 => x^y1 < x^y2
+ axiom forall uint256 x. forall uint256 y1. forall uint256 y2. forall uint256 base.
+ x >= base && y1 > y2 => _ghostPow(x, y1, base) >= _ghostPow(x, y2, base);
+ axiom forall uint256 x. forall uint256 y1. forall uint256 y2. forall uint256 base.
+ x < base && y1 > y2 => (_ghostPow(x, y1, base) <= _ghostPow(x, y2, base) && _ghostPow(x,y2, base) <= to_mathint(base));
+ axiom forall uint256 x. forall uint256 y. forall uint256 base.
+ x < base && y > base => (_ghostPow(x, y, base) <= to_mathint(x));
+ axiom forall uint256 x. forall uint256 y. forall uint256 base.
+ x < base && y <= base => (_ghostPow(x, y, base) >= to_mathint(x));
+ axiom forall uint256 x. forall uint256 y. forall uint256 base.
+ x >= base && y > base => (_ghostPow(x, y, base) >= to_mathint(x));
+ axiom forall uint256 x. forall uint256 y. forall uint256 base.
+ x >= base && y <= base => (_ghostPow(x, y, base) <= to_mathint(x));
+ /// x1 > x2 && y > 0 => x1^y > x2^y
+ axiom forall uint256 x1. forall uint256 x2. forall uint256 y. forall uint256 base.
+ x1 > x2 => _ghostPow(x1, y, base) >= _ghostPow(x2, y, base);
+
+ /* Additional axioms - potentially unsafe
+ /// x^y * x^(1-y) == x -> 0.01% relative error
+ axiom forall uint256 x. forall uint256 y. forall uint256 z.
+ (0 <= y && y <= ONE18() && z + y == to_mathint(ONE18())) =>
+ relativeErrorBound(_ghostPow(x, y) * _ghostPow(x, z), x * ONE18(), ONE18() / 10000);
+
+ /// (x^y)^(1/y) == x -> 1% relative error
+ axiom forall uint256 x. forall uint256 y. forall uint256 z.
+ (0 <= y && y <= ONE18() && z * y == ONE18()*ONE18() ) =>
+ relativeErrorBound(_ghostPow(_ghostPow(x, y), z), x, ONE18() / 100);
+ */
+}
+
+function CVLPow(uint256 x, uint256 y, uint256 base) returns (uint256, bool) {
+ if (y == 0) {return (base, false);}
+ if (x == 0) {return (0, false);}
+ mathint res = _ghostPow(x, y, base);
+ if (res > max_uint256) {
+ uint256 havoced;
+ return (havoced, true);
+ }
+ return (require_uint256(res), false);
+}
\ No newline at end of file
diff --git a/certora/specs/Governance.spec b/certora/specs/Governance.spec
new file mode 100644
index 00000000..f05935a0
--- /dev/null
+++ b/certora/specs/Governance.spec
@@ -0,0 +1,112 @@
+methods {
+ // Havocs here should be OK, but want to remove the linking issues from the tool
+ function _.calculateDTokenAddress() internal => NONDET;
+ // IERC20
+ function _.name() external => DISPATCHER(true);
+ function _.symbol() external => DISPATCHER(true);
+ function _.decimals() external => DISPATCHER(true);
+ function _.totalSupply() external => DISPATCHER(true);
+ function _.balanceOf(address) external => DISPATCHER(true);
+ function _.allowance(address,address) external => DISPATCHER(true);
+ function _.approve(address,uint256) external => DISPATCHER(true);
+ function _.transfer(address,uint256) external => DISPATCHER(true);
+ function _.transferFrom(address,address,uint256) external => DISPATCHER(true);
+
+ function checkAccountMagicValueMemory() external returns (bytes memory) envfree;
+ function checkVaultMagicValueMemory() external returns (bytes memory) envfree;
+
+ // Harness
+ function getAccountBalance(address) external returns (GovernanceHarness.Shares) envfree;
+ function getGovernorReceiver() external returns (address) envfree;
+ function getProtocolFeeConfig(address) external returns (address, uint16) envfree;
+ function getTotalShares() external returns (GovernanceHarness.Shares) envfree;
+ function getAccumulatedFees() external returns (GovernanceHarness.Shares);
+ function getLastAccumulated() external returns (uint256) envfree;
+
+ // protocolConfig
+ function ProtocolConfig.protocolFeeConfig(address) external returns (address, uint16) envfree;
+
+ // unresolved calls havocing all contracts
+
+ // We can't handle the low-level call in
+ // EthereumVaultConnector.checkAccountStatusInternal
+ // and so reroute it to RiskManager's status check with this summary.
+ function EthereumVaultConnector.checkVaultStatusInternal(address vault) internal returns (bool, bytes memory) =>
+ CVLCheckVaultStatusInternal();
+
+ function _.invokeHookTarget(address caller) internal => NONDET;
+
+ function _.balanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) external => NONDET;
+
+ function _.computeInterestRate(BaseHarness.VaultCache memory) internal => CONSTANT;
+
+}
+
+
+function CVLCheckVaultStatusInternalBool(env e) returns bool {
+ checkVaultStatus@withrevert(e);
+ return !lastReverted;
+}
+
+function CVLCheckVaultStatusInternal() returns (bool, bytes) {
+ // We need a new env for the first function.
+ // Since the vault calls the EVC, otherwise msg.sender
+ // would become the vault unless we declare a fresh environment.
+ env e;
+ return (CVLCheckVaultStatusInternalBool(e),
+ checkVaultMagicValueMemory());
+}
+
+
+// Collecting fees should increase the protocol’s and the governor’s asset (unless the governor is address(0))
+// STATUS: PASSING
+// https://prover.certora.com/output/65266/9207ef71046343e993e83f9dfa761eb1?anonymousKey=401a193cacbcbc774185473b0242384e3e8c5b4d
+rule feeCollectionIncreasesProtocolGovernerAssets(env e){
+
+ address protocolReceiver;
+ uint16 protocolFee;
+ protocolReceiver, protocolFee = getProtocolFeeConfig(currentContract);
+ require protocolFee > 0;
+ // require protocolReceiver != 0;
+ address governorReceiver = getGovernorReceiver();
+ require governorReceiver != 0;
+
+ // accumulated fee is not zero
+ uint112 fees = getAccumulatedFees(e);
+
+ // at fee == 1 the governor fee can be rounded down to zero and the 1 wei fee goes to the protocol
+ require fees >1;
+
+ uint112 protocolReceiverBal_before = getAccountBalance(protocolReceiver);
+ uint112 governorReceiverBal_before = getAccountBalance(governorReceiver);
+
+ convertFees(e);
+
+ uint112 protocolReceiverBal_after = getAccountBalance(protocolReceiver);
+ uint112 governorReceiverBal_after = getAccountBalance(governorReceiver);
+
+ assert protocolReceiverBal_after > protocolReceiverBal_before
+ && governorReceiverBal_after > governorReceiverBal_before,
+ "collecting fees should icnrease the shares of the governor and protocol";
+}
+
+// These are assumed elsewhere in the specs
+// Pasing. Run link: https://prover.certora.com/output/65266/c078d73b9aaf41b69de58a059ec9c0ea?anonymousKey=3c865aa300106c0b53d38a8dc479dc0668774e48
+rule LTVConfigProperties {
+ env e;
+ address collateral;
+ uint16 borrowLTV;
+ uint16 liquidationLTV;
+ uint32 rampDuration;
+ uint16 old_borrowLTVOut = getLTVHarness(e, collateral, false);
+ uint16 old_liquidationLTVOut = getLTVHarness(e, collateral, true);
+ require old_borrowLTVOut <= 10000 &&
+ old_liquidationLTVOut <= 10000 &&
+ old_liquidationLTVOut >= old_borrowLTVOut;
+ setLTV(e, collateral, borrowLTV, liquidationLTV, rampDuration);
+ uint16 borrowLTVOut = getLTVHarness(e, collateral, false);
+ uint16 liquidationLTVOut = getLTVHarness(e, collateral, true);
+ assert borrowLTVOut <= 10000 &&
+ liquidationLTVOut <= 10000 &&
+ liquidationLTVOut >= borrowLTVOut;
+}
\ No newline at end of file
diff --git a/certora/specs/HealthStatusInvariant.spec b/certora/specs/HealthStatusInvariant.spec
new file mode 100644
index 00000000..0b5ad404
--- /dev/null
+++ b/certora/specs/HealthStatusInvariant.spec
@@ -0,0 +1,197 @@
+import "Base.spec";
+import "LoadVaultSummary.spec";
+using DummyERC20A as ERC20a;
+using DummyETokenA as ETokenA; // Used to assume collaterals are ETokens.
+using DummyETokenB as ETokenB; // Allows for possibility of multiple
+ // addresses for different collaterals.
+
+methods {
+ function checkAccountMagicValue() external returns (bytes4) envfree;
+ function checkAccountMagicValueMemory() external returns (bytes memory) envfree;
+ function checkVaultMagicValueMemory() external returns (bytes memory) envfree;
+ function EVCHarness.areChecksDeferred() external returns (bool) envfree;
+ // healthStatusCheck reverts unless this is true. We assume it's true
+ // approximate the real situation where these checks get triggered
+ // by the EVC before which this flag will be set.
+ function EVCHarness.areChecksInProgress() external returns bool => CVLAreChecksInProgress();
+ // unresolved calls that havoc all contracts
+ // pure, so NONDET is safe
+ function _.isHookTarget() external => NONDET;
+ // calls external contract. Here we assume invokeHookTarget does
+ // not affect the vault's internal state especially user balances.
+ // This is a pretty safe assumption because it is not the EVC and
+ // access controls in the vault will not allow non-EVC calls to succeed.
+ // there is also the nonreentrant modifier in most places.
+ function _.invokeHookTarget(address caller) internal => NONDET;
+ // The following two are both related to balanceTrackerHook in the
+ // RewardStreams repository. The implementation of BalanceTrackerHook
+ // there does not affect the state of the vault contracts
+ // https://github.com/euler-xyz/reward-streams/blob/master/src/TrackingRewardStreams.sol#L31-L62
+ function _.tryBalanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) internal => NONDET;
+ function _.balanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) external => NONDET;
+ // just emits an event so NONDET is safe
+ function _.emitTransfer(address from, address to, uint256 value) external => NONDET;
+ // has an actual affect -- disables a controller, but this is only called by RiskManager.disableController which reverts unless the controller balance is 0. So I think this nondet is safe.
+ function EVCHarness.disableController(address account) external => NONDET;
+ // computeInterestRate is not strictly pure -- the implementations of
+ // this function seem to keep state to calculate the future interest rate
+ // but modeling this as returning an arbitrary should be OK. (There is
+ // technically a side effect of storing state but that state only
+ // affects this return value)
+ function _.computeInterestRate(address vault, uint256 cash, uint256 borrows) external => NONDET;
+ // onFlashLoan is from an external contract. Here we assume this function
+ // does not affect the vault's internal state especially user balances.
+ // This is a pretty safe assumption because it is not the EVC and
+ // access controls in the vault will not allow non-EVC calls to succeed.
+ // there is also the nonreentrant modifier in most places.
+ function _.onFlashLoan(bytes data) external => NONDET;
+ function EVCHarness.getCollaterals(address) external returns (address[] memory) envfree;
+
+ // EVC
+ function _.requireVaultStatusCheck() external => DISPATCHER(true);
+ function _.requireAccountAndVaultStatusCheck(address) external => DISPATCHER(true);
+
+ // Summaries
+ function _.safeTransferFrom(address token, address from, address to, uint256 value, address permit2) internal => CVLSafeTransferFrom(token, from, to, value) expect void;
+ function _.enforceCollateralTransfer(address collateral, uint256 amount,
+ address from, address receiver) internal =>
+ CVLEnforceCollateralTransfer(collateral, amount, from, receiver) expect void;
+ // We can't handle the low-level call in
+ // EthereumVaultConnector.checkAccountStatusInternal
+ // and so reroute it to RiskManager's status check with this summary.
+ function EthereumVaultConnector.checkAccountStatusInternal(address account) internal returns (bool, bytes memory) =>
+ CVLCheckAccountStatusInternal(account);
+ function EthereumVaultConnector.checkVaultStatusInternal(address vault) internal returns (bool, bytes memory) =>
+ CVLCheckVaultStatusInternal();
+}
+
+// We summarize EthereumVaultConnector.checkAccountStatusInternal
+// because we need to direct the low-level call to RiskManager.
+// checkAccountStatus and this linking doesn't happen automatically
+function CVLCheckAccountStatusInternalBool(env e, address account) returns bool {
+ address[] collaterals = evc.getCollaterals(account);
+ checkAccountStatus@withrevert(e, account, collaterals);
+ return !lastReverted;
+}
+
+function CVLCheckAccountStatusInternal(address account) returns (bool, bytes) {
+ // We need a new env for the first function.
+ // Since the vault calls the EVC, otherwise msg.sender
+ // would become the vault unless we declare a fresh environment.
+ env eEVC;
+ return (CVLCheckAccountStatusInternalBool(eEVC, account),
+ checkAccountMagicValueMemory());
+}
+
+function CVLCheckVaultStatusInternalBool(env e) returns bool {
+ checkVaultStatus@withrevert(e);
+ return !lastReverted;
+}
+
+function CVLCheckVaultStatusInternal() returns (bool, bytes) {
+ // We need a new env for the first function.
+ // Since the vault calls the EVC, otherwise msg.sender
+ // would become the vault unless we declare a fresh environment.
+ env eEVC;
+ return (CVLCheckVaultStatusInternalBool(eEVC),
+ checkVaultMagicValueMemory());
+}
+
+function CVLAreChecksInProgress() returns bool {
+ return true;
+}
+
+function CVLSafeTransferFrom(address token, address from, address to, uint256 value) {
+ // We need a new env since this will
+ // be a call from the vault to the ERC20 rather than a call
+ // from the original message sender to the ERC20.
+ // would become the vault unless we declare a fresh environment.
+ env e;
+ if (token == ERC20a) {
+ ERC20a.transferFrom(e, from, to, value);
+ } else if (token == ETokenA) {
+ ETokenA.transferFrom(e, from, to, value);
+ } else if (token == ETokenB) {
+ ETokenB.transferFrom(e, from, to, value);
+ }
+}
+
+/*
+* The prover struggles to reason about the low-level call operations involved
+* in the real EVCClient.enforceControlCollateral function, so we need
+* to emulate the real behavior here. Here's how it works in the real code:
+* - EVCClient calls evc.controlCollateral passing a call to `transfer(receiver, amount)` along with the collateral and from addresses
+* - In controlCollateral the from address is used to set the onBehalfOfAccount
+* and some authentication is done
+* - After this, callWithContextInternal invokes the transfer function
+* on the collateral address
+* - Collaterals in the EVK must all be Token.sol and token's transfer
+* implementation calls initOperation which enqueues an account status
+* check on the EVC for the onBehalfOfAccount it also gets from EVC.
+* Because onBehalfOfAccount was set to the from address in callWithContextInternal this status check is for the from account
+* To emulate this, we:
+* - explicitly call EToken.transferFrom using the expected addresses
+* - enqueue a status check on the evc for the "from" address
+*/
+function CVLEnforceCollateralTransfer(address collateral, uint256 amount, address from, address receiver) {
+ env e;
+ if (collateral == ETokenA) {
+ ETokenA.transferFromInternalHarnessed(e, from, receiver, amount);
+ } else if (collateral == ETokenB) {
+ ETokenB.transferFromInternalHarnessed(e, from, receiver, amount);
+ }
+}
+
+// Assuming the prices stay the same, a healthy account can never become
+// unhealthy. Here, our assumption that the prices do not change is implicit
+// in the fact that the summary for GetQuote is an uninterpreted function --
+// the prover will model it as a function so it will always return the same
+// value when given the same arguments.
+rule accountsStayHealthy_strategy (method f) filtered { f ->
+ // Literal selectors are used to avoid compilation errors when
+ // only some of the modules are in the verification scene
+ // sig:GovernanceModule.clearLTV(address).selector
+ f.selector != 0x8255d029 &&
+ // sig:GovernanceModule.setLTV(address,uint16,uint16,uint32).selector
+ f.selector != 0x4bca3d5b &&
+ // sig:InitializeModule.initialize(address).selector
+ f.selector != 0xc4d66de8 &&
+ // sig:TokenHarnes.transferFromInternalHarnessed (this is a harness method only)
+ f.selector != 0xd3110e86
+}{
+ env e;
+ calldataarg args;
+ address account;
+ address[] collaterals = evc.getCollaterals(account);
+ require collaterals.length <= 2; // loop bound
+ require oracleAddress != 0;
+ // not sure the following 4 are really needed
+ require account != erc20;
+ require account != oracleAddress;
+ require account != evc;
+ require account != unitOfAccount;
+ require evc.areChecksDeferred();
+
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenA));
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenB));
+ if (collaterals.length > 0) {
+ require collaterals[0] == ETokenA || collaterals[0] == ETokenB;
+ }
+ if (collaterals.length > 1) {
+ require collaterals[1] == ETokenA || collaterals[1] == ETokenB;
+ }
+
+ bool healthyBefore = checkLiquidityReturning(e, account, collaterals);
+ f(e, args);
+ // The only way to call a vault funciton is through EVC's call, batch,
+ // or permit. During all of these status checks are deferred and at the end
+ // these call restoreExecutionContext which triggers the deferred checks.
+ // This excplicit call to checkStatusAll is a way to get a setup that
+ // approximates the real situation.
+ // We proved separately that EVC really does always call checkStatus all
+ // at the end of a call/batch.
+ // run: https://prover.certora.com/output/65266/2523dd890b324c9cb6c1fcec767e030e/?anonymousKey=5c7f3132f51538a96a5d8d4fb0de61f4ed892ccc
+ evc.checkStatusAllExt(e);
+ bool healthyAfter = checkLiquidityReturning(e, account, collaterals);
+ assert healthyBefore => healthyAfter;
+}
\ No newline at end of file
diff --git a/certora/specs/LiquidateHealthStatus.spec b/certora/specs/LiquidateHealthStatus.spec
new file mode 100644
index 00000000..53770e5e
--- /dev/null
+++ b/certora/specs/LiquidateHealthStatus.spec
@@ -0,0 +1,401 @@
+import "Base.spec";
+import "LoadVaultSummary.spec";
+using DummyERC20A as ERC20a;
+using DummyETokenA as ETokenA; // Used to assume collaterals are ETokens.
+using DummyETokenB as ETokenB; // Allows for possibility of multiple
+ // addresses for different collaterals.
+
+methods {
+ function checkAccountMagicValue() external returns (bytes4) envfree;
+ function checkAccountMagicValueMemory() external returns (bytes memory) envfree;
+ function checkVaultMagicValueMemory() external returns (bytes memory) envfree;
+ function EVCHarness.areChecksDeferred() external returns (bool) envfree;
+ // healthStatusCheck reverts unless this is true. We assume it's true
+ // approximate the real situation where these checks get triggered
+ // by the EVC before which this flag will be set.
+ function EVCHarness.areChecksInProgress() external returns bool => CVLAreChecksInProgress();
+ // unresolved calls that havoc all contracts
+ function _.isHookTarget() external => NONDET;
+ function _.invokeHookTarget(address caller) internal => NONDET;
+ function _.tryBalanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) internal => NONDET;
+ function _.balanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) external => NONDET;
+ function _.emitTransfer(address from, address to, uint256 value) external => NONDET;
+ function EVCHarness.disableController(address account) external => NONDET;
+ function _.computeInterestRate(address vault, uint256 cash, uint256 borrows) external => NONDET;
+ function _.onFlashLoan(bytes data) external => NONDET;
+
+ // Harness
+ function LiquidationHSHarness.hasDebtSocialization() external returns (bool) envfree;
+
+ // EVC
+ function _.requireVaultStatusCheck() external => DISPATCHER(true);
+ function _.requireAccountAndVaultStatusCheck(address) external => DISPATCHER(true);
+
+ // Summaries
+ function _.safeTransferFrom(address token, address from, address to, uint256 value, address permit2) internal => CVLSafeTransferFrom(token, from, to, value) expect void;
+ function _.enforceCollateralTransfer(address collateral, uint256 amount,
+ address from, address receiver) internal =>
+ CVLEnforceCollateralTransfer(collateral, amount, from, receiver) expect void;
+ // To deal with changes between LTV values:
+ // function _.getLTV(address collateral, bool liquidation) internal => CVLGetLTV(collateral, liquidation) expect (BaseHarness.ConfigAmount);
+ // We can't handle the low-level call in
+ // EthereumVaultConnector.checkAccountStatusInternal
+ // and so reroute it to RiskManager's status check with this summary.
+ function EthereumVaultConnector.checkAccountStatusInternal(address account) internal returns (bool, bytes memory) =>
+ CVLCheckAccountStatusInternal(account);
+ function EthereumVaultConnector.checkVaultStatusInternal(address vault) internal returns (bool, bytes memory) =>
+ CVLCheckVaultStatusInternal();
+
+ function _.EVCRequireStatusChecks(address account) internal =>
+ EVCRequireStatusChecksCVL(account) expect void;
+}
+
+//-----------------------------------------------------------------------------
+// Summaries and Ghost State
+//-----------------------------------------------------------------------------
+
+persistent ghost address accountToCheckGhost;
+function EVCRequireStatusChecksCVL(address account) {
+ accountToCheckGhost = account;
+}
+
+// We summarize EthereumVaultConnector.checkAccountStatusInternal
+// because we need to direct the low-level call to RiskManager.
+// checkAccountStatus and this linking doesn't happen automatically
+function CVLCheckAccountStatusInternalBool(env e, address account) returns bool {
+ address[] collaterals = evc.getCollaterals(e, account);
+ checkAccountStatus@withrevert(e, account, collaterals);
+ return !lastReverted;
+}
+
+function CVLCheckAccountStatusInternal(address account) returns (bool, bytes) {
+ // We need a new env for the first function.
+ // Since the vault calls the EVC, otherwise msg.sender
+ // would become the vault unless we declare a fresh environment.
+ env eEVC;
+ return (CVLCheckAccountStatusInternalBool(eEVC, account),
+ checkAccountMagicValueMemory());
+}
+
+function CVLCheckVaultStatusInternalBool(env e) returns bool {
+ checkVaultStatus@withrevert(e);
+ return !lastReverted;
+}
+
+function CVLCheckVaultStatusInternal() returns (bool, bytes) {
+ // We need a new env for the first function.
+ // Since the vault calls the EVC, otherwise msg.sender
+ // would become the vault unless we declare a fresh environment.
+ env eEVC;
+ return (CVLCheckVaultStatusInternalBool(eEVC),
+ checkVaultMagicValueMemory());
+}
+
+function CVLAreChecksInProgress() returns bool {
+ return true;
+}
+
+function CVLSafeTransferFrom(address token, address from, address to, uint256 value) {
+ // We need a new env since this will
+ // be a call from the vault to the ERC20 rather than a call
+ // from the original message sender to the ERC20.
+ // would become the vault unless we declare a fresh environment.
+ env e;
+ if (token == ERC20a) {
+ ERC20a.transferFrom(e, from, to, value);
+ } else if (token == ETokenA) {
+ ETokenA.transferFrom(e, from, to, value);
+ } else if (token == ETokenB) {
+ ETokenB.transferFrom(e, from, to, value);
+ }
+}
+
+/*
+* The prover struggles to reason about the low-level call operations involved
+* in the real EVCClient.enforceControlCollateral function, so we need
+* to emulate the real behavior here. Here's how it works in the real code:
+* - EVCClient calls evc.controlCollateral passing a call to `transfer(receiver, amount)` along with the collateral and from addresses
+* - In controlCollateral the from address is used to set the onBehalfOfAccount
+* and some authentication is done
+* - After this, callWithContextInternal invokes the transfer function
+* on the collateral address
+* - Collaterals in the EVK must all be Token.sol and token's transfer
+* implementation calls initOperation which enqueues an account status
+* check on the EVC for the onBehalfOfAccount it also gets from EVC.
+* Because onBehalfOfAccount was set to the from address in callWithContextInternal this status check is for the from account
+* To emulate this, we:
+* - explicitly call EToken.transferFrom using the expected addresses
+* - enqueue a status check on the evc for the "from" address
+*/
+// Because calling to requireAccountStatusCheck on EVC is expensive
+// for the prover, instead assign which account gets checked to a ghost
+function CVLEnforceCollateralTransfer(address collateral, uint256 amount, address from, address receiver) {
+ env e;
+ if (collateral == ETokenA) {
+ ETokenA.transferFromInternalHarnessed(e, from, receiver, amount);
+ } else if (collateral == ETokenB) {
+ ETokenB.transferFromInternalHarnessed(e, from, receiver, amount);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Rules
+//-----------------------------------------------------------------------------
+/*
+For Liquidation.liquidate we need to split this rule into cases:
+ - account checked != liquidator and account checked != violator
+ - account checked == liquidator and account checked != violator and:
+ - debt socialization disabled
+ - debt socialization enabled
+These cases are handled separately:
+ - account checked == violator:
+ if this account was healthy before the call does nothing. This is not
+ only easy to see manually but we prove this in
+ checkLiquidation.healthy() in Liquidation.spec
+ - liquidator == violator:
+ In this case the call reverts which is easy to check but also
+ proved in liquidate_mustRevert in Liquidation.spec
+*/
+
+// passing: https://prover.certora.com/output/65266/132c942ca2a2463b84e15b77becdfa11/?anonymousKey=431faa27dd7b649306de7e37067b7a75e57271f8
+rule liquidateAccountsStayHealthy_liquidator_no_debt_socialization {
+ env e;
+ address account;
+ address[] collaterals = evc.getCollaterals(e, account);
+ require collaterals.length <= 2; // loop bound
+ require oracleAddress != 0;
+ // Vault cannot be a user of itself
+ require account != currentContract;
+ // Vault should not be used as a collateral
+ require collaterals[0] != currentContract;
+ require collaterals[1] != currentContract;
+ // not sure the following 4 are really needed
+ require account != erc20;
+ require account != oracleAddress;
+ require account != evc;
+ require account != unitOfAccount;
+ require evc.areChecksDeferred();
+
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenA));
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenB));
+ // Collaterals must be ETokens
+ if (collaterals.length > 0) {
+ require collaterals[0] == ETokenA;
+ }
+ if (collaterals.length > 1) {
+ require collaterals[1] == ETokenB;
+ }
+
+ address violator;
+ address collateral;
+ uint256 repayAssets;
+ uint256 minYieldBalance;
+
+ // disable debt socialization
+ require !hasDebtSocialization();
+
+ // initialize checked accounts to 0
+ require accountToCheckGhost == 0; // account checked in initialize
+
+ // account eq liquidator case
+ require collateral == ETokenA || collateral == ETokenB;
+ address liquidator = actualCaller(e);
+ require account == liquidator;
+ require violator != liquidator;
+
+ bool healthyBefore = checkLiquidityReturning(e, account, collaterals);
+ currentContract.liquidate(e, violator, collateral, repayAssets, minYieldBalance);
+
+ // replace the real call path involving the EVC calling back into the
+ // vault with a direct call on checkAccountStatus from the vault
+
+ if(accountToCheckGhost != 0) {
+ currentContract.checkAccountStatus(e, accountToCheckGhost, collaterals);
+ }
+
+ bool healthyAfter = checkLiquidityReturning(e, account, collaterals);
+ assert healthyBefore => healthyAfter;
+}
+
+// passing: https://prover.certora.com/output/65266/2d955907619c4c748e82791a3bb5843e/?anonymousKey=dfb93c10b2311ab0f99ffaf551bcd1fc4b7447b0
+rule liquidateAccountsStayHealthy_liquidator_with_debt_socialization {
+ env e;
+ address account;
+ address[] collaterals = evc.getCollaterals(e, account);
+ require collaterals.length <= 2; // loop bound
+ require oracleAddress != 0;
+ // Vault cannot be a user of itself
+ require account != currentContract;
+ // Vault should not be used as a collateral
+ require collaterals[0] != currentContract;
+ require collaterals[1] != currentContract;
+ // not sure the following 4 are really needed
+ require account != erc20;
+ require account != oracleAddress;
+ require account != evc;
+ require account != unitOfAccount;
+ require evc.areChecksDeferred();
+
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenA));
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenB));
+ // Collaterals must be ETokens
+ if (collaterals.length > 0) {
+ require collaterals[0] == ETokenA;
+ }
+ if (collaterals.length > 1) {
+ require collaterals[1] == ETokenB;
+ }
+
+ address violator;
+ address collateral;
+ uint256 repayAssets;
+ uint256 minYieldBalance;
+
+ // enable debt socialization
+ require hasDebtSocialization();
+
+ // initialize checked accounts to 0
+ require accountToCheckGhost == 0; // account checked in initialize
+
+ // account eq liquidator case
+ require collateral == collaterals[0] || collateral == collaterals[1];
+ address liquidator = actualCaller(e);
+ require account == liquidator;
+ require violator != liquidator;
+
+ bool healthyBefore = checkLiquidityReturning(e, account, collaterals);
+ currentContract.liquidate(e, violator, collateral, repayAssets, minYieldBalance);
+
+ // replace the real call path involving the EVC calling back into the
+ // vault with a direct call on checkAccountStatus from the vault
+
+ if(accountToCheckGhost != 0) {
+ currentContract.checkAccountStatus(e, accountToCheckGhost, collaterals);
+ }
+
+ bool healthyAfter = checkLiquidityReturning(e, account, collaterals);
+ assert healthyBefore => healthyAfter;
+}
+
+// passing: https://prover.certora.com/output/65266/83941c4b1c3448a6bd56c3edebf44ced/?anonymousKey=11866ef668f148318b2bea213560da6a5b6df937
+rule liquidateAccountsStayHealthy_not_violator {
+ env e;
+ address account;
+ address[] collaterals = evc.getCollaterals(e, account);
+ require collaterals.length <= 2; // loop bound
+ require oracleAddress != 0;
+ // Vault cannot be a user of itself
+ require account != currentContract;
+ // Vault should not be used as a collateral
+ require collaterals[0] != currentContract;
+ require collaterals[1] != currentContract;
+ // not sure the following 4 are really needed
+ require account != erc20;
+ require account != oracleAddress;
+ require account != evc;
+ require account != unitOfAccount;
+ require evc.areChecksDeferred();
+
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenA));
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenB));
+ // Collaterals must be ETokens
+ if (collaterals.length > 0) {
+ require collaterals[0] == ETokenA;
+ }
+ if (collaterals.length > 1) {
+ require collaterals[1] == ETokenB;
+ }
+
+ address violator;
+ address collateral;
+ uint256 repayAssets;
+ uint256 minYieldBalance;
+
+ address liquidator = actualCaller(e);
+
+ // initialize checked accounts to 0
+ require accountToCheckGhost == 0; // account checked in initialize
+
+ // account NE violator case
+ require account != violator;
+ require liquidator != violator;
+ require account != liquidator;
+
+ bool healthyBefore = checkLiquidityReturning(e, account, collaterals);
+ currentContract.liquidate(e, violator, collateral, repayAssets, minYieldBalance);
+ // The only way to call a vault funciton is through EVC's call, batch,
+ // or permit. During all of these status checks are deferred and at the end
+ // these call restoreExecutionContext which triggers the deferred checks.
+ // Replace the real call path involving the EVC calling back into the
+ // vault with a direct call on checkAccountStatus from the vault.
+ // (For the not_violator / not liquidator case, we can also directly
+ // call evc.checkStatusAll rather than using these ghosts and
+ // the direct call on checkAccountStatus, but for the liquidator case
+ // this will drop performance enough to hit a timeout)
+
+ if(accountToCheckGhost != 0) {
+ currentContract.checkAccountStatus(e, accountToCheckGhost, collaterals);
+ }
+
+ bool healthyAfter = checkLiquidityReturning(e, account, collaterals);
+ assert healthyBefore => healthyAfter;
+}
+
+rule liquidateAccountsStayHealthy_account_cur_contract {
+ env e;
+ address account;
+ address[] collaterals = evc.getCollaterals(e, account);
+ require collaterals.length <= 2; // loop bound
+ require oracleAddress != 0;
+ // Vault should not be used as a collateral
+ require collaterals[0] != currentContract;
+ require collaterals[1] != currentContract;
+ // not sure the following 4 are really needed
+ require account != erc20;
+ require account != oracleAddress;
+ require account != evc;
+ require account != unitOfAccount;
+ require evc.areChecksDeferred();
+
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenA));
+ require LTVConfigAssumptions(e, getLTVConfig(e, ETokenB));
+ // Collaterals must be ETokens
+ if (collaterals.length > 0) {
+ require collaterals[0] == ETokenA;
+ }
+ if (collaterals.length > 1) {
+ require collaterals[1] == ETokenB;
+ }
+
+ // vault is account of itself case
+ require account == currentContract;
+
+ address violator;
+ address collateral;
+ uint256 repayAssets;
+ uint256 minYieldBalance;
+
+ address liquidator = actualCaller(e);
+
+ // initialize checked accounts to 0
+ require accountToCheckGhost == 0; // account checked in initialize
+
+ bool healthyBefore = checkLiquidityReturning(e, account, collaterals);
+ currentContract.liquidate(e, violator, collateral, repayAssets, minYieldBalance);
+ // The only way to call a vault funciton is through EVC's call, batch,
+ // or permit. During all of these status checks are deferred and at the end
+ // these call restoreExecutionContext which triggers the deferred checks.
+ // Replace the real call path involving the EVC calling back into the
+ // vault with a direct call on checkAccountStatus from the vault.
+ // (For the not_violator / not liquidator case, we can also directly
+ // call evc.checkStatusAll rather than using these ghosts and
+ // the direct call on checkAccountStatus, but for the liquidator case
+ // this will drop performance enough to hit a timeout)
+
+ if(accountToCheckGhost != 0) {
+ currentContract.checkAccountStatus(e, accountToCheckGhost, collaterals);
+ }
+
+ bool healthyAfter = checkLiquidityReturning(e, account, collaterals);
+ assert healthyBefore => healthyAfter;
+}
\ No newline at end of file
diff --git a/certora/specs/Liquidation.spec b/certora/specs/Liquidation.spec
new file mode 100644
index 00000000..35fa1e4d
--- /dev/null
+++ b/certora/specs/Liquidation.spec
@@ -0,0 +1,198 @@
+// Passing
+// run: https://prover.certora.com/output/65266/5f1f37520d824e1aa7ab738a0147745e?anonymousKey=9521c3759d1f018559d571cd2a1502b04504399d
+
+import "Base.spec";
+methods {
+ function isRecognizedCollateralExt(address collateral) external returns (bool) envfree;
+ // unresolved calls that havoc all contracts
+ function _.isHookTarget() external => NONDET;
+ function _.invokeHookTarget(address caller) internal => NONDET;
+ function _.tryBalanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) internal => NONDET;
+ function _.balanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) external => NONDET;
+ function _.emitTransfer(address from, address to, uint256 value) external => NONDET;
+ function EVCHarness.disableController(address account) external => NONDET;
+ function _.computeInterestRate(address vault, uint256 cash, uint256 borrows) external => NONDET;
+ function _.onFlashLoan(bytes data) external => NONDET;
+ function _.safeTransferFrom(address token, address from, address to, uint256 value, address permit2) internal => NONDET;
+ function _.enforceCollateralTransfer(address collateral, uint256 amount,
+ address from, address receiver) internal => NONDET;
+
+
+ function EthereumVaultConnector.checkAccountStatusInternal(address account) internal returns (bool, bytes memory) with (env e) =>
+ CVLCheckAccountStatusInternal(e, account);
+ function EthereumVaultConnector.checkVaultStatusInternal(address vault) internal returns (bool, bytes memory) with(env e) =>
+ CVLCheckVaultStatusInternal(e);
+
+ function _.EVCRequireStatusChecks(address account) internal => NONDET;
+}
+
+
+// This returns an arbitrary account status of the prover's choosing. It is
+// similar to NONDETing checkAccountStatus internal and is a worakround
+// for the tool not supporting NONDET for byte return values.
+persistent ghost bool accountStatusGhost;
+function CVLCheckAccountStatusInternalBool(env e, address account) returns bool {
+ return accountStatusGhost;
+}
+
+function CVLCheckAccountStatusInternal(env e, address account) returns (bool, bytes) {
+ return (CVLCheckAccountStatusInternalBool(e, account),
+ checkAccountMagicValueMemory(e));
+}
+
+// This is using a similar pattern as CVLCheckAcountStatusInternal
+persistent ghost bool vaultStatusBool;
+function CVLCheckVaultStatusInternalBool(env e) returns bool {
+ return vaultStatusBool;
+}
+
+function CVLCheckVaultStatusInternal(env e) returns (bool, bytes) {
+ return (CVLCheckVaultStatusInternalBool(e),
+ checkVaultMagicValueMemory(e));
+}
+
+// passing
+// If violator healthy, checkLiquidation returns maxRepay and maxYield as 0.
+rule checkLiquidation_healthy() {
+ env e;
+ address liquidator;
+ address violator;
+ address collateral;
+ uint256 maxRepay;
+ uint256 maxYield;
+
+ require oracleAddress != 0;
+
+ uint256 liquidityCollateralValue;
+ uint256 liquidityLiabilityValue;
+ (liquidityCollateralValue, liquidityLiabilityValue) =
+ calculateLiquidityExternal(e, violator);
+
+ require liquidityCollateralValue > liquidityLiabilityValue;
+
+ (maxRepay, maxYield) = checkLiquidation(e, liquidator, violator, collateral);
+
+ assert maxRepay == 0;
+ assert maxYield == 0;
+}
+
+// passing run: https://prover.certora.com/output/65266/ed9699a14a114c0dbad76526a55ad493/?anonymousKey=f1f0a74c2c72ede7ce77f50fbf66541e8c4f03d7
+rule checkLiquidation_healthy_reverts() {
+ env e;
+ address account;
+ require oracleAddress != 0;
+
+ uint256 liquidityCollateralValue;
+ uint256 liquidityLiabilityValue;
+ address[] collaterals = getCollateralsExt(account);
+ require collaterals.length == 2; // loop unrolling bound
+ (liquidityCollateralValue, liquidityLiabilityValue) =
+ calculateLiquidityLiquidation(e, account);
+
+ // returns true if there is no liability
+ require liquidityLiabilityValue > 0;
+
+ // calculateLiquidity and checkLiquidity are only
+ // the same if the unitOfAccount is the same
+ // as the underlying asset -- otherwise the
+ // value of the unitOfAccount could change the value
+ // of the liability value returned by getLiabilityValue
+ require unitOfAccount == erc20;
+
+ // checkLiquidityReturning must return FALSE if collateral is not
+ // greater than liability.
+ assert checkLiquidityReturning(e, account, collaterals) <=>
+ (liquidityCollateralValue > liquidityLiabilityValue);
+}
+
+// passing
+// checkLiquidation must revert if:
+// - violator is the same account as liquidator
+// - collateral is not accepted
+// - collateral is not enabled collateral for the violator
+// - liability vault is not enabled as the only controller of the violator
+// - violator account status check is deferred
+// - price oracle is not configured
+rule checkLiquidation_mustRevert {
+ env e;
+ address liquidator;
+ address violator;
+ address collateral;
+ uint256 maxRepay;
+ uint256 maxYield;
+
+ require oracleAddress != 0;
+ bool selfLiquidate = liquidator == violator;
+ bool badCollateral = !isRecognizedCollateralExt(collateral);
+ bool enabledCollateral = isCollateralEnabledExt(violator, collateral);
+ bool vaultControlsViolator = vaultIsOnlyController(violator);
+ bool violatorStatusCheckDeferred = isAccountStatusCheckDeferredExt(violator);
+ bool oracleConfigured = vaultCacheOracleConfigured(e);
+
+ (maxRepay, maxYield) = checkLiquidation(e, liquidator, violator, collateral);
+
+ assert !selfLiquidate;
+ assert !badCollateral;
+ assert enabledCollateral;
+ assert vaultControlsViolator;
+ assert !violatorStatusCheckDeferred;
+ assert oracleConfigured;
+
+}
+
+// Passing.
+// The borrowing collateral value is lower than the liquidation collateral value
+rule getCollateralValue_borrowing_lower {
+ env e;
+ Liquidation.VaultCache vaultCache;
+ address account;
+ address collateral;
+
+ require LTVConfigAssumptions(e, getLTVConfig(e, collateral));
+
+ uint256 collateralValue_borrowing = getCollateralValueExt(e, vaultCache, account, collateral, false);
+
+ uint256 collateralValue_liquidation = getCollateralValueExt(e, vaultCache, account, collateral, true);
+
+ require collateralValue_liquidation > 0;
+ require collateralValue_borrowing > 0;
+
+ assert collateralValue_borrowing <= collateralValue_liquidation;
+
+}
+
+// passed
+// Liquidation must revert if:
+// - the liquidator is the violator (self liquidation)
+// - the collateral is not recognized
+// - the collateral is not enabled
+// - the vault does not control the liquidator
+// - the vault does not control the violator
+// - the status checks are not deferred for the violator
+// - the price oracle is not configured
+rule liquidate_mustRevert {
+ env e;
+ address violator;
+ address collateral;
+ uint256 repayAssets;
+ uint256 minYieldBalance;
+
+ address liquidator = getLiquidator(e);
+ bool selfLiquidation = violator == liquidator;
+ bool recognizedCollateral = isRecognizedCollateralExt(collateral);
+ bool enabledCollateral = isCollateralEnabledExt(violator, collateral);
+ bool violatorStatusCheckDeferred = isAccountStatusCheckDeferredExt(violator);
+ bool vaultControlsLiquidator = vaultIsController(liquidator);
+ bool vaultControlsViolator = vaultIsOnlyController(violator);
+ bool oracleConfigured = vaultCacheOracleConfigured(e);
+
+ liquidate(e, violator, collateral, repayAssets, minYieldBalance);
+ assert !selfLiquidation;
+ assert recognizedCollateral;
+ assert enabledCollateral;
+ assert vaultControlsLiquidator;
+ assert vaultControlsViolator;
+ assert !violatorStatusCheckDeferred;
+ assert oracleConfigured;
+}
+
diff --git a/certora/specs/LoadVaultSummary.spec b/certora/specs/LoadVaultSummary.spec
new file mode 100644
index 00000000..a6abc85b
--- /dev/null
+++ b/certora/specs/LoadVaultSummary.spec
@@ -0,0 +1,56 @@
+import "./Base.spec";
+methods {
+ function Cache.loadVault() internal returns (BaseHarness.VaultCache memory) with (env e) => CVLLoadVaultAssumeNoUpdate(e);
+
+ function storage_lastInterestAccumulatorUpdate() external returns (uint48) envfree;
+ function storage_cash() external returns (BaseHarness.Assets) envfree;
+ function storage_supplyCap() external returns (uint256) envfree;
+ function storage_borrowCap() external returns (uint256) envfree;
+ function storage_hookedOps() external returns (BaseHarness.Flags) envfree;
+ function storage_snapshotInitialized() external returns (bool) envfree;
+ function storage_totalShares() external returns (BaseHarness.Shares) envfree;
+ function storage_totalBorrows() external returns (BaseHarness.Owed) envfree;
+ function storage_accumulatedFees() external returns (BaseHarness.Shares) envfree;
+ function storage_interestAccumulator() external returns (uint256) envfree;
+ function storage_configFlags() external returns (BaseHarness.Flags) envfree;
+}
+
+
+
+// need to make sure successive calls only return different values
+// when this is actually possible in the real call...
+// * calls with the same env will return all the same values
+// the passage of time is not actually relevant to the spec because
+// in all the rules, only one env is ever created per rule.
+// The parts of the cache about interest are not relevant to the specs
+
+function CVLLoadVaultAssumeNoUpdate(env e) returns BaseHarness.VaultCache {
+ BaseHarness.VaultCache vaultCache;
+ uint48 lastUpdate = storage_lastInterestAccumulatorUpdate();
+ BaseHarness.Owed oldTotalBorrows = storage_totalBorrows();
+ BaseHarness.Shares oldTotalShares = storage_totalShares();
+ require vaultCache.cash == storage_cash();
+ uint48 timestamp48 = require_uint48(e.block.timestamp);
+ bool updated = timestamp48 != lastUpdate;
+ require !updated;
+ require vaultCache.lastInterestAccumulatorUpdate == lastUpdate;
+ require vaultCache.totalBorrows == oldTotalBorrows;
+ require vaultCache.totalShares == oldTotalShares;
+ require vaultCache.accumulatedFees == storage_accumulatedFees();
+ require vaultCache.interestAccumulator == storage_interestAccumulator();
+
+ // unmodified values
+ require vaultCache.supplyCap == storage_supplyCap();
+ require vaultCache.borrowCap == storage_borrowCap();
+ require vaultCache.hookedOps == storage_hookedOps();
+ require vaultCache.configFlags == storage_configFlags();
+ require vaultCache.snapshotInitialized == storage_snapshotInitialized();
+
+ require vaultCache.asset == erc20;
+ require vaultCache.oracle == oracleAddress;
+ require vaultCache.unitOfAccount == unitOfAccount;
+ require oracleAddress != 0;
+ require unitOfAccount != 0;
+
+ return vaultCache;
+}
\ No newline at end of file
diff --git a/certora/specs/RiskManager.spec b/certora/specs/RiskManager.spec
new file mode 100644
index 00000000..b0d75933
--- /dev/null
+++ b/certora/specs/RiskManager.spec
@@ -0,0 +1,146 @@
+import "Base.spec";
+import "./LoadVaultSummary.spec";
+
+// run: https://prover.certora.com/output/65266/4d1ba56cfd3c4aefbe2661e07fd5c95c/?anonymousKey=800abae52d40b2758c3f1f8c8a42ff82025533cd
+
+methods {
+ // envfree
+ function vaultIsOnlyController(address account) external returns (bool) envfree;
+
+}
+
+// passing: https://prover.certora.com/output/65266/8b94c232c4b14e3aab917cd7e94d501c/?anonymousKey=27f680520b4d7cbb9f387563d3f1bb45de8fc9a7
+rule ltv_borrowing_lower {
+ env e;
+ calldataarg args;
+
+ address account;
+
+
+ // based on loop bound
+ address[] collaterals = getCollateralsExt(account);
+ require collaterals.length == 2;
+ require LTVConfigAssumptions(e, getLTVConfig(collaterals[0]));
+ require LTVConfigAssumptions(e, getLTVConfig(collaterals[1]));
+
+ uint256 collateralValue_liquidation;
+ uint256 liabilityValue_liquidation;
+ (collateralValue_liquidation, liabilityValue_liquidation) = accountLiquidity(e, account, true);
+
+ uint256 collateralValue_borrowing;
+ uint256 liabilityValue_borrowing;
+ (collateralValue_borrowing, liabilityValue_borrowing) = accountLiquidity(e, account, false);
+
+ require collateralValue_liquidation > 0;
+ require collateralValue_borrowing > 0;
+
+ assert collateralValue_liquidation >= collateralValue_borrowing;
+
+}
+
+// passing run: https://prover.certora.com/output/65266/e768bd4519db456aac70651279b9f124/?anonymousKey=d78f56247abf57df2dec09115b4700e32946a1a9
+rule ltv_liabilities_equal{
+ env e;
+ calldataarg args;
+
+ address account;
+
+
+ // based on loop bound
+ address[] collaterals = getCollateralsExt(account);
+ require collaterals.length == 2;
+ require LTVConfigAssumptions(e, getLTVConfig(collaterals[0]));
+ require LTVConfigAssumptions(e, getLTVConfig(collaterals[1]));
+
+ uint256 collateralValue_liquidation;
+ uint256 liabilityValue_liquidation;
+ (collateralValue_liquidation, liabilityValue_liquidation) = accountLiquidity(e, account, true);
+
+ uint256 collateralValue_borrowing;
+ uint256 liabilityValue_borrowing;
+ (collateralValue_borrowing, liabilityValue_borrowing) = accountLiquidity(e, account, false);
+
+ require collateralValue_liquidation > 0;
+ require collateralValue_borrowing > 0;
+
+ assert liabilityValue_liquidation == liabilityValue_borrowing;
+}
+
+// passing
+// run: https://prover.certora.com/output/40726/f67c06400ebc412c88740b7efe675bc4/?anonymousKey=7101510a9ac7abec41dab0ee4f659219da5b98b9
+rule checkLiquidityReturningSameAsOriginal {
+ env e;
+ address account;
+ address[] collaterals = getCollateralsExt(account);
+ // rule out irrelevant reverts in calculateLiquidityExternal
+ // which are also ruled out by the EVC call/batch interface
+ // and the setup for the holy grail rule
+ require e.msg.sender == evc;
+ require evc.areChecksInProgress(e);
+
+ require collaterals.length <= 2; // loop bound
+ bool ret = checkLiquidityReturning(e, account, collaterals);
+ checkAccountStatus@withrevert(e, account, collaterals);
+ bool originalReverted = lastReverted;
+ assert ret <=> !originalReverted;
+}
+
+// Passing
+
+rule accountLiquidityMustRevert {
+ env e;
+ calldataarg args;
+ address account;
+ bool liquidation;
+
+ require oracleAddress != 0;
+
+ bool vaultControlsAccount = vaultIsOnlyController(account);
+ bool oracleConfigured = vaultCacheOracleConfigured(e);
+
+ accountLiquidity(e, account, liquidation);
+ // If we did not revert then: ...
+ assert vaultControlsAccount;
+ assert oracleConfigured;
+
+}
+
+// passing
+rule accountLiquidityFullMustRevert {
+ env e;
+ calldataarg args;
+ address account;
+ bool liquidation;
+
+ require oracleAddress != 0;
+
+ bool vaultControlsAccount = vaultIsOnlyController(account);
+ bool oracleConfigured = vaultCacheOracleConfigured(e);
+
+ accountLiquidityFull(e, account, liquidation);
+ // If we did not revert then: ...
+ assert vaultControlsAccount;
+ assert oracleConfigured;
+}
+
+// passing
+rule checkAccountStatusMustRevert {
+ env e;
+ calldataarg args;
+ address account;
+ address[] collaterals;
+ bool checksInProgress = evc.areChecksInProgress(e);
+ checkAccountStatus(e, account, collaterals);
+ assert e.msg.sender == evc;
+ assert checksInProgress;
+}
+
+// passing
+rule checkVaultStatusMustRevert {
+ env e;
+ calldataarg args;
+ bool checksInProgress = evc.areChecksInProgress(e);
+ checkVaultStatus(e);
+ assert e.msg.sender == evc;
+ assert checksInProgress;
+}
diff --git a/certora/specs/Vault.spec b/certora/specs/Vault.spec
new file mode 100644
index 00000000..b785de7a
--- /dev/null
+++ b/certora/specs/Vault.spec
@@ -0,0 +1,172 @@
+// all passing
+// run: https://prover.certora.com/output/65266/4e6a6aeb5af9454e87e8245498b0207d?anonymousKey=e924e53a6ff7a84beab51de18671463a166885b4
+methods {
+ // Track if a check was scheduled
+ function EVCClient.EVCRequireStatusChecks(address account) internal => CVLRequireStatusCheck(account);
+
+ // Track if balance forwarder hook is called
+ function _.balanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) external =>
+ CVLCalledBalanceForwarder(account, newAccountBalance) expect void;
+
+ // Workaround for lack of ability to summarize metadata
+ function Cache.loadVault() internal returns (Vault.VaultCache memory) => CVLLoadVault();
+ function Cache.updateVault() internal returns (Vault.VaultCache memory) => CVLLoadVault();
+
+ // IERC20
+ function _.name() external => DISPATCHER(true);
+ function _.symbol() external => DISPATCHER(true);
+ function _.decimals() external => DISPATCHER(true);
+ function _.totalSupply() external => DISPATCHER(true);
+ function _.balanceOf(address) external => DISPATCHER(true);
+ function _.allowance(address,address) external => DISPATCHER(true);
+ function _.approve(address,uint256) external => DISPATCHER(true);
+ function _.transfer(address,uint256) external => DISPATCHER(true);
+ function _.transferFrom(address,address,uint256) external => DISPATCHER(true);
+}
+
+persistent ghost bool calledStatusCheck;
+function CVLRequireStatusCheck(address account) {
+ calledStatusCheck = true;
+}
+
+function CVLLoadVault() returns Vault.VaultCache {
+ Vault.VaultCache vaultCache;
+ require vaultCache.oracle != 0;
+ return vaultCache;
+}
+
+definition isHookOperation(method f) returns bool =
+ f.selector == sig:Vault.deposit(uint256, address).selector ||
+ f.selector == sig:Vault.mint(uint256, address).selector ||
+ f.selector == sig:Vault.withdraw(uint256, address, address).selector ||
+ f.selector == sig:Vault.redeem(uint256, address, address).selector ||
+ f.selector == sig:Vault.skim(uint256, address).selector;
+
+rule status_checks_scheduled (method f) filtered { f ->
+ isHookOperation(f)
+}{
+ env e;
+ calldataarg args;
+ require calledStatusCheck == false;
+ f(e, args);
+ assert calledStatusCheck;
+}
+
+persistent ghost bool calledForwarder;
+function CVLCalledBalanceForwarder(address account, uint256 newAccountBalance) {
+ calledForwarder = true;
+}
+
+// If balance forwarding is enabled and OP is not disabled,
+// the Vault methods will call the balance forwarding hook
+// NOTE: these rules are not parametric because they need
+// to constrain to the case that the result is nonzero.
+rule balance_forwarding_called_deposit {
+ env e;
+ uint256 amount;
+ address receiver;
+ uint256 result;
+
+ uint256 balance;
+ bool forwarderEnabled;
+
+ require !calledForwarder;
+
+ // if balance forwarding is enabled and OP is not disabled
+ balance, forwarderEnabled = getBalanceAndForwarderExt(e, receiver);
+
+ require forwarderEnabled;
+ require !isDepositDisabled(e);
+ result = deposit(e, amount, receiver);
+
+ // // balance forwarding hook is called
+ assert result !=0 => calledForwarder;
+}
+
+// If balance forwarding is enabled and OP is not disabled,
+// mint will call the balance forwarding hook
+rule balance_forwarding_called_mint {
+ env e;
+ uint256 amount;
+ address receiver;
+ uint256 result;
+
+ uint256 balance;
+ bool forwarderEnabled;
+
+ require !calledForwarder;
+
+ // if balance forwarding is enabled and OP is not disabled
+ balance, forwarderEnabled = getBalanceAndForwarderExt(e, receiver);
+ require forwarderEnabled;
+ require !isMintDisabled(e);
+ result = mint(e, amount, receiver);
+
+ // balance forwarding hook is called
+ assert result != 0 => calledForwarder;
+}
+
+rule balance_forwarding_called_withdraw {
+ env e;
+ uint256 amount;
+ address receiver;
+ address owner;
+ uint256 result;
+
+ uint256 balance;
+ bool forwarderEnabled;
+
+ require !calledForwarder;
+
+ // if balance forwarding is enabled and OP is not disabled
+ balance, forwarderEnabled = getBalanceAndForwarderExt(e, owner);
+ require forwarderEnabled;
+ require !isWithdrawDisabled(e);
+ result = withdraw(e, amount, receiver, owner);
+
+ // balance forwarding hook is called
+ assert result !=0 => calledForwarder;
+}
+
+rule balance_forwarding_called_redeem {
+ env e;
+ uint256 amount;
+ address receiver;
+ address owner;
+ uint256 result;
+
+ uint256 balance;
+ bool forwarderEnabled;
+
+ require !calledForwarder;
+
+ // if balance forwarding is enabled and OP is not disabled
+ balance, forwarderEnabled = getBalanceAndForwarderExt(e, owner);
+ require forwarderEnabled;
+ require !isRedeemDisabled(e);
+ result = redeem(e, amount, receiver, owner);
+
+ // balance forwarding hook is called
+ assert result != 0 => calledForwarder;
+}
+
+rule balance_forwarding_called_skim {
+ env e;
+ uint256 amount;
+ address receiver;
+ uint256 result;
+
+ uint256 balance;
+ bool forwarderEnabled;
+
+ require !calledForwarder;
+
+ // if balance forwarding is enabled and OP is not disabled
+ balance, forwarderEnabled = getBalanceAndForwarderExt(e, receiver);
+ require forwarderEnabled;
+ require !isSkimDisabled(e);
+ result = skim(e, amount, receiver);
+
+ // balance forwarding hook is called
+ assert result != 0 => calledForwarder;
+}
\ No newline at end of file
diff --git a/certora/specs/VaultERC4626.spec b/certora/specs/VaultERC4626.spec
new file mode 100644
index 00000000..873ed88a
--- /dev/null
+++ b/certora/specs/VaultERC4626.spec
@@ -0,0 +1,571 @@
+/*
+ * This is a specification file to formally verify BorrowSystem.sol
+ * smart contract using the Certora Prover. For more information,
+ * visit: https://www.certora.com/
+ *
+ */
+
+
+// reference from the spec to additional contracts used in the verification
+
+import "Base.spec";
+import "./GhostPow.spec";
+import "./LoadVaultSummary.spec";
+
+using DummyERC20A as ERC20a;
+// using DummyERC20B as ERC20b;
+
+/*
+ Declaration of methods that are used in the rules. envfree indicate that
+ the method is not dependent on the environment (msg.value, msg.sender).
+ Methods that are not declared here are assumed to be dependent on env.
+*/
+methods {
+ function name() external returns string envfree;
+ function symbol() external returns string envfree;
+ function decimals() external returns uint8 envfree;
+ function asset() external returns address envfree;
+
+ function approve(address,uint256) external returns bool;
+ function deposit(uint256,address) external;
+ function mint(uint256,address) external;
+ function withdraw(uint256,address,address) external;
+ function redeem(uint256,address,address) external;
+
+
+ function permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external;
+ function DOMAIN_SEPARATOR() external returns bytes32;
+
+ //// #ERC20 methods
+
+ function ERC20a.transferFrom(address,address,uint256) external returns bool; // not envfree
+
+
+ function RPow.rpow(uint256 x, uint256 y, uint256 base) internal returns (uint256, bool) => CVLPow(x, y, base);
+
+ // See comment near CVLgetCurrentOnBehalfOfAccount definition.
+ function _.getCurrentOnBehalfOfAccount(address controller) external => CVLgetCurrentOnBehalfOfAccount(controller) expect (address, bool);
+
+ // These are unresolved calls that havoc contract state.
+ // Most of these cause these havocs because of a low-level call
+ // operation and are irrelevant for the rules.
+ function _.invokeHookTarget(address caller) internal => NONDET;
+ // another unresolved call that havocs all contracts
+ function _.requireVaultStatusCheck() external => NONDET;
+ function _.requireAccountAndVaultStatusCheck(address account) external => NONDET;
+ function EthereumVaultConnector.getAccountOwner(address account) external returns address => CVLGetAccountOwner(account);
+ // trySafeTransferFrom cannot be summarized as NONDET (due to return type
+ // that includes bytes memory). So it is summarized as
+ // DummyERC20a.transferFrom
+ function _.trySafeTransferFrom(address token, address from, address to, uint256 value) internal with (env e) => CVLTrySafeTransferFrom(e, token,from, to, value) expect (bool, bytes memory);
+ // safeTransferFrom is summarized as transferFrom
+ // from DummyERC20a to avoid dealing with the low-level `call`
+ function _.safeTransferFrom(address token, address from, address to, uint256 value, address permit2) internal => CVLSafeTransferFrom(token, from, to, value) expect void;
+ function _.tryBalanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) internal => NONDET;
+ function _.balanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) external => NONDET;
+ // This is NONDET to help avoid timeouts. It should be safe
+ // to NONDET since it is a private view function.
+ function _.resolve(Vault.AmountCap self) internal => CONSTANT;
+
+}
+
+// This is not in the scene for this config, so we just want it to be
+// an uninterpreted function rather than NONDET so that
+// we get the same value when this is called for different parts
+ghost address GhostOnBehalfOfAccount {
+ axiom GhostOnBehalfOfAccount != currentContract;
+ axiom GhostOnBehalfOfAccount != 0;
+}
+ghost CVLgetCurrentOnBehalfOfAccountBool(address) returns bool;
+function CVLgetCurrentOnBehalfOfAccount(address addr) returns (address, bool) {
+ return (GhostOnBehalfOfAccount,
+ CVLgetCurrentOnBehalfOfAccountBool(addr));
+}
+persistent ghost CVLGetAccountOwner(address) returns address;
+
+// Summarize trySafeTransferFrom as DummyERC20 transferFrom
+function CVLSafeTransferFrom(address token, address from, address to, uint256 value) {
+ env e;
+ ERC20a.transferFrom(e, from, to, value);
+}
+
+function CVLTrySafeTransferFrom(env e, address token, address from, address to, uint256 value) returns (bool, bytes) {
+ bytes ret;
+ return (ERC20a.transferFrom(e, from, to, value), ret);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// # asset To shares mathematical properties /////
+////////////////////////////////////////////////////////////////////////////////
+
+rule conversionOfZero {
+ env e;
+ uint256 convertZeroShares = convertToAssets(e, 0);
+ uint256 convertZeroAssets = convertToShares(e, 0);
+
+ assert convertZeroShares == 0,
+ "converting zero shares must return zero assets";
+ assert convertZeroAssets == 0,
+ "converting zero assets must return zero shares";
+}
+
+// passing
+// run: https://prover.certora.com/output/65266/e7e04c3291f843ba9fe0b81ea9a1f949/?anonymousKey=1828bc78fcb1ed87cf33d17878823becfad2ca23
+rule convertToAssetsWeakAdditivity() {
+ env e;
+ uint256 sharesA; uint256 sharesB;
+ require sharesA + sharesB < max_uint128
+ && convertToAssets(e, sharesA) + convertToAssets(e, sharesB) < to_mathint(max_uint256)
+ && convertToAssets(e, require_uint256(sharesA + sharesB)) < max_uint256;
+ assert convertToAssets(e, sharesA) + convertToAssets(e, sharesB) <= to_mathint(convertToAssets(e, require_uint256(sharesA + sharesB))),
+ "converting sharesA and sharesB to assets then summing them must yield a smaller or equal result to summing them then converting";
+}
+
+// passing
+// run: https://prover.certora.com/output/65266/3bd31b8e066543fc8097a0ffce93ee41/?anonymousKey=5b8d2876fecf3d8af7a550e203faa4d58bbedf5c
+rule convertToSharesWeakAdditivity() {
+ env e;
+ uint256 assetsA; uint256 assetsB;
+ require assetsA + assetsB < max_uint128
+ && convertToAssets(e, assetsA) + convertToAssets(e, assetsB) < to_mathint(max_uint256)
+ && convertToAssets(e, require_uint256(assetsA + assetsB)) < max_uint256;
+ assert convertToAssets(e, assetsA) + convertToAssets(e, assetsB) <= to_mathint(convertToAssets(e, require_uint256(assetsA + assetsB))),
+ "converting assetsA and assetsB to shares then summing them must yield a smaller or equal result to summing them then converting";
+}
+
+// passing
+// run: https://prover.certora.com/output/40748/614a8496d9784ba5873b9be6636d9f3e/?anonymousKey=a0622d3850471ef5d170484cbe7c5fec18646d61
+rule conversionWeakMonotonicity {
+ env e;
+ uint256 smallerShares; uint256 largerShares;
+ uint256 smallerAssets; uint256 largerAssets;
+
+ assert smallerShares < largerShares => convertToAssets(e, smallerShares) <= convertToAssets(e, largerShares),
+ "converting more shares must yield equal or greater assets";
+ assert smallerAssets < largerAssets => convertToShares(e, smallerAssets) <= convertToShares(e, largerAssets),
+ "converting more assets must yield equal or greater shares";
+}
+
+// passing
+// run: https://prover.certora.com/output/65266/302371dbde0246a28808b078c2164615/?anonymousKey=9759cd932017c8a142c5e1c4d6fa312b4ef94ae3
+rule conversionWeakIntegrity() {
+ env e;
+ uint256 sharesOrAssets;
+ assert convertToShares(e, convertToAssets(e, sharesOrAssets)) <= sharesOrAssets,
+ "converting shares to assets then back to shares must return shares less than or equal to the original amount";
+ assert convertToAssets(e, convertToShares(e, sharesOrAssets)) <= sharesOrAssets,
+ "converting assets to shares then back to assets must return assets less than or equal to the original amount";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// # Unit Test /////
+////////////////////////////////////////////////////////////////////////////////
+
+// passing with conf as here:
+// https://prover.certora.com/output/65266/3fd23869b2124c45aa47599c521a70e5?anonymousKey=4c63cefe6e66a12fc34d6c9c887c3481b67379f0
+rule depositMonotonicity() {
+
+ env e; storage start = lastStorage;
+
+ uint256 smallerAssets; uint256 largerAssets;
+ address receiver;
+ require currentContract != e.msg.sender && currentContract != receiver;
+
+ require largerAssets < max_uint256; // amount = max_uint256 deposits the full balance and we get a CEX for that case.
+
+ safeAssumptions(e, e.msg.sender, receiver);
+
+ deposit(e, smallerAssets, receiver);
+ uint256 smallerShares = balanceOf(e, receiver) ;
+
+ deposit(e, largerAssets, receiver) at start;
+ uint256 largerShares = balanceOf(e, receiver) ;
+
+ assert smallerAssets < largerAssets => smallerShares <= largerShares,
+ "when supply tokens outnumber asset tokens, a larger deposit of assets must produce an equal or greater number of shares";
+}
+
+//run: https://prover.certora.com/output/65266/8d021eab19f945cd86a3ef904b0aa6dc/?anonymousKey=bd4cc32f9af86278b0eceaae2316ea3e385c1cdf
+rule zeroDepositZeroShares(uint assets, address receiver)
+{
+ env e;
+
+ uint shares = deposit(e,assets, receiver);
+ // In this Vault, max_uint256 as an argument will transfer all assets
+ // to the vault. This precondition rules out the case where
+ // the depositor calls deposit with a balance of 0 in the underlying
+ // asset and gives max_uint256 as the shares.
+ require assets < max_uint256;
+
+ assert shares == 0 <=> assets == 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// # Valid State /////
+////////////////////////////////////////////////////////////////////////////////
+
+invariant assetsMoreThanSupply(env e)
+ totalAssets(e) >= totalSupply(e)
+ {
+ preserved {
+ require e.msg.sender != currentContract;
+ }
+ }
+
+invariant noAssetsIfNoSupply(env e)
+ ( totalAssets(e) == 0 => ( totalSupply(e) == 0 ))
+
+ {
+ preserved {
+ address any;
+ safeAssumptions(e, any, actualCaller(e));
+ safeAssumptions(e, any, actualCallerCheckController(e));
+ }
+ }
+
+invariant noSupplyIfNoAssets(env e)
+ noSupplyIfNoAssetsDef(e) // see definition in "helpers and miscellaneous" section
+ {
+ preserved {
+ safeAssumptions(e, _, e.msg.sender);
+ }
+ }
+
+
+
+persistent ghost mathint sumOfBalances {
+ init_state axiom sumOfBalances == 0;
+}
+
+hook Sstore currentContract.vaultStorage.users[KEY address addy].data Vault.PackedUserSlot newValue (Vault.PackedUserSlot oldValue) {
+ sumOfBalances = sumOfBalances + newValue - oldValue;
+}
+
+hook Sload Vault.PackedUserSlot val currentContract.vaultStorage.users[KEY address addy].data {
+ require sumOfBalances >= to_mathint(val);
+}
+
+// passing: https://prover.certora.com/output/65266/de3636d287d2473294463c07263fc11e/?anonymousKey=ac8f74e6c5c1298f0954a21fafd41cccf32b9ffb
+invariant totalSupplyIsSumOfBalances(env e)
+ // to_mathint(totalSupply(e)) == sumOfBalances + accumulatedFees(e);
+ to_mathint(totalSupply(e)) == sumOfBalances;
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// # State Transition /////
+////////////////////////////////////////////////////////////////////////////////
+
+//run: https://prover.certora.com/output/65266/3ef25c98a7e34422bcf177d853662b5f/?anonymousKey=ca43e967a607a404f34b39c70f6517e90dac0902
+rule totalsMonotonicity() {
+ method f; env e; calldataarg args;
+ require e.msg.sender != currentContract;
+ uint256 totalSupplyBefore = totalSupply(e);
+ uint256 totalAssetsBefore = totalAssets(e);
+ address receiver;
+ safeAssumptions(e, receiver, e.msg.sender);
+ callReceiverFunctions(f, e, receiver);
+
+ uint256 totalSupplyAfter = totalSupply(e);
+ uint256 totalAssetsAfter = totalAssets(e);
+
+ // possibly assert totalSupply and totalAssets must not change in opposite directions
+ assert totalSupplyBefore < totalSupplyAfter <=> totalAssetsBefore < totalAssetsAfter,
+ "if totalSupply changes by a larger amount, the corresponding change in totalAssets must remain the same or grow";
+ assert totalSupplyAfter == totalSupplyBefore => totalAssetsBefore == totalAssetsAfter,
+ "equal size changes to totalSupply must yield equal size changes to totalAssets";
+}
+
+rule underlyingCannotChange() {
+ address originalAsset = asset();
+
+ method f; env e; calldataarg args;
+ f(e, args);
+
+ address newAsset = asset();
+
+ assert originalAsset == newAsset,
+ "the underlying asset of a contract must not change";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// # High Level /////
+////////////////////////////////////////////////////////////////////////////////
+
+// passing
+// run: https://prover.certora.com/output/65266/a19010e64bb8424aa513be8b75d15cdf/?anonymousKey=87c73cdf676930336269396f2dbb3cac3d78b997
+rule dustFavorsTheHouse(uint assetsIn )
+{
+ env e;
+
+ require e.msg.sender != currentContract;
+ safeAssumptions(e,e.msg.sender,e.msg.sender);
+ uint256 totalSupplyBefore = totalSupply(e);
+
+ uint balanceBefore = userAssets(e, currentContract);
+
+ uint shares = deposit(e,assetsIn, e.msg.sender);
+ uint assetsOut = redeem(e,shares,e.msg.sender,e.msg.sender);
+
+ uint balanceAfter = userAssets(e, currentContract);
+ assert balanceAfter >= balanceBefore;
+}
+
+// passing:
+// run: https://prover.certora.com/output/65266/16c756cc79054db2822d8d77cd7d157b?anonymousKey=ab0d69f0506e327db1fd9180bf8b0259a7bf1f7b
+rule dustFavorsTheHouseAssets(uint assetsIn )
+{
+ env e;
+
+ require e.msg.sender != currentContract;
+ safeAssumptions(e,e.msg.sender,e.msg.sender);
+ uint256 totalAssetsBefore = totalAssets(e);
+
+ uint shares = deposit(e,assetsIn, e.msg.sender);
+ uint assetsOut = redeem(e,shares,e.msg.sender,e.msg.sender);
+ uint256 totalAssetsAfter = totalAssets(e);
+
+ assert totalAssetsAfter >= totalAssetsBefore;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// # Risk Analysis /////////
+////////////////////////////////////////////////////////////////////////////////
+
+
+invariant vaultSolvency(env e)
+ totalAssets(e) >= totalSupply(e) && userAssets(e, currentContract) >= require_uint256(cache_cash(e)) {
+ preserved {
+ requireInvariant totalSupplyIsSumOfBalances(e);
+ require e.msg.sender != currentContract;
+ require actualCaller(e) != currentContract;
+ require actualCallerCheckController(e) != currentContract;
+ require currentContract != asset();
+ }
+ }
+
+rule vaultSolvencyWithdraw_totals {
+ env e;
+ requireInvariant totalSupplyIsSumOfBalances(e);
+ require e.msg.sender != currentContract;
+ require actualCaller(e) != currentContract;
+ require currentContract != asset();
+ uint256 amount;
+ address receiver;
+ address owner;
+ require totalAssets(e) >= totalSupply(e);
+ require userAssets(e, currentContract) >= require_uint256(cache_cash(e));
+ withdraw(e, amount, receiver, owner);
+ assert totalAssets(e) >= totalSupply(e);
+}
+
+rule withdraw_amount_max {
+ env e;
+ require e.msg.sender != currentContract;
+ require actualCaller(e) != currentContract;
+ require currentContract != asset();
+ uint256 amount;
+ address receiver;
+ address owner;
+ withdraw(e, amount, receiver, owner);
+ assert amount <= max_uint112 || amount == max_uint256;
+}
+
+rule vaultSolvencyWithdraw_underlying {
+ env e;
+ requireInvariant totalSupplyIsSumOfBalances(e);
+ require e.msg.sender != currentContract;
+ require actualCaller(e) != currentContract;
+ require currentContract != asset();
+ uint256 amount;
+ address receiver;
+ address owner;
+ require userAssets(e, currentContract) >= require_uint256(cache_cash(e));
+ withdraw(e, amount, receiver, owner);
+ assert userAssets(e, currentContract) >= require_uint256(cache_cash(e));
+}
+
+rule redeemingAllValidity() {
+ env e;
+ address owner;
+ uint256 shares; require shares == balanceOf(e, owner);
+
+ safeAssumptions(e, _, owner);
+ redeem(e, shares, _, owner);
+ uint256 ownerBalanceAfter = balanceOf(e, owner);
+ assert ownerBalanceAfter == 0;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// # stakeholder properties (Risk Analysis ) //////////
+////////////////////////////////////////////////////////////////////////////////
+
+// passing. run: https://prover.certora.com/output/65266/48a3074474f1475baf13fe3cb9602567/?anonymousKey=9111d29e8d8ed721825b12f083128af396e5e814
+rule contributingProducesShares(method f)
+filtered {
+ f -> f.selector == sig:deposit(uint256,address).selector
+ || f.selector == sig:mint(uint256,address).selector
+}
+{
+ env e; uint256 assets; uint256 shares;
+ address contributor;
+
+ // need to minimize these
+ require actualCaller(e) == contributor;
+ require contributor == GhostOnBehalfOfAccount;
+ require actualCallerCheckController(e) == contributor;
+
+ address receiver;
+ require currentContract != contributor
+ && currentContract != receiver;
+
+ require previewDeposit(e, assets) + balanceOf(e, receiver) <= max_uint256; // safe assumption because call to _mint will revert if totalSupply += amount overflows
+ require shares + balanceOf(e, receiver) <= max_uint256; // same as above
+
+ safeAssumptions(e, contributor, receiver);
+
+ uint256 contributorAssetsBefore = userAssets(e, contributor);
+ uint256 receiverSharesBefore = balanceOf(e, receiver);
+
+ callContributionMethods(e, f, assets, shares, receiver);
+
+ uint256 contributorAssetsAfter = userAssets(e, contributor);
+ uint256 receiverSharesAfter = balanceOf(e, receiver);
+
+ assert contributorAssetsBefore > contributorAssetsAfter <=> receiverSharesBefore < receiverSharesAfter,
+ "a contributor's assets must decrease if and only if the receiver's shares increase";
+}
+
+// passing
+// run: https://prover.certora.com/output/65266/28a47dd30c6747cbbc4495de59e5f965?anonymousKey=2e86f97ff0030d5489503334c71961bb5978f331
+rule onlyContributionMethodsReduceAssets(method f) {
+ env e; calldataarg args;
+ address user; require user != currentContract;
+ uint256 userAssetsBefore = userAssets(e, user);
+
+ safeAssumptions(e, user, _);
+
+ f(e, args);
+
+ uint256 userAssetsAfter = userAssets(e, user);
+
+ assert userAssetsBefore > userAssetsAfter =>
+ (f.selector == sig:deposit(uint256,address).selector ||
+ f.selector == sig:mint(uint256,address).selector),
+ "a user's assets must not go down except on calls to contribution methods";
+}
+
+// passing
+// run: https://prover.certora.com/output/65266/8ead2419e398420286adb1f636a35249/?anonymousKey=f135ef5ad92b9e187a5df3ebce5499f693eae015
+rule reclaimingProducesAssets(method f)
+filtered {
+ f -> f.selector == sig:withdraw(uint256,address,address).selector
+ || f.selector == sig:redeem(uint256,address,address).selector
+}
+{
+ env e; uint256 assets; uint256 shares;
+ address receiver; address owner;
+ require currentContract != e.msg.sender
+ && currentContract != receiver
+ && currentContract != owner;
+
+ safeAssumptions(e, receiver, owner);
+
+ uint256 ownerSharesBefore = balanceOf(e, owner);
+ uint256 receiverAssetsBefore = userAssets(e, receiver);
+
+ callReclaimingMethods(e, f, assets, shares, receiver, owner);
+
+ uint256 ownerSharesAfter = balanceOf(e, owner);
+ uint256 receiverAssetsAfter = userAssets(e, receiver);
+
+ assert ownerSharesBefore > ownerSharesAfter <=> receiverAssetsBefore < receiverAssetsAfter,
+ "an owner's shares must decrease if and only if the receiver's assets increase";
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// # helpers and miscellaneous //////////
+////////////////////////////////////////////////////////////////////////////////
+
+definition noSupplyIfNoAssetsDef(env e) returns bool =
+ ( totalAssets(e) == 0 => ( totalSupply(e) == 0 ));
+
+
+function safeAssumptions(env e, address receiver, address owner) {
+ require currentContract != asset(); // Although this is not disallowed, we assume the contract's underlying asset is not the contract itself
+ requireInvariant totalSupplyIsSumOfBalances(e);
+ requireInvariant vaultSolvency(e);
+ requireInvariant noAssetsIfNoSupply(e);
+ requireInvariant noSupplyIfNoAssets(e);
+ requireInvariant assetsMoreThanSupply(e);
+
+ //// # Note : we don't want to use singleBalanceBounded and singleBalanceBounded invariants
+ ///// # but, it safe to assume that a single balance is less than sum of balances
+ require ( (receiver != owner => balanceOf(e, owner) + balanceOf(e, receiver) <= to_mathint(totalSupply(e))) &&
+ balanceOf(e, receiver) <= totalSupply(e) &&
+ balanceOf(e, owner) <= totalSupply(e));
+}
+
+
+// A helper function to set the receiver
+function callReceiverFunctions(method f, env e, address receiver) {
+ uint256 amount;
+ if (f.selector == sig:deposit(uint256,address).selector) {
+ deposit(e, amount, receiver);
+ } else if (f.selector == sig:mint(uint256,address).selector) {
+ mint(e, amount, receiver);
+ } else if (f.selector == sig:withdraw(uint256,address,address).selector) {
+ address owner;
+ withdraw(e, amount, receiver, owner);
+ } else if (f.selector == sig:redeem(uint256,address,address).selector) {
+ address owner;
+ redeem(e, amount, receiver, owner);
+ } else {
+ calldataarg args;
+ f(e, args);
+ }
+}
+
+
+function callContributionMethods(env e, method f, uint256 assets, uint256 shares, address receiver) {
+ if (f.selector == sig:deposit(uint256,address).selector) {
+ deposit(e, assets, receiver);
+ }
+ if (f.selector == sig:mint(uint256,address).selector) {
+ mint(e, shares, receiver);
+ }
+}
+
+function callReclaimingMethods(env e, method f, uint256 assets, uint256 shares, address receiver, address owner) {
+ if (f.selector == sig:withdraw(uint256,address,address).selector) {
+ withdraw(e, assets, receiver, owner);
+ }
+ if (f.selector == sig:redeem(uint256,address,address).selector) {
+ redeem(e, shares, receiver, owner);
+ }
+}
+
+function callFunctionsWithReceiverAndOwner(env e, method f, uint256 assets, uint256 shares, address receiver, address owner) {
+ if (f.selector == sig:withdraw(uint256,address,address).selector) {
+ withdraw(e, assets, receiver, owner);
+ }
+ if (f.selector == sig:redeem(uint256,address,address).selector) {
+ redeem(e, shares, receiver, owner);
+ }
+ if (f.selector == sig:deposit(uint256,address).selector) {
+ deposit(e, assets, receiver);
+ }
+ if (f.selector == sig:mint(uint256,address).selector) {
+ mint(e, shares, receiver);
+ }
+ if (f.selector == sig:transferFrom(address,address,uint256).selector) {
+ transferFrom(e, owner, receiver, shares);
+ }
+ else {
+ calldataarg args;
+ f(e, args);
+ }
+}
\ No newline at end of file
diff --git a/docs/specs.md b/docs/specs.md
index 972d177b..4c7c7a66 100644
--- a/docs/specs.md
+++ b/docs/specs.md
@@ -1,8 +1,8 @@
|Requirement ID |Title |Description |
|------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|EVK-60|Beacon Proxy |The [beacon proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon) is a proxy for which the beacon contract is the `msg.sender` address which constructs it. The constructor allows to pass up to 128 bytes of `trailingData` that must be appended to any `delegatecall` performed by the proxy. |
-|EVK-11|DToken |DToken is a contract deployed on EVault contract initialization. It implements a subset of `ERC20` view functions that are related to the vault debt.
Functions implemented:
`name` - returns the name of the associated `EVault` preceded with "Debt token of "
`symbol` - returns the symbol of the associated `EVault` preceded with "d"
`decimals` - returns the decimals of the associated `EVault`
`totalSupply` - returns the total borrows of the associated `EVault`
`balanceOf` - returns individual debt of the user in the associated `EVault`
`allowance` - always returns 0
`asset` - returns the asset of the associated `EVault`
`emitTransfer` - emits the `Transfer` event. Callable only by the associated `EVault`
**DToken contract blocks calls to all the other public ERC20 functions.**
[https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken](https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken) |
-|EVK-9 |Dispatch |Implements helper functions and modifiers used for calls dispatching.
For code organisation purposes, and also to comfortably stay below code size limits at full optimisation levels, the component implementation contracts are organised into modules. The primary entry point contract `EVault` serves as a dispatcher that determines which module should be invoked. `EVault` inherits from *all* the modules, although functions that are overridden by a dispatching routine are considered dead code by the Solidity compiler and are not compiled in. In addition to being included in the `EVault` dispatcher contract, modules are also deployed separately so that they can be invoked with `delegatecall`.
This pattern allows functions to be handled in the following ways:
* Implemented directly: No external module is invoked. This is the most gas efficient, and is especially important for view methods that are called frequently by other contracts.
* `use(MODULE_XXX)`: The indicated module is invoked with `delegatecall`
* `useView(MODULE_XXX)`: The implementation contract uses `staticcall` to call `viewDelegate()` on itself, which then does a `delegatecall` to the indicated module.
In order to implement a function directly, it is sufficient to have no mention of it in the dispatcher. However, for documentation and consistency, the code overrides with a function that calls the corresponding module's function with `super()`. This wrapper function is inlined away by the compiler.
To delegate a function to a module, the code overwrites the function signature in the dispatcher with either the `use` or `useView` modifier and an empty code block as implementation. The empty code block removes the function, while the modifier causes the router to `delegatecall` into the module.
Modules are static, meaning they cannot be upgraded. Code upgrades require deploying a new implementation which refers to the new module, and then updating the implementation storage slot in the factory. Only upgradeable instances will be affected.
[**`delegatecall` into view functions**​](https://docs.euler.finance/euler-vault-kit-white-paper/#delegatecall-into-view-functions)
Solidity doesn't allow for view functions to `delegatecall`. To be able to remove view functions from the dispatcher codebase and `delegatecall` them instead into the modules, the dispatching mechanism uses the `useView` modifier. This modifier makes the view function `staticcall` back into the dispatcher to a `viewDelegate` function which is `non-payable`. This `viewDelegate` function can now `delegatecall` into the implementation of the view function in the module.
The issue, background and a proposed patch to the Solidity compiler described in [this solc issue](https://github.com/ethereum/solidity/issues/14577).
[**Gas vs Code-Size Tradeoff**​](https://docs.euler.finance/euler-vault-kit-white-paper/#gas-vs-code-size-tradeoff)
The top level module dispatch system in `EVault` serves as a dispatcher where you can mix and match where the code should physically be located - in the contract itself, or `delegatecall`ed to one of the modules. The routing of specific functions to specific modules is hardcoded into the dispatcher.
In order to decide if a function should be implemented directly or delegated to a module, its gas-importance and code size should be evaluated. Functions that are frequently called on-chain will benefit from being implemented directly to avoid the `delegatecall` overhead. This is especially important for `view` functions because of the `viewDelegate()` overhead. On the other hand, large functions should be delegated to ensure that the `EVault` dispatcher can fit within the 24 Kb code-size limit,
[**`callThroughEVC`**​](https://docs.euler.finance/euler-vault-kit-white-paper/#callthroughevc)
The `EVault` dispatcher makes use of another modifier called `callThroughEVC`. This modifier has two execution flows. Either it executes the function normally (dispatched or directly) if the call comes from the EVC, or calls into the EVC's `call()` method with the existing calldata prepended with the address of the dispatcher, `msg.sender`, and `msg.value`, which then calls back into the Vault for normal execution.
[https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules](https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules)|
+|EVK-11|DToken |DToken is a contract deployed on EVault contract initialization. It implements a subset of `ERC20` view functions that are related to the vault debt.
Functions implemented:
`name` - returns the name of the associated `EVault` preceded with "Debt token of "
`symbol` - returns the symbol of the associated `EVault` followed by "-DEBT"
`decimals` - returns the decimals of the associated `EVault`
`totalSupply` - returns the total borrows of the associated `EVault`
`balanceOf` - returns individual debt of the user in the associated `EVault`
`allowance` - always returns 0
`asset` - returns the asset of the associated `EVault`
`emitTransfer` - emits the `Transfer` event. Callable only by the associated `EVault`
**DToken contract blocks calls to all the other public ERC20 functions.**
[https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken](https://docs.euler.finance/euler-vault-kit-white-paper/#dtoken) |
+|EVK-9 |Dispatch |Implements helper functions and modifiers used for calls dispatching.
For code organisation purposes, and also to comfortably stay below code size limits at full optimisation levels, the component implementation contracts are organised into modules. The primary entry point contract `EVault` serves as a dispatcher that determines which module should be invoked. `EVault` inherits from *all* the modules, although functions that are overridden by a dispatching routine are considered dead code by the Solidity compiler and are not compiled in. In addition to being included in the `EVault` dispatcher contract, modules are also deployed separately so that they can be invoked with `delegatecall`.
This pattern allows functions to be handled in the following ways:
* Implemented directly: No external module is invoked. This is the most gas efficient, and is especially important for view methods that are called frequently by other contracts.
* `use(MODULE_XXX)`: The indicated module is invoked with `delegatecall`
* `useView(MODULE_XXX)`: The implementation contract uses `staticcall` to call `viewDelegate()` on itself, which then does a `delegatecall` to the indicated module.
In order to implement a function directly, it is sufficient to have no mention of it in the dispatcher. However, for documentation and consistency, the code overrides with a function that calls the corresponding module's function with `super()`. This wrapper function is inlined away by the compiler.
To delegate a function to a module, the code overwrites the function signature in the dispatcher with either the `use` or `useView` modifier and an empty code block as implementation. The empty code block removes the function, while the modifier causes the router to `delegatecall` into the module.
Modules are static, meaning they cannot be upgraded. Code upgrades require deploying a new implementation which refers to the new module, and then updating the implementation storage slot in the factory. Only upgradeable instances will be affected.
[**`delegatecall` into view functions**​](https://docs.euler.finance/euler-vault-kit-white-paper/#delegatecall-into-view-functions)
Solidity doesn't allow for view functions to `delegatecall`. To be able to remove view functions from the dispatcher codebase and `delegatecall` them instead into the modules, the dispatching mechanism uses the `useView` modifier. This modifier makes the view function `staticcall` back into the dispatcher to a `viewDelegate` function which is `payable` for gas optimization. This `viewDelegate` function can now `delegatecall` into the implementation of the view function in the module.
The issue, background and a proposed patch to the Solidity compiler described in [this solc issue](https://github.com/ethereum/solidity/issues/14577).
[**Gas vs Code-Size Tradeoff**​](https://docs.euler.finance/euler-vault-kit-white-paper/#gas-vs-code-size-tradeoff)
The top level module dispatch system in `EVault` serves as a dispatcher where you can mix and match where the code should physically be located - in the contract itself, or `delegatecall`ed to one of the modules. The routing of specific functions to specific modules is hardcoded into the dispatcher.
In order to decide if a function should be implemented directly or delegated to a module, its gas-importance and code size should be evaluated. Functions that are frequently called on-chain will benefit from being implemented directly to avoid the `delegatecall` overhead. This is especially important for `view` functions because of the `viewDelegate()` overhead. On the other hand, large functions should be delegated to ensure that the `EVault` dispatcher can fit within the 24 Kb code-size limit,
[**`callThroughEVC`**​](https://docs.euler.finance/euler-vault-kit-white-paper/#callthroughevc)
The `EVault` dispatcher makes use of another modifier called `callThroughEVC`. This modifier has two execution flows. Either it executes the function normally (dispatched or directly) if the call comes from the EVC, or calls into the EVC's `call()` method with the existing calldata prepended with the address of the dispatcher, `msg.sender`, and `msg.value`, which then calls back into the Vault for normal execution.
[https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules](https://docs.euler.finance/euler-vault-kit-white-paper/#static-modules)|
|EVK-35|Dispatch `callThroughEVC` |`callThroughEVC` is a modifier which:
* if `msg.sender` is the EVC, executes the body of the function * if `msg.sender` is not the EVC, invokes the EVC `call` function with the following arguments:
* `targetContract` must be equal to `address(this)`
* `onBehalfOfAccount` must be equal to `msg.sender`
* `value` must be equal to the `msg.value`
* `data` must be equal to the `msg.data`
`callThroughEVC` reverts if the call to the EVC is unsuccessful.
The expectation of the call to the EVC is that the EVC will call back into this contract and execute the function, on which `callThroughEVC` modifier was used, within the EVC checks deferred context.
[https://evc.wtf/docs/concepts/internals/call](https://evc.wtf/docs/concepts/internals/call)
[https://evc.wtf/docs/concepts/internals/checks-deferrable-call](https://evc.wtf/docs/concepts/internals/checks-deferrable-call) |
|EVK-34|Dispatch `useView` |`useView` is a modifier which `staticcall`s into `function viewDelegate()` on this contract (`address(this)`). The calldata is as follows:
```bash viewDelegate.selector + module address + (msg.data with the proxy metadata stripped at the end) + caller address ```
`useView` reverts if `staticcall` is unsuccessful.
`viewDelegate` is a function which `delegatecall`s into the `module` address provided by `useView` with as per current `msg.data`.
`viewDelegate` reverts if not self-called OR if `delegatecall` is unsuccessful. |
|EVK-33|Dispatch `use` |`use` is a modifier which `delegatecall`s into provided `module` address as per current `msg.data`. `use` reverts if `delegatecall` is unsuccessful. |
@@ -16,7 +16,8 @@
|EVK-71|Miscellaneous Hooked Operations |Some of the operation types of the vault may be configured to call the hook target when being performed thanks to which the vault supports a limited hooking functionality. When operation is performed, the vault must check if the corresponding operation is set in hooked ops.
If the corresponding operation is set in the hooked ops AND the hook target configured is a compatible smart contract, the hook target is `call`ed using the same `msg.data` as was provided to the vault, along with the EVC-authenticated caller appended as trailing calldata. If the call is successful, the execution of the operation must carry on as usual. If the call is unsuccessful, the operation must revert with the hook target error bubbled up.
If the corresponding operation is set in the hooked ops AND the hook target configured is address zero, then the vault operation fails unconditionally as it is considered disabled.
In addition to user-invokable functions, the operation than can also be hooked is `checkVaultStatus`. The hook target will be invoked when the EVC calls `checkVaultStatus` on the vault, which is typically at the end of any batch that has interacted with the vault. This hook can be used to reject operations that violate "post-condition" properties of the vault.
[https://docs.euler.finance/euler-vault-kit-white-paper/#hooks](https://docs.euler.finance/euler-vault-kit-white-paper/#hooks) |
|EVK-65|Miscellaneous Interest and Fees Accrual |Any state-modifying operation that affects the vault balances and liabilities, must operate on the up to date vault state. In order to keep the vault state up to date, when loading the cache, the vault state is updated considering the following:
* the interest is accrued since the last update time using the current interest rate
* the fees are cut from the accrued interest as per the interest fee parameter
* new vault shares are minted to grant ownership over the fees to the virtual temporary fee holder account
[https://docs.euler.finance/euler-vault-kit-white-paper/#interest](https://docs.euler.finance/euler-vault-kit-white-paper/#interest) |
|EVK-69|Miscellaneous LTV Ramping |Vaults must implement LTV ramping allowing the governor to change the LTV of the collateral without putting outstanding loans into immediate violation.
When using the `setLTV` function, the governor can specify the target LTV and the ramp duration. When an account wants to take on a new liability or modify its existing position, the account health must be evaluated using the target LTV value configured. For the purpose of liquidations however, the account health must be evaluated by calculating the current LTV, assuming it changes linearly from the original LTV stored to the target LTV configured in the configured ramp duration time.
[https://docs.euler.finance/euler-vault-kit-white-paper/#ltv-ramping](https://docs.euler.finance/euler-vault-kit-white-paper/#ltv-ramping) |
-|EVK-74|Miscellaneous Pull Assets |Whenever the underlying assets are being pulled into the vault, the operation must try to transfer the assets from the specified account to the vault directly. If the direct transfer fails and `permit2` contract address is configured, the operation must try to transfer the assets from the specified account to the vault using `permit2` contract. |
+|EVK-74|Miscellaneous Pull Assets |Whenever the underlying assets are being pulled into the vault, if `permit2` contract address is configured and the transferred amount fits `uint160`, the operation must try to transfer the assets from the specified account to the vault using `permit2` contract. If the transfer using `permit2` contract fails or is not possible, the operation must try to transfer the assets from the specified account to the vault directly.
+ |
|EVK-73|Miscellaneous Push Assets |Whenever the underlying assets are being pushed out of the vault, the operation must revert if:
* the receiver address is `address(0)`, OR
* if asset receiver validation enabled AND the receiver address is a known virtual account |
|EVK-63|Miscellaneous Re-entrancy |Each state-modifying function must be re-entrancy protected.
Each static function that reads regions of the state that may be unsafe to access during an ongoing operation on the vault must be read-only re-entrancy protected.
The installed hook target contract address can bypass the vault's read-only reentrancy protection. This means that hook functions can call view methods on the vault during their operation. However, hooks cannot perform state changing operations because of the normal reentrancy lock. |
|EVK-68|Miscellaneous Vault Status Checks and Snapshots |Any state-modifying function of the vault which loads the vault cache must schedule the vault status check. This is in order not only to potentially check whether the vault status is valid at the end of the call frame, but also to update the interest rate.
Before the vault status check is scheduled, if the supply cap or the borrow cap can be exceeded (any of them is not defined as max possible value), a snapshot of the initial state of the vault must be created which includes:
* the total amount of assets held by the vault
* the total amount of liabilities issued by the vault
If the status of any of the vaults for which the vault status check has been scheduled is invalid, the call frame in which checks are performed must revert.
[https://evc.wtf/docs/concepts/internals/vault-status-checks](https://evc.wtf/docs/concepts/internals/vault-status-checks) |
@@ -33,7 +34,6 @@
|EVK-54|Module Borrowing `repay` |If operation enabled, `repay` removes exactly `amount` of underlying tokens as liability for the `receiver` and transfers the underlying tokens from the authenticated account.
This operation is always called through the EVC.
This operation schedules the vault status check.
This operation affects:
* liability balance of the authenticated account
* total liability balance
* total balance of the underlying assets held by the vault |
|EVK-58|Module Borrowing `touch` |If operation enabled, `touch` updates the vault state.
This operation is always called through the EVC.
This operation schedules the vault status check. |
|EVK-2 |Module Governance |Implements the functions allowing the governor to configure the vault.
The vault uses EVC authentication for the governor, which means that governor actions can be batched together and simulated. However, the vault does not accept advanced EVC authentication methods like sub-accounts, operators or `controlCollateral`.
*Context:*
*Immediately after creation, the factory will call* `initialize` *on the proxy, passing in the creator's address as a parameter. The vault will set its governor to the creator's address. This governor can invoke methods that modify the configuration of the vault.*
*At this point, the creator should configure the vault as desired and then decide if the vault is to be governed or not.*
* *If so, the creator retains the governor role or transfers it to another address.*
* *If not, then the ownership is revoked by setting the governor to* `address(0)`*. No more governance changes can happen on this vault and it is considered finalized.*
*If limited governance is desired, the creator can transfer ownership to a smart contract that can only invoke a sub-set of governance methods, perhaps with only certain parameters, or under certain conditions.*
*Using the same code-base and factories, the Euler Vault Kit allows construction of both managed and unmanaged lending products. Managed vaults are intended to be long-lived and are therefore suitable for passive deposits. If market conditions change, an active governor can reconfigure the vault to optimize or protect users. Alternatively, unmanaged vaults are configured statically, and the users themselves (or a higher-level contract) must actively monitor for risks/opportunities and shift their deposits and positions to new vaults as necessary.*
[https://docs.euler.finance/euler-vault-kit-white-paper/#governed-vs-finalised](https://docs.euler.finance/euler-vault-kit-white-paper/#governed-vs-finalised) |
-|EVK-25|Module Governance `clearLTV` |`clearLTV` allows the current governor to clear the LTV config for a given collateral keeping the storage slot initialized. |
|EVK-18|Module Governance `convertFees` |If operation enabled, `convertFees` allows anyone to split accrued vault fees and transfer them to the governor-specified fee receiver and Protocol Config-specified fee receiver. The accrued vault fees are split proportionally as per Protocol Config-specified fee share that cannot exceed `MAX_PROTOCOL_FEE_SHARE`. Immediately after the fees are split, the amount of fees accrued by the vault must be set to 0.
This operation affects:
* shares balance of the governor-specified fee receiver (if it's configured)
* shares balance of the Protocol Config-specified fee receiver
* shares balance of the accumulated fees
[https://docs.euler.finance/euler-vault-kit-white-paper/#fees](https://docs.euler.finance/euler-vault-kit-white-paper/#fees) |
|EVK-28|Module Governance `setCaps` |`setCaps` allows the current governor to set a supply cap and a borrow cap as per the following specification:
*Supply cap and borrow cap are 16-bit decimal floating point values:*
*\* The least significant 6 bits are the exponent*
*\* The most significant 10 bits are the mantissa, scaled by 100*
*\* The special value of 0 means limit is not set*
*\* This is so that uninitialised storage implies no limit*
*\* For an actual cap value of 0, use a zero mantissa and non-zero exponent*
When converted to assets, the supply cap cannot exceed `2 * MAX_SANE_AMOUNT` and the borrow cap cannot exceed `MAX_SANE_AMOUNT`.
[https://docs.euler.finance/euler-vault-kit-white-paper/#supply-and-borrow-caps](https://docs.euler.finance/euler-vault-kit-white-paper/#supply-and-borrow-caps) |
|EVK-72|Module Governance `setConfigFlags` |`setConfigFlags` allows the vault governor to specify the additional configuration of the vault which refers to:
* debt socialization on liquidation
* asset receiver validation whenever the assets are pushed out of the vault |
@@ -72,8 +72,8 @@
|EVK-13|Protocol Config |The Protocol Config contract is the representative of the DAO's interests in the ecosystem. What vaults allow this contract to control is strictly limited.
Functions implemented that are called by the vaults:
`isValidInterestFee` - determines whether the value of the interest fee is allowed; vaults only invoke this if the governor attempts to set an interest fee outside of the range <`GUARANTEED_INTEREST_FEE_MIN`, `GUARANTEED_INTEREST_FEE_MAX`>
`protocolFeeConfig` - called when fees are converted; it returns the following the recipient address for the DAO's share of the fees and the fraction of the interest fees that should be sent to the DAO
[https://docs.euler.finance/euler-vault-kit-white-paper/#protocolconfig](https://docs.euler.finance/euler-vault-kit-white-paper/#protocolconfig) |
|EVK-83|Sequence Registry |The Sequence Registry contract provides an interface for reserving sequence IDs. This contract maintains sequence counters associated with opaque designator strings. Each counter starts at 1. Anybody can reserve a sequence ID by calling `reserveSeqIdfunction`. The only guarantee provided is that no two reservations for the same designator will get the same ID. |
|EVK-75|Synthetic Asset Vault |Synthetic asset vault is a special configuration of a vault which uses hooks to disable `mint`, `redeem`, `skim` and `repayWithShares` operations. The `deposit` operation is also disabled for all the callers except the underlying synth asset address itself, disallowing deposits from normal users.
Instead of a utilization based interest rate model, synthetic vaults use a reactive interest rate model which adjusts the interest rate based on the trading price of the synthetic asset. This mechanism, the peg stability module and the savings rate module aim to keep the synthetic asset pegged as tight as possible to the peg asset. The price feed used when borrowing from the synthetic vault is the asset which the synthetic asset is pegged to, creating a CDP based synthetic asset. |
-|EVK-81|Synthetic Asset Vault `ERC20Collateral` |`ERC20Collateral` is an `ERC20`-compatible token with the EVC support which allows it to be used as collateral in other vault.
`ERC20Collateral` tokens comply with the following specification:
* Whenever the contract is called via the EVC (`msg.sender == EVC`), the current on behalf of account is fetched from the EVC and used as an authenticated caller address. This ensures liquidations can be processed via `EVC.controlCollateral` function
* The account status check is requested after every operation that can potentially negatively affect the account health (i.e. whenever the tokens are transferred out of the account). This ensures the account is never unhealthy after the operation |
-|EVK-77|Synthetic Asset Vault `ESynth` |`ESynth` is an `ERC20`-compatible token with the EVC support which, thanks to relying on the EVC authentication and requesting the account status checks on token transfers and burns, allows it to be used as collateral in other vault. It is meant to be used as an underlying asset of the synthetic asset vault.
`ESynth` tokens comply with the following specification:
**ERC20Collateral**
`ESynth` inherits from `ERC20Collateral` making it compliant with the `ERC20Collateral` specification.
**Minting**
The owner of the contract can set a minting capacity by calling `setCapacity(address minter, uint128 capacity)` for any address which allows them to mint the synthetic asset up to the defined amount. Minters mint by calling `mint(address account, uint256 amount)`.
**Burning**
Any address can burn the synthetic from another address given they have the allowance to do so. The owner is exempt from this restriction when burning from the `ESynth` contract itself. When one burns from an address and they have a previously minted amount, the minted amount is reduced by the burned amount, freeing up their minting capacity. Burning can be done by calling `burn(address account, uint256 amount)`. Account status check is requested for the account from which the assets are burned.
**Allocating to the synthetic asset vault**
The owner can allocate assets held by the asset contract itself to a synthetic asset vault created specifically for this asset by calling `allocate(address vault, uint256 amount)`. This serves as a protocol deposit to the vault. Any allocation needs to be first minted by a minter or owner into the synthetic asset contract. The vault into which the assets are allocated must be integrated with the same EVC instance as the `ESynth` contract itself. On allocation, the vault is added to the addresses whose balances are ignored when calculating the `totalSupply`.
**Deallocating from the synthetic asset vault**
The owner can deallocate synthetic assets from the vault by calling `deallocate(address vault, uint256 amount)`. This serves as a protocol withdraw from the synthetic asset vault. Assets deallocated from the vault will be transferred into the synthetic asset contract itself and can in turn be burned by the owner.
**Total supply adjustments**
Since the protocol deposits into synthetic asset vaults are not backed by any collateral and are not in circulation, they are excluded from the `totalSupply` calculation. After calling `allocate()`, target vaults are automatically excluded. Additional addresses whose balances should be ignored can be managed by the owner by calling `addIgnoredForTotalSupply(address account)` and `removeIgnoredForTotalSupply(address account)`. |
-|EVK-79|Synthetic Asset Vault `EulerSavingsRate` |`EulerSavingsRate` is a `ERC4626`-compatible vault which allows users to deposit the underlying asset and receive interest in the form of the same underlying asset. On withdraw, redeem and transfers, the account status checks must be requested for the account which health might be negatively affected. Thanks to that, the shares of the `EulerSavingsRate`vault might be used as collateral by other EVC-compatible vaults.
Account balances must be tracked internally in a donation attack resistant manner.
Anyone can transfer the underlying asset into the vault and call `gulp` which distributes those directly transferred assets to the shareholders over the `INTEREST_SMEAR` period. Accrued interest must adjust the exchange rate accordingly.
On `gulp`, any interest which has not been yet distributed is smeared for an additional `INTEREST_SMEAR` period. In theory, this means that interest could be smeared indefinitely by continuously calling `gulp`. In practice, it is expected that the interest will keep accruing, negating any negative side effects which may come from the smearing mechanism. |
-|EVK-78|Synthetic Asset Vault `IRMSynth` |Synthetic asset vaults use a different interest rate model than the standard vaults. The `IRMSynth` interest rate model is a simple reactive rate model which adjusts the interest rate up when it trades below the `targetQuote` and down when it trades above or at the `targetQuote`.
**Parameters**
* `targetQuote` price being targeted by the IRM
* `MAX_RATE` maximum rate charged
* `BASE_RATE` minimum and starting rate for the IRM
* `ADJUST_FACTOR` factor by which the previous rate is adjusted per `ADJUST_INTERVAL`
* `ADJUST_INTERVAL` time that needs to pass before the rate can be adjusted again
**Algorithm**
1. If the `ADJUST_INTERVAL` did not pass, the previous rate must be returned 2. If the synthetic asset trades below the `targetQuote`, the rate must be raised by the factor of `ADJUST_FACTOR` 3. If the synthetic asset is trading at the `targetQuote` or above, the rate must be lowered by the factor of `ADJUST_FACTOR` 4. Minimum `BASE_RATE` must be enforced 5. Maximum `MAX_RATE` must be enforced 6. The updated rate the timestamp of the last update must be stored |
+|EVK-81|Synthetic Asset Vault `ERC20EVKCompatible` |`ERC20EVKCompatible` is an `ERC20`-compatible token with the EVC support.
Whenever the contract is called via the EVC (`msg.sender == EVC`), the current on behalf of account is fetched from the EVC and used as an authenticated caller address. This ensures the `ERC20EVKCompatible` is compatible with the EVC authentication system. |
+|EVK-77|Synthetic Asset Vault `ESynth` |`ESynth` is an `ERC20`-compatible token with the EVC support. It is meant to be used as an underlying asset of the synthetic asset vault.
`ESynth` tokens comply with the following specification:
**ERC20EVCCompatible**
`ESynth` inherits from `ERC20EVCCompatible` making it compliant with the `ERC20EVCCompatible` specification.
**Minting**
The owner of the contract can set a minting capacity by calling `setCapacity(address minter, uint128 capacity)` for any address which allows them to mint the synthetic asset up to the defined amount. Minters mint by calling `mint(address account, uint256 amount)`.
**Burning**
Any address can burn the synthetic from another address given they have the allowance to do so. The owner is exempt from this restriction when burning from the `ESynth` contract itself. When one burns from an address and they have a previously minted amount, the minted amount is reduced by the burned amount, freeing up their minting capacity. Burning can be done by calling `burn(address account, uint256 amount)`.
**Allocating to the synthetic asset vault**
The owner can allocate assets held by the asset contract itself to a synthetic asset vault created specifically for this asset by calling `allocate(address vault, uint256 amount)`. This serves as a protocol deposit to the vault. Any allocation needs to be first minted by a minter or owner into the synthetic asset contract. The vault into which the assets are allocated must be integrated with the same EVC instance as the `ESynth` contract itself. On allocation, the vault is added to the addresses whose balances are ignored when calculating the `totalSupply`.
**Deallocating from the synthetic asset vault**
The owner can deallocate synthetic assets from the vault by calling `deallocate(address vault, uint256 amount)`. This serves as a protocol withdraw from the synthetic asset vault. Assets deallocated from the vault will be transferred into the synthetic asset contract itself and can in turn be burned by the owner.
**Total supply adjustments**
Since the protocol deposits into synthetic asset vaults are not backed by any collateral and are not in circulation, they are excluded from the `totalSupply` calculation. After calling `allocate()`, target vaults are automatically excluded. Additional addresses whose balances should be ignored can be managed by the owner by calling `addIgnoredForTotalSupply(address account)` and `removeIgnoredForTotalSupply(address account)`. |
+|EVK-79|Synthetic Asset Vault `EulerSavingsRate` |`EulerSavingsRate` is a `ERC4626`-compatible vault with the EVC support which allows users to deposit the underlying asset and receive interest in the form of the same underlying asset.
Account balances must be tracked internally in a donation attack resistant manner.
Anyone can transfer the underlying asset into the vault and call `gulp` which distributes those directly transferred assets to the shareholders over the `INTEREST_SMEAR` period. Accrued interest must adjust the exchange rate accordingly.
On `gulp`, any interest which has not been yet distributed is smeared for an additional `INTEREST_SMEAR` period. In theory, this means that interest could be smeared indefinitely by continuously calling `gulp`. In practice, it is expected that the interest will keep accruing, negating any negative side effects which may come from the smearing mechanism. |
+|EVK-78|Synthetic Asset Vault `IRMSynth` |Synthetic asset vaults use a different interest rate model than the standard vaults. The `IRMSynth` interest rate model is a simple reactive rate model which adjusts the interest rate up when it trades below the `targetQuote` and down when it trades above or at the `targetQuote`.
**Parameters**
* `targetQuote` price being targeted by the IRM
* `MAX_RATE` maximum rate charged
* `BASE_RATE` minimum and starting rate for the IRM
* `ADJUST_FACTOR` factor by which the previous rate is adjusted per `ADJUST_INTERVAL`
* `ADJUST_INTERVAL` time that needs to pass before the rate can be adjusted again
**Algorithm**
1. If the `ADJUST_INTERVAL` did not pass, the previous rate must be returned 2. If the oracle configured returns a zero quote, the previous rate must be returned 3. If the synthetic asset trades below the `targetQuote`, the rate must be raised by the factor of `ADJUST_FACTOR` 4. If the synthetic asset is trading at the `targetQuote` or below, the rate must be lowered by the factor of `ADJUST_FACTOR` 5. Minimum `BASE_RATE` must be enforced 6. Maximum `MAX_RATE` must be enforced 7. The updated rate the timestamp of the last update must be stored |
|EVK-80|Synthetic Asset Vault `PegStabilityModule` |The `PegStabilityModule` is granted minting rights on the `ESynth` and must allow slippage-free conversion from and to the underlying asset as per configured `conversionPrice`. On deployment, the fee for swaps to synthetic asset and to underlying asset are defined. These fees must accrue to the `PegStabilityModule` contract and can not be withdrawn, serving as a permanent reserve to support the peg. Swapping to the synthetic asset is possible up to the minting cap granted for the `PegStabilityModule` in the `ESynth`. Swapping to the underlying asset is possible up to the amount of the underlying asset held by the `PegStabilityModule`. |
diff --git a/docs/whitepaper.md b/docs/whitepaper.md
index 3e970df3..54096c69 100644
--- a/docs/whitepaper.md
+++ b/docs/whitepaper.md
@@ -35,7 +35,6 @@ Dariusz Glowinski, Mick de Graaf, Kasper Pawlowski, Anton Totomanov, Tanya Roze,
* [Risk Adjustment](#risk-adjustment)
* [Borrowing vs Liquidation LTV](#borrowing-vs-liquidation-ltv)
* [Untrusted Collaterals](#untrusted-collaterals)
- * [Cleared versus 0 LTVs](#cleared-versus-0-ltvs)
* [Non-collateral Deposits](#non-collateral-deposits)
* [LTV Ramping](#ltv-ramping)
* [Supply and Borrow Caps](#supply-and-borrow-caps)
@@ -55,9 +54,12 @@ Dariusz Glowinski, Mick de Graaf, Kasper Pawlowski, Anton Totomanov, Tanya Roze,
* [Alternative Liquidations](#alternative-liquidations)
* [Perspectives](#perspectives)
* [Token Lists](#token-lists)
- * [Whitelist Perspective](#whitelist-perspective)
- * [Escrow Perspective](#escrow-perspective)
- * [Cluster Perspective](#cluster-perspective)
+ * [Custom Whitelist Perspective](#custom-whitelist-perspective)
+ * [Governed Perspective](#governed-perspective)
+ * [Escrowed Collateral Perspective](#escrowed-collateral-perspective)
+ * [Ungoverned Perspective](#ungoverned-perspective)
+ * [0x Perspective](#0x-perspective)
+ * [Nzx Perspective](#nzx-perspective)
* [Composing Vaults](#composing-vaults)
* [Collateral Interest](#collateral-interest)
* [Custom Collaterals](#custom-collaterals)
@@ -225,7 +227,7 @@ Since the virtual deposit shares are incorporated into the exchange rate, they w
In order to move tokens in or out of the vault, the vault code has internal abstractions `pullAssets` and `pushAssets`.
-`pullAssets` will first attempt to call `transferFrom` on the underlying asset using the vault's address as the recipient. If the user has given sufficient approval for the vault, this will succeed. Otherwise, `pullAssets` will attempt to use [Permit2](https://github.com/Uniswap/permit2) to transfer the assets into the vault. Permit2 can enable better user experiences because approvals can be created as signed messages that are bundled into the same EVC batch as a `deposit` (for example). Although the user does need to first add an approval for the Permit2 contract, this is a one-time operation and many users will already have done this when interacting with Uniswap or other apps.
+`pullAssets` will first attempt to use [Permit2](https://github.com/Uniswap/permit2) to transfer the assets into the vault. If this is unsuccessful, it will then attempt to call `transferFrom` on the underlying asset using the vault's address as the recipient. Permit2 can enable better user experiences because approvals can be created as signed messages that are bundled into the same EVC batch as a `deposit` (for example). Although the user does need to first add an approval for the Permit2 contract, this is a one-time operation and many users will already have done this when interacting with Uniswap or other apps.
`pushAssets` always simply uses `transfer` on the underlying asset. However, in order to prevent loss of funds, `pushAssets` will first check with the EVC to see if the recipient address is a known non-owner address (a virtual sub-account address). If so, it will refuse to transfer the assets. If the underlying asset is EVC-aware (perhaps a [nested vault](#nesting)), then the `CFG_EVC_COMPATIBLE_ASSET` config flag can be enabled, which prevents this check.
@@ -288,7 +290,7 @@ IRMs return the interest rate that _borrowers_ must pay. Depositors (aka supplie
The IRM interface specifies interest rates in terms of "second percent yield" (SPY) values which are per-second compounded interest rates scaled by `1e27`. Although not used by the contracts themselves, for consistency we recommend that conversion of SPY to annualised equivalents (in UIs and elsewhere) should use the number of seconds in the average Gregorian calendar year of 365.2425 days.
-When a vault has `address(0)` installed as an IRM, an interest rate of `0%` is assumed. If a call to the vault's IRM fails, the vault will ignore this failure and continue with the previous interest rate. An operation that runs out of gas in the IRM call but otherwise has enough gas to successfully complete could delay updating the interest rate, but this will be corrected on the next interaction with the vault.
+When a vault has `address(0)` installed as an IRM, an interest rate of `0%` is assumed. If a call to the vault's IRM fails, the vault will ignore this failure and continue with the previous interest rate. If an IRM fails immediately after install, `0%` will be used. An operation that runs out of gas in the IRM call but otherwise has enough gas to successfully complete could delay updating the interest rate, but this will be corrected on the next interaction with the vault.
Although most IRMs implement pure functions, when re-targeting the interest rate, vaults do not invoke them with `staticcall` in order to support stateful or reactive IRMs. The `computeInterestRate()` method should verify that `msg.sender == vault`. IRMs also must implement a corresponding `computeInterestRateView()` method which does not update state. Even though vaults store the current interest rate in their storage, `computeInterestRateView()` is useful for obtaining updated interest rates mid-batch (especially during a simulation) since the stored rate may be stale.
@@ -330,6 +332,8 @@ In order for it to detect impermissible user actions, the EVC requires that vaul
These methods are invoked by the EVC at [appropriate times](https://evc.wtf/docs/whitepaper#account-status-checks). Often this will be after all operations in a batch have been performed, because the EVC allows users to defer these checks as a type of flash liquidity. If a vault would like to indicate that a status check has failed, it reverts, aborting the transaction and all performed operations.
+`checkAccountStatus` is a `view` method (invoked with `STATICCALL`) in order to prevent malicious users from executing code on a regular `transfer` operation of vault shares, potentially protecting 3rd party contracts that don't properly implement reentrancy locks. On the other hand, `checkVaultStatus` is never invoked in this context so it can be invoked with regular `CALL`. The EVK takes advantage of this in order to record an initial snapshot of the vault's state.
+
### LTV
In order for borrowing to occur, the governor of the liability vault must decide on a set of collateral assets and their maximum allowed Loan To Value ratios (from here on just called LTVs). LTVs should be chosen carefully. In addition to the risk inherent in the underlying asset, the vault that contains the asset should also be considered. If the collateral itself can be loaned out against risky collateral or uses unsafe pricing oracles, lower LTVs may be appropriate.
@@ -368,22 +372,18 @@ Using vaults with illiquid or manipulable underlying assets could threaten the s
It is also critical to evaluate the smart contract code that implements each collateral vault. A badly coded or malicious vault could refuse to release funds in a liquidation event, or simply lie about the value of its holdings. For this reason it is recommended to only use vaults created by a known-good factory. A vault can be verified to have been created by a factory by calling the factory's `isProxy()` function.
-When evaluating whether a new or customised vault implementation is trustworthy, all the usual checks should be performed, such as verifying the code was audited, does not contain backdoors, etc. In addition, it is very important to verify that the `transfer` method does not invoke any external contracts that could run attacker code. This is because the EVK implementation [forgives](https://evc.wtf/docs/whitepaper#forgiveness) the account status check of an account after liquidation, and a malicious user could perform actions that get unexpectedly forgiven.
-
-For this reason, an important property of liquidation is that assets without an LTV can never be seized by liquidation (users can install any vaults in their EVC collateral set, trusted or not).
-
-#### Cleared versus 0 LTVs
+Care should also be taken when using collateral vaults with [hooks](#hooks) installed. Hooks can implement condition checks that (maliciously or not) could prevent liquidations from succeeding.
-By default, all prospective collaterals are considered to have unset, or _cleared_ LTVs. Only by calling `setLTV()` do they become available as collateral. If a governor decides that a vault is no longer suitable as collateral, it can either have its liquidation LTV (and necessarily its borrowing LTV) set to 0, or it can be cleared with the `clearLTV()` function.
-
-There is a subtle but important difference between the two: Former collaterals with a 0 LTV are still eligible to be seized in a liquidation, but cleared ones are not, for the reason described in [Untrusted Collaterals](#untrusted-collaterals).
+When evaluating whether a new or customised vault implementation is trustworthy, all the usual checks should be performed, such as verifying the code was audited, does not contain backdoors, etc. In addition, it is very important to verify that the `transfer` method does not invoke any external contracts that could run attacker code. This is because the EVK implementation [forgives](https://evc.wtf/docs/whitepaper#forgiveness) the account status check of an account after liquidation, and a malicious user could perform actions that get unexpectedly forgiven.
-So, if a vault can no longer be collateral for economic/market reasons, its LTV should be set to 0 (either with a ramp or in drastic cases without). On the other hand, if the vault's code is discovered to be buggy or malicious, its LTV should be cleared immediately.
+For this reason, an important property of liquidation is that assets without an LTV can never be seized by the liquidator (users can install any vaults in their EVC collateral set, trusted or not). Collateral assets can have their LTVs reduced back to `0`, but this does not remove the ability to liquidate them, so this cannot be used to recover a vault that was installed as a collateral but then was subsequently found to be unsafe (because of a bug in its `transfer` method).
#### Non-collateral Deposits
Although only vaults that were explicitly configured with an LTV can be used as collateral to support debt, when an account is not healthy the account's functionality will become limited. This includes failing withdrawals of non-collateral assets. Each account is considered a single position and when the position is unhealthy, the controller vault is considered within its rights to incentivize the user to repay their debt by any means. To fully segregate assets, use different sub-accounts to store deposits even when they aren't used as collateral.
+However, even when a user is in violation and access to non-collateral deposits are blocked, the EVC still prevents the controller vault from actually seizing those assets for liquidation. Even though technically possible, the EVK also does not attempt to seize a violator's shares in the liability vault itself (considering there is no reason to hold both shares and debt in the same vault).
+
### LTV Ramping
The LTV for one or more collateral assets can be modified by the governor. If the LTV is suddenly reduced, any oustanding borrowers might instantly be put into violation. Because of the [reverse dutch auction liquidation](#liquidation) system, these borrowers might unfairly lose a significant amount of value due to this action.
@@ -408,6 +408,8 @@ Even though it can't be the direct result of a user action, in some cases caps c
Note that this behaviour can in principle be exploited by opportunistically wrapping [gasless transactions](#gasless-transactions) that withdraw/repay into a surrounding batch that deposits/borrows an equivalent amount. The executor is effectively able to transfer the user's supply/borrow quota into their own account instead of reducing the capped value.
+In some very unusual circumstances, it has been discovered by auditors that due to rounding, a `repay()` operation could fail while a vault's supply cap is exceeded. In this event, a user should ensure they are not repaying multiple sub-accounts in the same batch, and/or pull all of their debt except for some dust to another sub-account and repay from there. Since these circumstances are so rare (we don't expect them to ever arise in the real world), it was not considered necessary to prevent this in the contracts.
+
### Hooks
Vaults support a limited hooking functionality. In order to make use of this, the governor should install a hook config, which consists of two parameters:
@@ -419,6 +421,8 @@ Most user-invokable external functions are allocated constants. For example, the
When a function is invoked, the vault checks if the corresponding operation is set in hooked ops. If so, the hook target is `call`ed using the same `msg.data` that was provided to the vault, along with the EVC-authenticated caller appended as trailing calldata. If the call to the hook target fails, then the vault operation will fail too. If the hook target is `address(0)` (or any non-contract address), then the operation fails unconditionally.
+When a vault is first created, all hooks are enabled, and the hook target is `address(0)`. Attempting to interact with the vault in this state will throw the `E_OperationDisabled` error. In order to make the vault functional, the desired hooks should be disabled, for example by calling `setHookConfig(address(0), 0)` to enable all operations. Vaults start in this disabled state to avoid a race condition where users could begin interacting with a partially configured vault, for instance by depositing before a supply cap has been configured.
+
In addition to user-invokable functions, hooks can also hook `checkVaultStatus`. This will be invoked when the EVC calls `checkVaultStatus` on the vault, which is typically at the end of any batch that has interacted with the vault. This hook can be used to reject operations that violate "post-condition" properties of the vault.
The installed hook target contract address can bypass the vault's read-only reentrancy protection. This means that hook functions can call view methods on the vault. However, hooks cannot perform state changing operations because of the normal reentrancy lock.
@@ -445,11 +449,9 @@ The main purpose of the hook system is to disable vault functionality, either pe
Inside a vault, each collateral is configured as the address of another vault, not the underlying asset (unless the asset is specially constructed to also function as a collateral vault). This means that the value of a user's collateral is in fact the value of the vault's shares. A vault share is not necessarily equal to a unit of the underlying asset because of the [exchange rate](#exchange-rate).
-Because converting quantities of shares to underlying asset amounts is itself a pricing operation, this responsibility is delegated to the price oracle.
+Because converting quantities of shares to underlying asset amounts is itself a pricing operation, this responsibility is delegated to the price oracle. In some cross-chain designs, the price oracle may also be responsible for determining the exchange rate of a corresponding vault on a separate chain.
-For vaults created with the Euler Vault Kit, the ERC-4626 `convertToAssets` function can be used to price shares in units of the underlying asset. This function is designed to be a reliable oracle. [Internal balance tracking](#internal-balance-tracking) prevents manipulation with direct transfer donations, and the [virtual deposit](#exchange-rate) minimises the impact of rounding-based "stealth deposits". See our [article](https://www.euler.finance/blog/exchange-rate-manipulation-in-erc4626-vaults) for more details on these protections.
-
-In some cross-chain designs, the price oracle is also responsible for determining the exchange rate of a corresponding vault on a separate chain.
+For vaults created with the Euler Vault Kit, the ERC-4626 `convertToAssets` function can be used to price shares in units of the underlying asset. This function is designed to be a reliable oracle. [Internal balance tracking](#internal-balance-tracking) prevents manipulation with direct transfer donations, and the [virtual deposit](#exchange-rate) minimises the impact of rounding-based "stealth deposits". See our [article](https://www.euler.finance/blog/exchange-rate-manipulation-in-erc4626-vaults) for more details on these protections. As an additional layer of defense, it is recommended that vaults have some small amount deposited into them at all times, since most hypothetical rounding-based attack vectors rely on empty or near-empty vaults.
### `IPriceOracle`
@@ -560,15 +562,17 @@ In essence, a liquidation is equivalent to a stop-loss order. As long as you set
**This section is still a work-in-progress and is subject to change**
-Since the EVK is a *kit*, it attempts to be maximally flexible and doesn't enforce policy decisions on vault creators. This means that it is possible to create vaults with insecure or malicious configurations. Furthermore, an otherwise secure vault may be insecure because it accapts an [insecure collateral as collateral](#untrusted-collaterals) (or a collateral vault itself accepts insecure collateral, etc, recursively).
+Since the EVK is a *kit*, it attempts to be maximally flexible and doesn't enforce policy decisions on vault creators. This means that it is possible to create vaults with insecure or malicious configurations. Furthermore, an otherwise secure vault may be insecure because it accepts an [insecure collateral as collateral](#untrusted-collaterals) (or a collateral vault itself accepts insecure collateral, etc, recursively).
Perspectives provide a mechanism for validating properties of a vault using on-chain verifiable logic. A perspective is any contract that implements the following interface:
- interface IPerspective {
- function perspectiveVerify(address vault, bool failEarly) external;
+```solidity
+interface IPerspective {
+ function perspectiveVerify(address vault, bool failEarly) external;
- function isVerified(address vault) external view returns (bool);
- }
+ function isVerified(address vault) external view returns (bool);
+}
+```
`perspectiveVerify()` will inspect the configuration of the provided vault and determine whether it meets the desired properties of this particular perspective and, if so, will record this fact in its storage. This recorded fact can be thought of as a cached or memoised value, so the gas-expensive verification only needs to happen once. Afterwards, `isVerified()` can be used to cheaply read this cached result.
@@ -580,34 +584,58 @@ The primary use-case of perspectives is to provide a permissionless, on-chain ap
Just like with Token Lists, user interfaces will allow users to import which perspectives they would like to use to filter vaults that don't meet their trust criteria. Advanced UIs may support special filtering features, such as "all vaults that meet 2 or more of the configured perspectives", or "all vaults on this perspective but *not* on this perspective".
-Although any contract that conforms to `IPerspective` can be used as a perspective, we will be publishing a flexible reference implementation that users or projects can adapt to fit their requirements.
+Although any contract that conforms to `IPerspective` can be used as a perspective, we have created a [flexible reference implementation](https://github.com/euler-xyz/evk-periphery/tree/master?tab=readme-ov-file#perspectives) that users or projects can adapt to fit their requirements.
+
+### Custom Whitelist Perspective
+
+The simplest form of perspective is a whitelist. This implementation is an ungoverned, immutable perspective that has a static list of vaults provided at creation.
-### Whitelist Perspective
+Immutable projects that have a set of custom vaults which don't pass the default perspectives could require users to import a custom perspective in order to use their vaults. As with Token List and other permissionless approaches, users must take caution to not import malicious phishing perspectives.
-The simplest form of perspective is a whitelist. This could be curated by a trusted operator, or could simply have a hard-coded list of a few vaults. Projects that have built custom vaults that don't pass the default perspectives could require users to import their own perspective address in order to use their vaults. As with Token List and other permissionless approaches, users must take caution to not import malicious phishing perspectives.
+### Governed Perspective
-### Escrow Perspective
+This is a type of whitelist perspective that has an active governor who can add or remove vaults according to whatever criteria they desire.
-Another simple perspective is called "escrow". These verify that vaults are configured to not allow borrowing, and are immutable and finalised. Since such vaults do not earn yield, they are only useful to store tokens for use as collateral in other vaults.
+These perspectives are intended to be operated by trusted parties that have been given a responsibility to curate vaults, such as UIs that must show a default list of vaults.
-Escrow vaults cannot have any collaterals configured, so an escrow perspective never needs to recurse into other vaults.
+### Escrowed Collateral Perspective
-### Cluster Perspective
+Another simple perspective is called "escrowed collateral". These verify that vaults are configured to not allow borrowing, and are immutable and finalised. Since such vaults do not earn yield, they are only useful to store tokens for use as collateral in other vaults.
-The most general-purpose perspective implementation class is called a "cluster". These types of perspectives first verify various desired properties about the queried vault. Typically they will require vaults to be finalised.
+Escrow vaults cannot have any collaterals configured, so an escrowed collateral perspective never needs to recurse into other vaults.
-Next, cluster perspectives attempt to verify each of the vault's configured collaterals. To do so, a cluster perspective uses a list of acceptable perspectives, which may or may not include itself. For each collateral, it recurses into each perspective, stopping as soon as one perspective accepts the collateral. If none do, the perspective itself will fail.
+### Ungoverned Perspective
-This method allows perspectives to delegate some decisions to other perspectives. This reduces the amount of work needed to create a perspective, and takes advantage of the fact that verification of vaults may already be cached in these other perspectives.
+The most sophisticated perspective implementation class is called an ungoverned perspective. This type of perspectives comprehensively verify various desired properties about the candiate vault. Typically they will require vaults to be finalised, and not have any unusual or unexpected configurations.
+
+Next, cluster perspectives attempt to verify each of the vault's configured collaterals. To do so, a cluster perspective uses a list of acceptable perspectives, which may or may not include itself. For each collateral, it recurses into each perspective, stopping as soon as one perspective accepts the collateral. If none do, the perspective itself will fail. This method allows perspectives to delegate some decisions to other perspectives. This reduces the amount of work needed to create a perspective, and takes advantage of the fact that verification of vaults may already be cached in these other perspectives.
+
+There are two sub-classes of ungoverned perspectives: "0x" and "nzx".
+
+#### 0x Perspective
+
+In this context, "0x" refers to *zero exposure*, where exposure refers to governance exposure. Vaults that match this perspective can only use as collateral vaults that match one of the following perspectives:
+
+* Escrow
+* The 0x perspective itself
+
+This means that not only is there no governance risk in a 0x vault itself, but neither in its collateral, or its transient collateral, etc.
+
+#### Nzx Perspective
+
+In some circumstances the 0x perspective may be too constraining, and there may be a set of vaults that are acceptable as collateral, even if they are governed. For this purpose a *non-zero exposure* perspective may be used. These vaults allow the following as collateral:
+
+* A Governed Perspective
+* Escrow
+* The Nzx perspective itself
-For instance, a perspective that wants to fully contain [rehypothecation risk](#collateral-interest) may only accept vaults that match [escrow perspectives](#escrow-perspective) as collateral.
## Composing Vaults
### Collateral Interest
-With [escrow](#escrow-perspective) vaults as collateral, borrowers will not earn any interest on their collateral even though they are paying interest on their borrow. Especially when employing leverage, where interest payments are magnified, having collateral interest offset the borrow interest often makes the difference between a profitable and unprofitable activity. Without interest on collateral, arbitraging interest rates by borrowing low-yielding assets collateralised by high-yielding assets (a "[carry trade](https://www.investopedia.com/terms/c/currencycarrytrade.asp)") would not be possible, leading to inefficient interest rate markets.
+With [escrow](#escrowed-collateral-perspective) vaults as collateral, borrowers will not earn any interest on their collateral even though they are paying interest on their borrow. Especially when employing leverage, where interest payments are magnified, having collateral interest offset the borrow interest often makes the difference between a profitable and unprofitable activity. Without interest on collateral, arbitraging interest rates by borrowing low-yielding assets collateralised by high-yielding assets (a "[carry trade](https://www.investopedia.com/terms/c/currencycarrytrade.asp)") would not be possible, leading to inefficient interest rate markets.
For borrowers to earn interest on their collateral, vaults must accept an interest-bearing vault as collateral. This is sometimes called rehypothecation. By creating vaults that do, mutually-collateralised ecosystems such as (original) Compound or selectively-collateralised ecosystems such as AAVE/Euler V1 can be constructed.
@@ -637,6 +665,10 @@ Nested vaults follow the same principle, however they are more convenient to use
Nested vaults can have `CFG_EVC_COMPATIBLE_ASSET` set, which disables a protection used by vaults to ensure that non-EVC-compatible tokens are not transferred to known sub-account addresses, where they would be lost.
+Since [bad debt socialisation](#bad-debt-socialisation) represents a sudden but predictable (on MEV time-scales) price change, opportunistic market participants may take advantage of this by borrowing and shorting the shares of bad-debt vault using a nested vault configuration. In some situations this could have adverse effects on vault depositors.
+
+Similar to the case of [Non-collateral Deposits](#non-collateral-deposits), accounts may have certain operations restricted on nested vaults when they are unhealthy. For example, a `repay()` operation on a nested vault that is insufficient to restore the account to health may fail, because an account status check is scheduled by the parent vault when its shares are moved.
+
### Bootstrapping
Nested vaults can help solve the liquidity bootstrapping problem of specialised vaults. Depositing into new vaults that are not currently paying interest has an opportunity cost at least equal to the interest that could be earned on an established vault. By using nested vaults, depositors can continue to earn the "base yield" of the established vault while simultaneously offering to provide liquidity to the new vault.
@@ -666,11 +698,15 @@ The owner can allocate synthetic assets held by the synthetic asset itself to a
#### Deallocating from a vault
-The owner can deallocatice synthetic assets from the vault by calling `deallocate(address vault, uint256 amount)` which serves as a protocol withdraw from the synthetic asset vault. Assets deallocated from the vault will be transfered into the synthetic asset contract itself and be burned by the owner seperately.
+The owner can deallocate synthetic assets from the vault by calling `deallocate(address vault, uint256 amount)` which serves as a protocol withdraw from the synthetic asset vault. Assets deallocated from the vault will be transfered into the synthetic asset contract itself and be burned by the owner seperately.
#### Total Supply adjustments
-Since protocol deposits into synthetic vaults are not backed by any collateral and are not in circulation they are excluded from the totalSupply calculation. After calling `allocate()`, target vaults are automatically excluded. Additional addresses whose balances should be ignored can be managed by the owner by calling `addIgnoredForTotalSupply(address account)` and `removeIgnoredForTotalSupply(address account)`.
+Since protocol deposits into synthetic vaults are not in circulation, they are excluded from the `totalSupply` calculation. After calling `allocate()`, target vaults are automatically excluded, as is the synth contract itself. Additional addresses whose balances should be ignored can be managed by the owner by calling `addIgnoredForTotalSupply(address account)` and `removeIgnoredForTotalSupply(address account)`.
+
+Since accrued interest is held by the vault, and therefore not directly in circulation, it is also excluded from the `totalSupply` calculation. If users accidentally transfer their synth tokens to an ignored contract they will also be considered out of circulation. For example, accidentally transferring synths to a synth vault would effectively burn them (as with transferring to any unprepared address), and remove them from `totalSupply`.
+
+Note that while performing a flash loan from a synth vault (or indeed a regular borrow), the `totalSupply` will reflect that the borrowed amount has in fact entered circulation for the duration of the loan. This can be considered a "flash mint" of the synth.
### `IRMSynth`
@@ -688,26 +724,25 @@ Synthetic assets use a different interest rate model than the standard vaults. T
#### Mechanism
1. If adjust interval did not pass, return previous rate
-2. If the oracle returns a zero quote, return previous rate
-3. If the synth trades below the target quote, raise the rate by 10% (proportional to the previous rate)
-4. If the synth is trading at the target quote or below, lower the rate by 10% (proportional to the previous rate)
-5. Enforce the minimum base rate
-6. Enforce the maximum rate
-7. Save the updated rate and when it last updated
+1. If the synth trades below the target quote, raise the rate by 10% (proportional to the previous rate)
+1. If the synth is trading at the target quote or greater, lower the rate by 10% (proportional to the previous rate)
+1. Enforce the minimum base rate
+1. Enforce the maximum rate
+1. Save the updated rate and when it last updated
### `EulerSavingsRate`
`EulerSavingsRate` is a ERC-4626 compatible vault which allows users to deposit the underlying asset and receive interest in the form of the same underlying asset. On withdraw, redeem and transfers the accountStatus of the user is checked by calling the EVC, allowing it to be used as collateral by other vaults.
-Any address can transfer the underlying asset into the vault and call `gulp()` which will distribute it to share holders in the vault over a "smeared" two week period. Accrued interest is added to the `totalAssets` of the vault, adjusting the exchange rate accordingly. On deposit and redeem accrued interest is added to the `totalDeposited` variable which tracks all deposits in the vault in a donation attack resistent manner.
+Any address can transfer the underlying asset into the vault and call `gulp()` which will distribute it to share holders in the vault over a "smeared" two week period. Accrued interest is reflected in the `totalAssets()` of the vault, adjusting the exchange rate accordingly. On deposit and redeem accrued interest is added to the internal `_totalAssets` variable which tracks all deposits in the vault in a donation attack resistent manner.
-On `gulp` any interest which has not been distributed is smeared for an additional two weeks, in theory this means that interest could be smeared indefinitely by continiously calling `gulp`, in practice it is expected that the interest will keep accruing, negating any negative side effects which may come from the smearing mechanism.
+On `gulp()` any interest which has not been distributed is smeared for an additional two weeks, in theory this means that interest could be smeared indefinitely by continiously calling `gulp()`, in practice it is expected that the interest will keep accruing, negating any negative side effects which may come from the smearing mechanism. Furthermore, the amount of interest that is delayed decreases exponentially over time.
### `PegStabilityModule`
-The `PegStabilityModule` will be granted minting rights on a `ESynth` and will allow slippage-free conversion from and to the underlying asset. On deployment a fee for swaps to synth and to underlying are set. These fees accrue to the `PegStabilityModule` contract and can not be withdrawn, serving as a permanent reserve to support the peg.
+The `PegStabilityModule` will be granted minting rights on a `ESynth` and will allow slippage-free conversion from and to the underlying asset. On deployment, a fee for swaps to synth and to underlying are set. These fees accrue to the `PegStabilityModule` contract and can not be withdrawn, serving as a permanent reserve to support the peg.
Swapping to the synthetic asset is possible up to the minting cap granted for the `PegStabilityModule` in the `ESynth`. Swapping to the underlying asset is possible up to the amount of the underlying asset held by the `PegStabilityModule`.
@@ -729,7 +764,7 @@ Perhaps most surprising is that neither the EVC nor the vault need to know anyth
EVC Operators are a way to delegate control over your account to another address. Typically this will be a smart contract with limited, specialised capabilities.
-The most obvious immediate application of operators is delegating permission to open, modify, or close a position depending on an on-chain indicator. For example, stop losses can be placed onto an order giving "keepers" the ability to close your position if a particular price oracle indicates a price level has been reached, in exchange for a execution fee.
+The most obvious immediate application of operators is delegating permissions to open, modify, or close a position depending on an on-chain indicator. For example, stop losses can be placed onto an order giving "keepers" the ability to close your position if a particular price oracle indicates a price level has been reached, in exchange for a execution fee.
### Gasless Transactions
@@ -781,7 +816,7 @@ Modules are static, meaning they cannot be upgraded. Code upgrades require deplo
#### Delegatecall into view functions
-Solidity doesn't allow for view functions to invoke `delegatecall`. To be able to remove view functions from the dispatcher codebase and `delegatecall` them instead into the modules, the dispatching mechanism uses the `useView` modifier. This modifier makes the view function `staticcall` back into the dispatcher to a `viewDelegate` function which is `non-payable`. This `viewDelegate` function can now `delegatecall` into the implementation of the view function in the module.
+Solidity doesn't allow for view functions to invoke `delegatecall`. To be able to remove view functions from the dispatcher codebase and `delegatecall` them instead into the modules, the dispatching mechanism uses the `useView` modifier. This modifier makes the view function `staticcall` back into the dispatcher to a `viewDelegate` function which is `payable` (potentially state-modifying). This `viewDelegate` function can now `delegatecall` into the implementation of the view function in the module.
To address this, a proposed patch to the Solidity compiler is available in [this solc issue](https://github.com/ethereum/solidity/issues/14577).
diff --git a/foundry.toml b/foundry.toml
index 0f00da9e..07530108 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -5,7 +5,6 @@ libs = ["lib"]
optimizer = true
optimizer_runs = 20_000
-solc = "0.8.23"
fs_permissions = [{ access = "read-write", path = "./"}]
@@ -38,5 +37,3 @@ runs = 1000
call_override = false
depth = 50
runs = 1000
-
-# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
diff --git a/lib/ethereum-vault-connector b/lib/ethereum-vault-connector
index 0229f62f..084b3228 160000
--- a/lib/ethereum-vault-connector
+++ b/lib/ethereum-vault-connector
@@ -1 +1 @@
-Subproject commit 0229f62f92856201e1f33bee9e59daf68938ba34
+Subproject commit 084b32284ba643921f8d21bff3ddaf0c4e08d754
diff --git a/src/EVault/Dispatch.sol b/src/EVault/Dispatch.sol
index ec148e69..7baaa177 100644
--- a/src/EVault/Dispatch.sol
+++ b/src/EVault/Dispatch.sol
@@ -60,6 +60,9 @@ abstract contract Dispatch is
address governance;
}
+ /// @notice EVault's constructor
+ /// @dev It is highly recommended to deploy fresh modules for every new EVault deployment. Particular care must
+ /// also be taken to ensure the modules are deployed with the exact same values of the `Integrations` struct.
constructor(Integrations memory integrations, DeployedModules memory modules) Base(integrations) {
MODULE_INITIALIZE = AddressUtils.checkContract(modules.initialize);
MODULE_TOKEN = AddressUtils.checkContract(modules.token);
diff --git a/src/EVault/EVault.sol b/src/EVault/EVault.sol
index d1149c27..bcd5d92c 100644
--- a/src/EVault/EVault.sol
+++ b/src/EVault/EVault.sol
@@ -83,7 +83,7 @@ contract EVault is Dispatch {
function creator() public view virtual override useView(MODULE_VAULT) returns (address) {}
- function deposit(uint256 amount, address receiver) public virtual override callThroughEVC returns (uint256) { return super.deposit(amount, receiver); }
+ function deposit(uint256 amount, address receiver) public virtual override callThroughEVC use(MODULE_VAULT) returns (uint256) {}
function mint(uint256 amount, address receiver) public virtual override callThroughEVC use(MODULE_VAULT) returns (uint256) {}
@@ -122,7 +122,7 @@ contract EVault is Dispatch {
function repayWithShares(uint256 amount, address receiver) public virtual override callThroughEVC use(MODULE_BORROWING) returns (uint256 shares, uint256 debt) {}
- function pullDebt(uint256 amount, address from) public virtual override callThroughEVC use(MODULE_BORROWING) returns (uint256) {}
+ function pullDebt(uint256 amount, address from) public virtual override callThroughEVC use(MODULE_BORROWING) {}
function flashLoan(uint256 amount, bytes calldata data) public virtual override use(MODULE_BORROWING) {}
@@ -151,7 +151,7 @@ contract EVault is Dispatch {
function disableController() public virtual override use(MODULE_RISKMANAGER) {}
- function checkAccountStatus(address account, address[] calldata collaterals) public virtual override returns (bytes4) { return super.checkAccountStatus(account, collaterals); }
+ function checkAccountStatus(address account, address[] calldata collaterals) public view virtual override returns (bytes4) { return super.checkAccountStatus(account, collaterals); }
function checkVaultStatus() public virtual override returns (bytes4) { return super.checkVaultStatus(); }
@@ -227,8 +227,6 @@ contract EVault is Dispatch {
function setLTV(address collateral, uint16 borrowLTV, uint16 liquidationLTV, uint32 rampDuration) public virtual override use(MODULE_GOVERNANCE) {}
- function clearLTV(address collateral) public virtual override use(MODULE_GOVERNANCE) {}
-
function setMaxLiquidationDiscount(uint16 newDiscount) public virtual override use(MODULE_GOVERNANCE) {}
function setLiquidationCoolOffTime(uint16 newCoolOffTime) public virtual override use(MODULE_GOVERNANCE) {}
diff --git a/src/EVault/IEVault.sol b/src/EVault/IEVault.sol
index 9ed1f988..b3bbebb0 100644
--- a/src/EVault/IEVault.sol
+++ b/src/EVault/IEVault.sol
@@ -245,13 +245,16 @@ interface IBorrowing {
/// @return shares Amount of shares burned
/// @return debt Amount of debt removed in assets
/// @dev Equivalent to withdrawing and repaying, but no assets are needed to be present in the vault
+ /// @dev Contrary to a regular `repay`, if account is unhealthy, the repay amount must bring the account back to
+ /// health, or the operation will revert during account status check
function repayWithShares(uint256 amount, address receiver) external returns (uint256 shares, uint256 debt);
/// @notice Take over debt from another account
/// @param amount Amount of debt in asset units (use max uint256 for all the account's debt)
/// @param from Account to pull the debt from
- /// @return Amount of debt pulled in asset units.
- function pullDebt(uint256 amount, address from) external returns (uint256);
+ /// @dev Due to internal debt precision accounting, the liability reported on either or both accounts after
+ /// calling `pullDebt` may not match the `amount` requested precisely
+ function pullDebt(uint256 amount, address from) external;
/// @notice Request a flash-loan. A onFlashLoan() callback in msg.sender will be invoked, which must repay the loan
/// to the main Euler address prior to returning.
@@ -284,9 +287,11 @@ interface ILiquidation {
/// @param violator Address that may be in collateral violation
/// @param collateral Collateral which is to be seized
/// @param repayAssets The amount of underlying debt to be transferred from violator to sender, in asset units (use
- /// max uint256 to repay the maximum possible amount).
+ /// max uint256 to repay the maximum possible amount). Meant as slippage check together with `minYieldBalance`
/// @param minYieldBalance The minimum acceptable amount of collateral to be transferred from violator to sender, in
- /// collateral balance units (shares for vaults)
+ /// collateral balance units (shares for vaults). Meant as slippage check together with `repayAssets`
+ /// @dev If `repayAssets` is set to max uint256 it is assumed the caller will perform their own slippage checks to
+ /// make sure they are not taking on too much debt. This option is mainly meant for smart contract liquidators
function liquidate(address violator, address collateral, uint256 repayAssets, uint256 minYieldBalance) external;
}
@@ -325,7 +330,7 @@ interface IRiskManager is IEVCVault {
/// @return magicValue Must return the bytes4 magic value 0xb168c58f (which is a selector of this function) when
/// account status is valid, or revert otherwise.
/// @dev Only callable by EVC during status checks
- function checkAccountStatus(address account, address[] calldata collaterals) external returns (bytes4);
+ function checkAccountStatus(address account, address[] calldata collaterals) external view returns (bytes4);
/// @notice Checks the status of the vault and reverts if caps are exceeded
/// @return magicValue Must return the bytes4 magic value 0x4b3d1223 (which is a selector of this function) when
@@ -381,7 +386,7 @@ interface IGovernance {
function protocolConfigAddress() external view returns (address);
/// @notice Retrieves the protocol fee share
- /// @return A percentage share of fees accrued belonging to the protocol. In wad scale (1e18)
+ /// @return A percentage share of fees accrued belonging to the protocol, in 1e4 scale
function protocolFeeShare() external view returns (uint256);
/// @notice Retrieves the address which will receive protocol's fees
@@ -431,6 +436,9 @@ interface IGovernance {
/// @notice Retrieves the maximum liquidation discount
/// @return The maximum liquidation discount in 1e4 scale
+ /// @dev The default value, which is zero, is deliberately bad, as it means there would be no incentive to liquidate
+ /// unhealthy users. The vault creator must take care to properly select the limit, given the underlying and
+ /// collaterals used.
function maxLiquidationDiscount() external view returns (uint16);
/// @notice Retrieves liquidation cool-off time, which must elapse after successful account status check before
@@ -483,11 +491,6 @@ interface IGovernance {
/// @param rampDuration Ramp duration in seconds
function setLTV(address collateral, uint16 borrowLTV, uint16 liquidationLTV, uint32 rampDuration) external;
- /// @notice Completely clears LTV configuratrion, signalling the collateral is not considered safe to liquidate
- /// anymore
- /// @param collateral Address of the collateral
- function clearLTV(address collateral) external;
-
/// @notice Set a new maximum liquidation discount
/// @param newDiscount New maximum liquidation discount in 1e4 scale
/// @dev If the discount is zero (the default), the liquidators will not be incentivized to liquidate unhealthy
@@ -503,12 +506,16 @@ interface IGovernance {
/// @notice Set a new interest rate model contract
/// @param newModel The new IRM address
+ /// @dev If the new model reverts, perhaps due to governor error, the vault will silently use a zero interest
+ /// rate. Governor should make sure the new interest rates are computed as expected.
function setInterestRateModel(address newModel) external;
/// @notice Set a new hook target and a new bitmap indicating which operations should call the hook target.
- /// Operations are defined in Constants.sol
- /// @param newHookTarget The new hook target address
+ /// Operations are defined in Constants.sol.
+ /// @param newHookTarget The new hook target address. Use address(0) to simply disable hooked operations
/// @param newHookedOps Bitmask with the new hooked operations
+ /// @dev All operations are initially disabled in a newly created vault. The vault creator must set their
+ /// own configuration to make the vault usable
function setHookConfig(address newHookTarget, uint32 newHookedOps) external;
/// @notice Set new bitmap indicating which config flags should be enabled. Flags are defined in Constants.sol
diff --git a/src/EVault/modules/Borrowing.sol b/src/EVault/modules/Borrowing.sol
index 4d318724..0f4217f1 100644
--- a/src/EVault/modules/Borrowing.sol
+++ b/src/EVault/modules/Borrowing.sol
@@ -128,19 +128,17 @@ abstract contract BorrowingModule is IBorrowing, AssetTransfers, BalanceUtils, L
}
/// @inheritdoc IBorrowing
- function pullDebt(uint256 amount, address from) public virtual nonReentrant returns (uint256) {
+ function pullDebt(uint256 amount, address from) public virtual nonReentrant {
(VaultCache memory vaultCache, address account) = initOperation(OP_PULL_DEBT, CHECKACCOUNT_CALLER);
if (from == account) revert E_SelfTransfer();
Assets assets = amount == type(uint256).max ? getCurrentOwed(vaultCache, from).toAssetsUp() : amount.toAssets();
- if (assets.isZero()) return 0;
+ if (assets.isZero()) return;
transferBorrow(vaultCache, from, account, assets);
emit PullDebt(from, account, assets.toUint());
-
- return assets.toUint();
}
/// @inheritdoc IBorrowing
diff --git a/src/EVault/modules/Governance.sol b/src/EVault/modules/Governance.sol
index ed017351..5c728edb 100644
--- a/src/EVault/modules/Governance.sol
+++ b/src/EVault/modules/Governance.sol
@@ -56,9 +56,9 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT
uint16 liquidationLTV,
uint16 initialLiquidationLTV,
uint48 targetTimestamp,
- uint32 rampDuration,
- bool initialized
+ uint32 rampDuration
);
+
/// @notice Set an interest rate model contract address
/// @param newInterestRateModel Address of the new IRM
event GovSetInterestRateModel(address newInterestRateModel);
@@ -122,7 +122,11 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT
/// @inheritdoc IGovernance
function protocolFeeShare() public view virtual reentrantOK returns (uint256) {
+ if (vaultStorage.feeReceiver == address(0)) return CONFIG_SCALE;
+
(, uint256 protocolShare) = protocolConfig.protocolFeeConfig(address(this));
+ if (protocolShare > MAX_PROTOCOL_FEE_SHARE) return MAX_PROTOCOL_FEE_SHARE;
+
return protocolShare;
}
@@ -261,16 +265,19 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT
}
/// @inheritdoc IGovernance
- /// @dev When the collateral asset is no longer deemed suitable to sustain debt (and not because of code issues, see
- /// `clearLTV`), its LTV setting can be set to 0. Setting a zero liquidation LTV also enforces a zero borrowing LTV
- /// (`newBorrowLTV <= newLiquidationLTV`). In such cases, the collateral becomes immediately ineffective for new
- /// borrows. However, for liquidation purposes, the LTV can be ramped down over a period of time (`rampDuration`).
- /// This ramping helps users avoid hard liquidations with maximum discounts and gives them a chance to close their
- /// positions in an orderly fashion. The choice of `rampDuration` depends on market conditions assessed by the
- /// governor. They may decide to forgo the ramp entirely by setting the duration to zero, presumably in light of
- /// extreme market conditions, where ramping would pose a threat to the vault's solvency. In any case, when the
- /// liquidation LTV reaches its target of 0, this asset will no longer support the debt, but it will still be
- /// possible to liquidate it at a discount and use the proceeds to repay an unhealthy loan.
+ /// @dev When the collateral asset is no longer deemed suitable to sustain debt, its LTV setting can be set to 0.
+ /// Setting a zero liquidation LTV also enforces a zero borrowing LTV (`newBorrowLTV <= newLiquidationLTV`).
+ /// In such cases, the collateral becomes immediately ineffective for new borrows. However, for liquidation
+ /// purposes, the LTV can be ramped down over a period of time (`rampDuration`). This ramping helps users avoid hard
+ /// liquidations with maximum discounts and gives them a chance to close their positions in an orderly fashion.
+ /// The choice of `rampDuration` depends on market conditions assessed by the governor. They may decide to forgo
+ /// the ramp entirely by setting the duration to zero, presumably in light of extreme market conditions, where
+ /// ramping would pose a threat to the vault's solvency. In any case, when the liquidation LTV reaches its target
+ /// of 0, this asset will no longer support the debt, but it will still be possible to liquidate it at a discount
+ /// and use the proceeds to repay an unhealthy loan.
+ /// Setting the LTV to zero will not be sufficient if the collateral is found to be unsafe to call liquidation on,
+ /// either due to a bug or a code upgrade that allows its transfer function to make arbitrary external calls.
+ /// In such cases, pausing the vault and conducting an orderly wind-down is recommended.
function setLTV(address collateral, uint16 borrowLTV, uint16 liquidationLTV, uint32 rampDuration)
public
virtual
@@ -295,7 +302,13 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT
vaultStorage.ltvLookup[collateral] = newLTV;
- if (!currentLTV.initialized) vaultStorage.ltvList.push(collateral);
+ if (!currentLTV.isRecognizedCollateral()) vaultStorage.ltvList.push(collateral);
+
+ if (!newLiquidationLTV.isZero()) {
+ // Ensure that this collateral can be priced by the configured oracle
+ (, IPriceOracle _oracle, address _unitOfAccount) = ProxyUtils.metadata();
+ _oracle.getQuote(1e18, collateral, _unitOfAccount);
+ }
emit GovSetLTV(
collateral,
@@ -303,24 +316,15 @@ abstract contract GovernanceModule is IGovernance, BalanceUtils, BorrowUtils, LT
newLTV.liquidationLTV.toUint16(),
newLTV.initialLiquidationLTV.toUint16(),
newLTV.targetTimestamp,
- newLTV.rampDuration,
- !currentLTV.initialized
+ newLTV.rampDuration
);
}
- /// @inheritdoc IGovernance
- /// @dev When LTV configuration is cleared, attempt to liquidate the collateral will revert.
- /// Clearing should only be executed when the collateral is found to be unsafe to liquidate,
- /// because e.g. it does external calls on transfer, which would be a critical security threat.
- function clearLTV(address collateral) public virtual nonReentrant governorOnly {
- uint16 originalLTV = getLTV(collateral, true).toUint16();
- vaultStorage.ltvLookup[collateral].clear();
-
- emit GovSetLTV(collateral, 0, 0, originalLTV, 0, 0, false);
- }
-
/// @inheritdoc IGovernance
function setMaxLiquidationDiscount(uint16 newDiscount) public virtual nonReentrant governorOnly {
+ // Discount equal 1e4 would cause division by zero error during liquidation
+ if (newDiscount == CONFIG_SCALE) revert E_BadMaxLiquidationDiscount();
+
vaultStorage.maxLiquidationDiscount = newDiscount.toConfigAmount();
emit GovSetMaxLiquidationDiscount(newDiscount);
}
diff --git a/src/EVault/modules/Initialize.sol b/src/EVault/modules/Initialize.sol
index 00c40255..bbab093c 100644
--- a/src/EVault/modules/Initialize.sol
+++ b/src/EVault/modules/Initialize.sol
@@ -37,7 +37,6 @@ abstract contract InitializeModule is IInitialize, BorrowUtils {
(IERC20 asset,,) = ProxyUtils.metadata();
// Make sure the asset is a contract. Token transfers using a library will not revert if address has no code.
AddressUtils.checkContract(address(asset));
- // Other constraints on values should be enforced by product line
// Create sidecar DToken
@@ -49,6 +48,8 @@ abstract contract InitializeModule is IInitialize, BorrowUtils {
vaultStorage.interestAccumulator = INITIAL_INTEREST_ACCUMULATOR;
vaultStorage.interestFee = DEFAULT_INTEREST_FEE.toConfigAmount();
vaultStorage.creator = vaultStorage.governorAdmin = proxyCreator;
+ // all operations are initially disabled
+ vaultStorage.hookedOps = Flags.wrap(OP_MAX_VALUE - 1);
{
string memory underlyingSymbol = getTokenSymbol(address(asset));
diff --git a/src/EVault/modules/Liquidation.sol b/src/EVault/modules/Liquidation.sol
index 07e5bb3e..8ce05719 100644
--- a/src/EVault/modules/Liquidation.sol
+++ b/src/EVault/modules/Liquidation.sol
@@ -16,6 +16,11 @@ import "../shared/types/Types.sol";
abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtils {
using TypesLib for uint256;
+ // Minimum debt value, before liquidation, which enables debt socialization.
+ // With small debt positions, when vault is nearly empty, rounding in the pricing oracle
+ // may have an outsized impact on vault's shares to assets exchange rate after debt socialization.
+ uint256 constant MIN_SOCIALIZATION_LIABILITY_VALUE = 1e6;
+
struct LiquidationCache {
address liquidator;
address violator;
@@ -24,6 +29,7 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil
Assets liability;
Assets repay;
uint256 yieldBalance;
+ uint256 liabilityValue;
}
/// @inheritdoc ILiquidation
@@ -62,7 +68,7 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil
address violator,
address collateral,
uint256 desiredRepay
- ) private view returns (LiquidationCache memory liqCache) {
+ ) internal view returns (LiquidationCache memory liqCache) {
// Init cache
liqCache.liquidator = liquidator;
@@ -118,16 +124,19 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil
{
// Check account health
- (uint256 collateralAdjustedValue, uint256 liabilityValue) =
+ uint256 collateralAdjustedValue;
+ (collateralAdjustedValue, liqCache.liabilityValue) =
calculateLiquidity(vaultCache, liqCache.violator, liqCache.collaterals, true);
// no violation
- if (collateralAdjustedValue > liabilityValue) return liqCache;
+ if (collateralAdjustedValue > liqCache.liabilityValue || liqCache.liabilityValue == 0) {
+ return liqCache;
+ }
// Compute discount
// discountFactor = health score = 1 - discount
- uint256 discountFactor = collateralAdjustedValue * 1e18 / liabilityValue;
+ uint256 discountFactor = collateralAdjustedValue * 1e18 / liqCache.liabilityValue;
{
uint256 minDiscountFactor;
unchecked {
@@ -154,18 +163,20 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil
return liqCache;
}
- uint256 maxRepayValue = liabilityValue;
+ uint256 maxRepayValue = liqCache.liabilityValue;
uint256 maxYieldValue = maxRepayValue * 1e18 / discountFactor;
- // Limit yield to borrower's available collateral, and reduce repay if necessary. This can happen when borrower
- // has multiple collaterals and seizing all of this one won't bring the violator back to solvency
+ // Limit yield to borrower's available collateral, and reduce repay if necessary. This can happen when
+ // seizing all of the collateral won't bring the violator back to solvency. It can happen simply because
+ // the account health is very low, or because borrower has multiple collaterals and liquidating this one
+ // is not sufficient.
if (collateralValue < maxYieldValue) {
maxRepayValue = collateralValue * discountFactor / 1e18;
maxYieldValue = collateralValue;
}
- liqCache.repay = (maxRepayValue * liqCache.liability.toUint() / liabilityValue).toAssets();
+ liqCache.repay = (maxRepayValue * liqCache.liability.toUint() / liqCache.liabilityValue).toAssets();
liqCache.yieldBalance = maxYieldValue * collateralBalance / collateralValue;
return liqCache;
@@ -209,7 +220,8 @@ abstract contract LiquidationModule is ILiquidation, BalanceUtils, LiquidityUtil
// Handle debt socialization
if (
- vaultCache.configFlags.isNotSet(CFG_DONT_SOCIALIZE_DEBT) && liqCache.liability > liqCache.repay
+ liqCache.liabilityValue >= MIN_SOCIALIZATION_LIABILITY_VALUE
+ && vaultCache.configFlags.isNotSet(CFG_DONT_SOCIALIZE_DEBT) && liqCache.liability > liqCache.repay
&& checkNoCollateral(liqCache.violator, liqCache.collaterals)
) {
Assets owedRemaining = liqCache.liability.subUnchecked(liqCache.repay);
diff --git a/src/EVault/modules/RiskManager.sol b/src/EVault/modules/RiskManager.sol
index f13b7f75..4f78db4d 100644
--- a/src/EVault/modules/RiskManager.sol
+++ b/src/EVault/modules/RiskManager.sol
@@ -64,10 +64,11 @@ abstract contract RiskManagerModule is IRiskManager, LiquidityUtils {
/// @dev The function doesn't have a reentrancy lock, because onlyEVCChecks provides equivalent behaviour. It
/// ensures that the caller is the EVC, in 'checks in progress' state. In this state EVC will not accept any calls.
/// Since all the functions which modify vault state use callThroughEVC modifier, they are effectively blocked while
- /// the function executes. There are non-view functions without callThroughEVC modifier (`flashLoan`,
- /// `disableCollateral`), but they don't change the vault's storage.
+ /// the function executes. There are non-view functions without `callThroughEVC` modifier (`flashLoan`,
+ /// `disableController`), but they don't change the vault's storage.
function checkAccountStatus(address account, address[] calldata collaterals)
public
+ view
virtual
reentrantOK
onlyEVCChecks
@@ -108,6 +109,8 @@ abstract contract RiskManagerModule is IRiskManager, LiquidityUtils {
// Borrows are rounded down, because total assets could increase during repays.
// This could happen when repaid user debt is rounded up to assets and used to increase cash,
// while totalBorrows would be adjusted by only the exact debt, less than the increase in cash.
+ // If multiple accounts need to repay while the supply cap is exceeded they should do so in
+ // separate batches.
uint256 supply = vaultCache.cash.toUint() + vaultCache.totalBorrows.toAssetsDown().toUint();
if (supply > vaultCache.supplyCap && supply > prevSupply) revert E_SupplyCapExceeded();
diff --git a/src/EVault/modules/Token.sol b/src/EVault/modules/Token.sol
index 3aec5ed3..fab2fbbc 100644
--- a/src/EVault/modules/Token.sol
+++ b/src/EVault/modules/Token.sol
@@ -18,12 +18,12 @@ abstract contract TokenModule is IToken, BalanceUtils {
/// @inheritdoc IERC20
function name() public view virtual reentrantOK returns (string memory) {
- return bytes(vaultStorage.name).length > 0 ? vaultStorage.name : "Unnamed Euler Vault";
+ return vaultStorage.name;
}
/// @inheritdoc IERC20
function symbol() public view virtual reentrantOK returns (string memory) {
- return bytes(vaultStorage.symbol).length > 0 ? vaultStorage.symbol : "UNKNOWN";
+ return vaultStorage.symbol;
}
/// @inheritdoc IERC20
diff --git a/src/EVault/shared/AssetTransfers.sol b/src/EVault/shared/AssetTransfers.sol
index 83a269d0..095b1305 100644
--- a/src/EVault/shared/AssetTransfers.sol
+++ b/src/EVault/shared/AssetTransfers.sol
@@ -20,15 +20,16 @@ abstract contract AssetTransfers is Base {
vaultStorage.cash = vaultCache.cash = vaultCache.cash + amount;
}
- /// @dev If the `CFG_EVC_COMPATIBLE_ASSET` flag is set, the function will protect users from mistakenly sending
- /// funds to the EVC sub-accounts. Functions that push tokens out (`withdraw`, `redeem`, `borrow`) accept a
- /// `receiver` argument. If the user sets one of their sub-accounts (not the owner) as the receiver, funds would be
- /// lost because a regular asset doesn't support the EVC's sub-accounts. The private key to a sub-account (not the
- /// owner) is not known, so the user would not be able to move the funds out. The function will make a best effort
- /// to prevent this by checking if the receiver of the token is recognized by EVC as a non-owner sub-account. In
- /// other words, if there is an account registered in EVC as the owner for the intended receiver, the transfer will
- /// be prevented. However, there is no guarantee that EVC will have the owner registered. If the asset itself is
- /// compatible with EVC, it is safe to not set the flag and send the asset to a non-owner sub-account.
+ /// @dev If the `CFG_EVC_COMPATIBLE_ASSET` flag is not set (default), the function will protect users from
+ /// mistakenly sending funds to the EVC sub-accounts. Functions that push tokens out (`withdraw`, `redeem`,
+ /// `borrow`) accept a `receiver` argument. If the user sets one of their sub-accounts (not the owner) as the
+ /// receiver, funds would be lost because a regular asset doesn't support the EVC's sub-accounts. The private key to
+ /// a sub-account (not the owner) is not known, so the user would not be able to move the funds out. The function
+ /// will make a best effort to prevent this by checking if the receiver of the token is recognized by EVC as a
+ /// non-owner sub-account. In other words, if there is an account registered in EVC as the owner for the intended
+ /// receiver, the transfer will be prevented. However, there is no guarantee that EVC will have the owner
+ /// registered. If the asset itself is compatible with EVC, it is safe to set the flag and send the asset to a
+ /// non-owner sub-account.
function pushAssets(VaultCache memory vaultCache, address to, Assets amount) internal virtual {
if (
to == address(0)
diff --git a/src/EVault/shared/BalanceUtils.sol b/src/EVault/shared/BalanceUtils.sol
index 1c6105e8..7f68706d 100644
--- a/src/EVault/shared/BalanceUtils.sol
+++ b/src/EVault/shared/BalanceUtils.sol
@@ -80,6 +80,10 @@ abstract contract BalanceUtils is Base {
Shares newFromBalance = origFromBalance.subUnchecked(amount);
user.setBalance(newFromBalance);
+ if (fromBalanceForwarderEnabled) {
+ balanceTracker.balanceTrackerHook(from, newFromBalance.toUint(), isControlCollateralInProgress());
+ }
+
// update to
user = vaultStorage.users[to];
@@ -89,11 +93,7 @@ abstract contract BalanceUtils is Base {
Shares newToBalance = origToBalance + amount;
user.setBalance(newToBalance);
- if (fromBalanceForwarderEnabled) {
- balanceTracker.balanceTrackerHook(from, newFromBalance.toUint(), isControlCollateralInProgress());
- }
-
- if (toBalanceForwarderEnabled && from != to) {
+ if (toBalanceForwarderEnabled) {
balanceTracker.balanceTrackerHook(to, newToBalance.toUint(), false);
}
}
@@ -104,7 +104,7 @@ abstract contract BalanceUtils is Base {
// Allowance
function setAllowance(address owner, address spender, uint256 amount) internal {
- if (spender == owner) revert E_SelfApproval();
+ if (spender == owner) return;
vaultStorage.users[owner].eTokenAllowance[spender] = amount;
emit Approval(owner, spender, amount);
diff --git a/src/EVault/shared/BorrowUtils.sol b/src/EVault/shared/BorrowUtils.sol
index 69329ad0..e60faf14 100644
--- a/src/EVault/shared/BorrowUtils.sol
+++ b/src/EVault/shared/BorrowUtils.sol
@@ -95,13 +95,17 @@ abstract contract BorrowUtils is Base {
toOwed = toOwed + amount;
setUserBorrow(vaultCache, to, toOwed);
- logRepay(from, assets, fromOwedPrev.toAssetsUp(), fromOwed.toAssetsUp());
+ // with small fractional debt amounts the interest calculation could be negative in `logRepay`
+ Assets fromPrevAssets = fromOwedPrev.toAssetsUp();
+ Assets fromAssets = fromOwed.toAssetsUp();
+ Assets repayAssets = fromPrevAssets > assets + fromAssets ? fromPrevAssets.subUnchecked(fromAssets) : assets;
+ logRepay(from, repayAssets, fromPrevAssets, fromAssets);
// with small fractional debt amounts the interest calculation could be negative in `logBorrow`
Assets toPrevAssets = toOwedPrev.toAssetsUp();
Assets toAssets = toOwed.toAssetsUp();
- if (assets + toPrevAssets > toAssets) assets = toAssets - toPrevAssets;
- logBorrow(to, assets, toPrevAssets, toAssets);
+ Assets borrowAssets = assets + toPrevAssets > toAssets ? toAssets.subUnchecked(toPrevAssets) : assets;
+ logBorrow(to, borrowAssets, toPrevAssets, toAssets);
}
function computeInterestRate(VaultCache memory vaultCache) internal virtual returns (uint256) {
diff --git a/src/EVault/shared/Cache.sol b/src/EVault/shared/Cache.sol
index fcbb9f6b..01a59f4e 100644
--- a/src/EVault/shared/Cache.sol
+++ b/src/EVault/shared/Cache.sol
@@ -39,7 +39,7 @@ contract Cache is Storage, Errors {
// Takes a VaultCache struct, overwrites it with VaultStorage data and, if time has passed since VaultStorage
// was last updated, updates VaultStorage.
// Returns a boolean if the cache is different from storage. VaultCache param is updated to this block.
- function initVaultCache(VaultCache memory vaultCache) private view returns (bool dirty) {
+ function initVaultCache(VaultCache memory vaultCache) internal view returns (bool dirty) {
dirty = false;
// Proxy metadata
diff --git a/src/EVault/shared/Constants.sol b/src/EVault/shared/Constants.sol
index ac206290..b0fcbbc2 100644
--- a/src/EVault/shared/Constants.sol
+++ b/src/EVault/shared/Constants.sol
@@ -11,8 +11,8 @@ uint256 constant MAX_SANE_AMOUNT = type(uint112).max;
// Last 31 bits are zeros to ensure max debt rounded up equals max sane amount.
uint256 constant MAX_SANE_DEBT_AMOUNT = uint256(MAX_SANE_AMOUNT) << INTERNAL_DEBT_PRECISION_SHIFT;
// proxy trailing calldata length in bytes.
-// Three addresses, 20 bytes each: vault underlying asset, oracle and unit of account.
-uint256 constant PROXY_METADATA_LENGTH = 60;
+// Three addresses, 20 bytes each: vault underlying asset, oracle and unit of account + 4 empty bytes.
+uint256 constant PROXY_METADATA_LENGTH = 64;
// gregorian calendar
uint256 constant SECONDS_PER_YEAR = 365.2425 * 86400;
// max interest rate accepted from IRM. 1,000,000% APY: floor(((1000000 / 100 + 1)**(1/(86400*365.2425)) - 1) * 1e27)
diff --git a/src/EVault/shared/Errors.sol b/src/EVault/shared/Errors.sol
index a4bf6de8..e4c63e34 100644
--- a/src/EVault/shared/Errors.sol
+++ b/src/EVault/shared/Errors.sol
@@ -9,7 +9,6 @@ pragma solidity ^0.8.0;
contract Errors {
error E_Initialized();
error E_ProxyMetadata();
- error E_SelfApproval();
error E_SelfTransfer();
error E_InsufficientAllowance();
error E_InsufficientCash();
@@ -53,6 +52,7 @@ contract Errors {
error E_BadAssetReceiver();
error E_BadSharesOwner();
error E_BadSharesReceiver();
+ error E_BadMaxLiquidationDiscount();
error E_LTVBorrow();
error E_LTVLiquidation();
error E_NotHookTarget();
diff --git a/src/EVault/shared/Events.sol b/src/EVault/shared/Events.sol
index be629466..433eb207 100644
--- a/src/EVault/shared/Events.sol
+++ b/src/EVault/shared/Events.sol
@@ -99,7 +99,7 @@ abstract contract Events {
/// @param from Account from which the debt is taken
/// @param to Account taking on the debt
/// @param assets Amount of debt transferred in assets
- event PullDebt(address from, address to, uint256 assets);
+ event PullDebt(address indexed from, address indexed to, uint256 assets);
/// @notice Socialize debt after liquidating all of the unhealthy account's collateral
/// @param account Address holding an unhealthy borrow
diff --git a/src/EVault/shared/LiquidityUtils.sol b/src/EVault/shared/LiquidityUtils.sol
index 1de279dd..e6b68e28 100644
--- a/src/EVault/shared/LiquidityUtils.sol
+++ b/src/EVault/shared/LiquidityUtils.sol
@@ -30,9 +30,9 @@ abstract contract LiquidityUtils is BorrowUtils, LTVUtils {
liabilityValue = getLiabilityValue(vaultCache, account, vaultStorage.users[account].getOwed(), liquidation);
}
- // Check that the value of the collateral, adjusted for borrowing LTV, is equal or greater than the liability value.
- // Since this function uses bid/ask prices, it should only be used within the account status check, and not
- // for determining whether an account can be liquidated (which uses mid-point prices).
+ // Check that there is no liability, or the value of the collateral, adjusted for borrowing LTV, is greater than the
+ // liability value. Since this function uses bid/ask prices, it should only be used within the account status check,
+ // and not for determining whether an account can be liquidated (which uses mid-point prices).
function checkLiquidity(VaultCache memory vaultCache, address account, address[] memory collaterals)
internal
view
diff --git a/src/EVault/shared/lib/SafeERC20Lib.sol b/src/EVault/shared/lib/SafeERC20Lib.sol
index d1b99af6..49161cc3 100644
--- a/src/EVault/shared/lib/SafeERC20Lib.sol
+++ b/src/EVault/shared/lib/SafeERC20Lib.sol
@@ -11,8 +11,7 @@ import {IPermit2} from "../../../interfaces/IPermit2.sol";
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice The library provides helpers for ERC20 transfers, including Permit2 support
library SafeERC20Lib {
- error E_TransferFromFailed(bytes errorTransferFrom, bytes errorPermit2);
- error E_Permit2AmountOverflow();
+ error E_TransferFromFailed(bytes errorPermit2, bytes errorTransferFrom);
// If no code exists under the token address, the function will succeed. EVault ensures this is not the case in
// `initialize`.
@@ -26,18 +25,21 @@ library SafeERC20Lib {
}
function safeTransferFrom(IERC20 token, address from, address to, uint256 value, address permit2) internal {
- (bool success, bytes memory tryData) = trySafeTransferFrom(token, from, to, value);
- bytes memory fallbackData;
- if (!success && permit2 != address(0)) {
- if (value > type(uint160).max) {
- revert E_TransferFromFailed(tryData, abi.encodePacked(E_Permit2AmountOverflow.selector));
- }
- // it's now safe to down-cast value to uint160
- (success, fallbackData) =
+ bool success;
+ bytes memory permit2Data;
+ bytes memory transferData;
+
+ if (permit2 != address(0) && value <= type(uint160).max) {
+ // it's safe to down-cast value to uint160
+ (success, permit2Data) =
permit2.call(abi.encodeCall(IPermit2.transferFrom, (from, to, uint160(value), address(token))));
}
- if (!success) revert E_TransferFromFailed(tryData, fallbackData);
+ if (!success) {
+ (success, transferData) = trySafeTransferFrom(token, from, to, value);
+ }
+
+ if (!success) revert E_TransferFromFailed(permit2Data, transferData);
}
// If no code exists under the token address, the function will succeed. EVault ensures this is not the case in
diff --git a/src/EVault/shared/types/ConfigAmount.sol b/src/EVault/shared/types/ConfigAmount.sol
index 30e8a867..d3f1b640 100644
--- a/src/EVault/shared/types/ConfigAmount.sol
+++ b/src/EVault/shared/types/ConfigAmount.sol
@@ -10,7 +10,7 @@ import "../Constants.sol";
/// @custom:security-contact security@euler.xyz
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice Library for `ConfigAmount` custom type
-/// @dev ConfigAmounts are floating point values encoded in 16 bits with a 1e4 precision.
+/// @dev ConfigAmounts are fixed point values encoded in 16 bits with a 1e4 precision.
/// @dev The type is used to store protocol configuration values.
library ConfigAmountLib {
function isZero(ConfigAmount self) internal pure returns (bool) {
diff --git a/src/EVault/shared/types/LTVConfig.sol b/src/EVault/shared/types/LTVConfig.sol
index a1a3c73d..9e1e53e5 100644
--- a/src/EVault/shared/types/LTVConfig.sol
+++ b/src/EVault/shared/types/LTVConfig.sol
@@ -7,7 +7,7 @@ import {ConfigAmount} from "./Types.sol";
/// @title LTVConfig
/// @notice This packed struct is used to store LTV configuration of a collateral
struct LTVConfig {
- // Packed slot: 2 + 2 + 2 + 6 + 4 + 1 = 17
+ // Packed slot: 2 + 2 + 2 + 6 + 4 = 16
// The value of borrow LTV for originating positions
ConfigAmount borrowLTV;
// The value of fully converged liquidation LTV
@@ -18,8 +18,6 @@ struct LTVConfig {
uint48 targetTimestamp;
// The time it takes for the liquidation LTV to converge from the initial value to the fully converged value
uint32 rampDuration;
- // A flag indicating the LTV configuration was initialized for the collateral
- bool initialized;
}
/// @title LTVConfigLib
@@ -68,16 +66,6 @@ library LTVConfigLib {
newLTV.initialLiquidationLTV = self.getLTV(true);
newLTV.targetTimestamp = uint48(block.timestamp + rampDuration);
newLTV.rampDuration = rampDuration;
- newLTV.initialized = true;
- }
-
- // When LTV is cleared, the collateral can't be liquidated, as it's deemed unsafe
- function clear(LTVConfig storage self) internal {
- self.borrowLTV = ConfigAmount.wrap(0);
- self.liquidationLTV = ConfigAmount.wrap(0);
- self.initialLiquidationLTV = ConfigAmount.wrap(0);
- self.targetTimestamp = 0;
- self.rampDuration = 0;
}
}
diff --git a/src/GenericFactory/GenericFactory.sol b/src/GenericFactory/GenericFactory.sol
index 43946010..7fa3597a 100644
--- a/src/GenericFactory/GenericFactory.sol
+++ b/src/GenericFactory/GenericFactory.sol
@@ -123,12 +123,15 @@ contract GenericFactory is MetaProxyDeployer {
if (desiredImplementation == address(0) || desiredImplementation != _implementation) revert E_Implementation();
+ // The provided trailing data is prefixed with 4 zero bytes to avoid potential selector clashing in case the
+ // proxy is called with empty calldata.
+ bytes memory prefixTrailingData = abi.encodePacked(bytes4(0), trailingData);
address proxy;
if (upgradeable) {
- proxy = address(new BeaconProxy(trailingData));
+ proxy = address(new BeaconProxy(prefixTrailingData));
} else {
- proxy = deployMetaProxy(desiredImplementation, trailingData);
+ proxy = deployMetaProxy(desiredImplementation, prefixTrailingData);
}
proxyLookup[proxy] =
@@ -149,7 +152,7 @@ contract GenericFactory is MetaProxyDeployer {
/// @param newImplementation Address of the new implementation contract
/// @dev Upgrades all existing BeaconProxies to the new logic immediately
function setImplementation(address newImplementation) external nonReentrant adminOnly {
- if (newImplementation == address(0)) revert E_BadAddress();
+ if (newImplementation.code.length == 0) revert E_BadAddress();
implementation = newImplementation;
emit SetImplementation(newImplementation);
}
diff --git a/src/InterestRateModels/IRMLinearKink.sol b/src/InterestRateModels/IRMLinearKink.sol
index 3c5b2357..fb3cab74 100644
--- a/src/InterestRateModels/IRMLinearKink.sol
+++ b/src/InterestRateModels/IRMLinearKink.sol
@@ -19,7 +19,7 @@ contract IRMLinearKink is IIRM {
/// @notice Utilization at which the slope of the interest rate function changes. In type(uint32).max scale.
uint256 public immutable kink;
- constructor(uint256 baseRate_, uint256 slope1_, uint256 slope2_, uint256 kink_) {
+ constructor(uint256 baseRate_, uint256 slope1_, uint256 slope2_, uint32 kink_) {
baseRate = baseRate_;
slope1 = slope1_;
slope2 = slope2_;
diff --git a/src/ProtocolConfig/ProtocolConfig.sol b/src/ProtocolConfig/ProtocolConfig.sol
index c0f2ce72..71fd403c 100644
--- a/src/ProtocolConfig/ProtocolConfig.sol
+++ b/src/ProtocolConfig/ProtocolConfig.sol
@@ -87,7 +87,7 @@ contract ProtocolConfig is IProtocolConfig {
minInterestFee = 0.1e4;
maxInterestFee = 1e4;
- protocolFeeShare = 0.1e4;
+ protocolFeeShare = 0.5e4;
}
/// @inheritdoc IProtocolConfig
diff --git a/src/SequenceRegistry/SequenceRegistry.sol b/src/SequenceRegistry/SequenceRegistry.sol
index 2a9cca1c..6a3bb754 100644
--- a/src/SequenceRegistry/SequenceRegistry.sol
+++ b/src/SequenceRegistry/SequenceRegistry.sol
@@ -19,7 +19,7 @@ contract SequenceRegistry is ISequenceRegistry {
/// @param designator The opaque designator string
/// @param id The reserved ID, which is unique per designator
/// @param caller The msg.sender who reserved the ID
- event SequenceIdReserved(string indexed designator, uint256 indexed id, address indexed caller);
+ event SequenceIdReserved(string designator, uint256 indexed id, address indexed caller);
/// @inheritdoc ISequenceRegistry
function reserveSeqId(string calldata designator) external returns (uint256) {
diff --git a/src/Synths/ERC20Collateral.sol b/src/Synths/ERC20Collateral.sol
deleted file mode 100644
index d7901961..00000000
--- a/src/Synths/ERC20Collateral.sol
+++ /dev/null
@@ -1,74 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-pragma solidity ^0.8.0;
-
-import {ERC20, Context} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
-import {ERC20Permit} from "openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol";
-import {ReentrancyGuard} from "openzeppelin-contracts/utils/ReentrancyGuard.sol";
-import {IEVC, EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
-
-/// @title ERC20Collateral
-/// @custom:security-contact security@euler.xyz
-/// @author Euler Labs (https://www.eulerlabs.com/)
-/// @notice ERC20Collateral is an ERC20-compatible token with the EVC support which allows it to be used as collateral
-/// in other vaults.
-abstract contract ERC20Collateral is EVCUtil, ERC20Permit, ReentrancyGuard {
- constructor(IEVC _evc_, string memory _name_, string memory _symbol_)
- EVCUtil(address(_evc_))
- ERC20(_name_, _symbol_)
- ERC20Permit(_name_)
- {}
-
- /// @notice Transfers a certain amount of tokens to a recipient.
- /// @dev Overriden to add reentrancy protection.
- /// @param to The recipient of the transfer.
- /// @param amount The amount shares to transfer.
- /// @return A boolean indicating whether the transfer was successful.
- function transfer(address to, uint256 amount) public virtual override nonReentrant returns (bool) {
- return super.transfer(to, amount);
- }
-
- /// @notice Transfers a certain amount of tokens from a sender to a recipient.
- /// @dev Overriden to add reentrancy protection.
- /// @param from The sender of the transfer.
- /// @param to The recipient of the transfer.
- /// @param amount The amount of shares to transfer.
- /// @return A boolean indicating whether the transfer was successful.
- function transferFrom(address from, address to, uint256 amount)
- public
- virtual
- override
- nonReentrant
- returns (bool)
- {
- return super.transferFrom(from, to, amount);
- }
-
- /// @notice Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
- /// (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
- /// this function.
- /// @dev Overriden to require account status checks on transfers from non-zero addresses. The account status check
- /// must be required on any operation that reduces user's balance. Note that the user balance cannot be modified
- // outside of this function as the account status check must always be requested after the balance is modified which
- // is ensured by this function. If any user balance modifications are done outside of this function, the contract
- // must be modified to request the account status check appropriately.
- /// @param from The address from which tokens are transferred or burned.
- /// @param to The address to which tokens are transferred or minted.
- /// @param value The amount of tokens to transfer, mint, or burn.
- function _update(address from, address to, uint256 value) internal virtual override {
- super._update(from, to, value);
-
- if (from != address(0)) {
- evc.requireAccountStatusCheck(from);
- }
- }
-
- /// @notice Retrieves the message sender in the context of the EVC.
- /// @dev Overriden due to the conflict with the Context definition.
- /// @dev This function returns the account on behalf of which the current operation is being performed, which is
- /// either msg.sender or the account authenticated by the EVC.
- /// @return The address of the message sender.
- function _msgSender() internal view virtual override (EVCUtil, Context) returns (address) {
- return EVCUtil._msgSender();
- }
-}
diff --git a/src/Synths/ERC20EVCCompatible.sol b/src/Synths/ERC20EVCCompatible.sol
new file mode 100644
index 00000000..c0b678c9
--- /dev/null
+++ b/src/Synths/ERC20EVCCompatible.sol
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.0;
+
+import {ERC20, Context} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
+import {ERC20Permit} from "openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol";
+import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
+
+/// @title ERC20EVCCompatible
+/// @custom:security-contact security@euler.xyz
+/// @author Euler Labs (https://www.eulerlabs.com/)
+/// @notice ERC20EVCCompatible is an ERC20-compatible token with the EVC support.
+abstract contract ERC20EVCCompatible is EVCUtil, ERC20Permit {
+ constructor(address _evc_, string memory _name_, string memory _symbol_)
+ EVCUtil(_evc_)
+ ERC20(_name_, _symbol_)
+ ERC20Permit(_name_)
+ {}
+
+ /// @notice Retrieves the message sender in the context of the EVC.
+ /// @dev Overridden due to the conflict with the Context definition.
+ /// @dev This function returns the account on behalf of which the current operation is being performed, which is
+ /// either msg.sender or the account authenticated by the EVC.
+ /// @return The address of the message sender.
+ function _msgSender() internal view virtual override (EVCUtil, Context) returns (address) {
+ return EVCUtil._msgSender();
+ }
+}
diff --git a/src/Synths/ESynth.sol b/src/Synths/ESynth.sol
index 5ab4e219..e059d294 100644
--- a/src/Synths/ESynth.sol
+++ b/src/Synths/ESynth.sol
@@ -4,17 +4,15 @@ pragma solidity ^0.8.0;
import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
-import {IEVC, EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
-import {ERC20Collateral, ERC20, Context} from "./ERC20Collateral.sol";
+import {ERC20EVCCompatible, Context} from "./ERC20EVCCompatible.sol";
import {IEVault} from "../EVault/IEVault.sol";
/// @title ESynth
/// @custom:security-contact security@euler.xyz
/// @author Euler Labs (https://www.eulerlabs.com/)
-/// @notice ESynth is an ERC20-compatible token with the EVC support which, thanks to relying on the EVC authentication
-/// and requesting the account status checks on token transfers and burns, allows it to be used as collateral in other
-/// vault. It is meant to be used as an underlying asset of the synthetic asset vault.
-contract ESynth is ERC20Collateral, Ownable {
+/// @notice ESynth is an ERC20-compatible token with the EVC support. It is meant to be used as an underlying asset of
+/// the synthetic asset vault.
+contract ESynth is ERC20EVCCompatible, Ownable {
using EnumerableSet for EnumerableSet.AddressSet;
struct MinterData {
@@ -22,24 +20,31 @@ contract ESynth is ERC20Collateral, Ownable {
uint128 minted;
}
+ /// @notice contains the minting capacity and minted amount for each minter.
mapping(address => MinterData) public minters;
+ /// @notice contains the list of addresses to ignore for the total supply.
EnumerableSet.AddressSet internal ignoredForTotalSupply;
+ /// @notice Emitted when the minting capacity for a minter is set.
+ /// @param minter The address of the minter.
+ /// @param capacity The capacity set for the minter.
event MinterCapacitySet(address indexed minter, uint256 capacity);
error E_CapacityReached();
error E_NotEVCCompatible();
- constructor(IEVC evc_, string memory name_, string memory symbol_)
- ERC20Collateral(evc_, name_, symbol_)
+ constructor(address evc_, string memory name_, string memory symbol_)
+ ERC20EVCCompatible(evc_, name_, symbol_)
Ownable(msg.sender)
- {}
+ {
+ ignoredForTotalSupply.add(address(this));
+ }
/// @notice Sets the minting capacity for a minter.
/// @dev Can only be called by the owner of the contract.
/// @param minter The address of the minter to set the capacity for.
/// @param capacity The capacity to set for the minter.
- function setCapacity(address minter, uint128 capacity) external onlyOwner {
+ function setCapacity(address minter, uint128 capacity) external onlyEVCAccountOwner onlyOwner {
minters[minter].capacity = capacity;
emit MinterCapacitySet(minter, capacity);
}
@@ -47,7 +52,7 @@ contract ESynth is ERC20Collateral, Ownable {
/// @notice Mints a certain amount of tokens to the account.
/// @param account The account to mint the tokens to.
/// @param amount The amount of tokens to mint.
- function mint(address account, uint256 amount) external nonReentrant {
+ function mint(address account, uint256 amount) external {
address sender = _msgSender();
MinterData memory minterCache = minters[sender];
@@ -73,7 +78,7 @@ contract ESynth is ERC20Collateral, Ownable {
/// have an allowance for the sender.
/// @param burnFrom The account to burn the tokens from.
/// @param amount The amount of tokens to burn.
- function burn(address burnFrom, uint256 amount) external nonReentrant {
+ function burn(address burnFrom, uint256 amount) external {
address sender = _msgSender();
MinterData memory minterCache = minters[sender];
@@ -101,7 +106,7 @@ contract ESynth is ERC20Collateral, Ownable {
/// @dev Adds the vault to the list of accounts to ignore for the total supply.
/// @param vault The vault to deposit the cash in.
/// @param amount The amount of cash to deposit.
- function allocate(address vault, uint256 amount) external onlyOwner {
+ function allocate(address vault, uint256 amount) external onlyEVCAccountOwner onlyOwner {
if (IEVault(vault).EVC() != address(evc)) {
revert E_NotEVCCompatible();
}
@@ -113,17 +118,17 @@ contract ESynth is ERC20Collateral, Ownable {
/// @notice Withdraw cash from the attached vault to this contract.
/// @param vault The vault to withdraw the cash from.
/// @param amount The amount of cash to withdraw.
- function deallocate(address vault, uint256 amount) external onlyOwner {
+ function deallocate(address vault, uint256 amount) external onlyEVCAccountOwner onlyOwner {
IEVault(vault).withdraw(amount, address(this), address(this));
}
/// @notice Retrieves the message sender in the context of the EVC.
- /// @dev Overriden due to the conflict with the Context definition.
+ /// @dev Overridden due to the conflict with the Context definition.
/// @dev This function returns the account on behalf of which the current operation is being performed, which is
/// either msg.sender or the account authenticated by the EVC.
- /// @return The address of the message sender.
- function _msgSender() internal view virtual override (ERC20Collateral, Context) returns (address) {
- return ERC20Collateral._msgSender();
+ /// @return msgSender The address of the message sender.
+ function _msgSender() internal view virtual override (ERC20EVCCompatible, Context) returns (address msgSender) {
+ return ERC20EVCCompatible._msgSender();
}
// -------- TotalSupply Management --------
@@ -131,39 +136,58 @@ contract ESynth is ERC20Collateral, Ownable {
/// @notice Adds an account to the list of accounts to ignore for the total supply.
/// @param account The account to add to the list.
/// @return success True when the account was not on the list and was added. False otherwise.
- function addIgnoredForTotalSupply(address account) external onlyOwner returns (bool success) {
+ function addIgnoredForTotalSupply(address account) external onlyEVCAccountOwner onlyOwner returns (bool success) {
return ignoredForTotalSupply.add(account);
}
/// @notice Removes an account from the list of accounts to ignore for the total supply.
/// @param account The account to remove from the list.
/// @return success True when the account was on the list and was removed. False otherwise.
- function removeIgnoredForTotalSupply(address account) external onlyOwner returns (bool success) {
+ function removeIgnoredForTotalSupply(address account)
+ external
+ onlyEVCAccountOwner
+ onlyOwner
+ returns (bool success)
+ {
return ignoredForTotalSupply.remove(account);
}
/// @notice Checks if an account is ignored for the total supply.
/// @param account The account to check.
- function isIgnoredForTotalSupply(address account) public view returns (bool) {
+ /// @return isIgnored True if the account is ignored for the total supply. False otherwise.
+ function isIgnoredForTotalSupply(address account) external view returns (bool isIgnored) {
return ignoredForTotalSupply.contains(account);
}
/// @notice Retrieves all the accounts ignored for the total supply.
- /// @return The list of accounts ignored for the total supply.
- function getAllIgnoredForTotalSupply() public view returns (address[] memory) {
+ /// @return accounts List of accounts ignored for the total supply.
+ function getAllIgnoredForTotalSupply() external view returns (address[] memory accounts) {
return ignoredForTotalSupply.values();
}
/// @notice Retrieves the total supply of the token.
- /// @dev Overriden to exclude the ignored accounts from the total supply.
- /// @return The total supply of the token.
- function totalSupply() public view override returns (uint256) {
- uint256 total = super.totalSupply();
+ /// @dev Overridden to exclude the ignored accounts from the total supply.
+ /// @return total Total supply of the token.
+ function totalSupply() public view override returns (uint256 total) {
+ total = super.totalSupply();
uint256 ignoredLength = ignoredForTotalSupply.length(); // cache for efficiency
- for (uint256 i = 0; i < ignoredLength; i++) {
+ for (uint256 i = 0; i < ignoredLength; ++i) {
total -= balanceOf(ignoredForTotalSupply.at(i));
}
return total;
}
+
+ /// @dev Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be
+ /// called by the current owner.
+ /// NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is
+ /// only available to the owner.
+ function renounceOwnership() public virtual override onlyEVCAccountOwner {
+ super.renounceOwnership();
+ }
+
+ /// @dev Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.
+ function transferOwnership(address newOwner) public virtual override onlyEVCAccountOwner {
+ super.transferOwnership(newOwner);
+ }
}
diff --git a/src/Synths/EulerSavingsRate.sol b/src/Synths/EulerSavingsRate.sol
index 30cf87bf..da03ee51 100644
--- a/src/Synths/EulerSavingsRate.sol
+++ b/src/Synths/EulerSavingsRate.sol
@@ -6,16 +6,13 @@ import {Math} from "openzeppelin-contracts/utils/math/Math.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
import {ERC4626} from "openzeppelin-contracts/token/ERC20/extensions/ERC4626.sol";
-import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector.sol";
import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
/// @title EulerSavingsRate
/// @custom:security-contact security@euler.xyz
/// @author Euler Labs (https://www.eulerlabs.com/)
-/// @notice EulerSavingsRate is a ERC4626-compatible vault which allows users to deposit the underlying asset and
-/// receive interest in the form of the same underlying asset. On withdraw, redeem and transfers, the account status
-/// checks must be requested for the account which health might be negatively affected. Thanks to that, the shares of
-/// the EulerSavingsRate vault might be used as collateral by other EVC-compatible vaults.
+/// @notice EulerSavingsRate is a ERC4626-compatible vault with the EVC support which allows users to deposit the
+/// underlying asset and receive interest in the form of the same underlying asset.
/// @dev Do NOT use with fee on transfer tokens
/// @dev Do NOT use with rebasing tokens
contract EulerSavingsRate is EVCUtil, ERC4626 {
@@ -24,7 +21,11 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
uint8 internal constant UNLOCKED = 1;
uint8 internal constant LOCKED = 2;
+ /// @notice The virtual amount added to total shares and total assets.
uint256 internal constant VIRTUAL_AMOUNT = 1e6;
+ /// @notice At least 10 times the virtual amount of shares should exist for gulp to be enabled
+ uint256 internal constant MIN_SHARES_FOR_GULP = VIRTUAL_AMOUNT * 10;
+
uint256 public constant INTEREST_SMEAR = 2 weeks;
struct ESRSlot {
@@ -34,18 +35,15 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
uint8 locked;
}
+ /// @notice Multiple state variables stored in a single storage slot.
ESRSlot internal esrSlot;
+ /// @notice The total assets accounted for in the vault.
uint256 internal _totalAssets;
error Reentrancy();
- /// @notice Modifier to require an account status check on the EVC.
- /// @dev Calls `requireAccountStatusCheck` function from EVC for the specified account after the function body.
- /// @param account The address of the account to check.
- modifier requireAccountStatusCheck(address account) {
- _;
- evc.requireAccountStatusCheck(account);
- }
+ event Gulped(uint256 gulped, uint256 interestLeft);
+ event InterestUpdated(uint256 interestAccrued, uint256 interestLeft);
modifier nonReentrant() {
if (esrSlot.locked == LOCKED) revert Reentrancy();
@@ -55,8 +53,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
esrSlot.locked = UNLOCKED;
}
- constructor(IEVC _evc, address _asset, string memory _name, string memory _symbol)
- EVCUtil(address(_evc))
+ constructor(address _evc, address _asset, string memory _name, string memory _symbol)
+ EVCUtil(_evc)
ERC4626(IERC20(_asset))
ERC20(_name, _symbol)
{
@@ -69,63 +67,6 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
return _totalAssets + interestAccrued();
}
- function maxRedeem(address owner) public view override returns (uint256) {
- // If account has borrows, withdrawal might be reverted by the controller during account status checks.
- // The vault has no way to verify or enforce the behaviour of the controller, which the account owner
- // has enabled. It will therefore assume that all of the assets would be witheld by the controller and
- // under-estimate the return amount to zero.
- // Integrators who handle borrowing should implement custom logic to work with the particular controllers
- // they want to support.
- if (evc.getControllers(owner).length > 0) {
- return 0;
- }
-
- return super.maxRedeem(owner);
- }
-
- function maxWithdraw(address owner) public view override returns (uint256) {
- // If account has borrows, withdrawal might be reverted by the controller during account status checks.
- // The vault has no way to verify or enforce the behaviour of the controller, which the account owner
- // has enabled. It will therefore assume that all of the assets would be witheld by the controller and
- // under-estimate the return amount to zero.
- // Integrators who handle borrowing should implement custom logic to work with the particular controllers
- // they want to support.
- if (evc.getControllers(owner).length > 0) {
- return 0;
- }
-
- return super.maxWithdraw(owner);
- }
-
- /// @notice Transfers a certain amount of tokens to a recipient.
- /// @param to The recipient of the transfer.
- /// @param amount The amount shares to transfer.
- /// @return A boolean indicating whether the transfer was successful.
- function transfer(address to, uint256 amount)
- public
- override (ERC20, IERC20)
- nonReentrant
- requireAccountStatusCheck(_msgSender())
- returns (bool)
- {
- return super.transfer(to, amount);
- }
-
- /// @notice Transfers a certain amount of tokens from a sender to a recipient.
- /// @param from The sender of the transfer.
- /// @param to The recipient of the transfer.
- /// @param amount The amount of shares to transfer.
- /// @return A boolean indicating whether the transfer was successful.
- function transferFrom(address from, address to, uint256 amount)
- public
- override (ERC20, IERC20)
- nonReentrant
- requireAccountStatusCheck(from)
- returns (bool)
- {
- return super.transferFrom(from, to, amount);
- }
-
/// @notice Deposits a certain amount of assets to the vault.
/// @param assets The amount of assets to deposit.
/// @param receiver The recipient of the shares.
@@ -137,38 +78,30 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
/// @notice Mints a certain amount of shares to the account.
/// @param shares The amount of assets to mint.
/// @param receiver The account to mint the shares to.
- /// @return The amount of assets spend.
+ /// @return The amount of assets spent.
function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256) {
return super.mint(shares, receiver);
}
- /// @notice Deposits a certain amount of assets to the vault.
- /// @param assets The amount of assets to deposit.
- /// @param receiver The recipient of the shares.
- /// @return The amount of shares minted.
- function withdraw(uint256 assets, address receiver, address owner)
- public
- override
- nonReentrant
- requireAccountStatusCheck(owner)
- returns (uint256)
- {
+ /// @notice Withdraws a certain amount of assets from the vault.
+ /// @dev Overwritten to update the accrued interest and update _totalAssets.
+ /// @param assets The amount of assets to withdraw.
+ /// @param receiver The recipient of the assets.
+ /// @param owner The holder of shares to burn.
+ /// @return The amount of shares burned.
+ function withdraw(uint256 assets, address receiver, address owner) public override nonReentrant returns (uint256) {
// Move interest to totalAssets
updateInterestAndReturnESRSlotCache();
return super.withdraw(assets, receiver, owner);
}
/// @notice Redeems a certain amount of shares for assets.
+ /// @dev Overwritten to update the accrued interest and update _totalAssets.
/// @param shares The amount of shares to redeem.
/// @param receiver The recipient of the assets.
+ /// @param owner The account from which the shares are redeemed.
/// @return The amount of assets redeemed.
- function redeem(uint256 shares, address receiver, address owner)
- public
- override
- nonReentrant
- requireAccountStatusCheck(owner)
- returns (uint256)
- {
+ function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) {
// Move interest to totalAssets
updateInterestAndReturnESRSlotCache();
return super.redeem(shares, receiver, owner);
@@ -183,8 +116,8 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
}
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
- _totalAssets = _totalAssets + assets;
super._deposit(caller, receiver, assets, shares);
+ _totalAssets = _totalAssets + assets;
}
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
@@ -199,17 +132,23 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
function gulp() public nonReentrant {
ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache();
+ // Do not gulp if total supply is too low
+ if (totalSupply() < MIN_SHARES_FOR_GULP) return;
+
uint256 assetBalance = IERC20(asset()).balanceOf(address(this));
uint256 toGulp = assetBalance - _totalAssets - esrSlotCache.interestLeft;
uint256 maxGulp = type(uint168).max - esrSlotCache.interestLeft;
if (toGulp > maxGulp) toGulp = maxGulp; // cap interest, allowing the vault to function
+ esrSlotCache.lastInterestUpdate = uint40(block.timestamp);
esrSlotCache.interestSmearEnd = uint40(block.timestamp + INTEREST_SMEAR);
esrSlotCache.interestLeft += uint168(toGulp); // toGulp <= maxGulp <= max uint168
// write esrSlotCache back to storage in a single SSTORE
esrSlot = esrSlotCache;
+
+ emit Gulped(toGulp, esrSlotCache.interestLeft);
}
/// @notice Updates the interest and returns the ESR storage slot cache.
@@ -218,18 +157,23 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
ESRSlot memory esrSlotCache = esrSlot;
uint256 accruedInterest = interestAccruedFromCache(esrSlotCache);
- // it's safe to down-cast because the accrued interest is a fraction of interest left
- esrSlotCache.interestLeft -= uint168(accruedInterest);
- esrSlotCache.lastInterestUpdate = uint40(block.timestamp);
- // write esrSlotCache back to storage in a single SSTORE
- esrSlot = esrSlotCache;
- // Move interest accrued to totalAssets
- _totalAssets = _totalAssets + accruedInterest;
+ if (accruedInterest > 0) {
+ // it's safe to down-cast because the accrued interest is a fraction of interest left
+ esrSlotCache.interestLeft -= uint168(accruedInterest);
+ esrSlotCache.lastInterestUpdate = uint40(block.timestamp);
+ // write esrSlotCache back to storage in a single SSTORE
+ esrSlot = esrSlotCache;
+ // Move interest accrued to totalAssets
+ _totalAssets = _totalAssets + accruedInterest;
+
+ emit InterestUpdated(accruedInterest, esrSlotCache.interestLeft);
+ }
return esrSlotCache;
}
/// @notice Returns the amount of interest accrued.
+ /// @return The amount of interest accrued.
function interestAccrued() public view returns (uint256) {
return interestAccruedFromCache(esrSlot);
}
@@ -253,6 +197,7 @@ contract EulerSavingsRate is EVCUtil, ERC4626 {
}
/// @notice Returns the ESR storage slot as a struct.
+ /// @return The ESR storage slot as a struct.
function getESRSlot() public view returns (ESRSlot memory) {
return esrSlot;
}
diff --git a/src/Synths/IRMSynth.sol b/src/Synths/IRMSynth.sol
index f4963eda..2bb6d9c9 100644
--- a/src/Synths/IRMSynth.sol
+++ b/src/Synths/IRMSynth.sol
@@ -2,8 +2,8 @@
pragma solidity ^0.8.0;
-import "../InterestRateModels/IIRM.sol";
-import "../interfaces/IPriceOracle.sol";
+import {IIRM} from "../InterestRateModels/IIRM.sol";
+import {IPriceOracle} from "../interfaces/IPriceOracle.sol";
import {IERC20} from "../EVault/IEVault.sol";
/// @title IRMSynth
@@ -20,10 +20,15 @@ contract IRMSynth is IIRM {
uint216 public constant ADJUST_ONE = 1.0e18;
uint216 public constant ADJUST_INTERVAL = 1 hours;
+ /// @notice The address of the synthetic asset.
address public immutable synth;
+ /// @notice The address of the reference asset.
address public immutable referenceAsset;
+ /// @notice The address of the oracle.
IPriceOracle public immutable oracle;
+ /// @notice The target quote which the IRM will try to maintain.
uint256 public immutable targetQuote;
+ /// @notice The amount of the quote asset to use for the quote.
uint256 public immutable quoteAmount;
struct IRMData {
@@ -36,7 +41,9 @@ contract IRMSynth is IIRM {
error E_ZeroAddress();
error E_InvalidQuote();
- constructor(address synth_, address referenceAsset_, address oracle_, uint256 targetQuoute_) {
+ event InterestUpdated(uint256 rate);
+
+ constructor(address synth_, address referenceAsset_, address oracle_, uint256 targetQuote_) {
if (synth_ == address(0) || referenceAsset_ == address(0) || oracle_ == address(0)) {
revert E_ZeroAddress();
}
@@ -44,7 +51,7 @@ contract IRMSynth is IIRM {
synth = synth_;
referenceAsset = referenceAsset_;
oracle = IPriceOracle(oracle_);
- targetQuote = targetQuoute_;
+ targetQuote = targetQuote_;
quoteAmount = 10 ** IERC20(synth_).decimals();
// Refusing to proceed with worthless asset
@@ -54,21 +61,26 @@ contract IRMSynth is IIRM {
}
irmStorage = IRMData({lastUpdated: uint40(block.timestamp), lastRate: BASE_RATE});
+
+ emit InterestUpdated(BASE_RATE);
}
+ /// @notice Computes the interest rate and updates the storage if necessary.
+ /// @return The interest rate.
function computeInterestRate(address, uint256, uint256) external override returns (uint256) {
- IRMData memory irmCache = irmStorage;
- (uint216 rate, bool updated) = _computeRate(irmCache);
+ (uint216 rate, bool updated) = _computeRate(irmStorage);
if (updated) {
irmStorage = IRMData({lastUpdated: uint40(block.timestamp), lastRate: rate});
+ emit InterestUpdated(rate);
}
return rate;
}
- function computeInterestRateView(address, uint256, uint256) external view override returns (uint256) {
- (uint216 rate,) = _computeRate(irmStorage);
+ /// @return rate The new interest rate
+ function computeInterestRateView(address, uint256, uint256) external view override returns (uint256 rate) {
+ (rate,) = _computeRate(irmStorage);
return rate;
}
@@ -89,7 +101,7 @@ contract IRMSynth is IIRM {
// If the quote is less than the target, increase the rate
rate = rate * ADJUST_FACTOR / ADJUST_ONE;
} else {
- // If the quote is greater than the target, decrease the rate
+ // If the quote is greater than or equal to the target, decrease the rate
rate = rate * ADJUST_ONE / ADJUST_FACTOR;
}
@@ -103,6 +115,8 @@ contract IRMSynth is IIRM {
return (rate, updated);
}
+ /// @notice Retrieves the packed IRM data as a struct.
+ /// @return The IRM data.
function getIRMData() external view returns (IRMData memory) {
return irmStorage;
}
diff --git a/src/Synths/PegStabilityModule.sol b/src/Synths/PegStabilityModule.sol
index adc64b18..f5ec5d3f 100644
--- a/src/Synths/PegStabilityModule.sol
+++ b/src/Synths/PegStabilityModule.sol
@@ -2,64 +2,78 @@
pragma solidity ^0.8.0;
-import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol";
+import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
+import {Math} from "openzeppelin-contracts/utils/math/Math.sol";
import {ESynth} from "./ESynth.sol";
/// @title PegStabilityModule
/// @custom:security-contact security@euler.xyz
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice The PegStabilityModule is granted minting rights on the ESynth and must allow slippage-free conversion from
-/// and to the underlying asset as per configured conversionPrice. On deployment, the fee for swaps to synthetic asset
+/// and to the underlying asset as per configured CONVERSION_PRICE. On deployment, the fee for swaps to synthetic asset
/// and to underlying asset are defined. These fees must accrue to the PegStabilityModule contract and can not be
/// withdrawn, serving as a permanent reserve to support the peg. Swapping to the synthetic asset is possible up to the
/// minting cap granted for the PegStabilityModule in the ESynth. Swapping to the underlying asset is possible up to the
/// amount of the underlying asset held by the PegStabilityModule.
contract PegStabilityModule is EVCUtil {
using SafeERC20 for IERC20;
+ using Math for uint256;
uint256 public constant BPS_SCALE = 100_00;
uint256 public constant PRICE_SCALE = 1e18;
+ /// @notice The synthetic asset.
ESynth public immutable synth;
+ /// @notice The underlying asset.
IERC20 public immutable underlying;
- uint256 public immutable conversionPrice; // 1e18 = 1 SYNTH == 1 UNDERLYING, 0.01e18 = 1 SYNTH == 0.01 UNDERLYING
+ /// @notice The fee for swapping to the underlying asset in basis points.
uint256 public immutable TO_UNDERLYING_FEE;
+ /// @notice The fee for swapping to the synthetic asset in basis points.
uint256 public immutable TO_SYNTH_FEE;
+ /// @notice The conversion price between the synthetic and underlying asset.
+ uint256 public immutable CONVERSION_PRICE;
error E_ZeroAddress();
error E_FeeExceedsBPS();
+ error E_ZeroConversionPrice();
/// @param _evc The address of the EVC.
/// @param _synth The address of the synthetic asset.
/// @param _underlying The address of the underlying asset.
- /// @param toUnderlyingFeeBPS The fee for swapping to the underlying asset in basis points. eg: 100 = 1%
- /// @param toSynthFeeBPS The fee for swapping to the synthetic asset in basis points. eg: 100 = 1%
+ /// @param _toUnderlyingFeeBPS The fee for swapping to the underlying asset in basis points. eg: 100 = 1%
+ /// @param _toSynthFeeBPS The fee for swapping to the synthetic asset in basis points. eg: 100 = 1%
/// @param _conversionPrice The conversion price between the synthetic and underlying asset.
- /// eg: 1e18 = 1 SYNTH == 1 UNDERLYING, 0.01e18 = 1 SYNTH == 0.01 UNDERLYING
+ /// @dev _conversionPrice = 10**underlyingDecimals corresponds to 1:1 peg
+ /// @dev if underlying is 18 decimals, _conversionPrice = 1e18 corresponds to 1:1 peg
+ /// @dev if underlying is 6 decimals, _conversionPrice = 1e6 corresponds to 1:1 peg
constructor(
address _evc,
address _synth,
address _underlying,
- uint256 toUnderlyingFeeBPS,
- uint256 toSynthFeeBPS,
+ uint256 _toUnderlyingFeeBPS,
+ uint256 _toSynthFeeBPS,
uint256 _conversionPrice
) EVCUtil(_evc) {
- if (toUnderlyingFeeBPS >= BPS_SCALE || toSynthFeeBPS >= BPS_SCALE) {
+ if (_synth == address(0) || _underlying == address(0)) {
+ revert E_ZeroAddress();
+ }
+
+ if (_toUnderlyingFeeBPS >= BPS_SCALE || _toSynthFeeBPS >= BPS_SCALE) {
revert E_FeeExceedsBPS();
}
- if (_evc == address(0) || _synth == address(0) || _underlying == address(0)) {
- revert E_ZeroAddress();
+ if (_conversionPrice == 0) {
+ revert E_ZeroConversionPrice();
}
synth = ESynth(_synth);
underlying = IERC20(_underlying);
- TO_UNDERLYING_FEE = toUnderlyingFeeBPS;
- TO_SYNTH_FEE = toSynthFeeBPS;
- conversionPrice = _conversionPrice;
+ TO_UNDERLYING_FEE = _toUnderlyingFeeBPS;
+ TO_SYNTH_FEE = _toSynthFeeBPS;
+ CONVERSION_PRICE = _conversionPrice;
}
/// @notice Swaps the given amount of synth to underlying given an input amount of synth.
@@ -130,27 +144,33 @@ contract PegStabilityModule is EVCUtil {
/// @param amountIn The amount of synth to swap.
/// @return The amount of underlying received.
function quoteToUnderlyingGivenIn(uint256 amountIn) public view returns (uint256) {
- return amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) * conversionPrice / BPS_SCALE / PRICE_SCALE;
+ return amountIn.mulDiv(
+ (BPS_SCALE - TO_UNDERLYING_FEE) * CONVERSION_PRICE, BPS_SCALE * PRICE_SCALE, Math.Rounding.Floor
+ );
}
/// @notice Quotes the amount of underlying given an output amount of synth.
/// @param amountOut The amount of underlying to receive.
/// @return The amount of synth swapped.
function quoteToUnderlyingGivenOut(uint256 amountOut) public view returns (uint256) {
- return amountOut * BPS_SCALE * PRICE_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE) / conversionPrice;
+ return amountOut.mulDiv(
+ BPS_SCALE * PRICE_SCALE, (BPS_SCALE - TO_UNDERLYING_FEE) * CONVERSION_PRICE, Math.Rounding.Ceil
+ );
}
/// @notice Quotes the amount of synth given an input amount of underlying.
/// @param amountIn The amount of underlying to swap.
/// @return The amount of synth received.
function quoteToSynthGivenIn(uint256 amountIn) public view returns (uint256) {
- return amountIn * (BPS_SCALE - TO_SYNTH_FEE) * PRICE_SCALE / BPS_SCALE / conversionPrice;
+ return
+ amountIn.mulDiv((BPS_SCALE - TO_SYNTH_FEE) * PRICE_SCALE, BPS_SCALE * CONVERSION_PRICE, Math.Rounding.Floor);
}
/// @notice Quotes the amount of synth given an output amount of underlying.
/// @param amountOut The amount of synth to receive.
/// @return The amount of underlying swapped.
function quoteToSynthGivenOut(uint256 amountOut) public view returns (uint256) {
- return amountOut * BPS_SCALE * conversionPrice / (BPS_SCALE - TO_SYNTH_FEE) / PRICE_SCALE;
+ return
+ amountOut.mulDiv(BPS_SCALE * CONVERSION_PRICE, (BPS_SCALE - TO_SYNTH_FEE) * PRICE_SCALE, Math.Rounding.Ceil);
}
}
diff --git a/src/interfaces/IHookTarget.sol b/src/interfaces/IHookTarget.sol
index 45ef3ac3..70ca06ed 100644
--- a/src/interfaces/IHookTarget.sol
+++ b/src/interfaces/IHookTarget.sol
@@ -10,5 +10,5 @@ interface IHookTarget {
/// @notice If given contract is a hook target, it is expected to return the bytes4 magic value that is the selector
/// of this function
/// @return The bytes4 magic value (0x87439e04) that is the selector of this function
- function isHookTarget() external returns (bytes4);
+ function isHookTarget() external view returns (bytes4);
}
diff --git a/test/invariants/Setup.t.sol b/test/invariants/Setup.t.sol
index 69bfed5d..5be5bc9c 100644
--- a/test/invariants/Setup.t.sol
+++ b/test/invariants/Setup.t.sol
@@ -103,12 +103,14 @@ contract Setup is BaseTest {
eTST = EVaultExtended(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount))
);
+ eTST.setHookConfig(address(0), 0);
eTST.setInterestRateModel(address(new IRMTestDefault()));
vaults.push(address(eTST));
eTST2 = EVaultExtended(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST2), address(oracle), unitOfAccount))
);
+ eTST2.setHookConfig(address(0), 0);
eTST2.setInterestRateModel(address(new IRMTestDefault()));
vaults.push(address(eTST2));
}
diff --git a/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol b/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol
index d0921a5e..bf0bf044 100644
--- a/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol
+++ b/test/invariants/handlers/modules/GovernanceModuleHandler.t.sol
@@ -44,14 +44,6 @@ contract GovernanceModuleHandler is BaseHandler {
assert(true);
}
- function clearLTV(uint256 i) external {
- address collateral = _getRandomBaseAsset(i);
-
- eTST.clearLTV(collateral);
-
- assert(true);
- }
-
function setInterestFee(uint16 interestFee) external {
eTST.setInterestFee(interestFee);
diff --git a/test/mocks/MockPriceOracle.sol b/test/mocks/MockPriceOracle.sol
index e5c41b2d..757e8aaf 100644
--- a/test/mocks/MockPriceOracle.sol
+++ b/test/mocks/MockPriceOracle.sol
@@ -53,8 +53,18 @@ contract MockPriceOracle {
}
function calculateQuote(address base, uint256 amount, uint256 p) internal view returns (uint256) {
- (bool success,) = base.staticcall(abi.encodeCall(IERC4626.asset, ()));
- if (base.code.length > 0 && success) amount = IEVault(base).convertToAssets(amount);
+ // While base is a vault (for the purpose of the mock, if it implements asset()), then call
+ // convertToAssets() to price its shares. This is similar to how EulerRouter implements
+ // "resolved" vaults.
+
+ while (base.code.length > 0) {
+ (bool success, bytes memory data) = base.staticcall(abi.encodeCall(IERC4626.asset, ()));
+ if (!success) break;
+
+ (address asset) = abi.decode(data, (address));
+ amount = IEVault(base).convertToAssets(amount);
+ base = asset;
+ }
return amount * p / 1e18;
}
diff --git a/test/unit/esr/ESR.Fuzz.t.sol b/test/unit/esr/ESR.Fuzz.t.sol
index 713b6e80..e3db0554 100644
--- a/test/unit/esr/ESR.Fuzz.t.sol
+++ b/test/unit/esr/ESR.Fuzz.t.sol
@@ -14,11 +14,18 @@ contract ESRFuzzTest is ESRTest {
//totalAssets should be equal to the balance after SMEAR has passed
function invariant_totalAssetsShouldBeEqualToBalanceAfterSMEAR() public {
- vm.assume(asset.balanceOf(address(esr)) <= type(uint168).max);
- if (asset.balanceOf(address(esr)) == 0) return;
+ if (asset.totalSupply() > type(uint248).max) return;
+ if (asset.balanceOf(address(esr)) == 0 || asset.balanceOf(address(esr)) > type(uint168).max - 1e7) return;
+
+ // min deposit requirement before gulp
+ doDeposit(user, 1e7);
+
+ uint256 balance = asset.balanceOf(address(esr));
+
esr.gulp();
skip(esr.INTEREST_SMEAR()); // make sure smear has passed
- assertEq(esr.totalAssets(), asset.balanceOf(address(esr)));
+
+ assertEq(esr.totalAssets(), balance);
}
function testFuzz_interestAccrued_under_uint168(uint256 interestAmount, uint256 depositAmount, uint256 timePassed)
@@ -38,6 +45,7 @@ contract ESRFuzzTest is ESRTest {
// this tests shows that when you have a very small deposit and a very large interestAmount minted to the contract
function testFuzz_gulp_under_uint168(uint256 interestAmount, uint256 depositAmount) public {
+ uint256 MIN_SHARES_FOR_GULP = 10 * 1e6;
depositAmount = bound(depositAmount, 0, type(uint112).max);
interestAmount = bound(interestAmount, 0, type(uint256).max - depositAmount); // this makes sure that the mint
// won't cause overflow
@@ -49,10 +57,50 @@ contract ESRFuzzTest is ESRTest {
EulerSavingsRate.ESRSlot memory esrSlot = esr.updateInterestAndReturnESRSlotCache();
- if (interestAmount <= type(uint168).max) {
- assertEq(esrSlot.interestLeft, interestAmount);
+ if (depositAmount >= MIN_SHARES_FOR_GULP) {
+ if (interestAmount <= type(uint168).max) {
+ assertEq(esrSlot.interestLeft, interestAmount);
+ } else {
+ assertEq(esrSlot.interestLeft, type(uint168).max);
+ }
+ } else {
+ assertEq(esrSlot.interestLeft, 0);
+ }
+ }
+
+ function testFuzz_conditionalAccruedInterestUpdate(uint32 interestAmount) public {
+ // min deposit requirement before gulp
+ doDeposit(user, 1e7);
+
+ // mint some interest to be distributed
+ asset.mint(address(esr), interestAmount);
+
+ uint256 balance = asset.balanceOf(address(esr));
+ uint256 totalAssets = esr.totalAssets();
+
+ esr.gulp();
+ skip(1);
+
+ if (interestAmount < esr.INTEREST_SMEAR()) {
+ assertEq(esr.totalAssets(), totalAssets);
+ assertEq(esr.totalAssets() + interestAmount, balance);
+ } else {
+ uint256 accruedInterest = interestAmount / esr.INTEREST_SMEAR();
+ assertEq(esr.totalAssets() + interestAmount - accruedInterest, balance);
+ vm.expectEmit();
+ emit EulerSavingsRate.InterestUpdated(accruedInterest, interestAmount - accruedInterest);
+ }
+
+ vm.recordLogs();
+ esr.gulp();
+
+ Vm.Log[] memory logs = vm.getRecordedLogs();
+ if (interestAmount < esr.INTEREST_SMEAR()) {
+ assertEq(logs.length, 1);
+ assertNotEq(logs[0].topics[0], EulerSavingsRate.InterestUpdated.selector);
} else {
- assertEq(esrSlot.interestLeft, type(uint168).max);
+ assertEq(logs.length, 2);
+ assertEq(logs[0].topics[0], EulerSavingsRate.InterestUpdated.selector);
}
}
diff --git a/test/unit/esr/ESR.General.t.sol b/test/unit/esr/ESR.General.t.sol
index b7f371cd..0bb9bc41 100644
--- a/test/unit/esr/ESR.General.t.sol
+++ b/test/unit/esr/ESR.General.t.sol
@@ -151,41 +151,4 @@ contract ESRGeneralTest is ESRTest {
uint256 balanceOfAddress1 = esr.balanceOf(address(1));
assertEq(balanceOfAddress1, balanceOfUser);
}
-
- // test the result of maxWithdraw when no controller is set
- function test_MaxWithdrawNoControllerSet() public {
- uint256 depositAmount = 100e18;
- doDeposit(user, depositAmount);
- uint256 maxWithdraw = esr.maxWithdraw(user);
- assertEq(maxWithdraw, depositAmount);
- }
-
- // test the result of maxWithdraw when controller is set
- function test_maxWithdrawControllerSet() public {
- uint256 depositAmount = 100e18;
- doDeposit(user, depositAmount);
- vm.prank(user);
- evc.enableController(address(user), address(statusCheck));
- uint256 maxWithdraw = esr.maxWithdraw(user);
- assertEq(maxWithdraw, 0);
- }
-
- // test the result of maxRedeem when no controller is set
- function test_maxRedeemNoControllerSet() public {
- uint256 depositAmount = 100e18;
- doDeposit(user, depositAmount);
- uint256 shares = esr.balanceOf(user);
- uint256 maxRedeem = esr.maxRedeem(user);
- assertEq(maxRedeem, shares);
- }
-
- // test the result of maxRedeem when controller is set
- function test_maxRedeemControllerSet() public {
- uint256 depositAmount = 100e18;
- doDeposit(user, depositAmount);
- vm.prank(user);
- evc.enableController(address(user), address(statusCheck));
- uint256 maxRedeem = esr.maxRedeem(user);
- assertEq(maxRedeem, 0);
- }
}
diff --git a/test/unit/esr/ESR.Gulp.t.sol b/test/unit/esr/ESR.Gulp.t.sol
index 65ee26d5..ba4aae06 100644
--- a/test/unit/esr/ESR.Gulp.t.sol
+++ b/test/unit/esr/ESR.Gulp.t.sol
@@ -82,4 +82,19 @@ contract ESRGulpTest is ESRTest {
assertEq(esrSlot.lastInterestUpdate, block.timestamp);
assertEq(esrSlot.interestSmearEnd, block.timestamp + esr.INTEREST_SMEAR());
}
+
+ function testGulpBelowMinSharesForGulp() public {
+ uint256 depositAmount = 1337;
+ doDeposit(user, depositAmount);
+
+ uint256 interestAmount = 10e18;
+ // Mint interest directly into the contract
+ asset.mint(address(esr), interestAmount);
+ esr.gulp();
+ skip(esr.INTEREST_SMEAR());
+
+ EulerSavingsRate.ESRSlot memory esrSlot = esr.getESRSlot();
+ assertEq(esr.totalAssets(), depositAmount);
+ assertEq(esrSlot.interestLeft, 0);
+ }
}
diff --git a/test/unit/esr/lib/ESRTest.sol b/test/unit/esr/lib/ESRTest.sol
index 8ae9c6fa..74dd1a29 100644
--- a/test/unit/esr/lib/ESRTest.sol
+++ b/test/unit/esr/lib/ESRTest.sol
@@ -20,7 +20,7 @@ contract ESRTest is Test {
function setUp() public virtual {
asset = new MockToken();
evc = new EVC();
- esr = new EulerSavingsRate(evc, address(asset), NAME, SYMBOL);
+ esr = new EulerSavingsRate(address(evc), address(asset), NAME, SYMBOL);
// Set a non zero timestamp
vm.warp(420);
diff --git a/test/unit/esvault/ESVaultTestBase.t.sol b/test/unit/esvault/ESVaultTestBase.t.sol
index 32b935b2..6afeb762 100644
--- a/test/unit/esvault/ESVaultTestBase.t.sol
+++ b/test/unit/esvault/ESVaultTestBase.t.sol
@@ -15,9 +15,9 @@ contract ESVaultTestBase is EVaultTestBase {
function setUp() public virtual override {
super.setUp();
- assetTSTAsSynth = ESynth(address(new ESynth(evc, "Test Synth", "TST")));
+ assetTSTAsSynth = ESynth(address(new ESynth(address(evc), "Test Synth", "TST")));
assetTST = TestERC20(address(assetTSTAsSynth));
- assetTST2AsSynth = ESynth(address(new ESynth(evc, "Test Synth 2", "TST2")));
+ assetTST2AsSynth = ESynth(address(new ESynth(address(evc), "Test Synth 2", "TST2")));
assetTST2 = TestERC20(address(assetTST2AsSynth));
eTST = createSynthEVault(address(assetTST));
diff --git a/test/unit/esynth/ESynth.totalSupply.t.sol b/test/unit/esynth/ESynth.totalSupply.t.sol
index 65c2e61c..ac55e343 100644
--- a/test/unit/esynth/ESynth.totalSupply.t.sol
+++ b/test/unit/esynth/ESynth.totalSupply.t.sol
@@ -2,7 +2,7 @@
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
-import {ESynth, IEVC, Ownable} from "../../../src/Synths/ESynth.sol";
+import {ESynth, Ownable} from "../../../src/Synths/ESynth.sol";
contract ESynthTotalSupplyTest is Test {
ESynth synth;
@@ -13,7 +13,7 @@ contract ESynthTotalSupplyTest is Test {
function setUp() public {
vm.startPrank(owner);
- synth = new ESynth(IEVC(makeAddr("evc")), "TestSynth", "TS");
+ synth = new ESynth(makeAddr("evc"), "TestSynth", "TS");
synth.setCapacity(owner, 1000000e18);
vm.stopPrank();
}
@@ -28,8 +28,9 @@ contract ESynthTotalSupplyTest is Test {
bool success = synth.addIgnoredForTotalSupply(ignored1);
address[] memory ignored = synth.getAllIgnoredForTotalSupply();
- assertEq(ignored.length, 1);
- assertEq(ignored[0], ignored1);
+ assertEq(ignored.length, 2);
+ assertEq(ignored[0], address(synth));
+ assertEq(ignored[1], ignored1);
assertTrue(success);
}
@@ -40,8 +41,9 @@ contract ESynthTotalSupplyTest is Test {
vm.stopPrank();
address[] memory ignored = synth.getAllIgnoredForTotalSupply();
- assertEq(ignored.length, 1);
- assertEq(ignored[0], ignored1);
+ assertEq(ignored.length, 2);
+ assertEq(ignored[0], address(synth));
+ assertEq(ignored[1], ignored1);
assertFalse(success);
}
@@ -57,7 +59,8 @@ contract ESynthTotalSupplyTest is Test {
vm.stopPrank();
address[] memory ignored = synth.getAllIgnoredForTotalSupply();
- assertEq(ignored.length, 0);
+ assertEq(ignored[0], address(synth));
+ assertEq(ignored.length, 1);
assertTrue(success);
}
@@ -67,29 +70,32 @@ contract ESynthTotalSupplyTest is Test {
vm.stopPrank();
address[] memory ignored = synth.getAllIgnoredForTotalSupply();
- assertEq(ignored.length, 0);
+ assertEq(ignored[0], address(synth));
+ assertEq(ignored.length, 1);
assertFalse(success);
}
- function test_totalSupply_nothingIgnored() public {
+ function test_totalSupply_nothingIgnoredExceptSynth() public {
vm.startPrank(owner);
- synth.mint(ignored1, 100);
- synth.mint(ignored2, 200);
- synth.mint(ignored3, 300);
+ synth.mint(address(synth), 100);
+ synth.mint(ignored1, 200);
+ synth.mint(ignored2, 300);
+ synth.mint(ignored3, 400);
vm.stopPrank();
- assertEq(synth.totalSupply(), 600);
+ assertEq(synth.totalSupply(), 900);
}
function test_TotalSupplyAddresses_ignored() public {
vm.startPrank(owner);
- synth.mint(ignored1, 100);
- synth.mint(ignored2, 200);
- synth.mint(ignored3, 300);
+ synth.mint(address(synth), 100);
+ synth.mint(ignored1, 200);
+ synth.mint(ignored2, 300);
+ synth.mint(ignored3, 400);
synth.addIgnoredForTotalSupply(ignored1);
synth.addIgnoredForTotalSupply(ignored2);
vm.stopPrank();
- assertEq(synth.totalSupply(), 300);
+ assertEq(synth.totalSupply(), 400);
}
}
diff --git a/test/unit/esynth/ESynthGeneral.t.sol b/test/unit/esynth/ESynthGeneral.t.sol
index 7c81306a..f606d98a 100644
--- a/test/unit/esynth/ESynthGeneral.t.sol
+++ b/test/unit/esynth/ESynthGeneral.t.sol
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.20;
+import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {ESynthTest} from "./lib/ESynthTest.sol";
import {stdError} from "forge-std/Test.sol";
import {Errors} from "../../../src/EVault/shared/Errors.sol";
@@ -130,4 +131,82 @@ contract ESynthGeneralTest is ESynthTest {
vm.expectRevert(ESynth.E_NotEVCCompatible.selector);
esynth.allocate(address(wrongEVC), amount);
}
+
+ function test_GovernanceModifiers(address owner, uint8 id, address nonOwner, uint128 amount) public {
+ vm.assume(owner != address(0) && owner != address(evc));
+ vm.assume(!evc.haveCommonOwner(owner, nonOwner) && nonOwner != address(evc));
+ vm.assume(id != 0);
+
+ vm.prank(owner);
+ esynth = ESynth(address(new ESynth(address(evc), "Test Synth", "TST")));
+
+ // succeeds if called directly by an owner
+ vm.prank(owner);
+ esynth.setCapacity(address(this), amount);
+
+ // fails if called by a non-owner
+ vm.prank(nonOwner);
+ vm.expectRevert();
+ esynth.setCapacity(address(this), amount);
+
+ // succeeds if called by an owner through the EVC
+ vm.prank(owner);
+ evc.call(address(esynth), owner, 0, abi.encodeCall(ESynth.setCapacity, (address(this), amount)));
+
+ // fails if called by non-owner through the EVC
+ vm.prank(nonOwner);
+ vm.expectRevert();
+ evc.call(address(esynth), nonOwner, 0, abi.encodeCall(ESynth.setCapacity, (address(this), amount)));
+
+ // fails if called by a sub-account of an owner through the EVC
+ vm.prank(owner);
+ vm.expectRevert();
+ evc.call(
+ address(esynth),
+ address(uint160(owner) ^ id),
+ 0,
+ abi.encodeCall(ESynth.setCapacity, (address(this), amount))
+ );
+
+ // fails if called by the owner operator through the EVC
+ vm.prank(owner);
+ evc.setAccountOperator(owner, nonOwner, true);
+ vm.prank(nonOwner);
+ vm.expectRevert();
+ evc.call(address(esynth), owner, 0, abi.encodeCall(ESynth.setCapacity, (address(this), amount)));
+ }
+
+ function test_RenounceTransferOwnership() public {
+ address OWNER = makeAddr("OWNER");
+ address OWNER2 = makeAddr("OWNER2");
+ address OWNER3 = makeAddr("OWNER3");
+
+ vm.prank(OWNER);
+ esynth = ESynth(address(new ESynth(address(evc), "Test Synth", "TST")));
+ assertEq(esynth.owner(), OWNER);
+
+ vm.prank(OWNER2);
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, OWNER2));
+ esynth.renounceOwnership();
+
+ vm.prank(OWNER2);
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, OWNER2));
+ esynth.transferOwnership(OWNER2);
+
+ vm.prank(OWNER);
+ esynth.transferOwnership(OWNER2);
+ assertEq(esynth.owner(), OWNER2);
+
+ vm.prank(OWNER2);
+ esynth.transferOwnership(OWNER3);
+ assertEq(esynth.owner(), OWNER3);
+
+ vm.prank(OWNER2);
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, OWNER2));
+ esynth.renounceOwnership();
+
+ vm.prank(OWNER3);
+ esynth.renounceOwnership();
+ assertEq(esynth.owner(), address(0));
+ }
}
diff --git a/test/unit/esynth/lib/ESynthTest.sol b/test/unit/esynth/lib/ESynthTest.sol
index 37e87135..4c32d8f7 100644
--- a/test/unit/esynth/lib/ESynthTest.sol
+++ b/test/unit/esynth/lib/ESynthTest.sol
@@ -19,7 +19,7 @@ contract ESynthTest is EVaultTestBase {
user1 = vm.addr(1001);
user2 = vm.addr(1002);
- esynth = ESynth(address(new ESynth(evc, "Test Synth", "TST")));
+ esynth = ESynth(address(new ESynth(address(evc), "Test Synth", "TST")));
assetTST = TestERC20(address(esynth));
eTST = createSynthEVault(address(assetTST));
diff --git a/test/unit/evault/EVaultTestBase.t.sol b/test/unit/evault/EVaultTestBase.t.sol
index ed537c2f..f413c7c1 100644
--- a/test/unit/evault/EVaultTestBase.t.sol
+++ b/test/unit/evault/EVaultTestBase.t.sol
@@ -133,6 +133,7 @@ contract EVaultTestBase is AssertionsCustomTypes, Test, DeployPermit2 {
eTST = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount))
);
+ eTST.setHookConfig(address(0), 0);
eTST.setInterestRateModel(address(new IRMTestDefault()));
eTST.setMaxLiquidationDiscount(0.2e4);
eTST.setFeeReceiver(feeReceiver);
@@ -140,6 +141,7 @@ contract EVaultTestBase is AssertionsCustomTypes, Test, DeployPermit2 {
eTST2 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST2), address(oracle), unitOfAccount))
);
+ eTST2.setHookConfig(address(0), 0);
eTST2.setInterestRateModel(address(new IRMTestDefault()));
eTST2.setMaxLiquidationDiscount(0.2e4);
eTST2.setFeeReceiver(feeReceiver);
@@ -152,6 +154,7 @@ contract EVaultTestBase is AssertionsCustomTypes, Test, DeployPermit2 {
IEVault v = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(asset), address(oracle), unitOfAccount))
);
+ v.setHookConfig(address(0), 0);
v.setInterestRateModel(address(new IRMTestDefault()));
v.setInterestFee(1e4);
diff --git a/test/unit/evault/modules/Governance/governorOnly.t.sol b/test/unit/evault/modules/Governance/governorOnly.t.sol
index 6f85d49e..553d5264 100644
--- a/test/unit/evault/modules/Governance/governorOnly.t.sol
+++ b/test/unit/evault/modules/Governance/governorOnly.t.sol
@@ -30,7 +30,6 @@ contract GovernanceTest_GovernorOnly is EVaultTestBase {
function test_GovernorAdmin() public {
eTST.setFeeReceiver(address(0));
eTST.setLTV(address(0), 0, 0, 0);
- eTST.clearLTV(address(0));
eTST.setMaxLiquidationDiscount(0);
eTST.setLiquidationCoolOffTime(0);
eTST.setInterestRateModel(address(0));
@@ -46,7 +45,6 @@ contract GovernanceTest_GovernorOnly is EVaultTestBase {
evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setFeeReceiver, address(0)));
evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setLTV, (address(0), 0, 0, 0)));
- evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.clearLTV, address(0)));
evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setMaxLiquidationDiscount, 0));
evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setLiquidationCoolOffTime, 0));
evc.call(address(eTST), address(this), 0, abi.encodeCall(eTST.setInterestRateModel, address(0)));
@@ -68,8 +66,6 @@ contract GovernanceTest_GovernorOnly is EVaultTestBase {
vm.expectRevert(Errors.E_Unauthorized.selector);
eTST.setLTV(address(0), 0, 0, 0);
vm.expectRevert(Errors.E_Unauthorized.selector);
- eTST.clearLTV(address(0));
- vm.expectRevert(Errors.E_Unauthorized.selector);
eTST.setMaxLiquidationDiscount(0);
vm.expectRevert(Errors.E_Unauthorized.selector);
eTST.setLiquidationCoolOffTime(0);
@@ -96,8 +92,6 @@ contract GovernanceTest_GovernorOnly is EVaultTestBase {
vm.expectRevert(Errors.E_Unauthorized.selector);
evc.call(address(eTST), subAccount, 0, abi.encodeCall(eTST.setLTV, (address(0), 0, 0, 0)));
vm.expectRevert(Errors.E_Unauthorized.selector);
- evc.call(address(eTST), subAccount, 0, abi.encodeCall(eTST.clearLTV, address(0)));
- vm.expectRevert(Errors.E_Unauthorized.selector);
evc.call(address(eTST), subAccount, 0, abi.encodeCall(eTST.setMaxLiquidationDiscount, 0));
vm.expectRevert(Errors.E_Unauthorized.selector);
evc.call(address(eTST), subAccount, 0, abi.encodeCall(eTST.setLiquidationCoolOffTime, 0));
diff --git a/test/unit/evault/modules/Governance/hookedOps.t.sol b/test/unit/evault/modules/Governance/hookedOps.t.sol
index d0eb481f..489faa0e 100644
--- a/test/unit/evault/modules/Governance/hookedOps.t.sol
+++ b/test/unit/evault/modules/Governance/hookedOps.t.sol
@@ -69,7 +69,7 @@ contract Governance_HookedOps is EVaultTestBase {
}
function getHookCalldata(bytes memory data, address sender) internal view returns (bytes memory) {
- data = abi.encodePacked(data, eTST.asset(), eTST.oracle(), eTST.unitOfAccount());
+ data = abi.encodePacked(data, bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount());
if (sender != address(0)) data = abi.encodePacked(data, sender);
diff --git a/test/unit/evault/modules/Governance/reserves.t.sol b/test/unit/evault/modules/Governance/reserves.t.sol
index 128afe07..4302a471 100644
--- a/test/unit/evault/modules/Governance/reserves.t.sol
+++ b/test/unit/evault/modules/Governance/reserves.t.sol
@@ -140,10 +140,10 @@ contract Governance_Reserves is EVaultTestBase {
}
function protocolShare(uint256 fees) internal pure returns (uint256) {
- return fees * 0.1e18 / 1e18;
+ return fees * 0.5e18 / 1e18;
}
function riskManagerShare(uint256 fees) internal pure returns (uint256) {
- return fees * (1e18 - 0.1e18) / 1e18;
+ return fees * (1e18 - 0.5e18) / 1e18;
}
}
diff --git a/test/unit/evault/modules/Governance/views.t.sol b/test/unit/evault/modules/Governance/views.t.sol
index a6150fbe..2e3af74e 100644
--- a/test/unit/evault/modules/Governance/views.t.sol
+++ b/test/unit/evault/modules/Governance/views.t.sol
@@ -6,12 +6,25 @@ import {EVaultTestBase} from "../../EVaultTestBase.t.sol";
contract Governance_views is EVaultTestBase {
function test_protocolFeeShare() public {
- assertEq(eTST.protocolFeeShare(), 0.1e4);
+ assertEq(eTST.feeReceiver(), feeReceiver);
+ assertEq(eTST.protocolFeeShare(), 0.5e4);
- startHoax(admin);
+ vm.prank(admin);
protocolConfig.setProtocolFeeShare(0.4e4);
-
assertEq(eTST.protocolFeeShare(), 0.4e4);
+
+ vm.prank(admin);
+ protocolConfig.setProtocolFeeShare(0.8e4);
+ assertEq(eTST.protocolFeeShare(), 0.5e4);
+
+ eTST.setFeeReceiver(address(0));
+ assertEq(eTST.feeReceiver(), address(0));
+ assertEq(eTST.protocolFeeShare(), 1e4);
+
+ vm.prank(admin);
+ protocolConfig.setProtocolFeeShare(0.4e4);
+ assertEq(eTST.feeReceiver(), address(0));
+ assertEq(eTST.protocolFeeShare(), 1e4);
}
function test_protocolFeeReceiver() public {
diff --git a/test/unit/evault/modules/Initialize/errors.t.sol b/test/unit/evault/modules/Initialize/errors.t.sol
index f1a807ed..18424e8e 100644
--- a/test/unit/evault/modules/Initialize/errors.t.sol
+++ b/test/unit/evault/modules/Initialize/errors.t.sol
@@ -29,7 +29,7 @@ contract InitializeTests is EVaultTestBase, MetaProxyDeployer {
}
function test_asset_is_a_contract() public {
- bytes memory trailingData = abi.encodePacked(address(0), address(1), address(2));
+ bytes memory trailingData = abi.encodePacked(bytes4(0), address(0), address(1), address(2));
address proxy = deployMetaProxy(address(new Initialize(integrations)), trailingData);
vm.expectRevert(Errors.E_BadAddress.selector);
diff --git a/test/unit/evault/modules/Liquidation/full.t.sol b/test/unit/evault/modules/Liquidation/full.t.sol
index 3ec6e028..6bfac052 100644
--- a/test/unit/evault/modules/Liquidation/full.t.sol
+++ b/test/unit/evault/modules/Liquidation/full.t.sol
@@ -11,6 +11,7 @@ import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector.
import {TestERC20} from "../../../../mocks/TestERC20.sol";
import {IRMTestFixed} from "../../../../mocks/IRMTestFixed.sol";
import {IRMTestZero} from "../../../../mocks/IRMTestZero.sol";
+import {IRMMax} from "../../../../mocks/IRMMax.sol";
import "forge-std/Test.sol";
@@ -50,16 +51,19 @@ contract VaultLiquidation_Test is EVaultTestBase {
eWETH = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetWETH), address(oracle), unitOfAccount))
);
+ eWETH.setHookConfig(address(0), 0);
eWETH.setInterestRateModel(address(new IRMTestZero()));
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
eTST3.setInterestRateModel(address(new IRMTestZero()));
eTST4 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST4), address(oracle), unitOfAccount))
);
+ eTST4.setHookConfig(address(0), 0);
eTST4.setInterestRateModel(address(new IRMTestZero()));
eTST.setLTV(address(eWETH), 0.3e4, 0.3e4, 0);
@@ -625,7 +629,7 @@ contract VaultLiquidation_Test is EVaultTestBase {
assertEq(eTST2.balanceOf(borrower), 0);
}
- function test_debtSocialization() public {
+ function test_debtSocialization_basic() public {
// set up liquidator to support the debt
startHoax(lender);
evc.enableController(lender, address(eTST));
@@ -701,6 +705,130 @@ contract VaultLiquidation_Test is EVaultTestBase {
assertEq(eTST.balanceOf(lender), maxYield);
}
+ function test_debtSocialization_minLiabilityValue() public {
+ // set up liquidator to support the debt
+ startHoax(lender);
+ evc.enableController(lender, address(eTST));
+ evc.enableCollateral(lender, address(eTST3));
+ evc.enableCollateral(lender, address(eTST2));
+
+ startHoax(address(this));
+ eTST.setLTV(address(eTST3), 0.95e4, 0.95e4, 0);
+ eTST.setLTV(address(eTST2), 0.99e4, 0.99e4, 0);
+
+ startHoax(borrower);
+ eTST2.redeem(type(uint256).max, borrower, borrower);
+ eTST2.deposit(2.7e6, borrower);
+
+ evc.enableController(borrower, address(eTST));
+ eTST.borrow(0.45e6, borrower);
+
+ startHoax(bystander);
+ evc.enableController(bystander, address(eTST));
+ eTST.borrow(1e6, bystander);
+
+ uint256 collateralValue;
+ uint256 liabilityValue;
+
+ vm.stopPrank();
+ eTST.setInterestRateModel(address(new IRMMax()));
+
+ // withdraw remaining deposit to bump utilization
+ startHoax(lender);
+ eTST.redeem(eTST.maxRedeem(lender), lender, lender);
+
+ uint256 snapshot = vm.snapshot();
+
+ (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true);
+
+ // just below min socialization liability value
+ assertEq(liabilityValue, 0.99e6);
+
+ uint256 prevTotalBorrows = eTST.totalBorrows();
+ assertEq(prevTotalBorrows, 1.45e6);
+
+ // collateral price falls
+ oracle.setPrice(address(assetTST2), unitOfAccount, 0.3e18);
+
+ (uint256 maxRepay, uint256 maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2));
+
+ // full collateral is liquidatable, bad debt will remain
+ assertEq(maxYield, 2.7e6);
+
+ address[] memory collaterals = evc.getCollaterals(borrower);
+ assertEq(collaterals.length, 1);
+
+ eTST.liquidate(borrower, address(eTST2), maxRepay, 0);
+
+ (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true);
+ assertEq(collateralValue, 0);
+ // non socialized bad debt remains
+ assertEq(liabilityValue, 0.3339e6);
+
+ // total borrows unchanged
+ assertEq(eTST.totalBorrows(), prevTotalBorrows);
+
+ // wait for remaining bad debt to increase above the socialization limit
+
+ skip(50 days);
+
+ (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true);
+
+ // value above max
+ assertGt(liabilityValue, 1e6);
+
+ uint256 borrowerPrevDebt = eTST.debtOf(borrower);
+ uint256 lenderPrevDebt = eTST.debtOf(lender);
+ prevTotalBorrows = eTST.totalBorrows();
+
+ (maxRepay, maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2));
+ // both repay and yield are zero, but socialization can happen
+ assertEq(maxRepay, 0);
+ assertEq(maxYield, 0);
+
+ eTST.liquidate(borrower, address(eTST2), type(uint256).max, 0);
+
+ // no new debt for liquidator
+ assertEq(eTST.debtOf(lender), lenderPrevDebt);
+ // bad debt is removed and socialized
+ assertEq(eTST.debtOf(borrower), 0);
+ assertEq(eTST.totalBorrows(), prevTotalBorrows - borrowerPrevDebt);
+
+ // if debt value was originally above the cap, socialization would happen in the first liquidation
+
+ vm.revertTo(snapshot);
+
+ skip(3 days);
+
+ (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true);
+
+ // just above min socialization liability value
+ assertEq(liabilityValue, 1.067803e6);
+
+ prevTotalBorrows = eTST.totalBorrows();
+ borrowerPrevDebt = eTST.debtOf(borrower);
+
+ // collateral price falls
+ oracle.setPrice(address(assetTST2), unitOfAccount, 0.3e18);
+
+ (maxRepay, maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2));
+
+ // full collateral is liquidatable, bad debt will remain
+ assertEq(maxYield, 2.7e6);
+
+ eTST.liquidate(borrower, address(eTST2), maxRepay, 0);
+
+ (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, true);
+ assertEq(collateralValue, 0);
+ // debt is socialized
+ assertEq(liabilityValue, 0);
+ assertEq(eTST.debtOf(borrower), 0);
+
+ // total borrows lowered
+ assertLt(eTST.totalBorrows(), prevTotalBorrows);
+ assertApproxEqAbs(eTST.totalBorrows(), prevTotalBorrows - (borrowerPrevDebt - maxRepay), 1);
+ }
+
function test_zeroCollateralWorth() public {
// set up liquidator to support the debt
startHoax(lender);
@@ -820,6 +948,57 @@ contract VaultLiquidation_Test is EVaultTestBase {
assertEq(eTST.totalBorrows(), 0);
}
+ function test_zeroLiabilityWorth() public {
+ // set up liquidator to support the debt
+ startHoax(lender);
+ evc.enableController(lender, address(eTST));
+ evc.enableCollateral(lender, address(eTST3));
+ evc.enableCollateral(lender, address(eTST2));
+
+ startHoax(borrower);
+ evc.enableCollateral(borrower, address(eTST3));
+ evc.enableController(borrower, address(eTST));
+ eTST.borrow(5e18, borrower);
+
+ startHoax(address(this));
+ eTST.setLTV(address(eTST3), 0.95e4, 0.95e4, 0);
+
+ // liability is worthless now (could be a result of rounding down in real scenario)
+ oracle.setPrice(address(assetTST), unitOfAccount, 0);
+
+ (uint256 collateralValue, uint256 liabilityValue) = eTST.accountLiquidity(borrower, false);
+ assertEq(liabilityValue, 0);
+ assertGt(collateralValue, 0);
+
+ (uint256 maxRepay, uint256 maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2));
+ assertEq(maxRepay, 0);
+ assertEq(maxYield, 0);
+
+ // now collateral is worthless
+ oracle.setPrice(address(assetTST2), unitOfAccount, 0);
+
+ (collateralValue, liabilityValue) = eTST.accountLiquidity(borrower, false);
+ // both values zero now
+ assertEq(liabilityValue, 0);
+ assertEq(collateralValue, 0);
+
+ (maxRepay, maxYield) = eTST.checkLiquidation(lender, borrower, address(eTST2));
+ assertEq(maxRepay, 0);
+ assertEq(maxYield, 0);
+
+ uint256 debtBefore = eTST.debtOf(borrower);
+ uint256 balanceBefore = eTST2.balanceOf(borrower);
+
+ // liquidation is a no-op
+ startHoax(lender);
+ vm.expectEmit();
+ emit Events.Liquidate(lender, borrower, address(eTST2), 0, 0);
+ eTST.liquidate(borrower, address(eTST2), type(uint256).max, 0);
+
+ assertEq(eTST.debtOf(borrower), debtBefore);
+ assertEq(eTST2.balanceOf(borrower), balanceBefore);
+ }
+
function test_zeroLTVCollateral() public {
// set up liquidator to support the debt
startHoax(lender);
@@ -917,16 +1096,15 @@ contract VaultLiquidation_Test is EVaultTestBase {
eTST.liquidate(borrower, address(eTST2), 0, 0);
- //violator
- assertEq(eTST.debtOf(borrower), 0);
+ // violator
assertEq(eTST2.balanceOf(borrower), 0);
+ // debt is not socialized because it's under MIN_SOCIALIZATION_LIABILITY_VALUE
+ assertEq(eTST.debtOf(borrower), 2);
+ assertEq(eTST.totalBorrows(), 2);
// liquidator:
assertEq(eTST.debtOf(lender), 0);
assertEq(eTST2.balanceOf(lender), 45);
-
- // total borrows
- assertEq(eTST.totalBorrows(), 0);
}
// yield value converted to balance rounds down to 0. equivalent to pullDebt
@@ -1395,6 +1573,7 @@ contract VaultLiquidation_Test is EVaultTestBase {
address(0), true, abi.encodePacked(address(assetTST), address(oracle), address(assetTST))
)
);
+ eTSTx.setHookConfig(address(0), 0);
eTSTx.setLTV(address(eTST2), 0.95e4, 0.95e4, 0);
eTSTx.setMaxLiquidationDiscount(0.2e4);
@@ -1655,6 +1834,11 @@ contract VaultLiquidation_Test is EVaultTestBase {
vm.expectRevert(Errors.E_ConfigAmountTooLargeToEncode.selector);
eTST.setMaxLiquidationDiscount(1e4 + 1);
+ // bad
+ startHoax(address(this));
+ vm.expectRevert(Errors.E_BadMaxLiquidationDiscount.selector);
+ eTST.setMaxLiquidationDiscount(1e4);
+
// set ok
eTST.setMaxLiquidationDiscount(0.111e4);
assertEq(0.111e4, eTST.maxLiquidationDiscount());
@@ -1989,6 +2173,43 @@ contract VaultLiquidation_Test is EVaultTestBase {
eTST.liquidate(borrower, address(eTST2), maxRepay, 0);
}
+ function test_liquidate_zeroCollateral() public {
+ startHoax(lender);
+ evc.enableController(lender, address(eTST));
+ evc.enableCollateral(lender, address(eTST3));
+ evc.enableCollateral(lender, address(eTST2));
+
+ startHoax(borrower);
+ evc.enableCollateral(borrower, address(eTST3));
+ evc.enableController(borrower, address(eTST));
+ eTST.borrow(5e18, borrower);
+
+ assertEq(eTST3.cash(), 100e18);
+ assertEq(eTST3.balanceOf(borrower), 0);
+
+ assertEq(eTST.debtOf(borrower), 5e18);
+ assertEq(eTST.debtOf(lender), 0);
+
+ startHoax(address(this));
+ eTST.setLTV(address(eTST3), 0.95e4, 0.95e4, 0);
+ oracle.setPrice(address(assetTST), unitOfAccount, 2.5e18);
+
+ (uint256 collateralValue, uint256 liabilityValue) = eTST.accountLiquidity(borrower, false);
+ uint256 healthScore = collateralValue * 1e18 / liabilityValue;
+ assertApproxEqAbs(healthScore, 0.96e18, 0.001e18);
+
+ startHoax(lender);
+ eTST.liquidate(borrower, address(eTST3), 0, 0);
+
+ eTST.liquidate(borrower, address(eTST3), type(uint256).max, 0);
+
+ vm.expectRevert(Errors.E_ExcessiveRepayAmount.selector);
+ eTST.liquidate(borrower, address(eTST3), 1, 0);
+
+ assertEq(eTST.debtOf(borrower), 5e18);
+ assertEq(eTST.debtOf(lender), 0);
+ }
+
function getRiskAdjustedValue(uint256 amount, uint256 price, uint256 factor) public pure returns (uint256) {
return amount * price / 1e18 * factor / 1e18;
}
diff --git a/test/unit/evault/modules/Token/actions.t.sol b/test/unit/evault/modules/Token/actions.t.sol
index 48e1cfbd..c23e7007 100644
--- a/test/unit/evault/modules/Token/actions.t.sol
+++ b/test/unit/evault/modules/Token/actions.t.sol
@@ -305,9 +305,10 @@ contract ERC20Test_Actions is EVaultTestBase {
}
function test_Approve_RevertsWhen_SelfApproval(uint256 allowance) public {
- vm.expectRevert(Errors.E_SelfApproval.selector);
vm.prank(alice);
eTST.approve(alice, allowance);
+ // no-op
+ assertEq(eTST.allowance(alice, alice), 0);
}
function test_Approve_RevertsWhen_EVCOnBehalfOfAccountNotAuthenticated(uint256 allowance) public {
@@ -323,8 +324,7 @@ contract ERC20Test_Actions is EVaultTestBase {
assertEq(eTST.allowance(alice, alice), 0);
startHoax(alice);
- // revert on self-approve of eVault
- vm.expectRevert(Errors.E_SelfApproval.selector);
+ // no-op
eTST.approve(alice, 10);
assertEq(eTST.allowance(alice, alice), 0);
@@ -337,8 +337,7 @@ contract ERC20Test_Actions is EVaultTestBase {
assertEq(eTST.allowance(alice, alice), 0);
startHoax(alice);
- // revert on self-approve of eVault
- vm.expectRevert(Errors.E_SelfApproval.selector);
+ // no-op
eTST.approve(alice, 0);
assertEq(eTST.allowance(alice, alice), 0);
@@ -351,8 +350,7 @@ contract ERC20Test_Actions is EVaultTestBase {
assertEq(eTST.allowance(alice, alice), 0);
startHoax(alice);
- // revert on self-approve of eVault
- vm.expectRevert(Errors.E_SelfApproval.selector);
+ // no-op
eTST.approve(alice, type(uint256).max);
assertEq(eTST.allowance(alice, alice), 0);
diff --git a/test/unit/evault/modules/Vault/amountLimits.t.sol b/test/unit/evault/modules/Vault/amountLimits.t.sol
index 6a49f00f..cbbddf6a 100644
--- a/test/unit/evault/modules/Vault/amountLimits.t.sol
+++ b/test/unit/evault/modules/Vault/amountLimits.t.sol
@@ -25,6 +25,7 @@ contract VaultTest_AmountLimits is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
assetTST.mint(user1, type(uint256).max / 2);
startHoax(user1);
diff --git a/test/unit/evault/modules/Vault/balancesNoInterest.t.sol b/test/unit/evault/modules/Vault/balancesNoInterest.t.sol
index 0f26b06e..b149d379 100644
--- a/test/unit/evault/modules/Vault/balancesNoInterest.t.sol
+++ b/test/unit/evault/modules/Vault/balancesNoInterest.t.sol
@@ -63,8 +63,8 @@ contract VaultTest_BalancesNoInterest is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds balance"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0),
+ abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds balance")
)
);
eTST.deposit(1, user1);
diff --git a/test/unit/evault/modules/Vault/batch.t.sol b/test/unit/evault/modules/Vault/batch.t.sol
index 701075d9..ca766a0b 100644
--- a/test/unit/evault/modules/Vault/batch.t.sol
+++ b/test/unit/evault/modules/Vault/batch.t.sol
@@ -30,6 +30,7 @@ contract VaultTest_Batch is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
startHoax(address(this));
eTST.setInterestRateModel(address(new IRMTestZero()));
diff --git a/test/unit/evault/modules/Vault/borrow.t.sol b/test/unit/evault/modules/Vault/borrow.t.sol
index 04125b1c..d5093ae9 100644
--- a/test/unit/evault/modules/Vault/borrow.t.sol
+++ b/test/unit/evault/modules/Vault/borrow.t.sol
@@ -179,8 +179,8 @@ contract VaultTest_Borrow is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0),
+ abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance")
)
);
eTST.repay(type(uint256).max, borrower);
@@ -679,6 +679,50 @@ contract VaultTest_Borrow is EVaultTestBase {
assertEq(eTST.debtOf(borrower), 0);
}
+ function test_repayLogsTransferDebt() external {
+ eTST.setInterestRateModel(address(new IRMTestFixed()));
+
+ startHoax(borrower);
+
+ evc.enableController(borrower, address(eTST));
+ evc.enableCollateral(borrower, address(eTST2));
+ assetTST.approve(address(eTST), type(uint256).max);
+ assetTST.mint(borrower, 1000e18);
+
+ eTST.borrow(1, borrower);
+
+ assetTST2.transfer(borrower2, type(uint256).max / 2);
+
+ startHoax(borrower2);
+
+ assetTST2.approve(address(eTST2), type(uint256).max);
+ eTST2.deposit(10e18, borrower2);
+
+ evc.enableController(borrower2, address(eTST));
+ evc.enableCollateral(borrower2, address(eTST2));
+ assetTST.approve(address(eTST), type(uint256).max);
+ assetTST.mint(borrower2, 1000e18);
+
+ skip(10 days);
+
+ // a little interest accrued (0.3%)
+ assertEq(owedTo1e5(eTST.debtOfExact(borrower)), 1.00274e5);
+
+ // record interest in storage
+ startHoax(borrower);
+ eTST.borrow(1, borrower);
+
+ // now borrower in LogRepay would receive amount = 2, prevOwed = 3, owed = 0.
+ // Amount is adjusted to 3 and interest accrued is 0, so no event is emitted
+ startHoax(borrower2);
+ vm.recordLogs();
+ vm.expectEmit();
+ emit Events.Repay(borrower, 3);
+ eTST.pullDebt(2, borrower);
+
+ assertEq(vm.getRecordedLogs().length, 11); // InterestAccrued would be the 12th event
+ }
+
function test_borrowLogsTransferDebt() external {
eTST.setInterestRateModel(address(new IRMTestFixed()));
diff --git a/test/unit/evault/modules/Vault/borrowIsolation.t.sol b/test/unit/evault/modules/Vault/borrowIsolation.t.sol
index 28e9efa9..ca3ef168 100644
--- a/test/unit/evault/modules/Vault/borrowIsolation.t.sol
+++ b/test/unit/evault/modules/Vault/borrowIsolation.t.sol
@@ -29,6 +29,7 @@ contract VaultTest_BorrowIsolation is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
startHoax(address(this));
eTST.setInterestRateModel(address(new IRMTestZero()));
diff --git a/test/unit/evault/modules/Vault/caps.t.sol b/test/unit/evault/modules/Vault/caps.t.sol
index d24cfecf..6fd16be9 100644
--- a/test/unit/evault/modules/Vault/caps.t.sol
+++ b/test/unit/evault/modules/Vault/caps.t.sol
@@ -32,6 +32,7 @@ contract VaultTest_Caps is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
eTST.setLTV(address(eTST2), 0.3e4, 0.3e4, 0);
eTST.setLTV(address(eTST3), 1e4, 1e4, 0);
diff --git a/test/unit/evault/modules/Vault/conversion.t.sol b/test/unit/evault/modules/Vault/conversion.t.sol
index d7015113..66f369cf 100644
--- a/test/unit/evault/modules/Vault/conversion.t.sol
+++ b/test/unit/evault/modules/Vault/conversion.t.sol
@@ -46,6 +46,7 @@ contract VaultTest_Conversion is EVaultTestBase {
eTST0 = EVaultHarness(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount))
);
+ eTST0.setHookConfig(address(0), 0);
eTST0.setInterestRateModel(address(new IRMTestDefault()));
}
diff --git a/test/unit/evault/modules/Vault/decimals.t.sol b/test/unit/evault/modules/Vault/decimals.t.sol
index d71be668..ad52c0a2 100644
--- a/test/unit/evault/modules/Vault/decimals.t.sol
+++ b/test/unit/evault/modules/Vault/decimals.t.sol
@@ -33,11 +33,13 @@ contract VaultTest_Decimals is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
assetTST4 = new TestERC20("Test TST 4", "TST4", 0, false);
eTST4 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST4), address(oracle), unitOfAccount))
);
+ eTST4.setHookConfig(address(0), 0);
startHoax(address(this));
eTST.setInterestRateModel(address(new IRMTestZero()));
diff --git a/test/unit/evault/modules/Vault/deposit.t.sol b/test/unit/evault/modules/Vault/deposit.t.sol
index f4cdd67b..2d22c14d 100644
--- a/test/unit/evault/modules/Vault/deposit.t.sol
+++ b/test/unit/evault/modules/Vault/deposit.t.sol
@@ -11,6 +11,8 @@ import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector.
import "../../../../../src/EVault/shared/types/Types.sol";
+import "forge-std/Test.sol";
+
contract VaultTest_Deposit is EVaultTestBase {
using TypesLib for uint256;
@@ -32,11 +34,11 @@ contract VaultTest_Deposit is EVaultTestBase {
user = vm.addr(userPK);
user1 = makeAddr("user1");
- assetTST.mint(user1, type(uint256).max);
+ assetTST.mint(user1, type(uint256).max / 4);
hoax(user1);
assetTST.approve(address(eTST), type(uint256).max);
- assetTST.mint(user, type(uint256).max);
+ assetTST.mint(user, type(uint256).max / 4);
startHoax(user);
assetTST.approve(address(eTST), type(uint256).max);
}
@@ -243,8 +245,8 @@ contract VaultTest_Deposit is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0),
+ abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance")
)
);
eTST.deposit(amount, user);
@@ -275,8 +277,8 @@ contract VaultTest_Deposit is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0),
+ abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance")
)
);
eTST.deposit(amount, user);
@@ -339,8 +341,8 @@ contract VaultTest_Deposit is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"),
- abi.encodeWithSelector(InsufficientAllowance.selector, amount - 1)
+ abi.encodeWithSelector(InsufficientAllowance.selector, amount - 1),
+ abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance")
)
);
evc.batch(items);
@@ -352,8 +354,8 @@ contract VaultTest_Deposit is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 1)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 1),
+ abi.encodeWithSignature("Error(string)", "ERC20: transfer amount exceeds allowance")
)
);
eTST.deposit(amount, user);
@@ -368,4 +370,63 @@ contract VaultTest_Deposit is EVaultTestBase {
assertEq(eTST.totalSupply(), 2 * amount);
assertEq(eTST.totalAssets(), 2 * amount);
}
+
+ function test_deposit_stealthDonation() public {
+ startHoax(address(this));
+ oracle.setPrice(address(assetTST), unitOfAccount, 1e18);
+ oracle.setPrice(address(assetTST2), unitOfAccount, 1e18);
+ eTST.setLTV(address(eTST2), 0.9e4, 0.9e4, 0);
+
+ startHoax(user1);
+ assetTST.mint(user1, 10e18);
+ assetTST.approve(address(eTST), type(uint256).max);
+ assetTST2.mint(user1, 100e18);
+ assetTST2.approve(address(eTST2), type(uint256).max);
+ eTST2.deposit(10e18, user1);
+ evc.enableController(user1, address(eTST));
+ evc.enableCollateral(user1, address(eTST2));
+
+ // attacker deposits 1 wei from one account
+ startHoax(user);
+ eTST.deposit(1, user);
+
+ // borrows it from a second for 1 second
+ startHoax(user1);
+ eTST.borrow(1, user1);
+ skip(1);
+
+ // 1 wei of debt accrued
+ assertEq(eTST.debtOf(user1), 2);
+ eTST.repay(type(uint256).max, user1);
+ eTST.disableController();
+
+ // and exchange rate is pushed above 1
+ assertEq(1.000000999999e18, eTST.convertToAssets(1e18)); // ~= 1.000001
+
+ assertEq(eTST.totalAssets(), 2); // 1 deposited + 1 interest repaid
+ assertEq(eTST.cash(), 2);
+ assertEq(eTST.totalSupply(), 1); // 1 deposited initially
+
+ // in a loop deposit max assets to create 1 share but not enough for 2. The rounding remainder is a stealth
+ // donation
+ for (uint256 i; i < 1000; i++) {
+ eTST.deposit(2, user1);
+ // 2 were deposited, but they round down to 1 share
+ assertEq(eTST.balanceOf(user1), 1);
+ // which is redeemable for 1 asset, the rest is donated
+ assertEq(eTST.maxWithdraw(user1), 1);
+
+ // reset account
+ eTST.withdraw(1, user1, user1);
+ assertEq(eTST.balanceOf(user1), 0);
+ }
+
+ // after 1000 iterations, 1000 assets were stealth donated
+ assertEq(eTST.totalAssets(), 1002);
+ assertEq(eTST.cash(), 1002);
+ assertEq(eTST.totalSupply(), 1);
+
+ // it will take 1e6 loops to reach exchange rate of 2
+ assertEq(1.001000998999001e18, eTST.convertToAssets(1e18)); // ~= 1.001
+ }
}
diff --git a/test/unit/evault/modules/Vault/liquidity.t.sol b/test/unit/evault/modules/Vault/liquidity.t.sol
index c7347a74..321e2959 100644
--- a/test/unit/evault/modules/Vault/liquidity.t.sol
+++ b/test/unit/evault/modules/Vault/liquidity.t.sol
@@ -33,6 +33,7 @@ contract VaultTest_Liquidity is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
startHoax(address(this));
eTST.setInterestRateModel(address(new IRMTestZero()));
diff --git a/test/unit/evault/modules/Vault/ltv.t.sol b/test/unit/evault/modules/Vault/ltv.t.sol
index 1e2fdd5e..7e5bccd9 100644
--- a/test/unit/evault/modules/Vault/ltv.t.sol
+++ b/test/unit/evault/modules/Vault/ltv.t.sol
@@ -109,36 +109,6 @@ contract VaultTest_LTV is EVaultTestBase {
eTST.setLTV(address(eTST2), 1e4 + 1, 1e4 + 1, 0);
}
- function test_clearLtv() public {
- eTST.setLTV(address(eTST2), 0.5e4, 0.5e4, 0);
-
- startHoax(borrower);
- evc.enableCollateral(borrower, address(eTST2));
- evc.enableController(borrower, address(eTST));
- vm.stopPrank();
-
- // No borrow, liquidation is a no-op
- (uint256 maxRepay, uint256 maxYield) = eTST.checkLiquidation(depositor, borrower, address(eTST2));
- assertEq(maxRepay, 0);
- assertEq(maxYield, 0);
-
- // setting LTV to 0 doesn't change anything yet
- eTST.setLTV(address(eTST2), 0, 0, 0);
-
- (maxRepay, maxYield) = eTST.checkLiquidation(depositor, borrower, address(eTST2));
- assertEq(maxRepay, 0);
- assertEq(maxYield, 0);
-
- // collateral without LTV
- vm.expectRevert(Errors.E_BadCollateral.selector);
- eTST.checkLiquidation(depositor, borrower, address(eTST));
-
- // same error after clearing LTV
- eTST.clearLTV(address(eTST2));
- vm.expectRevert(Errors.E_BadCollateral.selector);
- eTST.checkLiquidation(depositor, borrower, address(eTST2));
- }
-
function test_ltvList() public {
assertEq(eTST.LTVList().length, 0);
diff --git a/test/unit/evault/modules/Vault/maliciousToken.t.sol b/test/unit/evault/modules/Vault/maliciousToken.t.sol
index d2d96dd7..c1de5f38 100644
--- a/test/unit/evault/modules/Vault/maliciousToken.t.sol
+++ b/test/unit/evault/modules/Vault/maliciousToken.t.sol
@@ -91,8 +91,8 @@ contract VaultTest_MaliciousToken is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("Error(string)", "revert behaviour"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0),
+ abi.encodeWithSignature("Error(string)", "revert behaviour")
)
);
eTST2.repay(1e18, user3);
@@ -103,8 +103,8 @@ contract VaultTest_MaliciousToken is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("Error(string)", "revert behaviour"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0),
+ abi.encodeWithSignature("Error(string)", "revert behaviour")
)
);
startHoax(user1);
@@ -120,8 +120,8 @@ contract VaultTest_MaliciousToken is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("E_Reentrancy()"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0),
+ abi.encodeWithSignature("E_Reentrancy()")
)
);
eTST.deposit(1e18, user1);
@@ -135,8 +135,8 @@ contract VaultTest_MaliciousToken is EVaultTestBase {
vm.expectRevert(
abi.encodeWithSelector(
SafeERC20Lib.E_TransferFromFailed.selector,
- abi.encodeWithSignature("E_Reentrancy()"),
- abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0)
+ abi.encodeWithSelector(IAllowanceTransfer.AllowanceExpired.selector, 0),
+ abi.encodeWithSignature("E_Reentrancy()")
)
);
eTST.deposit(1e18, user1);
diff --git a/test/unit/evault/modules/Vault/nested.t.sol b/test/unit/evault/modules/Vault/nested.t.sol
index a9405e08..c648fc72 100644
--- a/test/unit/evault/modules/Vault/nested.t.sol
+++ b/test/unit/evault/modules/Vault/nested.t.sol
@@ -29,6 +29,7 @@ contract VaultTest_Nested is EVaultTestBase {
eTSTNested = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(eTST), address(oracle), unitOfAccount))
);
+ eTSTNested.setHookConfig(address(0), 0);
eTSTNested.setInterestRateModel(address(new IRMTestDefault()));
depositor = makeAddr("depositor");
@@ -143,6 +144,7 @@ contract VaultTest_Nested is EVaultTestBase {
eTSTDoubleNested = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(eTSTNested), address(oracle), unitOfAccount))
);
+ eTSTDoubleNested.setHookConfig(address(0), 0);
eTSTDoubleNested.setInterestRateModel(address(new IRMTestDefault()));
eTSTDoubleNested.setLTV(address(eTST2), 0.9e4, 0.9e4, 0);
diff --git a/test/unit/evault/modules/Vault/pullDebt.t.sol b/test/unit/evault/modules/Vault/pullDebt.t.sol
index 4c3ae1a0..b5e27fa7 100644
--- a/test/unit/evault/modules/Vault/pullDebt.t.sol
+++ b/test/unit/evault/modules/Vault/pullDebt.t.sol
@@ -33,9 +33,11 @@ contract VaultTest_PullDebt is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
eTST4 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST4), address(oracle), unitOfAccount))
);
+ eTST4.setHookConfig(address(0), 0);
eTST.setInterestRateModel(address(new IRMTestZero()));
eTST4.setInterestRateModel(address(new IRMTestZero()));
diff --git a/test/unit/evault/modules/Vault/repayWithShares.sol b/test/unit/evault/modules/Vault/repayWithShares.sol
index 753a604e..17e7330b 100644
--- a/test/unit/evault/modules/Vault/repayWithShares.sol
+++ b/test/unit/evault/modules/Vault/repayWithShares.sol
@@ -30,6 +30,7 @@ contract VaultTest_RepayWithShares is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
startHoax(address(this));
eTST.setInterestRateModel(address(new IRMTestZero()));
diff --git a/test/unit/evault/modules/Vault/selfCollateral.t.sol b/test/unit/evault/modules/Vault/selfCollateral.t.sol
new file mode 100644
index 00000000..ebb777d0
--- /dev/null
+++ b/test/unit/evault/modules/Vault/selfCollateral.t.sol
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pragma solidity ^0.8.0;
+
+import {EVaultTestBase, IEVault, IRMTestDefault} from "../../EVaultTestBase.t.sol";
+
+import "../../../../../src/EVault/shared/types/Types.sol";
+import "../../../../../src/EVault/shared/Constants.sol";
+
+contract VaultTest_SelfCollateral is EVaultTestBase {
+ using TypesLib for uint256;
+
+ IEVault public eeTST;
+
+ function setUp() public override {
+ super.setUp();
+
+ eeTST = IEVault(
+ factory.createProxy(address(0), true, abi.encodePacked(address(eTST), address(oracle), unitOfAccount))
+ );
+ eeTST.setInterestRateModel(address(new IRMTestDefault()));
+
+ oracle.setPrice(address(assetTST), unitOfAccount, 1e18);
+ oracle.setPrice(address(eTST), unitOfAccount, 1e18);
+ oracle.setPrice(address(eeTST), unitOfAccount, 1e18);
+ }
+
+ function test_selfCollateralDisallowed() public {
+ vm.expectRevert(Errors.E_InvalidLTVAsset.selector);
+ eTST.setLTV(address(eTST), 0.9e4, 0.9e4, 0);
+
+ vm.expectRevert(Errors.E_Reentrancy.selector);
+ eTST.setLTV(address(eeTST), 0.9e4, 0.9e4, 0);
+ }
+}
diff --git a/test/unit/evault/modules/Vault/withdraw.t.sol b/test/unit/evault/modules/Vault/withdraw.t.sol
index 0cef846c..dc861fba 100644
--- a/test/unit/evault/modules/Vault/withdraw.t.sol
+++ b/test/unit/evault/modules/Vault/withdraw.t.sol
@@ -37,6 +37,7 @@ contract VaultTest_Withdraw is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
eTST.setInterestRateModel(address(new IRMTestZero()));
eTST2.setInterestRateModel(address(new IRMTestZero()));
diff --git a/test/unit/evault/shared/EVCClient.t.sol b/test/unit/evault/shared/EVCClient.t.sol
index e6af00c7..1968bfbd 100644
--- a/test/unit/evault/shared/EVCClient.t.sol
+++ b/test/unit/evault/shared/EVCClient.t.sol
@@ -56,6 +56,7 @@ contract EVCClientUnitTest is EVaultTestBase {
eTST3 = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount))
);
+ eTST3.setHookConfig(address(0), 0);
eTST3.setInterestRateModel(address(new IRMTestDefault()));
}
@@ -171,6 +172,7 @@ contract EVCClientUnitTest is EVaultTestBase {
IEVault v = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST2), address(oracle), unitOfAccount))
);
+ v.setHookConfig(address(0), 0);
v.setInterestRateModel(address(new IRMTestDefault()));
return address(v);
diff --git a/test/unit/evault/shared/Reentrancy.t.sol b/test/unit/evault/shared/Reentrancy.t.sol
index c6202332..8a8dbf5a 100644
--- a/test/unit/evault/shared/Reentrancy.t.sol
+++ b/test/unit/evault/shared/Reentrancy.t.sol
@@ -170,9 +170,6 @@ contract MockHookTarget is Test, IHookTarget {
uint32(bound(amount2, 0, type(uint32).max))
);
- vm.expectRevert(Errors.E_Reentrancy.selector);
- eTST.clearLTV(account1);
-
vm.expectRevert(Errors.E_Reentrancy.selector);
eTST.setInterestRateModel(account1);
@@ -212,6 +209,7 @@ contract ReentrancyTest is EVaultTestBase {
eTST = IEVault(
factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount))
);
+ eTST.setHookConfig(address(0), 0);
vm.assume(sender != address(0) && sender != address(eTST));
@@ -406,9 +404,6 @@ contract ReentrancyTest is EVaultTestBase {
uint32(bound(amount2, 0, type(uint32).max))
);
- vm.expectRevert(Errors.E_Reentrancy.selector);
- eTST.clearLTV(account1);
-
vm.expectRevert(Errors.E_Reentrancy.selector);
eTST.setInterestRateModel(account1);
diff --git a/test/unit/factory/GenericFactory.t.sol b/test/unit/factory/GenericFactory.t.sol
index 21d5a23d..aeb80a07 100644
--- a/test/unit/factory/GenericFactory.t.sol
+++ b/test/unit/factory/GenericFactory.t.sol
@@ -42,12 +42,26 @@ contract FactoryTest is Test {
function test_setImplementationSimple() public {
vm.prank(upgradeAdmin);
- factory.setImplementation(address(1));
- assertEq(factory.implementation(), address(1));
+ vm.expectRevert(GenericFactory.E_BadAddress.selector);
+ factory.setImplementation(address(0));
+
+ vm.prank(upgradeAdmin);
+ vm.expectRevert(GenericFactory.E_BadAddress.selector);
+ factory.setImplementation(address(1e6));
+
+ vm.etch(address(1e6), address(this).code);
+ vm.prank(upgradeAdmin);
+ factory.setImplementation(address(1e6));
+ assertEq(factory.implementation(), address(1e6));
+
+ vm.prank(upgradeAdmin);
+ vm.expectRevert(GenericFactory.E_BadAddress.selector);
+ factory.setImplementation(address(2e6));
+ vm.etch(address(2e6), address(this).code);
vm.prank(upgradeAdmin);
- factory.setImplementation(address(2));
- assertEq(factory.implementation(), address(2));
+ factory.setImplementation(address(2e6));
+ assertEq(factory.implementation(), address(2e6));
}
function test_activateVaultDefaultImplementation() public {
@@ -231,10 +245,10 @@ contract FactoryTest is Test {
function test_Event_SetEVaultImplementation() public {
vm.expectEmit(true, false, false, false);
- emit GenericFactory.SetImplementation(address(1));
+ emit GenericFactory.SetImplementation(address(this));
vm.prank(upgradeAdmin);
- factory.setImplementation(address(1));
+ factory.setImplementation(address(this));
}
function test_Event_SetUpgradeAdmin() public {
@@ -318,6 +332,7 @@ contract FactoryTest is Test {
// Create and install mock eVault impl
MockEVault mockEvaultImpl = new MockEVault(address(factory), address(1));
+ MockEVault mockEvaultImplOther = new MockEVault(address(factory), address(2));
vm.prank(upgradeAdmin);
factory.setImplementation(address(mockEvaultImpl));
@@ -335,7 +350,7 @@ contract FactoryTest is Test {
// Change eVault impl
vm.prank(upgradeAdmin);
- factory.setImplementation(address(1));
+ factory.setImplementation(address(mockEvaultImplOther));
config = factory.getProxyConfig(address(eVaultNonUpg));
assertNotEq(config.implementation, factory.implementation());
diff --git a/test/unit/pegStabilityModules/PSM.t.sol b/test/unit/pegStabilityModules/PSM.t.sol
index 6bf3fb4d..aea6ab8d 100644
--- a/test/unit/pegStabilityModules/PSM.t.sol
+++ b/test/unit/pegStabilityModules/PSM.t.sol
@@ -5,7 +5,7 @@ pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import {PegStabilityModule, EVCUtil} from "../../../src/Synths/PegStabilityModule.sol";
-import {ESynth, IEVC} from "../../../src/Synths/ESynth.sol";
+import {ESynth} from "../../../src/Synths/ESynth.sol";
import {TestERC20} from "../../mocks/TestERC20.sol";
import {EthereumVaultConnector} from "ethereum-vault-connector/EthereumVaultConnector.sol";
import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
@@ -22,7 +22,7 @@ contract PSMTest is Test {
PegStabilityModule public psm;
- IEVC public evc;
+ EthereumVaultConnector public evc;
address public owner = makeAddr("owner");
address public wallet1 = makeAddr("wallet1");
@@ -32,52 +32,73 @@ contract PSMTest is Test {
// Deploy EVC
evc = new EthereumVaultConnector();
- // Deploy underlying
- underlying = new TestERC20("TestUnderlying", "TUNDERLYING", 18, false);
-
// Deploy synth
vm.prank(owner);
- synth = new ESynth(evc, "TestSynth", "TSYNTH");
+ synth = new ESynth(address(evc), "TestSynth", "TSYNTH");
+
+ // Deploy underlying
+ underlying = new TestERC20("TestUnderlying", "TUNDERLYING", 18, false);
// Deploy PSM
vm.prank(owner);
psm = new PegStabilityModule(
address(evc), address(synth), address(underlying), TO_UNDERLYING_FEE, TO_SYNTH_FEE, CONVERSION_PRICE
);
+ }
+
+ function fuzzSetUp(
+ uint8 underlyingDecimals,
+ uint256 _toUnderlyingFeeBPS,
+ uint256 _toSynthFeeBPS,
+ uint256 _conversionPrice
+ ) internal {
+ // Redeploy underlying
+ underlying = new TestERC20("TestUnderlying", "TUNDERLYING", underlyingDecimals, false);
+
+ // Redeploy PSM
+ vm.prank(owner);
+ psm = new PegStabilityModule(
+ address(evc), address(synth), address(underlying), _toUnderlyingFeeBPS, _toSynthFeeBPS, _conversionPrice
+ );
// Give PSM and wallets some underlying
- underlying.mint(address(psm), 100e18);
- underlying.mint(wallet1, 100e18);
- underlying.mint(wallet2, 100e18);
+ uint128 amount = uint128(1e6 * 10 ** underlyingDecimals);
+ underlying.mint(address(psm), amount);
+ underlying.mint(wallet1, amount);
+ underlying.mint(wallet2, amount);
// Approve PSM to spend underlying
vm.prank(wallet1);
- underlying.approve(address(psm), 100e18);
+ underlying.approve(address(psm), type(uint256).max);
vm.prank(wallet2);
- underlying.approve(address(psm), 100e18);
+ underlying.approve(address(psm), type(uint256).max);
// Set PSM as minter
+ amount = 1e6 * 10 ** 18;
vm.prank(owner);
- synth.setCapacity(address(psm), 100e18);
+ synth.setCapacity(address(psm), amount);
// Mint some synth to wallets
vm.startPrank(owner);
- synth.setCapacity(owner, 200e18);
- synth.mint(wallet1, 100e18);
- synth.mint(wallet2, 100e18);
+ synth.setCapacity(owner, uint128(2 * amount));
+ synth.mint(wallet1, amount);
+ synth.mint(wallet2, amount);
vm.stopPrank();
// Set approvals for PSM
vm.prank(wallet1);
- synth.approve(address(psm), 100e18);
+ synth.approve(address(psm), type(uint256).max);
vm.prank(wallet2);
- synth.approve(address(psm), 100e18);
+ synth.approve(address(psm), type(uint256).max);
}
function testConstructor() public view {
+ assertEq(address(psm.EVC()), address(evc));
assertEq(address(psm.synth()), address(synth));
+ assertEq(address(psm.underlying()), address(underlying));
assertEq(psm.TO_UNDERLYING_FEE(), TO_UNDERLYING_FEE);
assertEq(psm.TO_SYNTH_FEE(), TO_SYNTH_FEE);
+ assertEq(psm.CONVERSION_PRICE(), CONVERSION_PRICE);
}
function testConstructorToUnderlyingFeeExceedsBPS() public {
@@ -115,9 +136,21 @@ contract PSMTest is Test {
);
}
- function testSwapToUnderlyingGivenIn() public {
- uint256 amountIn = 10e18;
- uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) / BPS_SCALE;
+ function testConstructorZeroConversionPrice() public {
+ vm.expectRevert(PegStabilityModule.E_ZeroConversionPrice.selector);
+ new PegStabilityModule(address(evc), address(synth), address(underlying), TO_UNDERLYING_FEE, TO_SYNTH_FEE, 0);
+ }
+
+ function testSwapToUnderlyingGivenIn(uint8 underlyingDecimals, uint256 fee, uint256 amountInNoDecimals) public {
+ underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
+ fee = bound(fee, 0, BPS_SCALE - 1);
+ amountInNoDecimals = bound(amountInNoDecimals, 1, 100);
+ fuzzSetUp(underlyingDecimals, fee, 0, 10 ** underlyingDecimals);
+
+ uint256 amountIn = amountInNoDecimals * 10 ** 18;
+ uint256 expectedAmountOut = amountInNoDecimals * 10 ** underlyingDecimals * (BPS_SCALE - fee) / BPS_SCALE;
+
+ assertEq(psm.quoteToUnderlyingGivenIn(amountIn), expectedAmountOut);
uint256 swapperSynthBalanceBefore = synth.balanceOf(wallet1);
uint256 receiverBalanceBefore = underlying.balanceOf(wallet2);
@@ -135,9 +168,17 @@ contract PSMTest is Test {
assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore - expectedAmountOut);
}
- function testSwapToUnderlyingGivenOut() public {
- uint256 amountOut = 10e18;
- uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE);
+ function testSwapToUnderlyingGivenOut(uint8 underlyingDecimals, uint256 fee, uint256 amountOutNoDecimals) public {
+ underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
+ fee = bound(fee, 0, BPS_SCALE - 1);
+ amountOutNoDecimals = bound(amountOutNoDecimals, 1, 100);
+ fuzzSetUp(underlyingDecimals, fee, 0, 10 ** underlyingDecimals);
+
+ uint256 amountOut = amountOutNoDecimals * 10 ** underlyingDecimals;
+ uint256 expectedAmountIn =
+ (amountOutNoDecimals * 10 ** 18 * BPS_SCALE + BPS_SCALE - fee - 1) / (BPS_SCALE - fee);
+
+ assertEq(psm.quoteToUnderlyingGivenOut(amountOut), expectedAmountIn);
uint256 swapperSynthBalanceBefore = synth.balanceOf(wallet1);
uint256 receiverBalanceBefore = underlying.balanceOf(wallet2);
@@ -155,9 +196,16 @@ contract PSMTest is Test {
assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore - amountOut);
}
- function testSwapToSynthGivenIn() public {
- uint256 amountIn = 10e18;
- uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_SYNTH_FEE) / BPS_SCALE;
+ function testSwapToSynthGivenIn(uint8 underlyingDecimals, uint256 fee, uint256 amountInNoDecimals) public {
+ underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
+ fee = bound(fee, 0, BPS_SCALE - 1);
+ amountInNoDecimals = bound(amountInNoDecimals, 1, 100);
+ fuzzSetUp(underlyingDecimals, 0, fee, 10 ** underlyingDecimals);
+
+ uint256 amountIn = amountInNoDecimals * 10 ** underlyingDecimals;
+ uint256 expectedAmountOut = amountInNoDecimals * 10 ** 18 * (BPS_SCALE - fee) / BPS_SCALE;
+
+ assertEq(psm.quoteToSynthGivenIn(amountIn), expectedAmountOut);
uint256 swapperUnderlyingBalanceBefore = underlying.balanceOf(wallet1);
uint256 receiverSynthBalanceBefore = synth.balanceOf(wallet2);
@@ -175,9 +223,17 @@ contract PSMTest is Test {
assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore + amountIn);
}
- function testSwapToSynthGivenOut() public {
- uint256 amountOut = 10e18;
- uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_SYNTH_FEE);
+ function testSwapToSynthGivenOut(uint8 underlyingDecimals, uint256 fee, uint256 amountOutNoDecimals) public {
+ underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
+ fee = bound(fee, 0, BPS_SCALE - 1);
+ amountOutNoDecimals = bound(amountOutNoDecimals, 1, 100);
+ fuzzSetUp(underlyingDecimals, 0, fee, 10 ** underlyingDecimals);
+
+ uint256 amountOut = amountOutNoDecimals * 10 ** 18;
+ uint256 expectedAmountIn =
+ (amountOutNoDecimals * 10 ** underlyingDecimals * BPS_SCALE + BPS_SCALE - fee - 1) / (BPS_SCALE - fee);
+
+ assertEq(psm.quoteToSynthGivenOut(amountOut), expectedAmountIn);
uint256 swapperUnderlyingBalanceBefore = underlying.balanceOf(wallet1);
uint256 receiverSynthBalanceBefore = synth.balanceOf(wallet2);
@@ -195,66 +251,35 @@ contract PSMTest is Test {
assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore + expectedAmountIn);
}
- // Test quotes
- function testQuoteToUnderlyingGivenIn() public view {
- uint256 amountIn = 10e18;
- uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) / BPS_SCALE;
-
- uint256 amountOut = psm.quoteToUnderlyingGivenIn(amountIn);
-
- assertEq(amountOut, expectedAmountOut);
- }
-
- function testQuoteToUnderlyingGivenOut() public view {
- uint256 amountOut = 10e18;
- uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE);
+ function testSanityPriceConversions(uint8 underlyingDecimals, uint256 amount, uint256 multiplier) public {
+ underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
+ amount = bound(amount, 1, 100);
+ multiplier = bound(multiplier, 1, 10000);
+ fuzzSetUp(underlyingDecimals, 0, 0, 10 ** underlyingDecimals * multiplier / 100);
- uint256 amountIn = psm.quoteToUnderlyingGivenOut(amountOut);
+ uint256 synthAmount = amount * 10 ** 18;
+ uint256 underlyingAmount = amount * 10 ** underlyingDecimals * multiplier / 100;
- assertEq(amountIn, expectedAmountIn);
+ assertEq(psm.quoteToSynthGivenIn(underlyingAmount), synthAmount);
+ assertEq(psm.quoteToSynthGivenOut(synthAmount), underlyingAmount);
+ assertEq(psm.quoteToUnderlyingGivenIn(synthAmount), underlyingAmount);
+ assertEq(psm.quoteToUnderlyingGivenOut(underlyingAmount), synthAmount);
}
- function testQuoteToSynthGivenIn() public view {
- uint256 amountIn = 10e18;
- uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_SYNTH_FEE) / BPS_SCALE;
-
- uint256 amountOut = psm.quoteToSynthGivenIn(amountIn);
-
- assertEq(amountOut, expectedAmountOut);
+ function testRoundingPriceConversionsEqualDecimals() public view {
+ assertEq(psm.quoteToSynthGivenIn(1), 0);
+ assertEq(psm.quoteToSynthGivenOut(1), 2);
+ assertEq(psm.quoteToUnderlyingGivenIn(1), 0);
+ assertEq(psm.quoteToUnderlyingGivenOut(1), 2);
}
- function testQuoteToSynthGivenOut() public view {
- uint256 amountOut = 10e18;
- uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_SYNTH_FEE);
-
- uint256 amountIn = psm.quoteToSynthGivenOut(amountOut);
-
- assertEq(amountIn, expectedAmountIn);
- }
-
- function testSanityPriceConversionToSynth() public {
- uint256 price = 0.25e18;
-
- uint256 synthAmount = 1e18;
- uint256 underlyingAmount = 0.25e18;
-
- PegStabilityModule psmNoFee =
- new PegStabilityModule(address(evc), address(synth), address(underlying), 0, 0, price);
-
- assertEq(psmNoFee.quoteToSynthGivenIn(underlyingAmount), synthAmount);
- assertEq(psmNoFee.quoteToSynthGivenOut(synthAmount), underlyingAmount);
- }
-
- function testSanityPriceConversionToUnderlying() public {
- uint256 price = 0.25e18;
-
- uint256 synthAmount = 1e18;
- uint256 underlyingAmount = 0.25e18;
-
- PegStabilityModule psmNoFee =
- new PegStabilityModule(address(evc), address(synth), address(underlying), 0, 0, price);
+ function testRoundingPriceConversionsDiffDecimals(uint8 underlyingDecimals) public {
+ underlyingDecimals = uint8(bound(underlyingDecimals, 6, 17));
+ fuzzSetUp(underlyingDecimals, 0, 0, 10 ** underlyingDecimals);
- assertEq(psmNoFee.quoteToUnderlyingGivenIn(synthAmount), underlyingAmount);
- assertEq(psmNoFee.quoteToUnderlyingGivenOut(underlyingAmount), synthAmount);
+ assertEq(psm.quoteToSynthGivenIn(1), 10 ** (18 - underlyingDecimals));
+ assertEq(psm.quoteToSynthGivenOut(1), 1);
+ assertEq(psm.quoteToUnderlyingGivenIn(1), 0);
+ assertEq(psm.quoteToUnderlyingGivenOut(1), 10 ** (18 - underlyingDecimals));
}
}