├── .env.example ├── package.json ├── test ├── invariants │ ├── utils │ │ ├── PropertiesConstants.sol │ │ ├── Actor.sol │ │ └── Pretty.sol │ ├── base │ │ ├── BaseHooks.t.sol │ │ ├── ProtocolAssertions.t.sol │ │ ├── BaseTest.t.sol │ │ ├── BaseStorage.t.sol │ │ └── BaseHandler.t.sol │ ├── Tester.t.sol │ ├── TesterMedusa.t.sol │ ├── invariants │ │ ├── VaultRegularBorrowableInvariants.t.sol │ │ ├── VaultBorrowableWETHInvariants.t.sol │ │ ├── BaseInvariants.t.sol │ │ ├── VaultSimpleBorrowableInvariants.t.sol │ │ └── VaultSimpleInvariants.t.sol │ ├── HandlerAggregator.t.sol │ ├── hooks │ │ ├── HookAggregator.t.sol │ │ ├── VaultRegularBorrowableBeforeAfterHooks.t.sol │ │ ├── VaultSimpleBeforeAfterHooks.t.sol │ │ └── VaultSimpleBorrowableBeforeAfterHooks.t.sol │ ├── handlers │ │ ├── simulators │ │ │ ├── IRMHandler.t.sol │ │ │ ├── PriceOracleHandler.t.sol │ │ │ └── DonationAttackHandler.t.sol │ │ ├── VaultBorrowableETHHandler.t.sol │ │ ├── VaultRegularBorrowableHandler.t.sol │ │ ├── ERC20Handler.t.sol │ │ ├── VaultSimpleBorrowableHandler.t.sol │ │ └── EVCHandler.t.sol │ ├── _config │ │ └── echidna_config.yaml │ ├── helpers │ │ ├── VaultBaseGetters.sol │ │ └── extended │ │ │ └── VaultsExtended.sol │ ├── Setup.t.sol │ └── Invariants.t.sol ├── vaults │ ├── open-zeppelin │ │ └── A16zERC4626PropertyTest.sol │ └── solmate │ │ ├── A16zERC4626PropertyTest.sol │ │ ├── VaultBorrowableWETH.t.sol │ │ └── VaultSimpleBorrowable.t.sol ├── mocks │ ├── IRMMock.sol │ └── PriceOracleMock.sol ├── misc │ ├── SimpleWithdrawOperator.t.sol │ ├── ConditionalGaslessTx.t.sol │ ├── GaslessTx.t.sol │ └── LightweightOrderOperator.t.sol └── utils │ └── EVCPermitSignerECDSA.sol ├── Makefile ├── .gitignore ├── .gitmodules ├── .github └── workflows │ └── test.yml ├── src ├── utils │ ├── TipsPiggyBank.sol │ ├── SimpleConditionsEnforcer.sol │ └── EVCClient.sol ├── view │ ├── Types.sol │ └── BorrowableVaultLensForEVC.sol ├── interfaces │ ├── IIRM.sol │ └── IPriceOracle.sol ├── operators │ ├── SimpleWithdrawOperator.sol │ └── LightweightOrderOperator.sol ├── ERC20 │ ├── ERC20CollateralWrapper.sol │ ├── ERC20CollateralWrapperCapped.sol │ └── ERC20Collateral.sol └── vaults │ ├── solmate │ └── VaultBorrowableWETH.sol │ ├── VaultBase.sol │ └── open-zeppelin │ └── VaultSimple.sol ├── foundry.toml ├── medusa.json └── script └── 01_Deployment.s.sol /.env.example: -------------------------------------------------------------------------------- 1 | ANVIL_RPC_URL="http://127.0.0.1:8545" 2 | RPC_URL= 3 | MNEMONIC= -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evc-playground", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/euler-xyz/evc-playground.git", 6 | "author": "Euler Labs", 7 | "license": "GPL-2.0-or-later", 8 | "scripts": {}, 9 | "dependencies": {}, 10 | "devDependencies": {} 11 | } 12 | -------------------------------------------------------------------------------- /test/invariants/utils/PropertiesConstants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | abstract contract PropertiesConstants { 5 | // Constant echidna addresses 6 | address constant USER1 = address(0x10000); 7 | address constant USER2 = address(0x20000); 8 | address constant USER3 = address(0x30000); 9 | uint256 constant INITIAL_BALANCE = 1000e18; 10 | } 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Echidna 2 | echidna: 3 | echidna test/invariants/Tester.t.sol --contract Tester --config ./test/invariants/_config/echidna_config.yaml --corpus-dir ./test/invariants/_corpus/echidna/default/_data/corpus 4 | 5 | echidna-assert: 6 | echidna test/invariants/Tester.t.sol --test-mode assertion --contract Tester --config ./test/invariants/_config/echidna_config.yaml --corpus-dir ./test/invariants/_corpus/echidna/default/_data/corpus 7 | 8 | # Medusa 9 | medusa: 10 | medusa fuzz -------------------------------------------------------------------------------- /test/invariants/base/BaseHooks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {ProtocolAssertions} from "../base/ProtocolAssertions.t.sol"; 6 | // Test Contracts 7 | import {BaseTest} from "../base/BaseTest.t.sol"; 8 | 9 | /// @title BaseHooks 10 | /// @notice Contains common logic for all handlers 11 | /// @dev inherits all suite assertions since per-action assertions are implemented in the handlers 12 | contract BaseHooks is ProtocolAssertions {} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE - VSCode 2 | .vscode/ 3 | 4 | # Logs 5 | logs/ 6 | *.log 7 | 8 | # Dependency directories 9 | node_modules/ 10 | 11 | # Optional npm cache directory 12 | .npm/ 13 | 14 | # Compiler files 15 | cache/ 16 | out/ 17 | crytic-export/ 18 | _corpus/ 19 | 20 | # Ignores development broadcast logs 21 | !/broadcast 22 | /broadcast/*/31337/ 23 | /broadcast/**/dry-run/ 24 | 25 | # Ignores broadcast logs 26 | broadcast/ 27 | 28 | # webstorm files 29 | .idea/ 30 | 31 | # Dotenv file 32 | *.env 33 | 34 | # System Files 35 | .DS_Store 36 | Thumbs.db 37 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solmate"] 5 | path = lib/solmate 6 | url = https://github.com/transmissions11/solmate 7 | [submodule "lib/erc4626-tests"] 8 | path = lib/erc4626-tests 9 | url = https://github.com/a16z/erc4626-tests 10 | [submodule "lib/openzeppelin-contracts"] 11 | path = lib/openzeppelin-contracts 12 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 13 | [submodule "lib/ethereum-vault-connector"] 14 | path = lib/ethereum-vault-connector 15 | url = https://github.com/euler-xyz/ethereum-vault-connector 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /src/utils/TipsPiggyBank.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "solmate/utils/SafeTransferLib.sol"; 5 | 6 | /// @title TipsPiggyBank 7 | /// @notice This contract is used for handling tips by having a static deposit address and letting anyone withdraw the 8 | /// tokens to a specified receiver. 9 | contract TipsPiggyBank { 10 | using SafeTransferLib for ERC20; 11 | 12 | /// @notice Withdraws the specified token to the receiver. 13 | /// @param token The ERC20 token to be withdrawn. 14 | /// @param receiver The address to receive the withdrawn tokens. 15 | function withdraw(ERC20 token, address receiver) external { 16 | token.safeTransfer(receiver, token.balanceOf(address(this))); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc = "0.8.24" 6 | evm_version = "cancun" 7 | remappings = [ 8 | "forge-std/=lib/forge-std/src/", 9 | "solmate/=lib/solmate/src/", 10 | "openzeppelin/=lib/openzeppelin-contracts/contracts/", 11 | "erc4626-tests/=lib/erc4626-tests/", 12 | "evc/=lib/ethereum-vault-connector/src/" 13 | ] 14 | 15 | [profile.default.fuzz] 16 | seed = "0xee1d0f7d9556539a9c0e26aed5e63557" 17 | 18 | [profile.default.fmt] 19 | line_length = 120 20 | tab_width = 4 21 | bracket_spacing = false 22 | int_types = "long" 23 | multiline_func_header = "params_first" 24 | quote_style = "double" 25 | number_underscore = "preserve" 26 | override_spacing = true 27 | wrap_comments = true 28 | 29 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 30 | -------------------------------------------------------------------------------- /test/vaults/open-zeppelin/A16zERC4626PropertyTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC4626Test} from "erc4626-tests/ERC4626.test.sol"; 5 | import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; 6 | import "evc/EthereumVaultConnector.sol"; 7 | import "../../../src/vaults/open-zeppelin/VaultSimple.sol"; 8 | 9 | // source: 10 | // https://github.com/a16z/erc4626-tests 11 | 12 | contract ERC4626StdTest is ERC4626Test { 13 | IEVC _evc_; 14 | 15 | function setUp() public override { 16 | _evc_ = new EthereumVaultConnector(); 17 | _underlying_ = address(new MockERC20("Mock ERC20", "MERC20", 18)); 18 | _vault_ = address(new VaultSimple(address(_evc_), IERC20(_underlying_), "Mock ERC4626", "MERC4626")); 19 | _delta_ = 0; 20 | _vaultMayBeEmpty = false; 21 | _unlimitedAmount = false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/invariants/Tester.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Test Contracts 5 | import {Invariants} from "./Invariants.t.sol"; 6 | import {Setup} from "./Setup.t.sol"; 7 | 8 | /// @title Tester 9 | /// @notice Entry point for invariant testing, inherits all contracts, invariants & handler 10 | /// @dev Mono contract that contains all the testing logic 11 | contract Tester is Invariants, Setup { 12 | constructor() payable { 13 | setUp(); 14 | } 15 | 16 | /// @dev Foundry compatibility faster setup debugging 17 | function setUp() internal { 18 | // Deploy protocol contracts and protocol actors 19 | _setUp(); 20 | 21 | // Deploy actors 22 | _setUpActors(); 23 | 24 | // Initialize handler contracts 25 | _setUpHandlers(); 26 | } 27 | 28 | /// @dev Needed in order for foundry to recognise the contract as a test, faster debugging 29 | //function testAux() public view {} 30 | } 31 | -------------------------------------------------------------------------------- /src/view/Types.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | struct EVCUserInfo { 5 | address account; 6 | bytes19 addressPrefix; 7 | address owner; 8 | address[] enabledControllers; 9 | address[] enabledCollaterals; 10 | } 11 | 12 | struct VaultUserInfo { 13 | address account; 14 | address vault; 15 | uint256 shares; 16 | uint256 assets; 17 | uint256 borrowed; 18 | uint256 liabilityValue; 19 | uint256 collateralValue; 20 | bool isController; 21 | bool isCollateral; 22 | } 23 | 24 | struct VaultInfo { 25 | address vault; 26 | string vaultName; 27 | string vaultSymbol; 28 | uint8 vaultDecimals; 29 | address asset; 30 | string assetName; 31 | string assetSymbol; 32 | uint8 assetDecimals; 33 | uint256 totalShares; 34 | uint256 totalAssets; 35 | uint256 totalBorrowed; 36 | uint256 interestRateSPY; 37 | uint256 interestRateAPY; 38 | address irm; 39 | address oracle; 40 | } 41 | -------------------------------------------------------------------------------- /test/mocks/IRMMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "../../src/interfaces/IIRM.sol"; 5 | 6 | contract IRMMock is IIRM { 7 | uint256 internal interestRate; 8 | 9 | function setInterestRate(uint256 _interestRate) external { 10 | interestRate = _interestRate; 11 | } 12 | 13 | function computeInterestRateInternal(address, uint256, uint256) internal view returns (uint256) { 14 | return uint256((1e27 * interestRate) / 100) / (86400 * 365); // not SECONDS_PER_YEAR to avoid 15 | } 16 | 17 | function computeInterestRate(address vault, uint256, uint256) external view override returns (uint256) { 18 | if (msg.sender != vault) revert E_IRMUpdateUnauthorized(); 19 | 20 | return computeInterestRateInternal(address(0), 0, 0); 21 | } 22 | 23 | function computeInterestRateView(address, uint256, uint256) external view override returns (uint256) { 24 | return computeInterestRateInternal(address(0), 0, 0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/interfaces/IIRM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | interface IIRM { 6 | error E_IRMUpdateUnauthorized(); 7 | 8 | /// @notice Updates the interest rate for a given vault, asset, and utilisation. 9 | /// @param vault The address of the vault. 10 | /// @param cash The amount of assets in the vault. 11 | /// @param borrows The amount of assets borrowed from the vault. 12 | /// @return The updated interest rate in SPY (Second Percentage Yield). 13 | function computeInterestRate(address vault, uint256 cash, uint256 borrows) external returns (uint256); 14 | 15 | /// @notice Computes the interest rate for a given vault, asset and utilisation. 16 | /// @param vault The address of the vault. 17 | /// @param cash The amount of assets in the vault. 18 | /// @param borrows The amount of assets borrowed from the vault. 19 | /// @return The computed interest rate in SPY (Second Percentage Yield). 20 | function computeInterestRateView(address vault, uint256 cash, uint256 borrows) external view returns (uint256); 21 | } 22 | -------------------------------------------------------------------------------- /test/invariants/TesterMedusa.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Test Contracts 5 | import {Invariants} from "./Invariants.t.sol"; 6 | import {Setup} from "./Setup.t.sol"; 7 | 8 | /// @title TesterMedusa 9 | /// @notice Entry point for invariant testing, inherits all contracts, invariants & handler 10 | /// @dev Mono contract that contains all the testing logic 11 | contract TesterMedusa is Invariants, Setup { 12 | constructor() payable { 13 | /// @dev since medusa does not support initial balances yet, we need to deal some tokens to the contract 14 | vm.deal(address(this), 1e26 ether); 15 | 16 | setUp(); 17 | } 18 | 19 | /// @dev Foundry compatibility faster setup debugging 20 | function setUp() internal { 21 | // Deploy protocol contracts and protocol actors 22 | _setUp(); 23 | 24 | // Deploy actors 25 | _setUpActors(); 26 | 27 | // Initialize handler contracts 28 | _setUpHandlers(); 29 | } 30 | 31 | /// @dev Needed in order for foundry to recognise the contract as a test, faster debugging 32 | function testAux() public {} 33 | } 34 | -------------------------------------------------------------------------------- /test/invariants/invariants/VaultRegularBorrowableInvariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Base Contracts 5 | import {VaultRegularBorrowable} from "test/invariants/Setup.t.sol"; 6 | import {Actor} from "../utils/Actor.sol"; 7 | import {HandlerAggregator} from "../HandlerAggregator.t.sol"; 8 | 9 | /// @title VaultBorrowableWETHInvariants 10 | /// @notice Implements Invariants for the protocol 11 | /// @notice Implements View functions assertions for the protocol, checked in assertion testing mode 12 | /// @dev Inherits HandlerAggregator for checking actions in assertion testing mode 13 | abstract contract VaultRegularBorrowableInvariants is HandlerAggregator { 14 | /*///////////////////////////////////////////////////////////////////////////////////////////// 15 | // INVARIANTS SPEC: Handwritten / pseudo-code invariants // 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | VaultRegularBorrowable 19 | Invariant A: liquidation can only succed if violator is unhealthy (Post condition) 20 | 21 | */ 22 | 23 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 24 | } 25 | -------------------------------------------------------------------------------- /src/interfaces/IPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | interface IPriceOracle { 6 | /// @notice Returns the name of the price oracle. 7 | function name() external view returns (string memory); 8 | 9 | /// @notice Returns the quote for a given amount of base asset in quote asset. 10 | /// @param amount The amount of base asset. 11 | /// @param base The address of the base asset. 12 | /// @param quote The address of the quote asset. 13 | /// @return out The quote amount in quote asset. 14 | function getQuote(uint256 amount, address base, address quote) external view returns (uint256 out); 15 | 16 | /// @notice Returns the bid and ask quotes for a given amount of base asset in quote asset. 17 | /// @param amount The amount of base asset. 18 | /// @param base The address of the base asset. 19 | /// @param quote The address of the quote asset. 20 | /// @return bidOut The bid quote amount in quote asset. 21 | /// @return askOut The ask quote amount in quote asset. 22 | function getQuotes( 23 | uint256 amount, 24 | address base, 25 | address quote 26 | ) external view returns (uint256 bidOut, uint256 askOut); 27 | 28 | error PO_BaseUnsupported(); 29 | error PO_QuoteUnsupported(); 30 | error PO_Overflow(); 31 | error PO_NoPath(); 32 | } 33 | -------------------------------------------------------------------------------- /test/invariants/HandlerAggregator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Handler Contracts 5 | import {VaultSimpleHandler} from "./handlers/VaultSimpleHandler.t.sol"; 6 | import {VaultSimpleBorrowableHandler} from "./handlers/VaultSimpleBorrowableHandler.t.sol"; 7 | import {VaultRegularBorrowableHandler} from "./handlers/VaultRegularBorrowableHandler.t.sol"; 8 | import {VaultBorrowableETHHandler} from "./handlers/VaultBorrowableETHHandler.t.sol"; 9 | import {EVCHandler} from "./handlers/EVCHandler.t.sol"; 10 | import {ERC20Handler} from "./handlers/ERC20Handler.t.sol"; 11 | // Simulators 12 | import {DonationAttackHandler} from "./handlers/simulators/DonationAttackHandler.t.sol"; 13 | import {IRMHandler} from "./handlers/simulators/IRMHandler.t.sol"; 14 | import {PriceOracleHandler} from "./handlers/simulators/PriceOracleHandler.t.sol"; 15 | 16 | /// @notice Helper contract to aggregate all handler contracts, inherited in BaseInvariants 17 | abstract contract HandlerAggregator is 18 | VaultSimpleHandler, 19 | VaultSimpleBorrowableHandler, 20 | VaultRegularBorrowableHandler, 21 | VaultBorrowableETHHandler, 22 | EVCHandler, 23 | ERC20Handler, 24 | DonationAttackHandler, 25 | IRMHandler, 26 | PriceOracleHandler 27 | { 28 | /// @notice Helper function in case any handler requires additional setup 29 | function _setUpHandlers() internal {} 30 | } 31 | -------------------------------------------------------------------------------- /test/invariants/base/ProtocolAssertions.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Base 5 | import {BaseTest} from "./BaseTest.t.sol"; 6 | import {StdAsserts} from "test/invariants/utils/StdAsserts.sol"; 7 | import {VaultRegularBorrowable} from "test/invariants/Setup.t.sol"; 8 | 9 | /// @title ProtocolAssertions 10 | /// @notice Helper contract for protocol specific assertions 11 | abstract contract ProtocolAssertions is StdAsserts, BaseTest { 12 | /// @notice returns true if an account is healthy (liability <= collateral) 13 | function isAccountHealthy(uint256 _liability, uint256 _collateral) internal pure returns (bool) { 14 | return _liability <= _collateral; 15 | } 16 | 17 | /// @notice Checks wheter the account is healthy 18 | function isAccountHealthy(address _vault, address _account) internal view returns (bool) { 19 | (uint256 liabilityValue, uint256 collateralValue) = 20 | VaultRegularBorrowable(_vault).getAccountLiabilityStatus(_account); 21 | return isAccountHealthy(liabilityValue, collateralValue); 22 | } 23 | 24 | /// @notice Checks wheter the account is healthy 25 | function assertAccountIsHealthy(address _vault, address _account) internal { 26 | (uint256 liabilityValue, uint256 collateralValue) = 27 | VaultRegularBorrowable(_vault).getAccountLiabilityStatus(_account); 28 | assertLe(liabilityValue, collateralValue, "Account is unhealthy"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/invariants/invariants/VaultBorrowableWETHInvariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Actor} from "../utils/Actor.sol"; 5 | import {HandlerAggregator} from "../HandlerAggregator.t.sol"; 6 | 7 | /// @title VaultBorrowableWETHInvariants 8 | /// @notice Implements Invariants for the protocol 9 | /// @notice Implements View functions assertions for the protocol, checked in assertion testing mode 10 | /// @dev Inherits HandlerAggregator for checking actions in assertion testing mode 11 | abstract contract VaultBorrowableWETHInvariants is HandlerAggregator { 12 | /////////////////////////////////////////////////////////////////////////////////////////////// 13 | // INVARIANTS SPEC: Handwritten / pseudo-code invariants // 14 | /////////////////////////////////////////////////////////////////////////////////////////////// 15 | 16 | /* 17 | 18 | E.g. of an invariant spec 19 | Area 1 20 | Invariant A: totalSupply = sum of all balances 21 | Invariant B: totalSupply = sum of all balances 22 | 23 | */ 24 | 25 | /* 26 | 27 | E.g. of an invariant 28 | 29 | function assert_invariant_Area1_A(address _poolOwner) internal view { 30 | uint256 totalSupply = pool.totalSupply(); 31 | uint256 sumBalances = 0; 32 | for (uint256 i = 0; i < pool.numAccounts(); i++) { 33 | sumBalances += pool.balances(pool.account(i)); 34 | } 35 | assert(totalSupply == sumBalances); 36 | } 37 | */ 38 | } 39 | -------------------------------------------------------------------------------- /test/invariants/hooks/HookAggregator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Hook Contracts 5 | import {VaultSimpleBeforeAfterHooks} from "./VaultSimpleBeforeAfterHooks.t.sol"; 6 | import {VaultSimpleBorrowableBeforeAfterHooks} from "./VaultSimpleBorrowableBeforeAfterHooks.t.sol"; 7 | import {VaultRegularBorrowableBeforeAfterHooks} from "./VaultRegularBorrowableBeforeAfterHooks.t.sol"; 8 | 9 | /// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler 10 | abstract contract HookAggregator is 11 | VaultSimpleBeforeAfterHooks, 12 | VaultSimpleBorrowableBeforeAfterHooks, 13 | VaultRegularBorrowableBeforeAfterHooks 14 | { 15 | /// @notice Modular hook selector, per vault type 16 | function _before(address _vault, VaultType _type) internal { 17 | if (_type >= VaultType.Simple) { 18 | _svBefore(_vault); 19 | } 20 | if (_type >= VaultType.SimpleBorrowable) { 21 | _svbBefore(_vault); 22 | } 23 | if (_type == VaultType.RegularBorrowable) { 24 | _rvbBefore(_vault); 25 | } 26 | } 27 | 28 | /// @notice Modular hook selector, per vault type 29 | function _after(address _vault, VaultType _type) internal { 30 | if (_type >= VaultType.Simple) { 31 | _svAfter(_vault); 32 | } 33 | if (_type >= VaultType.SimpleBorrowable) { 34 | _svbAfter(_vault); 35 | } 36 | if (_type == VaultType.RegularBorrowable) { 37 | _rvbAfter(_vault); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/vaults/solmate/A16zERC4626PropertyTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC4626Test} from "erc4626-tests/ERC4626.test.sol"; 5 | import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; 6 | import "evc/EthereumVaultConnector.sol"; 7 | import "../../../src/vaults/solmate/VaultSimple.sol"; 8 | 9 | // source: 10 | // https://github.com/a16z/erc4626-tests 11 | 12 | contract ERC4626StdTest is ERC4626Test { 13 | IEVC _evc_; 14 | 15 | function setUp() public override { 16 | _evc_ = new EthereumVaultConnector(); 17 | _underlying_ = address(new MockERC20("Mock ERC20", "MERC20", 18)); 18 | _vault_ = address(new VaultSimple(address(_evc_), MockERC20(_underlying_), "Mock ERC4626", "MERC4626")); 19 | _delta_ = 0; 20 | _vaultMayBeEmpty = false; 21 | _unlimitedAmount = false; 22 | } 23 | 24 | // NOTE: The following test is relaxed to consider only smaller values (of type uint120), 25 | // since maxWithdraw() fails with large values (due to overflow). 26 | 27 | function test_maxWithdraw(Init memory init) public override { 28 | init = clamp(init, type(uint120).max); 29 | super.test_maxWithdraw(init); 30 | } 31 | 32 | function clamp(Init memory init, uint256 max) internal pure returns (Init memory) { 33 | for (uint256 i = 0; i < N; i++) { 34 | init.share[i] = init.share[i] % max; 35 | init.asset[i] = init.asset[i] % max; 36 | } 37 | init.yield = init.yield % int256(max); 38 | return init; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/invariants/utils/Actor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Interfaces 5 | import {IERC20} from "forge-std/interfaces/IERC20.sol"; 6 | 7 | /// @title Actor 8 | /// @notice Proxy contract for invariant suite actors to avoid Tester calling contracts 9 | /// @dev This expands the flexibility of the invariant suite 10 | contract Actor { 11 | /// @notice list of tokens to approve 12 | address[] internal tokens; 13 | /// @notice list of callers to approve tokens to 14 | address[] internal callers; 15 | 16 | constructor(address[] memory _tokens, address[] memory _callers) payable { 17 | tokens = _tokens; 18 | callers = _callers; 19 | for (uint256 i = 0; i < tokens.length; i++) { 20 | IERC20(tokens[i]).approve(callers[i], type(uint256).max); 21 | } 22 | } 23 | 24 | /// @notice Helper function to proxy a call to a target contract, used to avoid Tester calling contracts 25 | function proxy(address _target, bytes memory _calldata) public returns (bool success, bytes memory returnData) { 26 | (success, returnData) = address(_target).call(_calldata); 27 | } 28 | 29 | /// @notice Helper function to proxy a call and value to a target contract, used to avoid Tester calling contracts 30 | function proxy( 31 | address _target, 32 | bytes memory _calldata, 33 | uint256 value 34 | ) public returns (bool success, bytes memory returnData) { 35 | (success, returnData) = address(_target).call{value: value}(_calldata); 36 | } 37 | 38 | receive() external payable {} 39 | } 40 | -------------------------------------------------------------------------------- /test/vaults/solmate/VaultBorrowableWETH.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import "evc/EthereumVaultConnector.sol"; 6 | import {IRMMock} from "../../mocks/IRMMock.sol"; 7 | import {PriceOracleMock} from "../../mocks/PriceOracleMock.sol"; 8 | import "../../../src/vaults/solmate/VaultBorrowableWETH.sol"; 9 | 10 | contract VaultBorrowableWETHTest is Test { 11 | IEVC evc; 12 | WETH weth; 13 | VaultBorrowableWETH vault; 14 | IRMMock irm; 15 | PriceOracleMock oracle; 16 | 17 | function setUp() public { 18 | evc = new EthereumVaultConnector(); 19 | weth = new WETH(); 20 | irm = new IRMMock(); 21 | oracle = new PriceOracleMock(); 22 | 23 | vault = new VaultBorrowableWETH( 24 | address(evc), ERC20(address(weth)), irm, oracle, ERC20(address(0)), "WETH VAULT", "VWETH" 25 | ); 26 | } 27 | 28 | function test_depositAndWithdraw(address alice, uint128 amount) public { 29 | vm.assume(alice != address(0) && alice != address(evc) && alice != address(vault)); 30 | vm.assume(amount > 0); 31 | 32 | vm.deal(alice, amount); 33 | vm.prank(alice); 34 | vault.depositETH{value: amount}(alice); 35 | assertEq(weth.balanceOf(address(vault)), amount); 36 | assertEq(weth.balanceOf(alice), 0); 37 | assertEq(vault.balanceOf(alice), amount); 38 | 39 | vm.prank(alice); 40 | vault.withdraw(amount, alice, alice); 41 | assertEq(weth.balanceOf(address(vault)), 0); 42 | assertEq(weth.balanceOf(alice), amount); 43 | assertEq(vault.balanceOf(alice), 0); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/invariants/handlers/simulators/IRMHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseHandler} from "../../base/BaseHandler.t.sol"; 5 | 6 | /// @title IRMHandler 7 | /// @notice Handler test contract for the IRM actions 8 | contract IRMHandler is BaseHandler { 9 | /////////////////////////////////////////////////////////////////////////////////////////////// 10 | // STATE VARIABLES // 11 | /////////////////////////////////////////////////////////////////////////////////////////////// 12 | 13 | /////////////////////////////////////////////////////////////////////////////////////////////// 14 | // GHOST VARAIBLES // 15 | /////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | /////////////////////////////////////////////////////////////////////////////////////////////// 18 | // ACTIONS // 19 | /////////////////////////////////////////////////////////////////////////////////////////////// 20 | 21 | /// @notice This function simulates changes in the interest rate model 22 | function setInterestRate(uint256 _interestRate) external { 23 | irm.setInterestRate(_interestRate); 24 | } 25 | 26 | /////////////////////////////////////////////////////////////////////////////////////////////// 27 | // HELPERS // 28 | /////////////////////////////////////////////////////////////////////////////////////////////// 29 | } 30 | -------------------------------------------------------------------------------- /test/invariants/invariants/BaseInvariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Actor} from "../utils/Actor.sol"; 5 | import {HandlerAggregator} from "../HandlerAggregator.t.sol"; 6 | 7 | // Contracts 8 | import {VaultSimple} from "test/invariants/Setup.t.sol"; 9 | 10 | /// @title BaseInvariants 11 | /// @notice Implements Invariants for the protocol 12 | /// @notice Implements View functions assertions for the protocol, checked in assertion testing mode 13 | /// @dev Inherits HandlerAggregator for checking actions in assertion testing mode 14 | abstract contract BaseInvariants is HandlerAggregator { 15 | /*///////////////////////////////////////////////////////////////////////////////////////////// 16 | // INVARIANTS SPEC: Handwritten / pseudo-code invariants // 17 | /////////////////////////////////////////////////////////////////////////////////////////////// 18 | 19 | BaseInvariants 20 | Invariant A: reentrancyLock == REENTRANCY_UNLOCKED 21 | Invariant B: snapshot == 0 22 | TODO: at most we can only have one liability between calls 23 | */ 24 | 25 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 26 | 27 | function assert_VaultBase_invariantA(address _vault) internal { 28 | assertEq( 29 | VaultSimple(_vault).getReentrancyLock(), 1, string.concat("VaultBase_invariantA: ", vaultNames[_vault]) 30 | ); 31 | } 32 | 33 | function assert_VaultBase_invariantB(address _vault) internal { 34 | assertEq( 35 | VaultSimple(_vault).getSnapshotLength(), 0, string.concat("VaultBase_invariantB: ", vaultNames[_vault]) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/invariants/handlers/VaultBorrowableETHHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Actor} from "../utils/Actor.sol"; 5 | import {BaseHandler} from "../base/BaseHandler.t.sol"; 6 | 7 | /// @title VaultBorrowableETHHandler 8 | /// @notice Handler test contract for the VaultBorrowableETH actions 9 | contract VaultBorrowableETHHandler is BaseHandler { 10 | /////////////////////////////////////////////////////////////////////////////////////////////// 11 | // STATE VARIABLES // 12 | /////////////////////////////////////////////////////////////////////////////////////////////// 13 | 14 | /* 15 | 16 | E.g. num of active pools 17 | uint256 public activePools; 18 | 19 | */ 20 | 21 | /////////////////////////////////////////////////////////////////////////////////////////////// 22 | // GHOST VARAIBLES // 23 | /////////////////////////////////////////////////////////////////////////////////////////////// 24 | 25 | /* 26 | 27 | E.g. sum of all balances 28 | uint256 public ghost_sumBalances; 29 | 30 | */ 31 | 32 | /////////////////////////////////////////////////////////////////////////////////////////////// 33 | // ACTIONS // 34 | /////////////////////////////////////////////////////////////////////////////////////////////// 35 | 36 | /////////////////////////////////////////////////////////////////////////////////////////////// 37 | // HELPERS // 38 | /////////////////////////////////////////////////////////////////////////////////////////////// 39 | } 40 | -------------------------------------------------------------------------------- /src/operators/SimpleWithdrawOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "solmate/utils/SafeTransferLib.sol"; 6 | import "evc/interfaces/IEthereumVaultConnector.sol"; 7 | import "../vaults/solmate/VaultSimpleBorrowable.sol"; 8 | 9 | /// @title SimpleWithdrawOperator 10 | /// @notice This contract allows anyone, in exchange for a tip, to pull liquidity out 11 | /// of a heavily utilised vault on behalf of someone else. Thanks to this operator, 12 | /// a user can delegate the monitoring of their vault to someone else and go on with their life. 13 | contract SimpleWithdrawOperator { 14 | using SafeTransferLib for ERC20; 15 | 16 | IEVC public immutable evc; 17 | 18 | constructor(IEVC _evc) { 19 | evc = _evc; 20 | } 21 | 22 | /// @notice Allows anyone to withdraw on behalf of a onBehalfOfAccount. 23 | /// @dev Assumes that the onBehalfOfAccount owner had authorized the operator to withdraw on their behalf. 24 | /// @param vault The address of the vault. 25 | /// @param onBehalfOfAccount The address of the account on behalf of which the operation is being executed. 26 | function withdrawOnBehalf(address vault, address onBehalfOfAccount) external { 27 | ERC20 asset = ERC4626(vault).asset(); 28 | uint256 assets = VaultSimpleBorrowable(vault).maxWithdraw(onBehalfOfAccount); 29 | 30 | if (assets == 0) return; 31 | 32 | // if there's anything to withdraw, withdraw it to this contract 33 | evc.call( 34 | vault, 35 | onBehalfOfAccount, 36 | 0, 37 | abi.encodeWithSelector(VaultSimple.withdraw.selector, assets, address(this), onBehalfOfAccount) 38 | ); 39 | 40 | // transfer 1% of the withdrawn assets as a tip to the msg.sender 41 | asset.safeTransfer(msg.sender, assets / 100); 42 | 43 | // transfer the rest to the owner of onBehalfOfAccount (must be owner in case it's a sub-account) 44 | asset.safeTransfer(evc.getAccountOwner(onBehalfOfAccount), ERC20(asset).balanceOf(address(this))); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/invariants/handlers/simulators/PriceOracleHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseHandler} from "../../base/BaseHandler.t.sol"; 5 | 6 | /// @title PriceOracleHandler 7 | /// @notice Handler test contract for the PriceOracle actions 8 | contract PriceOracleHandler is BaseHandler { 9 | /////////////////////////////////////////////////////////////////////////////////////////////// 10 | // STATE VARIABLES // 11 | /////////////////////////////////////////////////////////////////////////////////////////////// 12 | 13 | /////////////////////////////////////////////////////////////////////////////////////////////// 14 | // GHOST VARAIBLES // 15 | /////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | /////////////////////////////////////////////////////////////////////////////////////////////// 18 | // ACTIONS // 19 | /////////////////////////////////////////////////////////////////////////////////////////////// 20 | 21 | /// @notice This function simulates changes in the interest rate model 22 | function setPrice(uint256 i, uint256 price) external { 23 | address baseAsset = _getRandomBaseAsset(i); 24 | 25 | oracle.setPrice(baseAsset, address(referenceAsset), price); 26 | } 27 | 28 | /// @notice This function simulates changes in the interest rate model 29 | function setResolvedAsset(uint256 i) external { 30 | address vaultAddress = _getRandomSupportedVault(i, VaultType.RegularBorrowable); 31 | 32 | oracle.setResolvedAsset(vaultAddress); 33 | } 34 | 35 | /////////////////////////////////////////////////////////////////////////////////////////////// 36 | // HELPERS // 37 | /////////////////////////////////////////////////////////////////////////////////////////////// 38 | } 39 | -------------------------------------------------------------------------------- /test/invariants/handlers/simulators/DonationAttackHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; 6 | 7 | // Contracts 8 | import {Actor} from "../../utils/Actor.sol"; 9 | import {BaseHandler} from "../../base/BaseHandler.t.sol"; 10 | 11 | /// @title DonationAttackHandler 12 | /// @notice Handler test contract for the DonationAttack actions 13 | contract DonationAttackHandler is BaseHandler { 14 | /////////////////////////////////////////////////////////////////////////////////////////////// 15 | // STATE VARIABLES // 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | // GHOST VARAIBLES // 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | // ACTIONS // 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | 26 | /// @notice This function transfers any amount of assets to a contract in the system 27 | /// @dev Flashloan simulator 28 | function donate(uint256 amount, uint256 j) external { 29 | address vaultAddress = _getRandomSupportedVault(j, VaultType.Simple); 30 | 31 | MockERC20 _token = MockERC20(_getRandomBaseAsset(j)); 32 | 33 | _token.mint(address(this), amount); 34 | 35 | _token.transfer(vaultAddress, amount); 36 | } 37 | 38 | /////////////////////////////////////////////////////////////////////////////////////////////// 39 | // HELPERS // 40 | /////////////////////////////////////////////////////////////////////////////////////////////// 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/SimpleConditionsEnforcer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | /// @title SimpleConditionsEnforcer 6 | /// @dev This contract is used to enforce conditions based on block timestamp and block number. 7 | contract SimpleConditionsEnforcer { 8 | /// @dev Enumeration for comparison types. 9 | enum ComparisonType { 10 | EQ, // Equal to 11 | GT, // Greater than 12 | LT, // Less than 13 | GE, // Greater than or equal to 14 | LE // Less than or equal to 15 | 16 | } 17 | 18 | /// @dev Error to be thrown when a condition is not met. 19 | error ConditionNotMet(); 20 | 21 | /// @dev Compares the current block timestamp with a given timestamp. 22 | /// @param ct The type of comparison to be made. 23 | /// @param timestamp The timestamp to compare with. 24 | function currentBlockTimestamp(ComparisonType ct, uint256 timestamp) external view { 25 | compare(block.timestamp, ct, timestamp); 26 | } 27 | 28 | /// @dev Compares the current block number with a given number. 29 | /// @param ct The type of comparison to be made. 30 | /// @param number The number to compare with. 31 | function currentBlockNumber(ComparisonType ct, uint256 number) external view { 32 | compare(block.number, ct, number); 33 | } 34 | 35 | /// @dev Compares two uint values based on the comparison type. 36 | /// @param value1 The first value to compare. 37 | /// @param ct The type of comparison to be made. 38 | /// @param value2 The second value to compare. 39 | function compare(uint256 value1, ComparisonType ct, uint256 value2) internal pure { 40 | if (ct == ComparisonType.EQ) { 41 | if (value1 != value2) revert ConditionNotMet(); 42 | } else if (ct == ComparisonType.GT) { 43 | if (value1 <= value2) revert ConditionNotMet(); 44 | } else if (ct == ComparisonType.LT) { 45 | if (value1 >= value2) revert ConditionNotMet(); 46 | } else if (ct == ComparisonType.GE) { 47 | if (value1 < value2) revert ConditionNotMet(); 48 | } else if (ct == ComparisonType.LE) { 49 | if (value1 > value2) revert ConditionNotMet(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/invariants/_config/echidna_config.yaml: -------------------------------------------------------------------------------- 1 | #codeSize max code size for deployed contratcs (default 24576, per EIP-170) 2 | codeSize: 224576 3 | 4 | #whether ot not to use the multi-abi mode of testing 5 | #it’s not working for us, see: https://github.com/crytic/echidna/issues/547 6 | #multi-abi: true 7 | 8 | #balanceAddr is default balance for addresses 9 | balanceAddr: 0xffffffffffffffffffffffff 10 | #balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) 11 | balanceContract: 0xffffffffffffffffffffffffffffffffffffffffffffffff 12 | 13 | #testLimit is the number of test sequences to run 14 | testLimit: 10000000 15 | 16 | #seqLen defines how many transactions are in a test sequence 17 | seqLen: 300 18 | 19 | #shrinkLimit determines how much effort is spent shrinking failing sequences 20 | shrinkLimit: 2500 21 | 22 | #propMaxGas defines gas cost at which a property fails 23 | propMaxGas: 1000000000 24 | 25 | #testMaxGas is a gas limit; does not cause failure, but terminates sequence 26 | testMaxGas: 1000000000 27 | 28 | # list of methods to filter 29 | #filterFunctions: ["openCdpExt"] 30 | # by default, blacklist methods in filterFunctions 31 | #filterBlacklist: false 32 | 33 | #stopOnFail makes echidna terminate as soon as any property fails and has been shrunk 34 | stopOnFail: false 35 | 36 | #coverage controls coverage guided testing 37 | coverage: true 38 | 39 | # list of file formats to save coverage reports in; default is all possible formats 40 | coverageFormats: ["lcov", "html"] 41 | 42 | #directory to save the corpus; by default is disabled 43 | corpusDir: "test/invariants/_corpus/echidna/default/_data/corpus" 44 | # constants for corpus mutations (for experimentation only) 45 | #mutConsts: [100, 1, 1] 46 | 47 | #remappings 48 | cryticArgs: ["--solc-remaps", "@crytic/properties/=lib/properties/ forge-std/=lib/forge-std/src/ ds-test/=lib/forge-std/lib/ds-test/src/ evc/=lib/ethereum-vault-connector/src/ solmate/=lib/solmate/src/ openzeppelin/=lib/openzeppelin-contracts/contracts/", "--compile-libraries=(Pretty,0xf01),(Strings,0xf02)"] 49 | 50 | deployContracts: [["0xf01", "Pretty"], ["0xf02", "Strings"]] 51 | 52 | # maximum value to send to payable functions 53 | maxValue: 100000000000000000000000 # 100000 eth 54 | 55 | #quiet produces (much) less verbose output 56 | quiet: false 57 | 58 | # concurrent workers 59 | workers: 8 60 | -------------------------------------------------------------------------------- /medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 12, 4 | "workerResetLimit": 50, 5 | "timeout": 0, 6 | "testLimit": 0, 7 | "callSequenceLength": 100, 8 | "corpusDirectory": "test/invariants/_corpus/medusa", 9 | "coverageEnabled": true, 10 | "deploymentOrder": [ 11 | "TesterMedusa" 12 | ], 13 | "targetContracts": [ 14 | "TesterMedusa" 15 | ], 16 | "constructorArgs": {}, 17 | "deployerAddress": "0x30000", 18 | "senderAddresses": [ 19 | "0x10000", 20 | "0x20000", 21 | "0x30000" 22 | ], 23 | "blockNumberDelayMax": 60480, 24 | "blockTimestampDelayMax": 604800, 25 | "blockGasLimit": 12500000000, 26 | "transactionGasLimit": 1250000000, 27 | "testing": { 28 | "stopOnFailedTest": true, 29 | "stopOnFailedContractMatching": true, 30 | "stopOnNoTests": true, 31 | "testAllContracts": false, 32 | "traceAll": false, 33 | "assertionTesting": { 34 | "enabled": true, 35 | "testViewMethods": true, 36 | "assertionModes": { 37 | "failOnCompilerInsertedPanic": false, 38 | "failOnAssertion": true, 39 | "failOnArithmeticUnderflow": false, 40 | "failOnDivideByZero": false, 41 | "failOnEnumTypeConversionOutOfBounds": false, 42 | "failOnIncorrectStorageAccess": false, 43 | "failOnPopEmptyArray": false, 44 | "failOnOutOfBoundsArrayAccess": false, 45 | "failOnAllocateTooMuchMemory": false, 46 | "failOnCallUninitializedVariable": false 47 | } 48 | }, 49 | "propertyTesting": { 50 | "enabled": true, 51 | "testPrefixes": [ 52 | "fuzz_", 53 | "echidna_" 54 | ] 55 | }, 56 | "optimizationTesting": { 57 | "enabled": false, 58 | "testPrefixes": [ 59 | "optimize_" 60 | ] 61 | } 62 | }, 63 | "chainConfig": { 64 | "codeSizeCheckDisabled": true, 65 | "cheatCodes": { 66 | "cheatCodesEnabled": true, 67 | "enableFFI": false 68 | } 69 | } 70 | }, 71 | "compilation": { 72 | "platform": "crytic-compile", 73 | "platformConfig": { 74 | "target": "test/invariants/TesterMedusa.t.sol", 75 | "solcVersion": "", 76 | "exportDirectory": "", 77 | "args": [ 78 | "--solc-remaps", 79 | "@crytic/properties/=lib/properties/ forge-std/=lib/forge-std/src/ ds-test/=lib/forge-std/lib/ds-test/src/ evc/=lib/ethereum-vault-connector/src/ solmate/=lib/solmate/src/ openzeppelin/=lib/openzeppelin-contracts/contracts/", 80 | "--compile-libraries=(Pretty,0xf01),(Strings,0xf02)" 81 | ] 82 | } 83 | }, 84 | "logging": { 85 | "level": "info", 86 | "logDirectory": "" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/invariants/helpers/VaultBaseGetters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | 6 | /// @title VaultBaseGetters 7 | /// @dev This contract provides getters for the private variables in VaultBase, via storage access 8 | contract VaultBaseGetters { 9 | uint256 internal constant REENTRANCY_LOCK_SLOT = 0; 10 | uint256 internal constant SNAPSHOT_SLOT = 1; 11 | 12 | /// @notice Gets the reentrancy lock 13 | function getReentrancyLock() external view returns (uint256 lock) { 14 | uint256 slot = REENTRANCY_LOCK_SLOT; 15 | 16 | assembly { 17 | lock := sload(slot) 18 | } 19 | } 20 | 21 | /// @notice Gets the snapshot length, using assembly to read from storage 22 | function getSnapshotLength() external view returns (uint256 snapshotLength) { 23 | uint256 slot = SNAPSHOT_SLOT; 24 | 25 | assembly { 26 | snapshotLength := sload(slot) 27 | } 28 | } 29 | 30 | /// @notice Gets the snapshot, using assembly to read bytes from storage 31 | function getSnapshot() external view returns (bytes memory snapshot) { 32 | uint256 slot = SNAPSHOT_SLOT; 33 | 34 | // Declared outside of the assembly block for easier debugging 35 | uint256 length; 36 | uint256 bytesLength; 37 | uint256 slotContent; 38 | uint256 slotData; 39 | 40 | assembly { 41 | // Calculate slot where data starts 42 | mstore(0, slot) 43 | slotData := keccak256(0, 0x20) 44 | 45 | slotContent := sload(slot) 46 | 47 | if gt(slotContent, 0) { 48 | // Load the length of the bytes 49 | bytesLength := sub(slotContent, 0x21) 50 | 51 | // Calculate the number of 32-byte chunks 52 | length := div(add(bytesLength, 0x1f), 0x20) 53 | 54 | // Update the free memory pointer 55 | mstore(0x40, add(snapshot, add(mul(length, 0x20), 0x20))) 56 | 57 | // Store the length of the bytes in memory 58 | let pointer := snapshot 59 | mstore(pointer, bytesLength) 60 | 61 | for { let i := 0 } lt(i, length) { i := add(i, 1) } { 62 | // Calculate the next slot to read 63 | let dataSlot := add(slotData, i) 64 | // Read the data from the slot 65 | let data := sload(dataSlot) 66 | 67 | // Calculate the next memory pointer & store the data 68 | pointer := add(pointer, 0x20) 69 | mstore(pointer, data) 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/misc/SimpleWithdrawOperator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import "solmate/test/utils/mocks/MockERC20.sol"; 6 | import "evc/EthereumVaultConnector.sol"; 7 | import "../../src/vaults/solmate/VaultSimpleBorrowable.sol"; 8 | import "../../src/operators/SimpleWithdrawOperator.sol"; 9 | 10 | contract SimpleWithdrawOperatorTest is Test { 11 | IEVC evc; 12 | MockERC20 asset; 13 | VaultSimpleBorrowable vault; 14 | SimpleWithdrawOperator withdrawOperator; 15 | 16 | function setUp() public { 17 | evc = new EthereumVaultConnector(); 18 | asset = new MockERC20("Asset", "ASS", 18); 19 | vault = new VaultSimpleBorrowable(address(evc), asset, "Vault", "VAU"); 20 | withdrawOperator = new SimpleWithdrawOperator(evc); 21 | } 22 | 23 | function test_SimpleWithdrawOperator(address alice, address bot) public { 24 | vm.assume( 25 | !evc.haveCommonOwner(alice, address(0)) && alice != address(evc) && bot != address(evc) 26 | && !evc.haveCommonOwner(alice, address(withdrawOperator)) && bot != address(vault) && bot != alice 27 | ); 28 | address alicesSubAccount = address(uint160(alice) ^ 1); 29 | 30 | asset.mint(alice, 100e18); 31 | 32 | // alice deposits into her main account and a subaccount 33 | vm.startPrank(alice); 34 | asset.approve(address(vault), type(uint256).max); 35 | vault.deposit(50e18, alice); 36 | vault.deposit(50e18, alicesSubAccount); 37 | 38 | // for simplicity, let's ignore the fact that nobody borrows from a vault 39 | 40 | // alice authorizes the operator to act on behalf of her subaccount 41 | evc.setAccountOperator(alicesSubAccount, address(withdrawOperator), true); 42 | vm.stopPrank(); 43 | 44 | assertEq(asset.balanceOf(address(alice)), 0); 45 | assertEq(vault.maxWithdraw(alice), 50e18); 46 | assertEq(vault.maxWithdraw(alicesSubAccount), 50e18); 47 | 48 | // assume that a keeper bot is monitoring the chain. when alice authorizes the operator, 49 | // the bot can call withdrawOnBehalf() function, withdraw on behalf of alice and get tipped 50 | vm.prank(bot); 51 | withdrawOperator.withdrawOnBehalf(address(vault), alicesSubAccount); 52 | 53 | assertEq(asset.balanceOf(alice), 49.5e18); 54 | assertEq(asset.balanceOf(bot), 0.5e18); 55 | assertEq(vault.maxWithdraw(alice), 50e18); 56 | assertEq(vault.maxWithdraw(alicesSubAccount), 0); 57 | 58 | // however, the bot cannot call withdrawOnBehalf() on behalf of alice's main account 59 | // because she didn't authorize the operator 60 | vm.prank(bot); 61 | vm.expectRevert(abi.encodeWithSelector(Errors.EVC_NotAuthorized.selector)); 62 | withdrawOperator.withdrawOnBehalf(address(vault), alice); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ERC20/ERC20CollateralWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; 6 | import "./ERC20Collateral.sol"; 7 | 8 | /// @title ERC20CollateralWrapper 9 | /// @notice It extends the ERC20Collateral contract so that any ERC20 token can be wrapped and used as collateral 10 | /// in the EVC ecosystem. 11 | contract ERC20CollateralWrapper is ERC20Collateral { 12 | error ERC20CollateralWrapper_InvalidAddress(); 13 | 14 | IERC20 private immutable _underlying; 15 | uint8 private immutable _decimals; 16 | 17 | constructor( 18 | address _evc_, 19 | IERC20 _underlying_, 20 | string memory _name_, 21 | string memory _symbol_ 22 | ) ERC20Collateral(_evc_, _name_, _symbol_) { 23 | if (address(_underlying_) == address(this)) { 24 | revert ERC20CollateralWrapper_InvalidAddress(); 25 | } 26 | 27 | _underlying = _underlying_; 28 | _decimals = IERC20Metadata(address(_underlying_)).decimals(); 29 | } 30 | 31 | /// @notice Returns the address of the underlying ERC20 token. 32 | /// @return The address of the underlying token. 33 | function underlying() external view returns (address) { 34 | return address(_underlying); 35 | } 36 | 37 | /// @notice Returns the number of decimals of the wrapper token. 38 | /// @dev The number of decimals of the wrapper token is the same as the number of decimals of the underlying token. 39 | /// @return The number of decimals of the wrapper token. 40 | function decimals() public view virtual override returns (uint8) { 41 | return _decimals; 42 | } 43 | 44 | /// @notice Wraps the specified amount of the underlying token into this ERC20 token. 45 | /// @param amount The amount of the underlying token to wrap. 46 | /// @param receiver The address to receive the wrapped tokens. 47 | /// @return True if the operation was successful. 48 | function wrap(uint256 amount, address receiver) public virtual nonReentrant returns (bool) { 49 | if (receiver == address(this)) { 50 | revert ERC20CollateralWrapper_InvalidAddress(); 51 | } 52 | 53 | SafeERC20.safeTransferFrom(IERC20(_underlying), _msgSender(), address(this), amount); 54 | _mint(receiver, amount); 55 | 56 | return true; 57 | } 58 | 59 | /// @notice Unwraps the specified amount of this ERC20 token back into the underlying token. 60 | /// @param amount The amount of this ERC20 token to unwrap. 61 | /// @param receiver The address to receive the underlying tokens. 62 | /// @return True if the operation was successful. 63 | function unwrap(uint256 amount, address receiver) public virtual nonReentrant returns (bool) { 64 | if (receiver == address(this)) { 65 | revert ERC20CollateralWrapper_InvalidAddress(); 66 | } 67 | 68 | address sender = _msgSender(); 69 | _burn(sender, amount); 70 | SafeERC20.safeTransfer(IERC20(_underlying), receiver, amount); 71 | evc.requireAccountStatusCheck(sender); 72 | 73 | return true; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/utils/EVCPermitSignerECDSA.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import "openzeppelin/utils/cryptography/MessageHashUtils.sol"; 6 | import "openzeppelin/utils/ShortStrings.sol"; 7 | import "evc/EthereumVaultConnector.sol"; 8 | 9 | // This contract is used only for testing purposes. 10 | // It's a utility contract that helps to sign permit message for the evc contract using ECDSA. 11 | abstract contract EIP712 { 12 | using ShortStrings for *; 13 | 14 | bytes32 internal constant _TYPE_HASH = 15 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 16 | 17 | bytes32 internal constant PERMIT_TYPEHASH = keccak256( 18 | "Permit(address signer,address sender,uint256 nonceNamespace,uint256 nonce,uint256 deadline,uint256 value,bytes data)" 19 | ); 20 | 21 | bytes32 internal immutable _hashedName; 22 | bytes32 internal immutable _hashedVersion; 23 | ShortString private immutable _name; 24 | ShortString private immutable _version; 25 | string private _nameFallback; 26 | string private _versionFallback; 27 | 28 | constructor(string memory name, string memory version) { 29 | _name = name.toShortStringWithFallback(_nameFallback); 30 | _version = version.toShortStringWithFallback(_versionFallback); 31 | _hashedName = keccak256(bytes(name)); 32 | _hashedVersion = keccak256(bytes(version)); 33 | } 34 | 35 | function _domainSeparatorV4() internal view returns (bytes32) { 36 | return _buildDomainSeparator(); 37 | } 38 | 39 | function _buildDomainSeparator() internal view virtual returns (bytes32); 40 | 41 | function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { 42 | return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); 43 | } 44 | } 45 | 46 | contract EVCPermitSignerECDSA is EIP712, Test { 47 | EthereumVaultConnector private immutable evc; 48 | uint256 internal privateKey; 49 | 50 | constructor(address _evc) 51 | EIP712(EthereumVaultConnector(payable(_evc)).name(), EthereumVaultConnector(payable(_evc)).version()) 52 | { 53 | evc = EthereumVaultConnector(payable(_evc)); 54 | } 55 | 56 | function setPrivateKey(uint256 _privateKey) external { 57 | privateKey = _privateKey; 58 | } 59 | 60 | function _buildDomainSeparator() internal view override returns (bytes32) { 61 | return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(evc))); 62 | } 63 | 64 | function signPermit( 65 | address signer, 66 | address sender, 67 | uint256 nonceNamespace, 68 | uint256 nonce, 69 | uint256 deadline, 70 | uint256 value, 71 | bytes calldata data 72 | ) external view returns (bytes memory signature) { 73 | bytes32 structHash = keccak256( 74 | abi.encode(PERMIT_TYPEHASH, signer, sender, nonceNamespace, nonce, deadline, value, keccak256(data)) 75 | ); 76 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _hashTypedDataV4(structHash)); 77 | signature = abi.encodePacked(r, s, v); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/invariants/hooks/VaultRegularBorrowableBeforeAfterHooks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | 6 | // Test Helpers 7 | import {Pretty, Strings} from "../utils/Pretty.sol"; 8 | 9 | // Contracts 10 | import {VaultRegularBorrowable} from "test/invariants/Setup.t.sol"; 11 | 12 | // Test Contracts 13 | import {BaseHooks} from "../base/BaseHooks.t.sol"; 14 | 15 | /// @title VaultRegularBorrowable Before After Hooks 16 | /// @notice Helper contract for before and after hooks 17 | /// @dev This contract is inherited by handlers 18 | abstract contract VaultRegularBorrowableBeforeAfterHooks is BaseHooks { 19 | using Strings for string; 20 | using Pretty for uint256; 21 | using Pretty for int256; 22 | using Pretty for bool; 23 | 24 | struct VaultRegularBorrowableBeforeAfterHooksVars { 25 | // VaultRegularBorrowable 26 | uint256 interestAccumulatorBefore; 27 | uint256 interestAccumulatorAfter; 28 | uint256 liabilityValueBefore; 29 | uint256 liabilityValueAfter; 30 | uint256 collateralValueBefore; 31 | uint256 collateralValueAfter; 32 | } 33 | 34 | VaultRegularBorrowableBeforeAfterHooksVars rvbVars; 35 | 36 | function _rvbBefore(address _vault) internal { 37 | VaultRegularBorrowable rvb = VaultRegularBorrowable(_vault); 38 | rvbVars.interestAccumulatorBefore = rvb.getInterestAccumulator(); 39 | (rvbVars.liabilityValueBefore, rvbVars.collateralValueBefore) = rvb.getAccountLiabilityStatus(address(actor)); 40 | } 41 | 42 | function _rvbAfter(address _vault) internal { 43 | VaultRegularBorrowable rvb = VaultRegularBorrowable(_vault); 44 | rvbVars.interestAccumulatorAfter = rvb.getInterestAccumulator(); 45 | (rvbVars.liabilityValueAfter, rvbVars.collateralValueAfter) = rvb.getAccountLiabilityStatus(address(actor)); 46 | 47 | // VaultSimple Post Conditions 48 | assert_rvbPostConditionA(); 49 | assert_rvbPostConditionB(); 50 | } 51 | 52 | /*///////////////////////////////////////////////////////////////////////////////////////////// 53 | // POST CONDITIONS // 54 | /////////////////////////////////////////////////////////////////////////////////////////////// 55 | 56 | VaultRegularBorrowable 57 | Post Condition A: Interest rate monotonically increases 58 | Post Condition B: A healthy account cant never be left unhealthy after a transaction 59 | 60 | */ 61 | 62 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 63 | 64 | function assert_rvbPostConditionA() internal { 65 | assertGe( 66 | rvbVars.interestAccumulatorAfter, 67 | rvbVars.interestAccumulatorBefore, 68 | "Interest rate must monotonically increase" 69 | ); 70 | } 71 | 72 | function assert_rvbPostConditionB() internal { 73 | if (isAccountHealthy(rvbVars.liabilityValueBefore, rvbVars.collateralValueBefore)) { 74 | assertTrue( 75 | isAccountHealthy(rvbVars.liabilityValueAfter, rvbVars.collateralValueAfter), 76 | "Account cannot be left unhealthy" 77 | ); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/vaults/solmate/VaultBorrowableWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "solmate/tokens/WETH.sol"; 6 | import "./VaultRegularBorrowable.sol"; 7 | 8 | /// @title VaultBorrowableWETH 9 | /// @notice This contract extends VaultRegularBorrowable with additional feature for handling ETH deposits (and 10 | /// redemption) into a WETH vault. 11 | contract VaultBorrowableWETH is VaultRegularBorrowable { 12 | WETH internal immutable weth; 13 | 14 | constructor( 15 | address _evc, 16 | ERC20 _asset, 17 | IIRM _irm, 18 | IPriceOracle _oracle, 19 | ERC20 _referenceAsset, 20 | string memory _name, 21 | string memory _symbol 22 | ) VaultRegularBorrowable(_evc, _asset, _irm, _oracle, _referenceAsset, _name, _symbol) { 23 | weth = WETH(payable(address(_asset))); 24 | } 25 | 26 | receive() external payable virtual {} 27 | 28 | /// @dev Deposits a certain amount of ETH for a receiver. 29 | /// @param receiver The receiver of the deposit. 30 | /// @return shares The shares equivalent to the deposited assets. 31 | function depositETH(address receiver) public payable virtual callThroughEVC nonReentrant returns (uint256 shares) { 32 | address msgSender = _msgSender(); 33 | 34 | createVaultSnapshot(); 35 | 36 | // Check for rounding error since we round down in previewDeposit. 37 | require((shares = _convertToShares(msg.value, false)) != 0, "ZERO_SHARES"); 38 | 39 | // Wrap received ETH into WETH. 40 | weth.deposit{value: msg.value}(); 41 | 42 | _totalAssets += msg.value; 43 | 44 | _mint(receiver, shares); 45 | 46 | emit Deposit(msgSender, receiver, msg.value, shares); 47 | 48 | requireAccountAndVaultStatusCheck(address(0)); 49 | } 50 | 51 | /// @notice Redeems a certain amount of shares for a receiver and sends the equivalent assets as ETH. 52 | /// @param shares The shares to redeem. 53 | /// @param receiver The receiver of the redemption. 54 | /// @param owner The owner of the shares. 55 | /// @return assets The assets equivalent to the redeemed shares. 56 | function redeemToETH( 57 | uint256 shares, 58 | address receiver, 59 | address owner 60 | ) public virtual callThroughEVC nonReentrant returns (uint256 assets) { 61 | address msgSender = _msgSender(); 62 | 63 | createVaultSnapshot(); 64 | 65 | if (msgSender != owner) { 66 | uint256 allowed = allowance[owner][msgSender]; // Saves gas for limited approvals. 67 | 68 | if (allowed != type(uint256).max) { 69 | allowance[owner][msgSender] = allowed - shares; 70 | } 71 | } 72 | 73 | // Check for rounding error since we round down in previewRedeem. 74 | require((assets = _convertToAssets(shares, false)) != 0, "ZERO_ASSETS"); 75 | 76 | _burn(owner, shares); 77 | 78 | emit Withdraw(msgSender, receiver, owner, assets, shares); 79 | 80 | // Convert WETH to ETH and send to the receiver. 81 | weth.withdraw(assets); 82 | 83 | (bool sent,) = receiver.call{value: assets}(""); 84 | require(sent, "Failed to send Ether"); 85 | 86 | _totalAssets -= assets; 87 | 88 | requireAccountAndVaultStatusCheck(owner); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/invariants/hooks/VaultSimpleBeforeAfterHooks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | 6 | // Test Helpers 7 | import {Pretty, Strings} from "../utils/Pretty.sol"; 8 | 9 | // Contracts 10 | import {VaultSimple} from "test/invariants/Setup.t.sol"; 11 | 12 | // Test Contracts 13 | import {BaseHooks} from "../base/BaseHooks.t.sol"; 14 | 15 | /// @title VaultSimple Before After Hooks 16 | /// @notice Helper contract for before and after hooks 17 | /// @dev This contract is inherited by handlers 18 | abstract contract VaultSimpleBeforeAfterHooks is BaseHooks { 19 | using Strings for string; 20 | using Pretty for uint256; 21 | using Pretty for int256; 22 | using Pretty for bool; 23 | 24 | struct VaultSimpleVars { 25 | // ERC4626 26 | uint256 totalSupplyBefore; 27 | uint256 totalSupplyAfter; 28 | // VaultBase 29 | uint256 reentrancyLockBefore; 30 | uint256 reentrancyLockAfter; 31 | uint256 snapshotLengthBefore; 32 | uint256 snapshotLengthAfter; 33 | // VaultSimple 34 | uint256 totalAssetsBefore; 35 | uint256 totalAssetsAfter; 36 | uint256 supplyCapBefore; 37 | uint256 supplyCapAfter; 38 | } 39 | 40 | VaultSimpleVars svVars; 41 | 42 | function _svBefore(address _vault) internal { 43 | // ERC4626 44 | VaultSimple sv = VaultSimple(_vault); 45 | svVars.totalSupplyBefore = sv.totalSupply(); 46 | // VaultBase 47 | svVars.reentrancyLockBefore = sv.getReentrancyLock(); 48 | svVars.snapshotLengthBefore = sv.getSnapshotLength(); 49 | // VaultSimple 50 | svVars.totalAssetsBefore = sv.totalAssets(); 51 | svVars.supplyCapBefore = sv.supplyCap(); 52 | } 53 | 54 | function _svAfter(address _vault) internal { 55 | // ERC4626 56 | VaultSimple sv = VaultSimple(_vault); 57 | svVars.totalSupplyAfter = sv.totalSupply(); 58 | // VaultBase 59 | svVars.reentrancyLockAfter = sv.getReentrancyLock(); 60 | svVars.snapshotLengthAfter = sv.getSnapshotLength(); 61 | // VaultSimple 62 | svVars.totalAssetsAfter = sv.totalAssets(); 63 | svVars.supplyCapAfter = sv.supplyCap(); 64 | 65 | // VaultSimple Post Conditions 66 | assert_VaultSimple_PcA(); 67 | } 68 | 69 | /*///////////////////////////////////////////////////////////////////////////////////////////// 70 | // POST CONDITIONS // 71 | /////////////////////////////////////////////////////////////////////////////////////////////// 72 | 73 | VaultSimple 74 | Post Condition A: 75 | (supplyCapAfter != 0) && (totalSupplyAfter >= totalSupplyBefore) => supplyCapAfter >= totalSupplyAfter 76 | 77 | */ 78 | 79 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 80 | 81 | function assert_VaultSimple_PcA() internal { 82 | assertTrue( 83 | (svVars.totalSupplyAfter > svVars.totalSupplyBefore && svVars.supplyCapAfter != 0) 84 | ? (svVars.supplyCapAfter >= svVars.totalSupplyAfter) 85 | : true, 86 | "(totalSupplyAfter > totalSupplyBefore)" 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/ERC20/ERC20CollateralWrapperCapped.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "./ERC20CollateralWrapper.sol"; 6 | 7 | /// @title ERC20WrapperForEVCCapped 8 | /// @notice It extends the ERC20CollateralWrapper contract by adding a supply cap to the wrapped token. 9 | contract ERC20CollateralWrapperCapped is ERC20CollateralWrapper { 10 | error ERC20CollateralWrapperCapped_SupplyCapExceeded(); 11 | 12 | uint256 private immutable _supplyCap; 13 | bytes private _totalSupplySnapshot; 14 | 15 | constructor( 16 | address _evc_, 17 | IERC20 _underlying_, 18 | string memory _name_, 19 | string memory _symbol_, 20 | uint256 _supplyCap_ 21 | ) ERC20CollateralWrapper(_evc_, _underlying_, _name_, _symbol_) { 22 | _supplyCap = _supplyCap_; 23 | } 24 | 25 | /// @notice Ensures operations do not exceed the supply cap by taking a snapshot of the total supply and scheduling 26 | /// a vault status check if needed. 27 | modifier requireSupplyCapCheck() { 28 | if (_supplyCap > 0 && _totalSupplySnapshot.length == 0) { 29 | _totalSupplySnapshot = abi.encode(totalSupply()); 30 | } 31 | 32 | _; 33 | 34 | if (_supplyCap > 0) { 35 | evc.requireVaultStatusCheck(); 36 | } 37 | } 38 | 39 | /// @notice Returns the supply cap for the wrapped token. 40 | /// @return The supply cap as a uint256. 41 | function getSupplyCap() external view returns (uint256) { 42 | return _supplyCap; 43 | } 44 | 45 | /// @notice Checks the vault status and ensures the final supply does not exceed the initial supply or supply cap. 46 | /// @dev Reverts with `ERC20WrapperForEVCCapped_SupplyCapExceeded` if the final supply exceeds the initial supply 47 | /// and the supply cap. 48 | /// @return The function selector for `checkVaultStatus` which is considered a magic value. 49 | function checkVaultStatus() external virtual onlyEVCWithChecksInProgress returns (bytes4) { 50 | uint256 initialSupply = abi.decode(_totalSupplySnapshot, (uint256)); 51 | uint256 finalSupply = totalSupply(); 52 | 53 | if (finalSupply > _supplyCap && finalSupply > initialSupply) { 54 | revert ERC20CollateralWrapperCapped_SupplyCapExceeded(); 55 | } 56 | 57 | delete _totalSupplySnapshot; 58 | return this.checkVaultStatus.selector; 59 | } 60 | 61 | /// @notice Wraps the specified amount of the underlying token into this ERC20 token. 62 | /// @param amount The amount of the underlying token to wrap. 63 | /// @param receiver The address to receive the wrapped tokens. 64 | /// @return True if the operation was successful. 65 | function wrap(uint256 amount, address receiver) public virtual override requireSupplyCapCheck returns (bool) { 66 | return super.wrap(amount, receiver); 67 | } 68 | 69 | /// @notice Unwraps the specified amount of this ERC20 token back into the underlying token. 70 | /// @param amount The amount of this ERC20 token to unwrap. 71 | /// @param receiver The address to receive the underlying tokens. 72 | /// @return True if the operation was successful. 73 | function unwrap(uint256 amount, address receiver) public virtual override requireSupplyCapCheck returns (bool) { 74 | return super.unwrap(amount, receiver); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/invariants/base/BaseTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {Vm} from "forge-std/Base.sol"; 6 | import {StdUtils} from "forge-std/StdUtils.sol"; 7 | 8 | // Utils 9 | import {Actor} from "../utils/Actor.sol"; 10 | import {PropertiesConstants} from "../utils/PropertiesConstants.sol"; 11 | import {StdAsserts} from "../utils/StdAsserts.sol"; 12 | 13 | // Base 14 | import {BaseStorage} from "./BaseStorage.t.sol"; 15 | 16 | // Contracts 17 | import { 18 | VaultSimpleExtended as VaultSimple, 19 | VaultSimpleBorrowableExtended as VaultSimpleBorrowable, 20 | VaultRegularBorrowableExtended as VaultRegularBorrowable, 21 | VaultBorrowableWETHExtended as VaultBorrowableWETH 22 | } from "test/invariants/helpers/extended/VaultsExtended.sol"; 23 | 24 | /// @notice Base contract for all test contracts extends BaseStorage 25 | /// @dev Provides setup modifier and cheat code setup 26 | /// @dev inherits Storage, Testing constants assertions and utils needed for testing 27 | abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdUtils { 28 | bool public IS_TEST = true; 29 | 30 | /////////////////////////////////////////////////////////////////////////////////////////////// 31 | // ACTOR PROXY MECHANISM // 32 | /////////////////////////////////////////////////////////////////////////////////////////////// 33 | 34 | /// @dev Actor proxy mechanism 35 | modifier setup() virtual { 36 | actor = actors[msg.sender]; 37 | _; 38 | actor = Actor(payable(address(0))); 39 | } 40 | 41 | /// @dev Solves medusa backward time warp issue 42 | modifier monotonicTimestamp(address _vault) virtual { 43 | if (block.timestamp < VaultSimple(_vault).getLastInterestUpdate()) { 44 | vm.warp(VaultSimple(_vault).getLastInterestUpdate()); 45 | } 46 | _; 47 | } 48 | 49 | /// @dev sets the bottom limit index af the vaults array that the property will be tested against 50 | modifier targetVaultsFrom(VaultType _vaultType) { 51 | limitVault = uint256(_vaultType); 52 | _; 53 | limitVault = 0; 54 | } 55 | 56 | /////////////////////////////////////////////////////////////////////////////////////////////// 57 | // CHEAT CODE SETUP // 58 | /////////////////////////////////////////////////////////////////////////////////////////////// 59 | 60 | /// @dev Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. 61 | address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); 62 | 63 | Vm internal constant vm = Vm(VM_ADDRESS); 64 | 65 | /////////////////////////////////////////////////////////////////////////////////////////////// 66 | // HELPERS // 67 | /////////////////////////////////////////////////////////////////////////////////////////////// 68 | 69 | function _makeAddr(string memory name) internal pure returns (address addr) { 70 | uint256 privateKey = uint256(keccak256(abi.encodePacked(name))); 71 | addr = vm.addr(privateKey); 72 | } 73 | 74 | function _getRandomActor(uint256 _i) internal view returns (address) { 75 | uint256 _actorIndex = _i % NUMBER_OF_ACTORS; 76 | return actorAddresses[_actorIndex]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ERC20/ERC20Collateral.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "openzeppelin/token/ERC20/ERC20.sol"; 6 | import "openzeppelin/utils/ReentrancyGuard.sol"; 7 | import "evc/utils/EVCUtil.sol"; 8 | 9 | /// @title ERC20Collateral 10 | /// @notice It extends the ERC20 token standard to add the EVC authentication and account status checks so that the 11 | /// token contract can be used as collateral in the EVC ecosystem. 12 | abstract contract ERC20Collateral is EVCUtil, ERC20, ReentrancyGuard { 13 | constructor(address _evc_, string memory _name_, string memory _symbol_) EVCUtil(_evc_) ERC20(_name_, _symbol_) {} 14 | 15 | /// @notice Transfers a certain amount of tokens to a recipient. 16 | /// @dev Overriden to add re-entrancy protection. 17 | /// @param to The recipient of the transfer. 18 | /// @param amount The amount shares to transfer. 19 | /// @return A boolean indicating whether the transfer was successful. 20 | function transfer(address to, uint256 amount) public virtual override nonReentrant returns (bool) { 21 | return super.transfer(to, amount); 22 | } 23 | 24 | /// @notice Transfers a certain amount of tokens from a sender to a recipient. 25 | /// @dev Overriden to add re-entrancy protection. 26 | /// @param from The sender of the transfer. 27 | /// @param to The recipient of the transfer. 28 | /// @param amount The amount of shares to transfer. 29 | /// @return A boolean indicating whether the transfer was successful. 30 | function transferFrom( 31 | address from, 32 | address to, 33 | uint256 amount 34 | ) public virtual override nonReentrant returns (bool) { 35 | return super.transferFrom(from, to, amount); 36 | } 37 | 38 | /// @notice Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` 39 | /// (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding 40 | /// this function. 41 | /// @dev Overriden to require account status checks on transfers from non-zero addresses. The account status check 42 | /// must be required on any operation that reduces user's balance. Note that the user balance cannot be modified 43 | /// after the account status check is required. If that's the case, the contract must be modified so that the 44 | /// account status check is required as the very last operation of the function. 45 | /// @param from The address from which tokens are transferred or burned. 46 | /// @param to The address to which tokens are transferred or minted. 47 | /// @param value The amount of tokens to transfer, mint, or burn. 48 | function _update(address from, address to, uint256 value) internal virtual override { 49 | super._update(from, to, value); 50 | 51 | if (from != address(0)) { 52 | evc.requireAccountStatusCheck(from); 53 | } 54 | } 55 | 56 | /// @notice Retrieves the message sender in the context of the EVC. 57 | /// @dev Overriden due to the conflict with the Context definition. 58 | /// @dev This function returns the account on behalf of which the current operation is being performed, which is 59 | /// either msg.sender or the account authenticated by the EVC. 60 | /// @return The address of the message sender. 61 | function _msgSender() internal view virtual override (EVCUtil, Context) returns (address) { 62 | return EVCUtil._msgSender(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/invariants/hooks/VaultSimpleBorrowableBeforeAfterHooks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | 6 | // Test Helpers 7 | import {Pretty, Strings} from "../utils/Pretty.sol"; 8 | 9 | // Contracts 10 | import {VaultSimpleBorrowable} from "test/invariants/Setup.t.sol"; 11 | 12 | // Test Contracts 13 | import {BaseHooks} from "../base/BaseHooks.t.sol"; 14 | 15 | /// @title VaultSimpleBorrowable Before After Hooks 16 | /// @notice Helper contract for before and after hooks 17 | /// @dev This contract is inherited by handlers 18 | abstract contract VaultSimpleBorrowableBeforeAfterHooks is BaseHooks { 19 | using Strings for string; 20 | using Pretty for uint256; 21 | using Pretty for int256; 22 | using Pretty for bool; 23 | 24 | struct VaultSimpleBorrowableVars { 25 | // VaultSimpleBorrowable 26 | uint256 borrowCapBefore; 27 | uint256 borrowCapAfter; 28 | uint256 totalBorrowedBefore; 29 | uint256 totalBorrowedAfter; 30 | bool controllerEnabledBefore; 31 | bool controllerEnabledAfter; 32 | uint256 userDebtBefore; 33 | uint256 userDebtAfter; 34 | } 35 | 36 | VaultSimpleBorrowableVars svbVars; 37 | 38 | function _svbBefore(address _vault) internal { 39 | VaultSimpleBorrowable svb = VaultSimpleBorrowable(_vault); 40 | svbVars.borrowCapBefore = svb.borrowCap(); 41 | svbVars.totalBorrowedBefore = svb.totalBorrowed(); 42 | svbVars.controllerEnabledBefore = evc.isControllerEnabled(address(actor), _vault); 43 | svbVars.userDebtBefore = svb.debtOf(address(actor)); 44 | } 45 | 46 | function _svbAfter(address _vault) internal { 47 | VaultSimpleBorrowable svb = VaultSimpleBorrowable(_vault); 48 | svbVars.borrowCapAfter = svb.borrowCap(); 49 | svbVars.totalBorrowedAfter = svb.totalBorrowed(); 50 | svbVars.controllerEnabledAfter = evc.isControllerEnabled(address(actor), _vault); 51 | svbVars.userDebtAfter = svb.debtOf(address(actor)); 52 | 53 | // VaultSimple Post Conditions 54 | assert_VaultSimpleBorrowable_PcA(); 55 | assert_VaultSimpleBorrowable_PcB(); 56 | } 57 | 58 | /*///////////////////////////////////////////////////////////////////////////////////////////// 59 | // POST CONDITIONS // 60 | /////////////////////////////////////////////////////////////////////////////////////////////// 61 | 62 | VaultSimpleBorrowable 63 | Post Condition A: (borrowCapAfter != 0) && (totalBorrowedAfter >= totalBorrowedBefore) 64 | => borrowCapAfter >= totalBorrowedAfter 65 | Post Condition B: Controller cannot be disabled if there is any liability 66 | */ 67 | 68 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 69 | 70 | function assert_VaultSimpleBorrowable_PcA() internal { 71 | assertTrue( 72 | (svbVars.totalBorrowedAfter > svbVars.totalBorrowedBefore && svbVars.borrowCapAfter != 0) 73 | ? (svbVars.borrowCapAfter >= svbVars.totalBorrowedAfter) 74 | : true, 75 | "(totalBorrowedAfter > totalBorrowedBefore)" 76 | ); 77 | } 78 | 79 | function assert_VaultSimpleBorrowable_PcB() internal { 80 | if (svbVars.userDebtBefore > 0) { 81 | assertEq(svbVars.controllerEnabledAfter, true, "Controller cannot be disabled if there is any liability"); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/mocks/PriceOracleMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "openzeppelin/interfaces/IERC4626.sol"; 5 | import "../../src/ERC20/ERC20CollateralWrapper.sol"; 6 | import "../../src/interfaces/IPriceOracle.sol"; 7 | 8 | contract PriceOracleMock is IPriceOracle { 9 | uint256 internal constant ADDRESS_MASK = (1 << 160) - 1; 10 | uint256 internal constant VAULT_MASK = 1 << 160; 11 | 12 | type AssetInfo is uint256; 13 | 14 | mapping(address asset => AssetInfo) public resolvedAssets; 15 | mapping(address base => mapping(address quote => uint256)) internal prices; 16 | 17 | function name() external pure returns (string memory) { 18 | return "PriceOracleMock"; 19 | } 20 | 21 | function setResolvedAsset(address asset) external { 22 | try IERC4626(asset).asset() returns (address underlying) { 23 | resolvedAssets[asset] = _setAssetInfo(underlying, true); 24 | } catch { 25 | resolvedAssets[asset] = _setAssetInfo(ERC20CollateralWrapper(asset).underlying(), false); 26 | } 27 | } 28 | 29 | function setPrice(address base, address quote, uint256 priceValue) external { 30 | prices[base][quote] = priceValue; 31 | } 32 | 33 | function getQuote(uint256 amount, address base, address quote) external view returns (uint256 out) { 34 | uint256 price; 35 | (amount, base, quote, price) = _resolveOracle(amount, base, quote); 36 | 37 | if (base == quote) { 38 | out = amount; 39 | } else { 40 | out = price * amount / 10 ** ERC20(base).decimals(); 41 | } 42 | } 43 | 44 | function getQuotes( 45 | uint256 amount, 46 | address base, 47 | address quote 48 | ) external view returns (uint256 bidOut, uint256 askOut) { 49 | uint256 price; 50 | (amount, base, quote, price) = _resolveOracle(amount, base, quote); 51 | 52 | if (base == quote) { 53 | bidOut = amount; 54 | } else { 55 | bidOut = price * amount / 10 ** ERC20(base).decimals(); 56 | } 57 | 58 | askOut = bidOut; 59 | } 60 | 61 | function _getAssetInfo(AssetInfo self) internal pure returns (address, bool) { 62 | return (address(uint160(AssetInfo.unwrap(self) & ADDRESS_MASK)), AssetInfo.unwrap(self) & VAULT_MASK != 0); 63 | } 64 | 65 | function _setAssetInfo(address asset, bool isVault) internal pure returns (AssetInfo) { 66 | return AssetInfo.wrap(uint160(asset) | (isVault ? VAULT_MASK : 0)); 67 | } 68 | 69 | function _resolveOracle( 70 | uint256 amount, 71 | address base, 72 | address quote 73 | ) internal view returns (uint256, address, address, uint256) { 74 | // Check the base case 75 | if (base == quote) return (amount, base, quote, 0); 76 | 77 | // 1. Check if base/quote is configured. 78 | uint256 price = prices[base][quote]; 79 | if (price > 0) return (amount, base, quote, price); 80 | 81 | // 2. Recursively resolve `base`. 82 | (address underlying, bool isVault) = _getAssetInfo(resolvedAssets[base]); 83 | if (underlying != address(0)) { 84 | amount = isVault ? IERC4626(base).convertToAssets(amount) : amount; 85 | return _resolveOracle(amount, underlying, quote); 86 | } 87 | 88 | // 3. Recursively resolve `quote`. 89 | (underlying, isVault) = _getAssetInfo(resolvedAssets[quote]); 90 | if (underlying != address(0)) { 91 | amount = isVault ? IERC4626(quote).convertToShares(amount) : amount; 92 | return _resolveOracle(amount, base, underlying); 93 | } 94 | 95 | revert PO_NoPath(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/vaults/VaultBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "evc/interfaces/IVault.sol"; 6 | import "../utils/EVCClient.sol"; 7 | 8 | /// @title VaultBase 9 | /// @dev This contract is an abstract base contract for Vaults. 10 | /// It declares functions that must be defined in the child contract in order to 11 | /// correctly implement the controller release, vault status snapshotting and account/vaults 12 | /// status checks. 13 | abstract contract VaultBase is IVault, EVCClient { 14 | error Reentrancy(); 15 | 16 | uint256 private constant REENTRANCY_UNLOCKED = 1; 17 | uint256 private constant REENTRANCY_LOCKED = 2; 18 | 19 | uint256 private reentrancyLock; 20 | bytes private snapshot; 21 | 22 | constructor(address _evc) EVCClient(_evc) { 23 | reentrancyLock = REENTRANCY_UNLOCKED; 24 | } 25 | 26 | /// @notice Prevents reentrancy 27 | modifier nonReentrant() virtual { 28 | if (reentrancyLock != REENTRANCY_UNLOCKED) { 29 | revert Reentrancy(); 30 | } 31 | 32 | reentrancyLock = REENTRANCY_LOCKED; 33 | 34 | _; 35 | 36 | reentrancyLock = REENTRANCY_UNLOCKED; 37 | } 38 | 39 | /// @notice Prevents read-only reentrancy (should be used for view functions) 40 | modifier nonReentrantRO() virtual { 41 | if (reentrancyLock != REENTRANCY_UNLOCKED) { 42 | revert Reentrancy(); 43 | } 44 | 45 | _; 46 | } 47 | 48 | /// @notice Creates a snapshot of the vault state 49 | function createVaultSnapshot() internal { 50 | // We delete snapshots on `checkVaultStatus`, which can only happen at the end of the EVC batch. Snapshots are 51 | // taken before any action is taken on the vault that affects the cault asset records and deleted at the end, so 52 | // that asset calculations are always based on the state before the current batch of actions. 53 | if (snapshot.length == 0) { 54 | snapshot = doCreateVaultSnapshot(); 55 | } 56 | } 57 | 58 | /// @notice Checks the vault status 59 | /// @dev Executed as a result of requiring vault status check on the EVC. 60 | function checkVaultStatus() external onlyEVCWithChecksInProgress returns (bytes4 magicValue) { 61 | doCheckVaultStatus(snapshot); 62 | delete snapshot; 63 | 64 | return IVault.checkVaultStatus.selector; 65 | } 66 | 67 | /// @notice Checks the account status 68 | /// @dev Executed on a controller as a result of requiring account status check on the EVC. 69 | function checkAccountStatus( 70 | address account, 71 | address[] calldata collaterals 72 | ) external view onlyEVCWithChecksInProgress returns (bytes4 magicValue) { 73 | doCheckAccountStatus(account, collaterals); 74 | 75 | return IVault.checkAccountStatus.selector; 76 | } 77 | 78 | /// @notice Creates a snapshot of the vault state 79 | /// @dev Must be overridden by child contracts 80 | function doCreateVaultSnapshot() internal virtual returns (bytes memory snapshot); 81 | 82 | /// @notice Checks the vault status 83 | /// @dev Must be overridden by child contracts 84 | function doCheckVaultStatus(bytes memory snapshot) internal virtual; 85 | 86 | /// @notice Checks the account status 87 | /// @dev Must be overridden by child contracts 88 | function doCheckAccountStatus(address, address[] calldata) internal view virtual; 89 | 90 | /// @notice Disables a controller for an account 91 | /// @dev Must be overridden by child contracts. Must call the EVC.disableController() only if it's safe to do so 92 | /// (i.e. the account has repaid their debt in full) 93 | function disableController() external virtual; 94 | } 95 | -------------------------------------------------------------------------------- /src/view/BorrowableVaultLensForEVC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | import "solmate/utils/FixedPointMathLib.sol"; 5 | import "evc/interfaces/IEthereumVaultConnector.sol"; 6 | import "../vaults/solmate/VaultRegularBorrowable.sol"; 7 | import "./Types.sol"; 8 | 9 | contract BorrowableVaultLensForEVC { 10 | uint256 internal constant SECONDS_PER_YEAR = 365.2425 * 86400; 11 | uint256 internal constant ONE = 1e27; 12 | IEVC public immutable evc; 13 | 14 | constructor(IEVC _evc) { 15 | evc = _evc; 16 | } 17 | 18 | function getEVCUserInfo(address account) external view returns (EVCUserInfo memory) { 19 | address owner; 20 | try evc.getAccountOwner(account) returns (address _owner) { 21 | owner = _owner; 22 | } catch { 23 | owner = account; 24 | } 25 | 26 | return EVCUserInfo({ 27 | account: account, 28 | addressPrefix: evc.getAddressPrefix(account), 29 | owner: owner, 30 | enabledControllers: evc.getControllers(account), 31 | enabledCollaterals: evc.getCollaterals(account) 32 | }); 33 | } 34 | 35 | function getVaultUserInfo(address account, address vault) external view returns (VaultUserInfo memory) { 36 | uint256 shares = ERC4626(vault).balanceOf(account); 37 | (uint256 liabilityValue, uint256 collateralValue) = 38 | VaultRegularBorrowable(vault).getAccountLiabilityStatus(account); 39 | 40 | return VaultUserInfo({ 41 | account: account, 42 | vault: vault, 43 | shares: shares, 44 | assets: ERC4626(vault).convertToAssets(shares), 45 | borrowed: VaultRegularBorrowable(vault).debtOf(account), 46 | liabilityValue: liabilityValue, 47 | collateralValue: collateralValue, 48 | isController: evc.isControllerEnabled(account, vault), 49 | isCollateral: evc.isCollateralEnabled(account, vault) 50 | }); 51 | } 52 | 53 | function getVaultInfo(address vault) external view returns (VaultInfo memory) { 54 | address asset = address(ERC4626(vault).asset()); 55 | uint256 interestRateSPY = VaultRegularBorrowable(vault).getInterestRate(); 56 | 57 | return VaultInfo({ 58 | vault: vault, 59 | vaultName: getStringOrBytes32(vault, ERC20(vault).name.selector), 60 | vaultSymbol: getStringOrBytes32(vault, ERC20(vault).symbol.selector), 61 | vaultDecimals: ERC20(vault).decimals(), 62 | asset: asset, 63 | assetName: getStringOrBytes32(asset, ERC20(asset).name.selector), 64 | assetSymbol: getStringOrBytes32(asset, ERC20(asset).symbol.selector), 65 | assetDecimals: ERC20(asset).decimals(), 66 | totalShares: ERC20(vault).totalSupply(), 67 | totalAssets: ERC4626(vault).totalAssets(), 68 | totalBorrowed: VaultRegularBorrowable(vault).totalBorrowed(), 69 | interestRateSPY: interestRateSPY, 70 | interestRateAPY: FixedPointMathLib.rpow(interestRateSPY + ONE, SECONDS_PER_YEAR, ONE) - ONE, 71 | irm: address(VaultRegularBorrowable(vault).irm()), 72 | oracle: address(VaultRegularBorrowable(vault).oracle()) 73 | }); 74 | } 75 | 76 | /// @dev for tokens like MKR which return bytes32 on name() or symbol() 77 | function getStringOrBytes32(address contractAddress, bytes4 selector) private view returns (string memory) { 78 | (bool success, bytes memory result) = contractAddress.staticcall(abi.encodeWithSelector(selector)); 79 | 80 | return success ? result.length == 32 ? string(abi.encodePacked(result)) : abi.decode(result, (string)) : ""; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/misc/ConditionalGaslessTx.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import "solmate/test/utils/mocks/MockERC20.sol"; 6 | import "evc/interfaces/IEthereumVaultConnector.sol"; 7 | import "../../src/vaults/solmate/VaultSimple.sol"; 8 | import "../../src/utils/SimpleConditionsEnforcer.sol"; 9 | import "../utils/EVCPermitSignerECDSA.sol"; 10 | 11 | contract ConditionalGaslessTxTest is Test { 12 | IEVC evc; 13 | MockERC20 asset; 14 | VaultSimple vault; 15 | SimpleConditionsEnforcer conditionsEnforcer; 16 | EVCPermitSignerECDSA permitSigner; 17 | 18 | function setUp() public { 19 | evc = new EthereumVaultConnector(); 20 | asset = new MockERC20("Asset", "ASS", 18); 21 | vault = new VaultSimple(address(evc), asset, "Vault", "VAU"); 22 | conditionsEnforcer = new SimpleConditionsEnforcer(); 23 | permitSigner = new EVCPermitSignerECDSA(address(evc)); 24 | } 25 | 26 | function test_ConditionalGaslessTx() public { 27 | uint256 alicePrivateKey = 0x12345; 28 | address alice = vm.addr(alicePrivateKey); 29 | permitSigner.setPrivateKey(alicePrivateKey); 30 | asset.mint(alice, 100e18); 31 | 32 | vm.prank(alice); 33 | asset.approve(address(vault), type(uint256).max); 34 | 35 | // alice deposits into her sub-account 1 36 | address alicesSubAccount = address(uint160(alice) ^ 1); 37 | vm.prank(alice); 38 | vault.deposit(100e18, alicesSubAccount); 39 | 40 | // alice signs the calldata that allows anyone to withdraw her sub-account deposit 41 | // after specified timestamp in the future. The same concept can be used for implementing 42 | // conditional orders (e.g. stop-loss, take-profit etc.). 43 | // the signed calldata can be executed by anyone using the permit() function on the evc 44 | IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); 45 | items[0] = IEVC.BatchItem({ 46 | targetContract: address(conditionsEnforcer), 47 | onBehalfOfAccount: alice, 48 | value: 0, 49 | data: abi.encodeWithSelector( 50 | SimpleConditionsEnforcer.currentBlockTimestamp.selector, SimpleConditionsEnforcer.ComparisonType.GE, 100 51 | ) 52 | }); 53 | items[1] = IEVC.BatchItem({ 54 | targetContract: address(vault), 55 | onBehalfOfAccount: alicesSubAccount, 56 | value: 0, 57 | data: abi.encodeWithSelector(VaultSimple.withdraw.selector, 100e18, alice, alicesSubAccount) 58 | }); 59 | 60 | bytes memory data = abi.encodeWithSelector(IEVC.batch.selector, items); 61 | bytes memory signature = permitSigner.signPermit(alice, address(0), 0, 0, type(uint256).max, 0, data); 62 | 63 | assertEq(asset.balanceOf(address(alice)), 0); 64 | assertEq(vault.maxWithdraw(alicesSubAccount), 100e18); 65 | 66 | // having the signature, anyone can execute the calldata on behalf of alice, but only after 67 | // the specified timestamp in the future. 68 | // -- evc.permit() 69 | // ---- evc.batch() 70 | // -------- conditionsEnforcer.currentBlockTimestamp() using evc.callInternal() to check the condition 71 | // -------- vault.withdraw() using evc.callInternal() to withdraw the funds 72 | vm.expectRevert(abi.encodeWithSelector(SimpleConditionsEnforcer.ConditionNotMet.selector)); 73 | evc.permit(alice, address(0), 0, 0, type(uint256).max, 0, data, signature); 74 | 75 | // succeeds if enough time elapses 76 | vm.warp(100); 77 | evc.permit(alice, address(0), 0, 0, type(uint256).max, 0, data, signature); 78 | 79 | assertEq(asset.balanceOf(address(alice)), 100e18); 80 | assertEq(vault.maxWithdraw(alicesSubAccount), 0); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /script/01_Deployment.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "forge-std/Script.sol"; 6 | import "solmate/test/utils/mocks/MockERC20.sol"; 7 | import "evc/EthereumVaultConnector.sol"; 8 | import "../src/vaults/solmate/VaultRegularBorrowable.sol"; 9 | import "../src/view/BorrowableVaultLensForEVC.sol"; 10 | import {IRMMock} from "../test/mocks/IRMMock.sol"; 11 | import {PriceOracleMock} from "../test/mocks/PriceOracleMock.sol"; 12 | 13 | /// @title Deployment script 14 | /// @notice This script is used for deploying the EVC and a couple vaults for testing purposes 15 | contract Deployment is Script { 16 | function run() public { 17 | uint256 deployerPrivateKey = vm.deriveKey(vm.envString("MNEMONIC"), 0); 18 | vm.startBroadcast(deployerPrivateKey); 19 | 20 | // deploy the EVC 21 | IEVC evc = new EthereumVaultConnector(); 22 | 23 | // deploy mock ERC-20 tokens 24 | MockERC20 asset1 = new MockERC20("Asset 1", "A1", 18); 25 | MockERC20 asset2 = new MockERC20("Asset 2", "A2", 18); 26 | MockERC20 asset3 = new MockERC20("Asset 3", "A3", 6); 27 | 28 | // mint some tokens to the deployer 29 | address deployer = vm.addr(deployerPrivateKey); 30 | asset1.mint(deployer, 1e6 * 1e18); 31 | asset2.mint(deployer, 1e6 * 1e18); 32 | asset3.mint(deployer, 1e6 * 1e6); 33 | 34 | // deply mock IRM 35 | IRMMock irm = new IRMMock(); 36 | 37 | // setup the IRM 38 | irm.setInterestRate(10); // 10% APY 39 | 40 | // deploy mock price oracle 41 | PriceOracleMock oracle = new PriceOracleMock(); 42 | 43 | // setup the price oracle 44 | oracle.setPrice(address(asset1), address(asset1), 1e18); // 1 A1 = 1 A1 45 | oracle.setPrice(address(asset2), address(asset1), 1e16); // 1 A2 = 0.01 A1 46 | oracle.setPrice(address(asset3), address(asset1), 1e18); // 1 A3 = 1 A1 47 | 48 | // deploy vaults 49 | VaultRegularBorrowable vault1 = 50 | new VaultRegularBorrowable(address(evc), asset1, irm, oracle, asset1, "Vault Asset 1", "VA1"); 51 | 52 | VaultRegularBorrowable vault2 = 53 | new VaultRegularBorrowable(address(evc), asset2, irm, oracle, asset1, "Vault Asset 2", "VA2"); 54 | 55 | VaultRegularBorrowable vault3 = 56 | new VaultRegularBorrowable(address(evc), asset3, irm, oracle, asset1, "Vault Asset 3", "VA3"); 57 | 58 | // setup the vaults 59 | vault1.setCollateralFactor(address(vault1), 95); // cf = 0.95, self-collateralization 60 | 61 | vault2.setCollateralFactor(address(vault2), 95); // cf = 0.95, self-collateralization 62 | vault2.setCollateralFactor(address(vault1), 50); // cf = 0.50 63 | 64 | vault3.setCollateralFactor(address(vault3), 95); // cf = 0.95, self-collateralization 65 | vault3.setCollateralFactor(address(vault1), 50); // cf = 0.50 66 | vault3.setCollateralFactor(address(vault2), 80); // cf = 0.8 67 | 68 | // setup the price oracle 69 | oracle.setResolvedAsset(address(vault1)); 70 | oracle.setResolvedAsset(address(vault2)); 71 | oracle.setResolvedAsset(address(vault3)); 72 | 73 | // deploy the lens 74 | BorrowableVaultLensForEVC lens = new BorrowableVaultLensForEVC(evc); 75 | 76 | vm.stopBroadcast(); 77 | 78 | // display the addresses 79 | console.log("Deployer", deployer); 80 | console.log("EVC", address(evc)); 81 | console.log("IRM", address(irm)); 82 | console.log("Price Oracle", address(oracle)); 83 | console.log("Asset 1", address(asset1)); 84 | console.log("Asset 2", address(asset2)); 85 | console.log("Asset 3", address(asset3)); 86 | console.log("Vault Asset 1", address(vault1)); 87 | console.log("Vault Asset 2", address(vault2)); 88 | console.log("Vault Asset 3", address(vault3)); 89 | console.log("Lens", address(lens)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/invariants/invariants/VaultSimpleBorrowableInvariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | 6 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | 8 | // Base Contracts 9 | import {VaultSimpleBorrowable} from "test/invariants/Setup.t.sol"; 10 | import {Actor} from "../utils/Actor.sol"; 11 | import {HandlerAggregator} from "../HandlerAggregator.t.sol"; 12 | 13 | /// @title VaultSimpleBorrowableInvariants 14 | /// @notice Implements Invariants for the protocol 15 | /// @notice Implements View functions assertions for the protocol, checked in assertion testing mode 16 | /// @dev Inherits HandlerAggregator for checking actions in assertion testing mode 17 | abstract contract VaultSimpleBorrowableInvariants is HandlerAggregator { 18 | /*////////////////////////////////////////////////////////////////////////////////////////////// 19 | // INVARIANTS SPEC: Handwritten / pseudo-code invariants // 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | VaultSimpleBorrowable 23 | Invariant A: totalBorrowed >= any account owed balance 24 | Invariant B: totalBorrowed == sum of all user debt 25 | Invariant C: sum of all user debt == 0 => totalBorrowed == 0 26 | Invariant D: User liability should always decrease after repayment (Implemented in the handler) 27 | Invariant E: Unhealthy users can not borrow (Implemented in the handler) 28 | Invariant F: If theres at least one borrow, the asset.balanceOf(vault) > 0 29 | */ 30 | 31 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 32 | 33 | function assert_VaultSimpleBorrowable_invariantA( 34 | address _vault, 35 | address _borrower 36 | ) internal monotonicTimestamp(_vault) { 37 | assertGe( 38 | VaultSimpleBorrowable(_vault).totalBorrowed(), 39 | VaultSimpleBorrowable(_vault).getOwed(_borrower), 40 | string.concat("VaultSimpleBorrowable_invariantA: ", vaultNames[_vault]) 41 | ); 42 | } 43 | 44 | function assert_VaultSimpleBorrowable_invariantB(address _vault) internal monotonicTimestamp(_vault) { 45 | assertApproxEqAbs( 46 | VaultSimpleBorrowable(_vault).totalBorrowed(), 47 | _getDebtSum(_vault), 48 | NUMBER_OF_ACTORS, 49 | string.concat("VaultSimpleBorrowable_invariantB: ", vaultNames[_vault]) 50 | ); 51 | } 52 | 53 | function assert_VaultSimpleBorrowable_invariantC(address _vault) internal monotonicTimestamp(_vault) { 54 | if (_getDebtSum(_vault) == 0) { 55 | assertEq( 56 | VaultSimpleBorrowable(_vault).totalBorrowed(), 57 | 0, 58 | string.concat("VaultSimpleBorrowable_invariantC: ", vaultNames[_vault]) 59 | ); 60 | } 61 | } 62 | 63 | function assert_VaultSimpleBorrowable_invariantE(address _vault) internal monotonicTimestamp(_vault) { 64 | if (VaultSimpleBorrowable(_vault).totalBorrowed() > 0) { 65 | assertGt( 66 | ERC20(address(VaultSimpleBorrowable(_vault).asset())).balanceOf(_vault), 67 | 0, 68 | string.concat("VaultSimpleBorrowable_invariantE: ", vaultNames[_vault]) 69 | ); 70 | } 71 | } 72 | 73 | ////////////////////////////////////////////////////////////////////////////////////////////// 74 | // HELPERS // 75 | ////////////////////////////////////////////////////////////////////////////////////////////// 76 | 77 | function _getDebtSum(address _vault) internal view returns (uint256 totalDebt) { 78 | for (uint256 i; i < NUMBER_OF_ACTORS; i++) { 79 | totalDebt += VaultSimpleBorrowable(_vault).debtOf(address(actorAddresses[i])); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/misc/GaslessTx.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import "solmate/test/utils/mocks/MockERC20.sol"; 6 | import "evc/interfaces/IEthereumVaultConnector.sol"; 7 | import "../../src/vaults/solmate/VaultSimple.sol"; 8 | import "../../src/utils/TipsPiggyBank.sol"; 9 | import "../utils/EVCPermitSignerECDSA.sol"; 10 | 11 | contract GaslessTxTest is Test { 12 | IEVC evc; 13 | MockERC20 asset; 14 | VaultSimple vault; 15 | TipsPiggyBank piggyBank; 16 | EVCPermitSignerECDSA permitSigner; 17 | 18 | function setUp() public { 19 | evc = new EthereumVaultConnector(); 20 | asset = new MockERC20("Asset", "ASS", 18); 21 | vault = new VaultSimple(address(evc), asset, "Vault", "VAU"); 22 | piggyBank = new TipsPiggyBank(); 23 | permitSigner = new EVCPermitSignerECDSA(address(evc)); 24 | } 25 | 26 | function test_GaslessTx() public { 27 | uint256 alicePrivateKey = 0x12345; 28 | address alice = vm.addr(alicePrivateKey); 29 | permitSigner.setPrivateKey(alicePrivateKey); 30 | asset.mint(alice, 100e18); 31 | 32 | vm.prank(alice); 33 | asset.approve(address(vault), type(uint256).max); 34 | 35 | // alice signs the calldata to deposit assets to the vault on her behalf. 36 | // the signed calldata can be executed by anyone using the permit() function on the evc. 37 | // additionally, she transfers 1% of the deposited amount to the tips piggy bank contract 38 | // that can be withdrawn by the relayer as a tip for sending the transaction 39 | IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); 40 | items[0] = IEVC.BatchItem({ 41 | targetContract: address(vault), 42 | onBehalfOfAccount: alice, 43 | value: 0, 44 | data: abi.encodeWithSelector(VaultSimple.deposit.selector, 100e18, alice) 45 | }); 46 | items[1] = IEVC.BatchItem({ 47 | targetContract: address(vault), 48 | onBehalfOfAccount: alice, 49 | value: 0, 50 | data: abi.encodeWithSelector(ERC20.transfer.selector, address(piggyBank), 1e18) 51 | }); 52 | 53 | bytes memory data = abi.encodeWithSelector(IEVC.batch.selector, items); 54 | bytes memory signature = permitSigner.signPermit(alice, address(0), 0, 0, type(uint256).max, 0, data); 55 | 56 | // having the signature, anyone can execute the calldata on behalf of alice and get tipped 57 | items[0] = IEVC.BatchItem({ 58 | targetContract: address(evc), 59 | onBehalfOfAccount: address(0), 60 | value: 0, 61 | data: abi.encodeWithSelector( 62 | IEVC.permit.selector, alice, address(0), 0, 0, type(uint256).max, 0, data, signature 63 | ) 64 | }); 65 | items[1] = IEVC.BatchItem({ 66 | targetContract: address(piggyBank), 67 | onBehalfOfAccount: address(this), 68 | value: 0, 69 | data: abi.encodeWithSelector(TipsPiggyBank.withdraw.selector, address(vault), address(this)) 70 | }); 71 | 72 | // -- evc.batch() 73 | // ---- evc.permit() 74 | // -------- evc.batch() 75 | // ---------------- vault.deposit() using evc.callInternal() 76 | // ---------------- vault.transfer() using evc.callInternal() to transfer the tip in form of the vault shares to 77 | // the piggy bank 78 | // ---- piggyBank.withdraw() to withdraw the tip to the relayer 79 | evc.batch(items); 80 | 81 | assertEq(asset.balanceOf(address(alice)), 0); 82 | assertEq(vault.maxWithdraw(alice), 99e18); 83 | assertEq(vault.maxWithdraw(address(this)), 1e18); 84 | 85 | // if we knew the relayer address when signing the calldata, we could have tipped the relayer without needing 86 | // the piggy bank contract. 87 | // in such situation, the shares transfer could be just a regular batch item and the relayer address could be 88 | // embedded 89 | // in the calldata directly 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/invariants/handlers/VaultRegularBorrowableHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {ERC4626} from "solmate/tokens/ERC4626.sol"; 6 | 7 | // Contracts 8 | import {Actor} from "../utils/Actor.sol"; 9 | import {BaseHandler, VaultRegularBorrowable} from "../base/BaseHandler.t.sol"; 10 | 11 | /// @title VaultRegularBorrowableHandler 12 | /// @notice Handler test contract for the VaultRegularBorrowable actions 13 | contract VaultRegularBorrowableHandler is BaseHandler { 14 | /////////////////////////////////////////////////////////////////////////////////////////////// 15 | // STATE VARIABLES // 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | // GHOST VARAIBLES // 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | // ACTIONS // 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | 26 | function liquidate(uint256 repayAssets, uint256 i, uint256 j) external setup { 27 | bool success; 28 | bytes memory returnData; 29 | 30 | address vaultAddress = _getRandomSupportedVault(j, VaultType.RegularBorrowable); 31 | 32 | address violator = _getActorWithDebt(vaultAddress); 33 | 34 | require(violator != address(0), "VaultRegularBorrowableHandler: no violator"); 35 | 36 | bool violatorStatus = isAccountHealthy(vaultAddress, violator); 37 | 38 | VaultRegularBorrowable vault = VaultRegularBorrowable(vaultAddress); 39 | 40 | repayAssets = clampBetween(repayAssets, 1, vault.debtOf(violator)); 41 | 42 | { 43 | // Get one of the three actors randomly 44 | address collateral = _getRandomAccountCollateral(i + j, address(actor)); 45 | 46 | _before(vaultAddress, VaultType.RegularBorrowable); 47 | (success, returnData) = actor.proxy( 48 | vaultAddress, 49 | abi.encodeWithSelector(VaultRegularBorrowable.liquidate.selector, violator, collateral, repayAssets) 50 | ); 51 | } 52 | if (success) { 53 | _after(vaultAddress, VaultType.RegularBorrowable); 54 | 55 | // VaultRegularBorrowable_invariantB 56 | assertFalse(violatorStatus); 57 | } 58 | } 59 | 60 | /////////////////////////////////////////////////////////////////////////////////////////////// 61 | // OWNER ACTIONS // 62 | /////////////////////////////////////////////////////////////////////////////////////////////// 63 | 64 | function setCollateralFactor(uint256 i, uint256 collateralFactor) public { 65 | address vaultAddress = _getRandomSupportedVault(i, VaultType.RegularBorrowable); 66 | 67 | VaultRegularBorrowable vault = VaultRegularBorrowable(vaultAddress); 68 | _before(vaultAddress, VaultType.RegularBorrowable); 69 | vault.setCollateralFactor(vaultAddress, collateralFactor); 70 | _after(vaultAddress, VaultType.RegularBorrowable); 71 | 72 | assert(true); 73 | } 74 | 75 | /////////////////////////////////////////////////////////////////////////////////////////////// 76 | // HELPERS // 77 | /////////////////////////////////////////////////////////////////////////////////////////////// 78 | 79 | function _getActorWithDebt(address vaultAddress) internal view returns (address) { 80 | VaultRegularBorrowable vault = VaultRegularBorrowable(vaultAddress); 81 | address _actor = address(actor); 82 | for (uint256 k; k < NUMBER_OF_ACTORS; k++) { 83 | if (_actor != actorAddresses[k] && vault.debtOf(address(actorAddresses[k])) > 0) { 84 | return address(actorAddresses[k]); 85 | } 86 | } 87 | return address(0); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/invariants/utils/Pretty.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | ///@notice https://github.com/one-hundred-proof/kyberswap-exploit/blob/main/lib/helpers/Pretty.sol 5 | library Strings { 6 | function concat(string memory _base, string memory _value) internal pure returns (string memory) { 7 | bytes memory _baseBytes = bytes(_base); 8 | bytes memory _valueBytes = bytes(_value); 9 | 10 | string memory _tmpValue = new string(_baseBytes.length + _valueBytes.length); 11 | bytes memory _newValue = bytes(_tmpValue); 12 | 13 | uint256 i; 14 | uint256 j; 15 | 16 | for (i = 0; i < _baseBytes.length; i++) { 17 | _newValue[j++] = _baseBytes[i]; 18 | } 19 | 20 | for (i = 0; i < _valueBytes.length; i++) { 21 | _newValue[j++] = _valueBytes[i]; 22 | } 23 | 24 | return string(_newValue); 25 | } 26 | } 27 | 28 | library Pretty { 29 | uint8 constant DEFAULT_DECIMALS = 18; 30 | 31 | function toBitString(uint256 n) external pure returns (string memory) { 32 | return uintToBitString(n, 256); 33 | } 34 | 35 | function toBitString(uint256 n, uint8 decimals) external pure returns (string memory) { 36 | return uintToBitString(n, decimals); 37 | } 38 | 39 | function pretty(uint256 n) external pure returns (string memory) { 40 | return n == type(uint256).max 41 | ? "type(uint256).max" 42 | : n == type(uint128).max ? "type(uint128).max" : _pretty(n, DEFAULT_DECIMALS); 43 | } 44 | 45 | function pretty(bool value) external pure returns (string memory) { 46 | return value ? "true" : "false"; 47 | } 48 | 49 | function pretty(uint256 n, uint8 decimals) external pure returns (string memory) { 50 | return _pretty(n, decimals); 51 | } 52 | 53 | function pretty(int256 n) external pure returns (string memory) { 54 | return _prettyInt(n, DEFAULT_DECIMALS); 55 | } 56 | 57 | function pretty(int256 n, uint8 decimals) external pure returns (string memory) { 58 | return _prettyInt(n, decimals); 59 | } 60 | 61 | function _pretty(uint256 n, uint8 decimals) internal pure returns (string memory) { 62 | bool pastDecimals = decimals == 0; 63 | uint256 place = 0; 64 | uint256 r; // remainder 65 | string memory s = ""; 66 | 67 | while (n != 0) { 68 | r = n % 10; 69 | n /= 10; 70 | place++; 71 | s = Strings.concat(toDigit(r), s); 72 | if (pastDecimals && place % 3 == 0 && n != 0) { 73 | s = Strings.concat("_", s); 74 | } 75 | if (!pastDecimals && place == decimals) { 76 | pastDecimals = true; 77 | place = 0; 78 | s = Strings.concat("_", s); 79 | } 80 | } 81 | if (pastDecimals && place == 0) { 82 | s = Strings.concat("0", s); 83 | } 84 | if (!pastDecimals) { 85 | uint256 i; 86 | uint256 upper = (decimals >= place ? decimals - place : 0); 87 | for (i = 0; i < upper; ++i) { 88 | s = Strings.concat("0", s); 89 | } 90 | s = Strings.concat("0_", s); 91 | } 92 | return s; 93 | } 94 | 95 | function _prettyInt(int256 n, uint8 decimals) internal pure returns (string memory) { 96 | bool isNegative = n < 0; 97 | string memory s = ""; 98 | if (isNegative) { 99 | s = "-"; 100 | } 101 | return Strings.concat(s, _pretty(uint256(isNegative ? -n : n), decimals)); 102 | } 103 | 104 | function toDigit(uint256 n) internal pure returns (string memory) { 105 | if (n == 0) { 106 | return "0"; 107 | } else if (n == 1) { 108 | return "1"; 109 | } else if (n == 2) { 110 | return "2"; 111 | } else if (n == 3) { 112 | return "3"; 113 | } else if (n == 4) { 114 | return "4"; 115 | } else if (n == 5) { 116 | return "5"; 117 | } else if (n == 6) { 118 | return "6"; 119 | } else if (n == 7) { 120 | return "7"; 121 | } else if (n == 8) { 122 | return "8"; 123 | } else if (n == 9) { 124 | return "9"; 125 | } else { 126 | revert("Not in range 0 to 10"); 127 | } 128 | } 129 | 130 | function uintToBitString(uint256 n, uint16 bits) internal pure returns (string memory) { 131 | string memory s = ""; 132 | for (uint256 i; i < bits; i++) { 133 | if (n % 2 == 0) { 134 | s = Strings.concat("0", s); 135 | } else { 136 | s = Strings.concat("1", s); 137 | } 138 | n = n / 2; 139 | } 140 | return s; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/invariants/handlers/ERC20Handler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | // Test Contracts 8 | import {Actor} from "../utils/Actor.sol"; 9 | import {BaseHandler} from "../base/BaseHandler.t.sol"; 10 | 11 | /// @title ERC20Handler 12 | /// @notice Handler test contract for ERC20 contacts 13 | contract ERC20Handler is BaseHandler { 14 | /////////////////////////////////////////////////////////////////////////////////////////////// 15 | // STATE VARIABLES // 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | // GHOST VARAIBLES // 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | // ACTIONS // 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | 26 | function approveTo(uint256 i, uint256 j, uint256 amount) external setup { 27 | bool success; 28 | bytes memory returnData; 29 | 30 | // Get one of the three actors randomly 31 | address spender = _getRandomActor(i); 32 | 33 | address erc20Address = _getRandomSupportedVault(j, VaultType.Simple); 34 | 35 | (success, returnData) = 36 | actor.proxy(erc20Address, abi.encodeWithSelector(ERC20.approve.selector, spender, amount)); 37 | 38 | if (success) { 39 | assert(true); 40 | } 41 | } 42 | 43 | function transfer(address to, uint256 j, uint256 amount) external setup { 44 | bool success; 45 | bytes memory returnData; 46 | 47 | address erc20Address = _getRandomSupportedVault(j, VaultType.Simple); 48 | 49 | (success, returnData) = actor.proxy(erc20Address, abi.encodeWithSelector(ERC20.transfer.selector, to, amount)); 50 | 51 | if (success) { 52 | ghost_sumSharesBalancesPerUser[erc20Address][address(actor)] -= amount; 53 | ghost_sumSharesBalancesPerUser[erc20Address][to] += amount; 54 | } 55 | } 56 | 57 | function transferTo(uint256 i, uint256 j, uint256 amount) external setup { 58 | bool success; 59 | bytes memory returnData; 60 | 61 | // Get one of the three actors randomly 62 | address to = _getRandomActor(i); 63 | 64 | address erc20Address = _getRandomSupportedVault(j, VaultType.Simple); 65 | 66 | (success, returnData) = actor.proxy(erc20Address, abi.encodeWithSelector(ERC20.transfer.selector, to, amount)); 67 | 68 | if (success) { 69 | ghost_sumSharesBalancesPerUser[erc20Address][address(actor)] -= amount; 70 | ghost_sumSharesBalancesPerUser[erc20Address][to] += amount; 71 | } 72 | } 73 | 74 | function transferFrom(uint256 i, uint256 j, address to, uint256 amount) external setup { 75 | bool success; 76 | bytes memory returnData; 77 | 78 | // Get one of the three actors randomly 79 | address from = _getRandomActor(i); 80 | 81 | address erc20Address = _getRandomSupportedVault(j, VaultType.Simple); 82 | 83 | (success, returnData) = 84 | actor.proxy(erc20Address, abi.encodeWithSelector(ERC20.transferFrom.selector, from, to, amount)); 85 | 86 | if (success) { 87 | ghost_sumSharesBalancesPerUser[erc20Address][from] -= amount; 88 | ghost_sumSharesBalancesPerUser[erc20Address][to] += amount; 89 | } 90 | } 91 | 92 | function transferFromTo(uint256 i, uint256 u, uint256 amount) external setup { 93 | bool success; 94 | bytes memory returnData; 95 | 96 | // Get one of the three actors randomly 97 | address from = _getRandomActor(i); 98 | // Get one of the three actors randomly 99 | address to = _getRandomActor(u); 100 | 101 | address erc20Address = _getRandomSupportedVault(u, VaultType.Simple); 102 | 103 | (success, returnData) = 104 | actor.proxy(erc20Address, abi.encodeWithSelector(ERC20.transferFrom.selector, from, to, amount)); 105 | 106 | if (success) { 107 | ghost_sumSharesBalancesPerUser[erc20Address][from] -= amount; 108 | ghost_sumSharesBalancesPerUser[erc20Address][to] += amount; 109 | } 110 | } 111 | 112 | /////////////////////////////////////////////////////////////////////////////////////////////// 113 | // HELPERS // 114 | /////////////////////////////////////////////////////////////////////////////////////////////// 115 | } 116 | -------------------------------------------------------------------------------- /test/invariants/base/BaseStorage.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; 6 | 7 | // Contracts 8 | import {VaultSimple} from "src/vaults/solmate/VaultSimple.sol"; 9 | import {VaultSimpleBorrowable} from "src/vaults/solmate/VaultSimpleBorrowable.sol"; 10 | import {VaultRegularBorrowable} from "src/vaults/solmate/VaultRegularBorrowable.sol"; 11 | import {VaultBorrowableWETH} from "src/vaults/solmate/VaultBorrowableWETH.sol"; 12 | import {VaultSimple as VaultSimpleOZ} from "src/vaults/open-zeppelin/VaultSimple.sol"; 13 | import {VaultRegularBorrowable as VaultRegularBorrowableOZ} from "src/vaults/open-zeppelin/VaultRegularBorrowable.sol"; 14 | 15 | // Mocks 16 | import {IRMMock} from "test/mocks/IRMMock.sol"; 17 | import {PriceOracleMock} from "test/mocks/PriceOracleMock.sol"; 18 | 19 | // Interfaces 20 | import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol"; 21 | 22 | // Utils 23 | import {Actor} from "../utils/Actor.sol"; 24 | 25 | /// @notice BaseStorage contract for all test contracts, works in tandem with BaseTest 26 | abstract contract BaseStorage { 27 | /////////////////////////////////////////////////////////////////////////////////////////////// 28 | // CONSTANTS // 29 | /////////////////////////////////////////////////////////////////////////////////////////////// 30 | 31 | uint256 constant MAX_TOKEN_AMOUNT = 1e29; 32 | 33 | uint256 constant ONE_DAY = 1 days; 34 | uint256 constant ONE_MONTH = ONE_YEAR / 12; 35 | uint256 constant ONE_YEAR = 365 days; 36 | 37 | uint256 internal constant NUMBER_OF_ACTORS = 3; 38 | uint256 internal constant INITIAL_ETH_BALANCE = 1e26; 39 | uint256 internal constant INITIAL_COLL_BALANCE = 1e21; 40 | 41 | uint256 internal constant diff_tolerance = 0.000000000002e18; //compared to 1e18 42 | uint256 internal constant MAX_PRICE_CHANGE_PERCENT = 1.05e18; //compared to 1e18 43 | 44 | /////////////////////////////////////////////////////////////////////////////////////////////// 45 | // ACTORS // 46 | /////////////////////////////////////////////////////////////////////////////////////////////// 47 | 48 | /// @notice Stores the actor during a handler call 49 | Actor internal actor; 50 | 51 | /// @notice Mapping of fuzzer user addresses to actors 52 | mapping(address => Actor) internal actors; 53 | 54 | /// @notice Array of all actor addresses 55 | address[] internal actorAddresses; 56 | 57 | /////////////////////////////////////////////////////////////////////////////////////////////// 58 | // SUITE STORAGE // 59 | /////////////////////////////////////////////////////////////////////////////////////////////// 60 | 61 | // VAULT CONTRACTS 62 | 63 | /// @notice VaultSimple contract 64 | VaultSimple internal vaultSimple; 65 | 66 | /// @notice VaultSimple contract 67 | VaultSimpleOZ internal vaultSimpleOZ; 68 | 69 | /// @notice VaultSimpleBorrowable contract 70 | VaultSimpleBorrowable internal vaultSimpleBorrowable; 71 | 72 | /// @notice VaultRegularBorrowable contract 73 | VaultRegularBorrowable internal vaultRegularBorrowable; 74 | 75 | /// @notice VaultRegularBorrowable contract 76 | VaultRegularBorrowableOZ internal vaultRegularBorrowableOZ; 77 | 78 | /// @notice VaultBorrowableETH contract 79 | VaultBorrowableWETH internal vaultBorrowableWETH; 80 | 81 | /// @notice Enum for vault types, used to limit accesses to vaults array by complexity 82 | enum VaultType { 83 | Simple, 84 | SimpleOz, 85 | SimpleBorrowable, 86 | RegularBorrowable, 87 | RegularBorrowableOz, 88 | BorrowableWETH 89 | } 90 | 91 | /// @notice Array of all vaults, sorted from most simple to most complex, for modular testing 92 | address[] internal vaults; 93 | 94 | /// @notice refencer to the vault in order to ease debugging broken invariants 95 | mapping(address => string) internal vaultNames; 96 | 97 | /// @notice Used in handlers, sets the upper limit index af the vaults array that the property will be tested 98 | /// against 99 | uint256 internal limitVault; 100 | 101 | // EVC 102 | 103 | /// @notice EVC contract 104 | IEVC internal evc; 105 | 106 | // TOKENS 107 | 108 | /// @notice MockERC20 contract 109 | MockERC20 internal referenceAsset; 110 | /// @notice Array of all reference assets 111 | address[] internal referenceAssets; 112 | 113 | /// @notice MockERC20 contract 114 | MockERC20 internal liabilityAsset; 115 | /// @notice MockERC20 contract 116 | MockERC20 internal collateralAsset1; 117 | /// @notice MockERC20 contract 118 | MockERC20 internal collateralAsset2; 119 | /// @notice Array of all base assets 120 | address[] internal baseAssets; 121 | 122 | // IRM AND ORACLE 123 | 124 | /// @notice Interest rates manager mock contract 125 | IRMMock internal irm; 126 | 127 | /// @notice Price oracle mock contract 128 | PriceOracleMock internal oracle; 129 | } 130 | -------------------------------------------------------------------------------- /test/invariants/handlers/VaultSimpleBorrowableHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Actor} from "../utils/Actor.sol"; 5 | import {BaseHandler, VaultSimpleBorrowable} from "../base/BaseHandler.t.sol"; 6 | 7 | /// @title VaultSimpleBorrowableHandler 8 | /// @notice Handler test contract for the VaultSimpleBorrowable actions 9 | contract VaultSimpleBorrowableHandler is BaseHandler { 10 | /////////////////////////////////////////////////////////////////////////////////////////////// 11 | // STATE VARIABLES // 12 | /////////////////////////////////////////////////////////////////////////////////////////////// 13 | 14 | /////////////////////////////////////////////////////////////////////////////////////////////// 15 | // GHOST VARAIBLES // 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | // ACTIONS // 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | function borrowTo(uint256 assets, uint256 i, uint256 j) external setup { 23 | bool success; 24 | bytes memory returnData; 25 | 26 | // Get one of the three actors randomly 27 | address receiver = _getRandomActor(i); 28 | 29 | address vaultAddress = _getRandomSupportedVault(j, VaultType.SimpleBorrowable); 30 | 31 | VaultSimpleBorrowable vault = VaultSimpleBorrowable(vaultAddress); 32 | 33 | bool isAccountHealthyBefore = isAccountHealthy(vaultAddress, receiver); 34 | 35 | _before(vaultAddress, VaultType.SimpleBorrowable); 36 | (success, returnData) = 37 | actor.proxy(vaultAddress, abi.encodeWithSelector(VaultSimpleBorrowable.borrow.selector, assets, receiver)); 38 | 39 | if (!isAccountHealthyBefore) { 40 | // VaultSimpleBorrowable_invariantD 41 | assert(!success); 42 | } else { 43 | if (success) { 44 | _after(vaultAddress, VaultType.SimpleBorrowable); 45 | } 46 | } 47 | } 48 | 49 | function repayTo(uint256 assets, uint256 i, uint256 j) external setup { 50 | bool success; 51 | bytes memory returnData; 52 | 53 | // Get one of the three actors randomly 54 | address receiver = _getRandomActor(i); 55 | 56 | address vaultAddress = _getRandomSupportedVault(j, VaultType.SimpleBorrowable); 57 | 58 | VaultSimpleBorrowable vault = VaultSimpleBorrowable(vaultAddress); 59 | 60 | (uint256 liabilityValueBefore, uint256 collateralValueBefore) = vault.getAccountLiabilityStatus(receiver); 61 | 62 | _before(vaultAddress, VaultType.SimpleBorrowable); 63 | (success, returnData) = 64 | actor.proxy(vaultAddress, abi.encodeWithSelector(VaultSimpleBorrowable.repay.selector, assets, receiver)); 65 | 66 | if (success) { 67 | _after(vaultAddress, VaultType.SimpleBorrowable); 68 | 69 | (uint256 liabilityValueAfter, uint256 collateralValueAfter) = vault.getAccountLiabilityStatus(receiver); 70 | 71 | // VaultSimpleBorrowable_invariantC 72 | assertLe(liabilityValueAfter, liabilityValueBefore, "Liability value must decrease"); 73 | } 74 | } 75 | 76 | function pullDebt(uint256 i, uint256 j, uint256 assets) external setup { 77 | bool success; 78 | bytes memory returnData; 79 | 80 | // Get one of the three actors randomly 81 | address from = _getRandomActor(i); 82 | 83 | address vaultAddress = _getRandomSupportedVault(j, VaultType.SimpleBorrowable); 84 | 85 | VaultSimpleBorrowable vault = VaultSimpleBorrowable(vaultAddress); 86 | 87 | _before(vaultAddress, VaultType.SimpleBorrowable); 88 | (success, returnData) = 89 | actor.proxy(vaultAddress, abi.encodeWithSelector(VaultSimpleBorrowable.pullDebt.selector, from, assets)); 90 | 91 | if (success) { 92 | _after(vaultAddress, VaultType.SimpleBorrowable); 93 | } 94 | } 95 | 96 | /////////////////////////////////////////////////////////////////////////////////////////////// 97 | // OWNER ACTIONS // 98 | /////////////////////////////////////////////////////////////////////////////////////////////// 99 | 100 | function setBorrowCap(uint256 j, uint256 newBorrowCap) external { 101 | address vaultAddress = _getRandomSupportedVault(j, VaultType.SimpleBorrowable); 102 | 103 | VaultSimpleBorrowable vault = VaultSimpleBorrowable(vaultAddress); 104 | 105 | // Since the owner is the deployer of the vault, we dont need to use a a proxy 106 | _before(vaultAddress, VaultType.SimpleBorrowable); 107 | vault.setBorrowCap(newBorrowCap); 108 | _after(vaultAddress, VaultType.SimpleBorrowable); 109 | 110 | assert(true); 111 | } 112 | 113 | /////////////////////////////////////////////////////////////////////////////////////////////// 114 | // HELPERS // 115 | /////////////////////////////////////////////////////////////////////////////////////////////// 116 | } 117 | -------------------------------------------------------------------------------- /src/utils/EVCClient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "evc/utils/EVCUtil.sol"; 6 | 7 | /// @title EVCClient 8 | /// @dev This contract is an abstract base contract for interacting with the Ethereum Vault Connector (EVC). 9 | /// It provides utility functions for authenticating callers in the context of the EVC, 10 | /// scheduling and forgiving status checks, and liquidating collateral shares. 11 | abstract contract EVCClient is EVCUtil { 12 | error SharesSeizureFailed(); 13 | 14 | constructor(address _evc) EVCUtil(_evc) {} 15 | 16 | /// @notice Retrieves the collaterals enabled for an account. 17 | /// @param account The address of the account. 18 | /// @return An array of addresses that are enabled collaterals for the account. 19 | function getCollaterals(address account) internal view returns (address[] memory) { 20 | return evc.getCollaterals(account); 21 | } 22 | 23 | /// @notice Checks whether a vault is enabled as a collateral for an account. 24 | /// @param account The address of the account. 25 | /// @param vault The address of the vault. 26 | /// @return A boolean value that indicates whether the vault is an enabled collateral for the account. 27 | function isCollateralEnabled(address account, address vault) internal view returns (bool) { 28 | return evc.isCollateralEnabled(account, vault); 29 | } 30 | 31 | /// @notice Retrieves the controllers enabled for an account. 32 | /// @param account The address of the account. 33 | /// @return An array of addresses that are the enabled controllers for the account. 34 | function getControllers(address account) internal view returns (address[] memory) { 35 | return evc.getControllers(account); 36 | } 37 | 38 | /// @notice Checks whether a vault is enabled as a controller for an account. 39 | /// @param account The address of the account. 40 | /// @param vault The address of the vault. 41 | /// @return A boolean value that indicates whether the vault is an enabled controller for the account. 42 | function isControllerEnabled(address account, address vault) internal view returns (bool) { 43 | return evc.isControllerEnabled(account, vault); 44 | } 45 | 46 | /// @notice Disables the controller for an account 47 | /// @dev Ensure that the account does not have any liabilities before doing this. 48 | /// @param account The address of the account. 49 | function disableController(address account) internal { 50 | evc.disableController(account); 51 | } 52 | 53 | /// @notice Schedules a status check for an account. 54 | /// @param account The address of the account. 55 | function requireAccountStatusCheck(address account) internal { 56 | evc.requireAccountStatusCheck(account); 57 | } 58 | 59 | /// @notice Schedules a status check for the calling vault. 60 | function requireVaultStatusCheck() internal { 61 | evc.requireVaultStatusCheck(); 62 | } 63 | 64 | /// @notice Schedules a status check for an account and the calling vault. 65 | /// @param account The address of the account. 66 | function requireAccountAndVaultStatusCheck(address account) internal { 67 | if (account == address(0)) { 68 | evc.requireVaultStatusCheck(); 69 | } else { 70 | evc.requireAccountAndVaultStatusCheck(account); 71 | } 72 | } 73 | 74 | /// @notice Forgives a previously deferred account status check. 75 | /// @dev Can only be called by the enabled controller of the account. 76 | /// @param account The address of the account. 77 | function forgiveAccountStatusCheck(address account) internal { 78 | evc.forgiveAccountStatusCheck(account); 79 | } 80 | 81 | /// @notice Checks whether the status check is deferred for a given account. 82 | /// @param account The address of the account. 83 | /// @return A boolean flag that indicates whether the status check is deferred. 84 | function isAccountStatusCheckDeferred(address account) internal view returns (bool) { 85 | return evc.isAccountStatusCheckDeferred(account); 86 | } 87 | 88 | /// @notice Checks whether the status check is deferred for a given vault. 89 | /// @param vault The address of the vault. 90 | /// @return A boolean flag that indicates whether the status check is deferred. 91 | function isVaultStatusCheckDeferred(address vault) internal view returns (bool) { 92 | return evc.isVaultStatusCheckDeferred(vault); 93 | } 94 | 95 | /// @notice Liquidates a certain amount of collateral shares from a violator's vault. 96 | /// @dev This function controls the collateral in order to transfers the specified amount of shares from the 97 | /// violator's vault to the liquidator. 98 | /// @param vault The address of the vault from which the shares are being liquidated. 99 | /// @param liquidated The address of the account which has the shares being liquidated. 100 | /// @param liquidator The address to which the liquidated shares are being transferred. 101 | /// @param shares The amount of shares to be liquidated. 102 | function liquidateCollateralShares( 103 | address vault, 104 | address liquidated, 105 | address liquidator, 106 | uint256 shares 107 | ) internal { 108 | // Control the collateral in order to transfer shares from the violator's vault to the liquidator. 109 | bytes memory result = evc.controlCollateral( 110 | vault, liquidated, 0, abi.encodeWithSignature("transfer(address,uint256)", liquidator, shares) 111 | ); 112 | 113 | if (!(result.length == 0 || abi.decode(result, (bool)))) { 114 | revert SharesSeizureFailed(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/invariants/helpers/extended/VaultsExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {ERC20} from "solmate/tokens/ERC20.sol"; 6 | import {ERC20 as ERC20OZ, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | 8 | // Contracts 9 | import { 10 | VaultSimple, 11 | VaultSimpleBorrowable, 12 | VaultRegularBorrowable, 13 | VaultBorrowableWETH, 14 | VaultSimpleOZ, 15 | VaultRegularBorrowableOZ 16 | } from "../../base/BaseStorage.t.sol"; 17 | 18 | // Test Contracts 19 | import {VaultBaseGetters} from "../VaultBaseGetters.sol"; 20 | 21 | // Interfaces 22 | import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol"; 23 | import {IIRM} from "src/interfaces/IIRM.sol"; 24 | import {IPriceOracle} from "src/interfaces/IPriceOracle.sol"; 25 | 26 | /////////////////////////////////////////////////////////////////////////////////////////////// 27 | // SOLMATE VAULTS // 28 | /////////////////////////////////////////////////////////////////////////////////////////////// 29 | 30 | /// @title VaultSimpleExtended 31 | /// @notice Extended version of VaultSimple, it implements extra getters 32 | contract VaultSimpleExtended is VaultSimple, VaultBaseGetters { 33 | constructor( 34 | address _evc, 35 | ERC20 _asset, 36 | string memory _name, 37 | string memory _symbol 38 | ) VaultSimple(_evc, _asset, _name, _symbol) {} 39 | 40 | function getLastInterestUpdate() external view returns (uint256 lastInterestUpdate_) { 41 | lastInterestUpdate_ = 0; 42 | } 43 | } 44 | 45 | /// @title VaultSimpleBorrowableExtended 46 | /// @notice Extended version of VaultSimpleBorrowable, it implements extra getters 47 | contract VaultSimpleBorrowableExtended is VaultSimpleBorrowable, VaultBaseGetters { 48 | constructor( 49 | address _evc, 50 | ERC20 _asset, 51 | string memory _name, 52 | string memory _symbol 53 | ) VaultSimpleBorrowable(_evc, _asset, _name, _symbol) {} 54 | 55 | function getLastInterestUpdate() external view returns (uint256 lastInterestUpdate_) { 56 | lastInterestUpdate_ = 0; 57 | } 58 | 59 | function getOwed(address _borrower) external view returns (uint256 owed_) { 60 | owed_ = owed[_borrower]; 61 | } 62 | 63 | function getInterestAccumulator() external view returns (uint256 interestAccumulator_) { 64 | interestAccumulator_ = 0; 65 | } 66 | } 67 | 68 | /// @title VaultRegularBorrowableExtended 69 | /// @notice Extended version of VaultVaultRegularBorrowableSimple, it implements extra getters 70 | contract VaultRegularBorrowableExtended is VaultRegularBorrowable, VaultBaseGetters { 71 | constructor( 72 | address _evc, 73 | ERC20 _asset, 74 | IIRM _irm, 75 | IPriceOracle _oracle, 76 | ERC20 _referenceAsset, 77 | string memory _name, 78 | string memory _symbol 79 | ) VaultRegularBorrowable(_evc, _asset, _irm, _oracle, _referenceAsset, _name, _symbol) {} 80 | 81 | function getLastInterestUpdate() external view returns (uint256 lastInterestUpdate_) { 82 | lastInterestUpdate_ = lastInterestUpdate; 83 | } 84 | 85 | function getOwed(address _borrower) external view returns (uint256 owed_) { 86 | owed_ = owed[_borrower]; 87 | } 88 | 89 | function getInterestAccumulator() external view returns (uint256 interestAccumulator_) { 90 | interestAccumulator_ = interestAccumulator; 91 | } 92 | } 93 | 94 | /// @title VaultBorrowableWETHExtended 95 | /// @notice Extended version of VaultBorrowable, it implements extra getters 96 | contract VaultBorrowableWETHExtended is VaultBorrowableWETH, VaultBaseGetters { 97 | constructor( 98 | address _evc, 99 | ERC20 _asset, 100 | IIRM _irm, 101 | IPriceOracle _oracle, 102 | ERC20 _referenceAsset, 103 | string memory _name, 104 | string memory _symbol 105 | ) VaultBorrowableWETH(_evc, _asset, _irm, _oracle, _referenceAsset, _name, _symbol) {} 106 | 107 | function getOwed(address _borrower) external view returns (uint256 owed_) { 108 | owed_ = owed[_borrower]; 109 | } 110 | } 111 | 112 | /////////////////////////////////////////////////////////////////////////////////////////////// 113 | // OPENZEPPELIN VAULTS // 114 | /////////////////////////////////////////////////////////////////////////////////////////////// 115 | 116 | /// @title VaultSimpleExtended 117 | /// @notice Extended version of VaultSimple, it implements extra getters 118 | contract VaultSimpleExtendedOZ is VaultSimpleOZ, VaultBaseGetters { 119 | constructor( 120 | address _evc, 121 | ERC20OZ _asset, 122 | string memory _name, 123 | string memory _symbol 124 | ) VaultSimpleOZ(_evc, _asset, _name, _symbol) {} 125 | 126 | function getLastInterestUpdate() external view returns (uint256 lastInterestUpdate_) { 127 | lastInterestUpdate_ = 0; 128 | } 129 | } 130 | 131 | /// @title VaultRegularBorrowableExtended 132 | /// @notice Extended version of VaultVaultRegularBorrowableSimple, it implements extra getters 133 | contract VaultRegularBorrowableExtendedOZ is VaultRegularBorrowableOZ, VaultBaseGetters { 134 | constructor( 135 | address _evc, 136 | ERC20 _asset, 137 | IIRM _irm, 138 | IPriceOracle _oracle, 139 | ERC20OZ _referenceAsset, 140 | string memory _name, 141 | string memory _symbol 142 | ) VaultRegularBorrowableOZ(_evc, IERC20(address(_asset)), _irm, _oracle, _referenceAsset, _name, _symbol) {} 143 | 144 | function getLastInterestUpdate() external view returns (uint256 lastInterestUpdate_) { 145 | lastInterestUpdate_ = lastInterestUpdate; 146 | } 147 | 148 | function getOwed(address _borrower) external view returns (uint256 owed_) { 149 | owed_ = owed[_borrower]; 150 | } 151 | 152 | function getInterestAccumulator() external view returns (uint256 interestAccumulator_) { 153 | interestAccumulator_ = interestAccumulator; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /test/vaults/solmate/VaultSimpleBorrowable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; 6 | import "evc/EthereumVaultConnector.sol"; 7 | import "../../../src/vaults/solmate/VaultSimpleBorrowable.sol"; 8 | 9 | contract VaultSimpleBorrowableTest is Test { 10 | IEVC evc; 11 | MockERC20 asset; 12 | VaultSimpleBorrowable vault; 13 | 14 | function setUp() public { 15 | evc = new EthereumVaultConnector(); 16 | asset = new MockERC20("Asset", "ASS", 18); 17 | vault = new VaultSimpleBorrowable(address(evc), asset, "Asset Vault", "vASS"); 18 | } 19 | 20 | function test_SimpleBorrowRepay(address alice, uint128 randomAmount) public { 21 | vm.assume(alice != address(0) && alice != address(evc) && alice != address(vault)); 22 | vm.assume(randomAmount > 10); 23 | 24 | uint256 amount = uint256(randomAmount); 25 | 26 | asset.mint(alice, amount); 27 | assertEq(asset.balanceOf(alice), amount); 28 | 29 | vm.prank(alice); 30 | asset.approve(address(vault), type(uint256).max); 31 | 32 | vm.prank(alice); 33 | uint256 shares = vault.deposit(amount, alice); 34 | assertEq(asset.balanceOf(alice), 0); 35 | assertEq(vault.balanceOf(alice), shares); 36 | 37 | // controller and collateral not enabled, hence borrow unsuccessful 38 | vm.prank(alice); 39 | vm.expectRevert(abi.encodeWithSelector(EVCUtil.ControllerDisabled.selector)); 40 | vault.borrow((amount * 9) / 10, alice); 41 | 42 | vm.prank(alice); 43 | evc.enableController(alice, address(vault)); 44 | 45 | // collateral still not enabled, hence borrow unsuccessful 46 | vm.prank(alice); 47 | vm.expectRevert(abi.encodeWithSelector(VaultSimpleBorrowable.AccountUnhealthy.selector)); 48 | vault.borrow((amount * 9) / 10, alice); 49 | 50 | vm.prank(alice); 51 | evc.enableCollateral(alice, address(vault)); 52 | 53 | // too much borrowed, hence borrow unsuccessful 54 | vm.prank(alice); 55 | vm.expectRevert(abi.encodeWithSelector(VaultSimpleBorrowable.AccountUnhealthy.selector)); 56 | vault.borrow((amount * 9) / 10 + 1, alice); 57 | 58 | // finally borrow is successful 59 | vm.prank(alice); 60 | vault.borrow((amount * 9) / 10, alice); 61 | assertEq(asset.balanceOf(alice), (amount * 9) / 10); 62 | assertEq(vault.balanceOf(alice), shares); 63 | assertEq(vault.debtOf(alice), (amount * 9) / 10); 64 | 65 | // repay is successful 66 | vm.prank(alice); 67 | vault.repay((amount * 9) / 10, alice); 68 | assertEq(asset.balanceOf(alice), 0); 69 | assertEq(vault.balanceOf(alice), shares); 70 | assertEq(vault.debtOf(alice), 0); 71 | 72 | // withdraw is successful 73 | vm.prank(alice); 74 | vault.withdraw(amount, alice, alice); 75 | assertEq(asset.balanceOf(alice), amount); 76 | assertEq(vault.balanceOf(alice), 0); 77 | assertEq(vault.debtOf(alice), 0); 78 | } 79 | 80 | function test_SimpleBorrowRepayWithBatch(address alice, uint128 randomAmount) public { 81 | vm.assume(alice != address(0) && alice != address(evc) && alice != address(vault)); 82 | vm.assume(randomAmount > 10); 83 | 84 | uint256 amount = uint256(randomAmount); 85 | 86 | asset.mint(alice, amount); 87 | assertEq(asset.balanceOf(alice), amount); 88 | 89 | vm.prank(alice); 90 | asset.approve(address(vault), type(uint256).max); 91 | 92 | IEVC.BatchItem[] memory items = new IEVC.BatchItem[](4); 93 | items[0] = IEVC.BatchItem({ 94 | targetContract: address(vault), 95 | onBehalfOfAccount: alice, 96 | value: 0, 97 | data: abi.encodeWithSelector(VaultSimple.deposit.selector, amount, alice) 98 | }); 99 | items[1] = IEVC.BatchItem({ 100 | targetContract: address(evc), 101 | onBehalfOfAccount: address(0), 102 | value: 0, 103 | data: abi.encodeWithSelector(IEVC.enableController.selector, alice, address(vault)) 104 | }); 105 | items[2] = IEVC.BatchItem({ 106 | targetContract: address(evc), 107 | onBehalfOfAccount: address(0), 108 | value: 0, 109 | data: abi.encodeWithSelector(IEVC.enableCollateral.selector, alice, address(vault)) 110 | }); 111 | items[3] = IEVC.BatchItem({ 112 | targetContract: address(vault), 113 | onBehalfOfAccount: alice, 114 | value: 0, 115 | data: abi.encodeWithSelector(VaultSimpleBorrowable.borrow.selector, (amount * 9) / 10 + 1, alice) 116 | }); 117 | 118 | // it will revert because of the borrow amount being too high 119 | vm.prank(alice); 120 | vm.expectRevert(abi.encodeWithSelector(VaultSimpleBorrowable.AccountUnhealthy.selector)); 121 | evc.batch(items); 122 | 123 | items[3].data = abi.encodeWithSelector(VaultSimpleBorrowable.borrow.selector, (amount * 9) / 10, alice); 124 | 125 | // now it will succeed 126 | vm.prank(alice); 127 | evc.batch(items); 128 | assertEq(asset.balanceOf(alice), (amount * 9) / 10); 129 | assertEq(vault.maxWithdraw(alice), amount - (amount * 9) / 10); 130 | assertEq(vault.debtOf(alice), (amount * 9) / 10); 131 | 132 | items = new IEVC.BatchItem[](2); 133 | items[0] = IEVC.BatchItem({ 134 | targetContract: address(vault), 135 | onBehalfOfAccount: alice, 136 | value: 0, 137 | data: abi.encodeWithSelector(VaultSimpleBorrowable.repay.selector, (amount * 9) / 10, alice) 138 | }); 139 | items[1] = IEVC.BatchItem({ 140 | targetContract: address(vault), 141 | onBehalfOfAccount: alice, 142 | value: 0, 143 | data: abi.encodeWithSelector(VaultSimple.withdraw.selector, amount, alice, alice) 144 | }); 145 | 146 | vm.prank(alice); 147 | evc.batch(items); 148 | assertEq(asset.balanceOf(alice), amount); 149 | assertEq(vault.maxWithdraw(alice), 0); 150 | assertEq(vault.debtOf(alice), 0); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /test/invariants/Setup.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; 7 | import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; 8 | 9 | // Contracts 10 | import { 11 | VaultSimpleExtended as VaultSimple, 12 | VaultSimpleBorrowableExtended as VaultSimpleBorrowable, 13 | VaultRegularBorrowableExtended as VaultRegularBorrowable, 14 | VaultBorrowableWETHExtended as VaultBorrowableWETH, 15 | VaultSimpleExtendedOZ as VaultSimpleOZ, 16 | VaultRegularBorrowableExtendedOZ as VaultRegularBorrowableOZ 17 | } from "test/invariants/helpers/extended/VaultsExtended.sol"; 18 | 19 | // Test Contracts 20 | import {IRMMock} from "../mocks/IRMMock.sol"; 21 | import {PriceOracleMock} from "../mocks/PriceOracleMock.sol"; 22 | import {Actor} from "./utils/Actor.sol"; 23 | import {BaseTest} from "./base/BaseTest.t.sol"; 24 | 25 | /// @title Setup 26 | /// @notice Setup contract for the invariant test Suite, inherited by Tester 27 | contract Setup is BaseTest { 28 | function _setUp() internal { 29 | // Deplopy EVC and needed contracts 30 | _deployProtocolCore(); 31 | 32 | // Deploy vaults 33 | _deployVaults(); 34 | 35 | // Set the initial mock prices 36 | _setDefaultPrices(); 37 | } 38 | 39 | function _deployProtocolCore() internal { 40 | // Deploy the EVC 41 | evc = new EthereumVaultConnector(); 42 | 43 | // Deploy the reference assets 44 | referenceAsset = new MockERC20("Reference Asset", "RA", 18); 45 | referenceAssets.push(address(referenceAsset)); 46 | 47 | // Deploy base assets 48 | liabilityAsset = new MockERC20("Liability Asset", "LA", 18); //TODO: add two liabilities 49 | collateralAsset1 = new MockERC20("Collateral Asset 1", "CA1", 18); 50 | collateralAsset2 = new MockERC20("Collateral Asset 2", "CA2", 6); 51 | baseAssets.push(address(liabilityAsset)); 52 | baseAssets.push(address(collateralAsset1)); 53 | baseAssets.push(address(collateralAsset2)); 54 | 55 | // Deploy the IRM and the Price Oracle 56 | irm = new IRMMock(); 57 | oracle = new PriceOracleMock(); 58 | } 59 | 60 | function _deployVaults() internal { 61 | // Deploy vaults 62 | /// @dev vaults are stored in the vaults array in the order of complexity, 63 | /// this helps with property inheritance and modularity 64 | vaultSimple = new VaultSimple(address(evc), collateralAsset1, "VaultSimple", "VS"); 65 | vaults.push(address(vaultSimple)); 66 | vaultNames[address(vaultSimple)] = "VaultSimple"; 67 | 68 | vaultSimpleOZ = new VaultSimpleOZ(address(evc), ERC20(address(collateralAsset1)), "VaultSimpleOZ", "VSOZ"); 69 | vaults.push(address(vaultSimpleOZ)); 70 | vaultNames[address(vaultSimpleOZ)] = "VaultSimpleOZ"; 71 | 72 | vaultSimpleBorrowable = 73 | new VaultSimpleBorrowable(address(evc), collateralAsset2, "VaultSimpleBorrowable", "VSB"); 74 | vaults.push(address(vaultSimpleBorrowable)); 75 | vaultNames[address(vaultSimpleBorrowable)] = "VaultSimpleBorrowable"; 76 | 77 | vaultRegularBorrowable = new VaultRegularBorrowable( 78 | address(evc), liabilityAsset, irm, oracle, referenceAsset, "VaultRegularBorrowable", "VRB" 79 | ); 80 | vaults.push(address(vaultRegularBorrowable)); 81 | vaultNames[address(vaultRegularBorrowable)] = "VaultRegularBorrowable"; 82 | 83 | vaultRegularBorrowableOZ = new VaultRegularBorrowableOZ( 84 | address(evc), 85 | liabilityAsset, 86 | irm, 87 | oracle, 88 | ERC20(address(referenceAsset)), 89 | "VaultRegularBorrowableOZ", 90 | "VRBOZ" 91 | ); 92 | vaults.push(address(vaultRegularBorrowableOZ)); 93 | vaultNames[address(vaultRegularBorrowableOZ)] = "VaultRegularBorrowableOZ"; 94 | 95 | //vaultBorrowableWETH = new VaultBorrowableWETH(evc, underlying, "VaultBorrowableWETH", "VBW"); 96 | //vaults.push(address(vaultBorrowableWETH)); 97 | } 98 | 99 | function _setDefaultPrices() internal { 100 | // Set the initial mock prices 101 | oracle.setResolvedAsset(address(vaultSimple)); 102 | oracle.setResolvedAsset(address(vaultSimpleOZ)); 103 | oracle.setResolvedAsset(address(vaultSimpleBorrowable)); 104 | oracle.setResolvedAsset(address(vaultRegularBorrowable)); 105 | oracle.setResolvedAsset(address(vaultRegularBorrowableOZ)); 106 | oracle.setPrice(address(liabilityAsset), address(referenceAsset), 1e17); // 1 LA = 0.1 RA 107 | oracle.setPrice(address(collateralAsset1), address(referenceAsset), 1e16); // 1 CA1 = 0.01 RA 108 | oracle.setPrice(address(collateralAsset2), address(referenceAsset), 1e17); // 1 CA2 = 0.1 RA 109 | } 110 | 111 | function _setUpActors() internal { 112 | address[] memory addresses = new address[](3); 113 | addresses[0] = USER1; 114 | addresses[1] = USER2; 115 | addresses[2] = USER3; 116 | 117 | address[] memory tokens = new address[](3); 118 | tokens[0] = address(liabilityAsset); 119 | tokens[1] = address(collateralAsset1); 120 | tokens[2] = address(collateralAsset2); 121 | 122 | for (uint256 i = 0; i < NUMBER_OF_ACTORS; i++) { 123 | // Deply actor proxies and approve system contracts 124 | address _actor = _setUpActor(addresses[i], tokens, vaults); 125 | 126 | // Mint initial balances to actors 127 | for (uint256 j = 0; j < tokens.length; j++) { 128 | MockERC20 _token = MockERC20(tokens[j]); 129 | _token.mint(_actor, INITIAL_BALANCE); 130 | } 131 | actorAddresses.push(_actor); 132 | } 133 | } 134 | 135 | function _setUpActor( 136 | address userAddress, 137 | address[] memory tokens, 138 | address[] memory callers 139 | ) internal returns (address actorAddress) { 140 | bool success; 141 | Actor _actor = new Actor(tokens, callers); 142 | actors[userAddress] = _actor; 143 | (success,) = address(_actor).call{value: INITIAL_ETH_BALANCE}(""); 144 | assert(success); 145 | actorAddress = address(_actor); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /test/invariants/Invariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Invariant Contracts 5 | import {BaseInvariants} from "./invariants/BaseInvariants.t.sol"; 6 | import {VaultSimpleInvariants} from "./invariants/VaultSimpleInvariants.t.sol"; 7 | import {VaultSimpleBorrowableInvariants} from "./invariants/VaultSimpleBorrowableInvariants.t.sol"; 8 | import {VaultRegularBorrowableInvariants} from "./invariants/VaultRegularBorrowableInvariants.t.sol"; 9 | import {VaultBorrowableWETHInvariants} from "./invariants/VaultBorrowableWETHInvariants.t.sol"; 10 | 11 | /// @title Invariants 12 | /// @notice Wrappers for the protocol invariants implemented in BaseInvariants 13 | /// @dev recognised by Echidna when property mode is activated 14 | /// @dev Inherits BaseInvariants that inherits HandlerAggregator 15 | abstract contract Invariants is 16 | BaseInvariants, 17 | VaultSimpleInvariants, 18 | VaultSimpleBorrowableInvariants, 19 | VaultRegularBorrowableInvariants, 20 | VaultBorrowableWETHInvariants 21 | { 22 | uint256 private constant REENTRANCY_UNLOCKED = 1; 23 | 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | // BASE INVARIANTS // 26 | /////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | function echidna_invariant_Base_invariantAB() public targetVaultsFrom(VaultType.Simple) returns (bool) { 29 | for (uint256 i = limitVault; i < vaults.length; i++) { 30 | assert_VaultBase_invariantA(vaults[i]); 31 | assert_VaultBase_invariantB(vaults[i]); 32 | } 33 | return true; 34 | } 35 | 36 | /////////////////////////////////////////////////////////////////////////////////////////////// 37 | // ERC4626 // 38 | /////////////////////////////////////////////////////////////////////////////////////////////// 39 | 40 | function echidna_invariant_ERC4626_assets_invariantAB() public targetVaultsFrom(VaultType.Simple) returns (bool) { 41 | for (uint256 i = limitVault; i < vaults.length; i++) { 42 | assert_ERC4626_assets_invariantA(vaults[i]); 43 | assert_ERC4626_assets_invariantB(vaults[i]); 44 | } 45 | return true; 46 | } 47 | 48 | function echidna_invariant_ERC4626_invariantC() public targetVaultsFrom(VaultType.Simple) returns (bool) { 49 | for (uint256 i = limitVault; i < vaults.length; i++) { 50 | assert_ERC4626_assets_invariantC(vaults[i]); 51 | } 52 | return true; 53 | } 54 | 55 | function echidna_invariant_ERC4626_invariantD() public targetVaultsFrom(VaultType.Simple) returns (bool) { 56 | for (uint256 i = limitVault; i < vaults.length; i++) { 57 | assert_ERC4626_assets_invariantD(vaults[i]); 58 | } 59 | return true; 60 | } 61 | 62 | function echidna_invariant_ERC4626_depositMintWithdrawRedeem_invariantA() 63 | public 64 | targetVaultsFrom(VaultType.Simple) 65 | returns (bool) 66 | { 67 | for (uint256 i = limitVault; i < vaults.length; i++) { 68 | for (uint256 j; j < NUMBER_OF_ACTORS; j++) { 69 | assert_ERC4626_deposit_invariantA(vaults[i], actorAddresses[j]); 70 | assert_ERC4626_mint_invariantA(vaults[i], actorAddresses[j]); 71 | assert_ERC4626_withdraw_invariantA(vaults[i], actorAddresses[j]); 72 | assert_ERC4626_redeem_invariantA(vaults[i], actorAddresses[j]); 73 | } 74 | } 75 | return true; 76 | } 77 | 78 | /////////////////////////////////////////////////////////////////////////////////////////////// 79 | // VAULT SIMPLE INVARIANTS // 80 | /////////////////////////////////////////////////////////////////////////////////////////////// 81 | 82 | function echidna_invariant_VaultSimple_invariantABCD() public targetVaultsFrom(VaultType.Simple) returns (bool) { 83 | for (uint256 i = limitVault; i < vaults.length; i++) { 84 | assert_VaultSimple_invariantA(vaults[i]); 85 | assert_VaultSimple_invariantB(vaults[i]); 86 | 87 | uint256 _sumBalanceOf; 88 | for (uint256 j; j < NUMBER_OF_ACTORS; j++) { 89 | _sumBalanceOf += assert_VaultSimple_invariantC(vaults[i], actorAddresses[j]); 90 | } 91 | } 92 | return true; 93 | } 94 | 95 | /////////////////////////////////////////////////////////////////////////////////////////////// 96 | // VAULT SIMPLE BORROWABLE INVARIANTS // 97 | /////////////////////////////////////////////////////////////////////////////////////////////// 98 | 99 | function echidna_invariant_VaultSimpleBorrowable_invariantAB() 100 | public 101 | targetVaultsFrom(VaultType.SimpleBorrowable) 102 | returns (bool) 103 | { 104 | for (uint256 i = limitVault; i < vaults.length; i++) { 105 | for (uint256 j; j < NUMBER_OF_ACTORS; j++) { 106 | assert_VaultSimpleBorrowable_invariantA(vaults[i], actorAddresses[j]); 107 | } 108 | assert_VaultSimpleBorrowable_invariantB(vaults[i]); 109 | } 110 | return true; 111 | } 112 | 113 | /////////////////////////////////////////////////////////////////////////////////////////////// 114 | // VAULT REGULAR BORROWABLE INVARIANTS // 115 | /////////////////////////////////////////////////////////////////////////////////////////////// 116 | 117 | /* function echidna_invariant_VaultRegularBorrowable_invariantA() 118 | public 119 | targetVaultsFrom(VaultType.RegularBorrowable) 120 | returns (bool) 121 | { 122 | for (uint256 i = limitVault; i < vaults.length; i++) { 123 | for (uint256 j; j < NUMBER_OF_ACTORS; j++) { 124 | } 125 | } 126 | return true; 127 | } */ 128 | 129 | /////////////////////////////////////////////////////////////////////////////////////////////// 130 | // VAULT BORROWABLE WETH INVARIANTS // 131 | /////////////////////////////////////////////////////////////////////////////////////////////// 132 | } 133 | -------------------------------------------------------------------------------- /test/invariants/base/BaseHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 6 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; 8 | 9 | // Contracts 10 | import {Actor} from "../utils/Actor.sol"; 11 | import {BaseStorage, VaultSimple, VaultSimpleBorrowable, VaultRegularBorrowable} from "../base/BaseStorage.t.sol"; 12 | import {HookAggregator} from "../hooks/HookAggregator.t.sol"; 13 | 14 | /// @title BaseHandler 15 | /// @notice Contains common logic for all handlers 16 | /// @dev inherits all suite assertions since per-action assertions are implemented in the handlers 17 | contract BaseHandler is HookAggregator { 18 | using EnumerableSet for EnumerableSet.AddressSet; 19 | 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | // SHARED VARAIBLES // 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | 24 | // SIMPLE VAULT 25 | 26 | /// @notice Sum of all balances in the vault 27 | mapping(address => uint256) internal ghost_sumBalances; 28 | 29 | /// @notice Sum of all balances per user in the vault 30 | mapping(address => mapping(address => uint256)) internal ghost_sumBalancesPerUser; 31 | 32 | /// @notice Sum of all shares balances in the vault 33 | mapping(address => uint256) internal ghost_sumSharesBalances; 34 | 35 | /// @notice Sum of all shares balances per user in the vault 36 | mapping(address => mapping(address => uint256)) internal ghost_sumSharesBalancesPerUser; 37 | 38 | // SIMPLE BORROWABLE VAULT 39 | 40 | /// @notice Track of the total amount borrowed per vault 41 | mapping(address => uint256) internal ghost_totalBorrowed; 42 | 43 | /// @notice Track of the total amount borrowed per user per vault 44 | mapping(address => mapping(address => uint256)) internal ghost_owedAmountPerUser; 45 | 46 | /// @notice Track the enabled collaterals 47 | mapping(address => EnumerableSet.AddressSet) internal ghost_accountCollaterals; 48 | 49 | /////////////////////////////////////////////////////////////////////////////////////////////// 50 | // HELPERS // 51 | /////////////////////////////////////////////////////////////////////////////////////////////// 52 | 53 | /// @notice Returns a random vault address that supports a specific vault type, this optimises the invariant suite 54 | function _getRandomSupportedVault(uint256 _i, VaultType _minVaultType) internal view returns (address) { 55 | require(uint8(_minVaultType) < vaults.length, "BaseHandler: invalid vault type"); 56 | 57 | // Randomize the vault selection 58 | uint256 randomValue = _randomize(_i, "randomVault"); 59 | 60 | // Use mod math to get a random vault from the range of vaults that support the specific type 61 | uint256 range = (vaults.length - 1) - uint256(_minVaultType) + 1; 62 | return vaults[uint256(_minVaultType) + (randomValue % range)]; 63 | } 64 | 65 | function _getRandomAccountCollateral(uint256 i, address account) internal view returns (address) { 66 | uint256 randomValue = _randomize(i, "randomAccountCollateral"); 67 | return ghost_accountCollaterals[account].at(randomValue % ghost_accountCollaterals[account].length()); 68 | } 69 | 70 | function _getRandomBaseAsset(uint256 i) internal view returns (address) { 71 | uint256 randomValue = _randomize(i, "randomBaseAsset"); 72 | return baseAssets[randomValue % baseAssets.length]; 73 | } 74 | 75 | /// @notice Helper function to randomize a uint256 seed with a string salt 76 | function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { 77 | return uint256(keccak256(abi.encodePacked(seed, salt))); 78 | } 79 | 80 | function _getRandomValue(uint256 modulus) internal view returns (uint256) { 81 | uint256 randomNumber = uint256(keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender))); 82 | return randomNumber % modulus; // Adjust the modulus to the desired range 83 | } 84 | 85 | /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor 86 | function _approve(address token, Actor actor_, address spender, uint256 amount) internal { 87 | bool success; 88 | bytes memory returnData; 89 | (success, returnData) = actor_.proxy(token, abi.encodeWithSelector(0x095ea7b3, spender, amount)); 90 | require(success, string(returnData)); 91 | } 92 | 93 | /// @notice Helper function to safely approve an amount of tokens to a spender 94 | function _approve(address token, address owner, address spender, uint256 amount) internal { 95 | vm.prank(owner); 96 | _safeApprove(token, spender, 0); 97 | vm.prank(owner); 98 | _safeApprove(token, spender, amount); 99 | } 100 | 101 | function _safeApprove(address token, address spender, uint256 amount) internal { 102 | (bool success, bytes memory retdata) = 103 | token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); 104 | assert(success); 105 | if (retdata.length > 0) assert(abi.decode(retdata, (bool))); 106 | } 107 | 108 | function _mint(address token, address receiver, uint256 amount) internal { 109 | MockERC20(token).mint(receiver, amount); 110 | } 111 | 112 | function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { 113 | _mint(token, owner, amount); 114 | _approve(token, owner, spender, amount); 115 | } 116 | 117 | function _mintApproveAndDeposit(address vault, address owner, uint256 amount) internal { 118 | VaultSimple _vault = VaultSimple(vault); 119 | _mintAndApprove(address(_vault.asset()), owner, vault, amount * 2); 120 | vm.prank(owner); 121 | _vault.deposit(amount, owner); 122 | } 123 | 124 | function _mintApproveAndMint(address vault, address owner, uint256 amount) internal { 125 | VaultSimple _vault = VaultSimple(vault); 126 | _mintAndApprove(address(_vault.asset()), owner, vault, _vault.convertToAssets(amount) * 2); 127 | _vault.mint(amount, owner); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/invariants/handlers/EVCHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; 6 | 7 | // Testing contracts 8 | import {Actor} from "../utils/Actor.sol"; 9 | import {BaseHandler, EnumerableSet} from "../base/BaseHandler.t.sol"; 10 | 11 | /// @title EVCHandler 12 | /// @notice Handler test contract for the EVC actions 13 | contract EVCHandler is BaseHandler { 14 | using EnumerableSet for EnumerableSet.AddressSet; 15 | 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | // STATE VARIABLES // 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | // GHOST VARAIBLES // 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | // ACTIONS // 26 | /////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | // TODO: 29 | // - setNonce 30 | // - setOperator 31 | 32 | function setAccountOperator(uint256 i, uint256 j, bool authorised) external setup { 33 | bool success; 34 | bytes memory returnData; 35 | 36 | address account = _getRandomActor(i); 37 | 38 | address operator = _getRandomActor(j); 39 | 40 | (success, returnData) = actor.proxy( 41 | address(evc), 42 | abi.encodeWithSelector(EthereumVaultConnector.setAccountOperator.selector, account, operator, authorised) 43 | ); 44 | 45 | if (success) { 46 | assert(true); 47 | } 48 | } 49 | 50 | // COLLATERAL 51 | 52 | function enableCollateral(uint256 i, uint256 j) external setup { 53 | bool success; 54 | bytes memory returnData; 55 | 56 | // Get one of the three actors randomly 57 | address account = _getRandomActor(i); 58 | 59 | address vaultAddress = _getRandomSupportedVault(j, VaultType.SimpleBorrowable); 60 | 61 | (success, returnData) = actor.proxy( 62 | address(evc), 63 | abi.encodeWithSelector(EthereumVaultConnector.enableCollateral.selector, account, vaultAddress) 64 | ); 65 | 66 | if (success) { 67 | ghost_accountCollaterals[address(actor)].add(vaultAddress); 68 | assert(true); 69 | } 70 | } 71 | 72 | function disableCollateral(uint256 i, uint256 j) external setup { 73 | bool success; 74 | bytes memory returnData; 75 | 76 | // Get one of the three actors randomly 77 | address account = _getRandomActor(i); 78 | 79 | address vaultAddress = _getRandomSupportedVault(j, VaultType.SimpleBorrowable); 80 | 81 | (success, returnData) = actor.proxy( 82 | address(evc), 83 | abi.encodeWithSelector(EthereumVaultConnector.disableCollateral.selector, account, vaultAddress) 84 | ); 85 | 86 | if (success) { 87 | ghost_accountCollaterals[address(actor)].remove(vaultAddress); 88 | assert(true); 89 | } 90 | } 91 | 92 | function reorderCollaterals(uint256 i, uint256 j, uint8 index1, uint8 index2) external setup { 93 | bool success; 94 | bytes memory returnData; 95 | 96 | // Get one of the three actors randomly 97 | address account = _getRandomActor(i); 98 | 99 | address vaultAddress = _getRandomSupportedVault(j, VaultType.SimpleBorrowable); 100 | 101 | (success, returnData) = actor.proxy( 102 | address(evc), 103 | abi.encodeWithSelector(EthereumVaultConnector.reorderCollaterals.selector, account, index1, index2) 104 | ); 105 | 106 | if (success) { 107 | assert(true); 108 | } 109 | } 110 | 111 | // CONTROLLER 112 | 113 | function enableController(uint256 i, uint256 j) external setup { 114 | bool success; 115 | bytes memory returnData; 116 | 117 | // Get one of the three actors randomly 118 | address account = _getRandomActor(i); 119 | 120 | address vaultAddress = _getRandomSupportedVault(j, VaultType.SimpleBorrowable); 121 | 122 | (success, returnData) = actor.proxy( 123 | address(evc), 124 | abi.encodeWithSelector(EthereumVaultConnector.enableController.selector, account, vaultAddress) 125 | ); 126 | 127 | if (success) { 128 | assert(true); 129 | } 130 | } 131 | 132 | function disableControllerEVC(uint256 i) external setup { 133 | bool success; 134 | bytes memory returnData; 135 | 136 | // Get one of the three actors randomly 137 | address account = _getRandomActor(i); 138 | 139 | address[] memory controllers = evc.getControllers(account); 140 | 141 | (success, returnData) = actor.proxy( 142 | address(evc), abi.encodeWithSelector(EthereumVaultConnector.disableController.selector, account) 143 | ); 144 | 145 | address[] memory controllersAfter = evc.getControllers(account); 146 | if (controllers.length == 0) { 147 | assertTrue(success); 148 | assertTrue(controllersAfter.length == 0); 149 | } else { 150 | assertEq(controllers.length, controllersAfter.length); 151 | } 152 | } 153 | 154 | function requireAccountStatusCheck(uint256 i) external setup { 155 | bytes memory returnData; 156 | 157 | // Get one of the three actors randomly 158 | address account = _getRandomActor(i); 159 | 160 | returnData = evc.call( 161 | address(evc), 162 | address(0), 163 | 0, 164 | abi.encodeWithSelector(EthereumVaultConnector.requireAccountStatusCheck.selector, account) 165 | ); 166 | } 167 | 168 | //TODO: 169 | // - batch 170 | // - batchRevert 171 | // - forgiveAccountStatusCheck 172 | // - requireVaultStatusCheck 173 | 174 | /////////////////////////////////////////////////////////////////////////////////////////////// 175 | // HELPERS // 176 | /////////////////////////////////////////////////////////////////////////////////////////////// 177 | } 178 | -------------------------------------------------------------------------------- /src/operators/LightweightOrderOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "solmate/tokens/ERC20.sol"; 6 | import "evc/interfaces/IEthereumVaultConnector.sol"; 7 | 8 | /// @title LightweightOrderOperator 9 | /// @notice This contract is used to manage orders submitted or signed by the user. 10 | /// The operations that do not require any kind of authentication (i.e. condition checks) 11 | /// can be submitted as non-EVC operations, while the operations that require authentication 12 | /// (i.e. vault interactions) can be submitted as EVC operations. 13 | /// Both the order submitter and the executor can be tipped in form of the ERC20 token 14 | /// as per the tipReceiver value. For that, the tip amount should be transferred to this 15 | /// contract during: 16 | /// - the submission step (using signed calldata of the EVC permit functionality) in order to 17 | /// tip the order submitter 18 | /// - the execution step (either while executing non-EVC or EVC operations) in order to 19 | /// tip the order executor 20 | /// The OrderOperator will always use the full amount of its balance at the time for the tip payout. 21 | /// Important: the submitter/executor must set the value of the tipReceiver variable to the address 22 | /// where they want to receive the tip. For safety, it should happen atomically during 23 | /// the EVC batch call, before the actual submission/execution of the order. 24 | /// NOTE: Because the operator contract can be made to invoke any arbitrary target contract with 25 | /// any arbitrary calldata, it should never be given any privileges, or hold any ETH or tokens. 26 | /// Also, one should never approve this contract to spend their ERC20 tokens. 27 | contract LightweightOrderOperator { 28 | enum OrderState { 29 | NONE, 30 | PENDING, 31 | CANCELLED, 32 | EXECUTED 33 | } 34 | 35 | struct Order { 36 | NonEVCBatchItem[] nonEVCOperations; 37 | IEVC.BatchItem[] EVCOperations; 38 | ERC20 submissionTipToken; 39 | ERC20 executionTipToken; 40 | uint256 salt; 41 | } 42 | 43 | struct NonEVCBatchItem { 44 | address targetContract; 45 | bytes data; 46 | } 47 | 48 | IEVC public immutable evc; 49 | 50 | mapping(bytes32 orderHash => OrderState state) public orderLookup; 51 | address internal tipReceiver; 52 | 53 | event OrderPending(Order order); 54 | event OrderExecuted(bytes32 indexed orderHash, address indexed caller); 55 | event OrderCancelled(bytes32 indexed orderHash); 56 | 57 | error NotAuthorized(); 58 | error InvalidOrderState(); 59 | error InvalidEVCOperations(); 60 | error InvalidNonEVCOperations(); 61 | error InvalidTip(); 62 | error EmptyError(); 63 | 64 | constructor(IEVC _evc) { 65 | evc = _evc; 66 | } 67 | 68 | /// @notice Only EVC can call a function with this modifier 69 | modifier onlyEVC() { 70 | if (msg.sender != address(evc)) { 71 | revert NotAuthorized(); 72 | } 73 | 74 | _; 75 | } 76 | 77 | /// @notice Sets the address that will receive the tips. Anyone can set it to any address anytime. 78 | /// @param _tipReceiver The address that will receive the tips 79 | function setTipReceiver(address _tipReceiver) external { 80 | tipReceiver = _tipReceiver; 81 | } 82 | 83 | /// @notice Executes an order that is either new or pending 84 | /// @param order The order to execute 85 | function execute(Order calldata order) external onlyEVC { 86 | bytes32 orderHash = keccak256(abi.encode(order)); 87 | 88 | if (orderLookup[orderHash] == OrderState.NONE) { 89 | _verifyOrder(order); 90 | } else if (orderLookup[orderHash] != OrderState.PENDING) { 91 | revert InvalidOrderState(); 92 | } 93 | 94 | orderLookup[orderHash] = OrderState.EXECUTED; 95 | 96 | // execute non-EVC operations, i.e. check conditions 97 | _batch(order.nonEVCOperations); 98 | 99 | // execute EVC operations 100 | evc.batch(order.EVCOperations); 101 | 102 | // payout the execution tip 103 | _payoutTip(order.executionTipToken); 104 | 105 | (address caller,) = evc.getCurrentOnBehalfOfAccount(address(0)); 106 | emit OrderExecuted(orderHash, caller); 107 | } 108 | 109 | /// @notice Submits an order so that it's publicly visible on-chain and can be executed by anyone 110 | /// @param order The order to submit 111 | function submit(Order calldata order) public onlyEVC { 112 | bytes32 orderHash = keccak256(abi.encode(order)); 113 | 114 | if (orderLookup[orderHash] != OrderState.NONE) { 115 | revert InvalidOrderState(); 116 | } 117 | 118 | orderLookup[orderHash] = OrderState.PENDING; 119 | 120 | _verifyOrder(order); 121 | 122 | // payout the submission tip 123 | _payoutTip(order.submissionTipToken); 124 | 125 | emit OrderPending(order); 126 | } 127 | 128 | /// @notice Cancels an order 129 | /// @param order The order to cancel 130 | function cancel(Order calldata order) external onlyEVC { 131 | bytes32 orderHash = keccak256(abi.encode(order)); 132 | 133 | if (orderLookup[orderHash] != OrderState.PENDING) { 134 | revert InvalidOrderState(); 135 | } 136 | 137 | orderLookup[orderHash] = OrderState.CANCELLED; 138 | 139 | (address onBehalfOfAccount,) = evc.getCurrentOnBehalfOfAccount(address(0)); 140 | address owner = evc.getAccountOwner(order.EVCOperations[0].onBehalfOfAccount); 141 | 142 | if (owner != onBehalfOfAccount || evc.isOperatorAuthenticated()) { 143 | revert NotAuthorized(); 144 | } 145 | 146 | emit OrderCancelled(orderHash); 147 | } 148 | 149 | /// @notice Executes a batch of non-EVC operations 150 | /// @param operations The operations to execute 151 | function _batch(NonEVCBatchItem[] calldata operations) internal { 152 | uint256 length = operations.length; 153 | for (uint256 i; i < length; ++i) { 154 | (bool success, bytes memory result) = operations[i].targetContract.call(operations[i].data); 155 | 156 | if (!success) revertBytes(result); 157 | } 158 | } 159 | 160 | /// @notice Pays out a tip 161 | /// @param tipToken The token to pay out 162 | function _payoutTip(ERC20 tipToken) internal { 163 | if (address(tipToken) != address(0)) { 164 | uint256 amount = tipToken.balanceOf(address(this)); 165 | address receiver = tipReceiver; 166 | 167 | if (amount > 0 && receiver == address(0)) { 168 | revert InvalidTip(); 169 | } 170 | 171 | tipToken.transfer(receiver, amount); 172 | } 173 | } 174 | 175 | /// @notice Verifies an order 176 | /// @param order The order to verify 177 | function _verifyOrder(Order calldata order) internal view { 178 | // get the account authenticated by the EVC 179 | (address onBehalfOfAccount,) = evc.getCurrentOnBehalfOfAccount(address(0)); 180 | address owner = evc.getAccountOwner(onBehalfOfAccount); 181 | if (owner != onBehalfOfAccount || evc.isOperatorAuthenticated()) { 182 | revert NotAuthorized(); 183 | } 184 | 185 | // verify that the non-EVC operations contain only operations that do not involve the EVC 186 | uint256 length = order.nonEVCOperations.length; 187 | for (uint256 i; i < length; ++i) { 188 | if (order.nonEVCOperations[i].targetContract == address(evc)) { 189 | revert InvalidNonEVCOperations(); 190 | } 191 | } 192 | 193 | // verify EVC operations 194 | length = order.EVCOperations.length; 195 | if (length == 0) { 196 | revert InvalidEVCOperations(); 197 | } 198 | 199 | // verify that the EVC operations contain only operations for the accounts belonging to the same user. 200 | // it's critical because if a user has authorized this operator for themselves, anyone else 201 | // could create a batch for their accounts and execute it 202 | for (uint256 i; i < length; ++i) { 203 | if ((uint160(order.EVCOperations[i].onBehalfOfAccount) | 0xff) != (uint160(onBehalfOfAccount) | 0xff)) { 204 | revert InvalidEVCOperations(); 205 | } 206 | } 207 | } 208 | 209 | function revertBytes(bytes memory errMsg) internal pure { 210 | if (errMsg.length != 0) { 211 | assembly { 212 | revert(add(32, errMsg), mload(errMsg)) 213 | } 214 | } 215 | revert EmptyError(); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/vaults/open-zeppelin/VaultSimple.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "openzeppelin/access/Ownable.sol"; 6 | import "openzeppelin/token/ERC20/extensions/ERC4626.sol"; 7 | import "../VaultBase.sol"; 8 | 9 | /// @title VaultSimple 10 | /// @dev It provides basic functionality for vaults. 11 | /// @notice In this contract, the EVC is authenticated before any action that may affect the state of the vault or an 12 | /// account. This is done to ensure that if it's EVC calling, the account is correctly authorized. This contract does 13 | /// not take the supply cap into account when calculating max deposit and max mint values. 14 | contract VaultSimple is VaultBase, Ownable, ERC4626 { 15 | event SupplyCapSet(uint256 newSupplyCap); 16 | 17 | error SnapshotNotTaken(); 18 | error SupplyCapExceeded(); 19 | 20 | uint256 internal _totalAssets; 21 | uint256 public supplyCap; 22 | 23 | constructor( 24 | address _evc, 25 | IERC20 _asset, 26 | string memory _name, 27 | string memory _symbol 28 | ) VaultBase(_evc) Ownable(msg.sender) ERC4626(_asset) ERC20(_name, _symbol) {} 29 | 30 | /// @notice Retrieves the message sender in the context of the EVC. 31 | /// @dev This function returns the account on behalf of which the current operation is being performed, which is 32 | /// either msg.sender or the account authenticated by the EVC. 33 | /// @return The address of the message sender. 34 | function _msgSender() internal view override (EVCUtil, Context) returns (address) { 35 | return EVCUtil._msgSender(); 36 | } 37 | 38 | /// @notice Sets the supply cap of the vault. 39 | /// @param newSupplyCap The new supply cap. 40 | function setSupplyCap(uint256 newSupplyCap) external onlyOwner { 41 | supplyCap = newSupplyCap; 42 | emit SupplyCapSet(newSupplyCap); 43 | } 44 | 45 | /// @notice Creates a snapshot of the vault. 46 | /// @dev This function is called before any action that may affect the vault's state. 47 | /// @return A snapshot of the vault's state. 48 | function doCreateVaultSnapshot() internal virtual override returns (bytes memory) { 49 | // make total assets snapshot here and return it: 50 | return abi.encode(_totalAssets); 51 | } 52 | 53 | /// @notice Checks the vault's status. 54 | /// @dev This function is called after any action that may affect the vault's state. 55 | /// @param oldSnapshot The snapshot of the vault's state before the action. 56 | function doCheckVaultStatus(bytes memory oldSnapshot) internal virtual override { 57 | // sanity check in case the snapshot hasn't been taken 58 | if (oldSnapshot.length == 0) revert SnapshotNotTaken(); 59 | 60 | // validate the vault state here: 61 | uint256 initialSupply = abi.decode(oldSnapshot, (uint256)); 62 | uint256 finalSupply = _convertToAssets(totalSupply(), Math.Rounding.Floor); 63 | 64 | // the supply cap can be implemented like this: 65 | if (supplyCap != 0 && finalSupply > supplyCap && finalSupply > initialSupply) { 66 | revert SupplyCapExceeded(); 67 | } 68 | } 69 | 70 | /// @notice Checks the status of an account. 71 | /// @dev This function is called after any action that may affect the account's state. 72 | function doCheckAccountStatus(address, address[] calldata) internal view virtual override { 73 | // no need to do anything here because the vault does not allow borrowing 74 | } 75 | 76 | /// @notice Disables the controller. 77 | /// @dev The controller is only disabled if the account has no debt. 78 | function disableController() external virtual override nonReentrant { 79 | // this vault doesn't allow borrowing, so we can't check that the account has no debt. 80 | // this vault should never be a controller, but user errors can happen 81 | EVCClient.disableController(_msgSender()); 82 | } 83 | 84 | /// @notice Returns the total assets of the vault. 85 | /// @return The total assets. 86 | function totalAssets() public view virtual override returns (uint256) { 87 | return _totalAssets; 88 | } 89 | 90 | /// @notice Converts assets to shares. 91 | /// @dev That function is manipulable in its current form as it uses exact values. Considering that other vaults may 92 | /// rely on it, for a production vault, a manipulation resistant mechanism should be implemented. 93 | /// @dev Considering that this function may be relied on by controller vaults, it's read-only re-entrancy protected. 94 | /// @param assets The assets to convert. 95 | /// @return The converted shares. 96 | function convertToShares(uint256 assets) public view virtual override nonReentrantRO returns (uint256) { 97 | return super.convertToShares(assets); 98 | } 99 | 100 | /// @notice Converts shares to assets. 101 | /// @dev That function is manipulable in its current form as it uses exact values. Considering that other vaults may 102 | /// rely on it, for a production vault, a manipulation resistant mechanism should be implemented. 103 | /// @dev Considering that this function may be relied on by controller vaults, it's read-only re-entrancy protected. 104 | /// @param shares The shares to convert. 105 | /// @return The converted assets. 106 | function convertToAssets(uint256 shares) public view virtual override nonReentrantRO returns (uint256) { 107 | return super.convertToAssets(shares); 108 | } 109 | 110 | /// @notice Transfers a certain amount of shares to a recipient. 111 | /// @param to The recipient of the transfer. 112 | /// @param amount The amount shares to transfer. 113 | /// @return A boolean indicating whether the transfer was successful. 114 | function transfer( 115 | address to, 116 | uint256 amount 117 | ) public virtual override (IERC20, ERC20) callThroughEVC nonReentrant returns (bool) { 118 | createVaultSnapshot(); 119 | bool result = super.transfer(to, amount); 120 | 121 | // despite the fact that the vault status check might not be needed for shares transfer with current logic, it's 122 | // added here so that if anyone changes the snapshot/vault status check mechanisms in the inheriting contracts, 123 | // they will not forget to add the vault status check here 124 | requireAccountAndVaultStatusCheck(_msgSender()); 125 | return result; 126 | } 127 | 128 | /// @notice Transfers a certain amount of shares from a sender to a recipient. 129 | /// @param from The sender of the transfer. 130 | /// @param to The recipient of the transfer. 131 | /// @param amount The amount of shares to transfer. 132 | /// @return A boolean indicating whether the transfer was successful. 133 | function transferFrom( 134 | address from, 135 | address to, 136 | uint256 amount 137 | ) public virtual override (IERC20, ERC20) callThroughEVC nonReentrant returns (bool) { 138 | createVaultSnapshot(); 139 | bool result = super.transferFrom(from, to, amount); 140 | 141 | // despite the fact that the vault status check might not be needed for shares transfer with current logic, it's 142 | // added here so that if anyone changes the snapshot/vault status check mechanisms in the inheriting contracts, 143 | // they will not forget to add the vault status check here 144 | requireAccountAndVaultStatusCheck(from); 145 | return result; 146 | } 147 | 148 | /// @notice Deposits a certain amount of assets for a receiver. 149 | /// @param assets The assets to deposit. 150 | /// @param receiver The receiver of the deposit. 151 | /// @return shares The shares equivalent to the deposited assets. 152 | function deposit( 153 | uint256 assets, 154 | address receiver 155 | ) public virtual override callThroughEVC nonReentrant returns (uint256 shares) { 156 | createVaultSnapshot(); 157 | shares = super.deposit(assets, receiver); 158 | _totalAssets += assets; 159 | requireVaultStatusCheck(); 160 | } 161 | 162 | /// @notice Mints a certain amount of shares for a receiver. 163 | /// @param shares The shares to mint. 164 | /// @param receiver The receiver of the mint. 165 | /// @return assets The assets equivalent to the minted shares. 166 | function mint( 167 | uint256 shares, 168 | address receiver 169 | ) public virtual override callThroughEVC nonReentrant returns (uint256 assets) { 170 | createVaultSnapshot(); 171 | assets = super.mint(shares, receiver); 172 | _totalAssets += assets; 173 | requireVaultStatusCheck(); 174 | } 175 | 176 | /// @notice Withdraws a certain amount of assets for a receiver. 177 | /// @param assets The assets to withdraw. 178 | /// @param receiver The receiver of the withdrawal. 179 | /// @param owner The owner of the assets. 180 | /// @return shares The shares equivalent to the withdrawn assets. 181 | function withdraw( 182 | uint256 assets, 183 | address receiver, 184 | address owner 185 | ) public virtual override callThroughEVC nonReentrant returns (uint256 shares) { 186 | createVaultSnapshot(); 187 | shares = super.withdraw(assets, receiver, owner); 188 | _totalAssets -= assets; 189 | requireAccountAndVaultStatusCheck(owner); 190 | } 191 | 192 | /// @notice Redeems a certain amount of shares for a receiver. 193 | /// @param shares The shares to redeem. 194 | /// @param receiver The receiver of the redemption. 195 | /// @param owner The owner of the shares. 196 | /// @return assets The assets equivalent to the redeemed shares. 197 | function redeem( 198 | uint256 shares, 199 | address receiver, 200 | address owner 201 | ) public virtual override callThroughEVC nonReentrant returns (uint256 assets) { 202 | createVaultSnapshot(); 203 | assets = super.redeem(shares, receiver, owner); 204 | _totalAssets -= assets; 205 | requireAccountAndVaultStatusCheck(owner); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /test/invariants/invariants/VaultSimpleInvariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | // Interfaces 4 | 5 | import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; 6 | import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; 7 | 8 | // Base Contracts 9 | import {VaultSimple} from "test/invariants/Setup.t.sol"; 10 | import {Actor} from "../utils/Actor.sol"; 11 | import {HandlerAggregator} from "../HandlerAggregator.t.sol"; 12 | import {console} from "forge-std/console.sol"; 13 | 14 | /// @title VaultSimpleInvariants 15 | /// @notice Implements Invariants for the protocol 16 | /// @notice Implements View functions assertions for the protocol, checked in assertion testing mode 17 | /// @dev Inherits HandlerAggregator for checking actions in assertion testing mode 18 | abstract contract VaultSimpleInvariants is HandlerAggregator { 19 | /*///////////////////////////////////////////////////////////////////////////////////////////// 20 | // INVARIANTS SPEC: Handwritten / pseudo-code invariants // 21 | /////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | VaultSimple 24 | Invariant A: underlying.balanceOf(vault) >= totalAssets 25 | Invariant B: totalSupply == sum of all minted shares 26 | Invariant C: balanceOf(actor) == sum of all shares owned by address 27 | Invariant D: totalSupply == sum of balanceOf(actors) 28 | 29 | ERC4626 30 | assets: 31 | Invariant A: asset MUST NOT revert 32 | Invariant B: totalAssets MUST NOT revert 33 | Invariant C: convertToShares MUST NOT show any variations depending on the caller 34 | Invariant D: convertToAssets MUST NOT show any variations depending on the caller 35 | 36 | deposit: 37 | Invariant A: maxDeposit MUST NOT revert 38 | Invariant B: previewDeposit MUST return close to and no more than shares minted at deposit if 39 | called in the same transaction 40 | Invariant C: deposit should return the same or more shares as previewDeposit if called in the 41 | same transaction 42 | 43 | mint: 44 | Invariant A: maxMint MUST NOT revert 45 | Invariant B: previewMint MUST return close to and no fewer than assets deposited at mint if 46 | called in the same transaction 47 | Invariant C: mint should return the same or fewer assets as previewMint if called in the 48 | same transaction 49 | 50 | withdraw: 51 | Invariant A: maxWithdraw MUST NOT revert 52 | Invariant B: previewWithdraw MUST return close to and no fewer than shares burned at withdraw if 53 | called in the same transaction 54 | Invariant C: withdraw should return the same or fewer shares as previewWithdraw if called in the 55 | same transaction 56 | 57 | redeem: 58 | Invariant A: maxRedeem MUST NOT revert 59 | Invariant B: previewRedeem MUST return close to and no more than assets redeemed at redeem if 60 | called in the same transaction 61 | Invariant C: redeem should return the same or more assets as previewRedeem if called in the 62 | same transaction 63 | 64 | roundtrip: 65 | Invariant A: redeem(deposit(a)) <= a 66 | Invariant B: 67 | s = deposit(a) 68 | s' = withdraw(a) 69 | s' >= s 70 | Invariant C: 71 | deposit(redeem(s)) <= s 72 | Invariant D: 73 | a = redeem(s) 74 | a' = mint(s) 75 | a' >= a 76 | Invariant E: 77 | withdraw(mint(s)) >= s 78 | Invariant F: 79 | a = mint(s) 80 | a' = redeem(s) 81 | a' <= a 82 | Invariant G: 83 | mint(withdraw(a)) >= a 84 | Invariant H: 85 | s = withdraw(a) 86 | s' = deposit(a) 87 | s' <= s 88 | 89 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 90 | 91 | /////////////////////////////////////////////////////////////////////////////////////////////// 92 | // VAULT SIMPLE // 93 | /////////////////////////////////////////////////////////////////////////////////////////////// 94 | 95 | function assert_VaultSimple_invariantA(address _vault) internal { 96 | assertGe( 97 | IERC20(address(VaultSimple(_vault).asset())).balanceOf(_vault), 98 | VaultSimple(_vault).totalAssets(), 99 | string.concat("VaultSimple_invariantA: ", vaultNames[_vault]) 100 | ); 101 | } 102 | 103 | function assert_VaultSimple_invariantB(address _vault) internal { 104 | uint256 totalSupply = VaultSimple(_vault).totalSupply(); 105 | 106 | assertApproxEqAbs( 107 | totalSupply, 108 | ghost_sumSharesBalances[_vault], 109 | NUMBER_OF_ACTORS, 110 | string.concat("VaultSimple_invariantB: ", vaultNames[_vault]) 111 | ); 112 | } 113 | 114 | function assert_VaultSimple_invariantC(address _vault, address _account) internal returns (uint256 balanceOf) { 115 | balanceOf = VaultSimple(_vault).balanceOf(_account); 116 | 117 | assertEq( 118 | balanceOf, 119 | ghost_sumSharesBalancesPerUser[_vault][_account], 120 | string.concat("VaultSimple_invariantC: ", vaultNames[_vault]) 121 | ); 122 | } 123 | 124 | function assert_VaultSimple_invariantD(address _vault, uint256 _sumBalances) internal { 125 | uint256 totalSupply = VaultSimple(_vault).totalSupply(); 126 | 127 | assertEq(totalSupply, _sumBalances, string.concat("VaultSimple_invariantD: ", vaultNames[_vault])); 128 | } 129 | 130 | /////////////////////////////////////////////////////////////////////////////////////////////// 131 | // ERC4626: ASSETS // 132 | /////////////////////////////////////////////////////////////////////////////////////////////// 133 | 134 | function assert_ERC4626_assets_invariantA(address _vault) internal { 135 | try IERC4626(_vault).asset() {} 136 | catch Error(string memory reason) { 137 | fail(string.concat("ERC4626_assets_invariantA: ", reason)); 138 | } 139 | } 140 | 141 | function assert_ERC4626_assets_invariantB(address _vault) internal { 142 | try IERC4626(_vault).totalAssets() returns (uint256 totalAssets) { 143 | totalAssets; 144 | } catch Error(string memory reason) { 145 | fail(string.concat("ERC4626_assets_invariantB: ", reason)); 146 | } 147 | } 148 | 149 | function assert_ERC4626_assets_invariantC(address _vault) internal monotonicTimestamp(_vault) { 150 | uint256 _assets = _getRandomValue(_maxAssets(_vault)); 151 | 152 | uint256 shares; 153 | bool notFirstLoop; 154 | for (uint256 i; i < NUMBER_OF_ACTORS; i++) { 155 | vm.prank(actorAddresses[i]); 156 | uint256 tempShares = IERC4626(_vault).convertToShares(_assets); 157 | 158 | if (notFirstLoop) { 159 | assertEq(shares, tempShares, string.concat("ERC4626_assets_invariantC: ", vaultNames[_vault])); 160 | } else { 161 | shares = tempShares; 162 | notFirstLoop = true; 163 | } 164 | } 165 | } 166 | 167 | function assert_ERC4626_assets_invariantD(address _vault) internal monotonicTimestamp(_vault) { 168 | uint256 _shares = _getRandomValue(_maxShares(_vault)); 169 | uint256 assets; 170 | bool notFirstLoop; 171 | for (uint256 i; i < NUMBER_OF_ACTORS; i++) { 172 | vm.prank(actorAddresses[i]); 173 | uint256 tempAssets = IERC4626(_vault).convertToAssets(_shares); 174 | 175 | if (notFirstLoop) { 176 | assertEq(assets, tempAssets, string.concat("ERC4626_assets_invariantD: ", vaultNames[_vault])); 177 | } else { 178 | assets = tempAssets; 179 | notFirstLoop = true; 180 | } 181 | } 182 | } 183 | 184 | /////////////////////////////////////////////////////////////////////////////////////////////// 185 | // ERC4626: DEPOSIT // 186 | /////////////////////////////////////////////////////////////////////////////////////////////// 187 | 188 | function assert_ERC4626_deposit_invariantA(address _vault, address _account) internal monotonicTimestamp(_vault) { 189 | try IERC4626(_vault).maxDeposit(_account) {} 190 | catch { 191 | assert(false); 192 | } 193 | } 194 | 195 | /////////////////////////////////////////////////////////////////////////////////////////////// 196 | // ERC4626: MINT // 197 | /////////////////////////////////////////////////////////////////////////////////////////////// 198 | 199 | function assert_ERC4626_mint_invariantA(address _vault, address _account) internal { 200 | try IERC4626(_vault).maxMint(_account) {} 201 | catch { 202 | assert(false); 203 | } 204 | } 205 | 206 | /////////////////////////////////////////////////////////////////////////////////////////////// 207 | // ERC4626: WITHDRAW // 208 | /////////////////////////////////////////////////////////////////////////////////////////////// 209 | 210 | function assert_ERC4626_withdraw_invariantA(address _vault, address _account) internal { 211 | try IERC4626(_vault).maxWithdraw(_account) {} 212 | catch { 213 | assert(false); 214 | } 215 | } 216 | 217 | /////////////////////////////////////////////////////////////////////////////////////////////// 218 | // ERC4626: REDEEM // 219 | /////////////////////////////////////////////////////////////////////////////////////////////// 220 | 221 | function assert_ERC4626_redeem_invariantA(address _vault, address _account) internal { 222 | try IERC4626(_vault).maxRedeem(_account) {} 223 | catch { 224 | assert(false); 225 | } 226 | } 227 | 228 | /////////////////////////////////////////////////////////////////////////////////////////////// 229 | // DISCARDED // 230 | /////////////////////////////////////////////////////////////////////////////////////////////// 231 | 232 | /* function assert_VaultSimple_invariantA(address _vault) internal { 233 | uint256 totalAssets = VaultSimple(_vault).totalAssets(); 234 | 235 | assertEq(totalAssets, ghost_sumBalances[_vault], string.concat("VaultSimple_invariantA: ", vaultNames[_vault])); 236 | } */ 237 | 238 | /////////////////////////////////////////////////////////////////////////////////////////////// 239 | // UTILS // 240 | /////////////////////////////////////////////////////////////////////////////////////////////// 241 | 242 | function _maxShares(address vault) internal view returns (uint256 shares) { 243 | shares = IERC4626(vault).totalSupply(); 244 | shares = shares == 0 ? 1 : shares; 245 | } 246 | 247 | function _maxAssets(address vault) internal view returns (uint256 assets) { 248 | assets = IERC4626(vault).totalAssets(); 249 | assets = assets == 0 ? 1 : assets; 250 | } 251 | 252 | function _max_withdraw(address from, address _vault) internal view virtual returns (uint256) { 253 | return IERC4626(_vault).convertToAssets(IERC20(_vault).balanceOf(from)); // may be different from 254 | // maxWithdraw(from) 255 | } 256 | 257 | function _max_redeem(address from, address _vault) internal view virtual returns (uint256) { 258 | return IERC20(_vault).balanceOf(from); // may be different from maxRedeem(from) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /test/misc/LightweightOrderOperator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import "solmate/test/utils/mocks/MockERC20.sol"; 6 | import "evc/EthereumVaultConnector.sol"; 7 | import "../../src/vaults/solmate/VaultSimple.sol"; 8 | import "../../src/operators/LightweightOrderOperator.sol"; 9 | import "../../src/utils/SimpleConditionsEnforcer.sol"; 10 | import "../utils/EVCPermitSignerECDSA.sol"; 11 | 12 | contract LightweightOrderOperatorTest is Test { 13 | IEVC evc; 14 | MockERC20 asset; 15 | VaultSimple vault; 16 | LightweightOrderOperator orderOperator; 17 | SimpleConditionsEnforcer conditionsEnforcer; 18 | EVCPermitSignerECDSA permitSigner; 19 | 20 | event OrderPending(LightweightOrderOperator.Order order); 21 | event OrderExecuted(bytes32 indexed orderHash, address indexed caller); 22 | event OrderCancelled(bytes32 indexed orderHash); 23 | 24 | function setUp() public { 25 | evc = new EthereumVaultConnector(); 26 | asset = new MockERC20("Asset", "ASS", 18); 27 | vault = new VaultSimple(address(evc), asset, "Vault", "VAU"); 28 | orderOperator = new LightweightOrderOperator(evc); 29 | conditionsEnforcer = new SimpleConditionsEnforcer(); 30 | permitSigner = new EVCPermitSignerECDSA(address(evc)); 31 | } 32 | 33 | function test_LightweightOrderOperator(uint256 alicePK) public { 34 | vm.assume( 35 | alicePK > 10 && alicePK < 115792089237316195423570985008687907852837564279074904382605163141518161494337 36 | ); 37 | 38 | address alice = vm.addr(alicePK); 39 | address alicesSubAccount = address(uint160(alice) ^ 1); 40 | vm.assume(alice != address(0) && alice != address(evc) && !evc.haveCommonOwner(alice, address(orderOperator))); 41 | permitSigner.setPrivateKey(alicePK); 42 | asset.mint(alice, 100e18); 43 | 44 | vm.prank(alice); 45 | asset.approve(address(vault), type(uint256).max); 46 | 47 | // alice authorizes the operator to act on behalf of her main account 48 | vm.prank(alice); 49 | evc.setAccountOperator(alice, address(orderOperator), true); 50 | 51 | // alice submits an order so that anyone can deposit on her behalf to her sub-account, 52 | // but only after a specified timestamp in the future. for that, the order can be 53 | // divided into two parts: evc operations and non-evc operations. 54 | // we can use the non-evc operations to check the conditions (as checking them 55 | // does not require any kind of authentication) and the evc operations 56 | // for the actual deposit. moreover, we can specify a tip that will be 57 | // paid to the operator when the order is executed. 58 | LightweightOrderOperator.NonEVCBatchItem[] memory nonEVCItems = 59 | new LightweightOrderOperator.NonEVCBatchItem[](1); 60 | nonEVCItems[0] = LightweightOrderOperator.NonEVCBatchItem({ 61 | targetContract: address(conditionsEnforcer), 62 | data: abi.encodeWithSelector( 63 | SimpleConditionsEnforcer.currentBlockTimestamp.selector, SimpleConditionsEnforcer.ComparisonType.GE, 100 64 | ) 65 | }); 66 | 67 | IEVC.BatchItem[] memory evcItems = new IEVC.BatchItem[](2); 68 | evcItems[0] = IEVC.BatchItem({ 69 | targetContract: address(vault), 70 | onBehalfOfAccount: alice, 71 | value: 0, 72 | data: abi.encodeWithSelector( 73 | VaultSimple.deposit.selector, 74 | 1e18, 75 | alicesSubAccount // deposit into alice's sub-account 76 | ) 77 | }); 78 | evcItems[1] = IEVC.BatchItem({ 79 | targetContract: address(vault), 80 | onBehalfOfAccount: alice, 81 | value: 0, 82 | data: abi.encodeWithSelector( 83 | VaultSimple.deposit.selector, 84 | 0.01e18, 85 | address(orderOperator) // deposit into the operator's account in order to payout the execution tip in the 86 | // vault's shares 87 | ) 88 | }); 89 | 90 | LightweightOrderOperator.Order memory order = LightweightOrderOperator.Order({ 91 | nonEVCOperations: nonEVCItems, 92 | EVCOperations: evcItems, 93 | submissionTipToken: ERC20(address(0)), // no tip for submission 94 | executionTipToken: ERC20(address(vault)), 95 | salt: 0 96 | }); 97 | 98 | vm.prank(alice); 99 | vm.expectEmit(false, false, false, true, address(orderOperator)); 100 | emit OrderPending(order); 101 | evc.call( 102 | address(orderOperator), alice, 0, abi.encodeWithSelector(LightweightOrderOperator.submit.selector, order) 103 | ); 104 | 105 | // anyone can execute the order now as long as the condition is met 106 | IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); 107 | items[0] = IEVC.BatchItem({ 108 | targetContract: address(orderOperator), 109 | onBehalfOfAccount: address(this), 110 | value: 0, 111 | data: abi.encodeWithSelector( 112 | LightweightOrderOperator.setTipReceiver.selector, 113 | address(this) // set the tip receiver to this contract 114 | ) 115 | }); 116 | items[1] = IEVC.BatchItem({ 117 | targetContract: address(orderOperator), 118 | onBehalfOfAccount: address(this), 119 | value: 0, 120 | data: abi.encodeWithSelector(LightweightOrderOperator.execute.selector, order) 121 | }); 122 | 123 | vm.expectRevert(SimpleConditionsEnforcer.ConditionNotMet.selector); 124 | evc.batch(items); 125 | 126 | // now it succeeds 127 | vm.warp(100); 128 | vm.expectEmit(false, false, false, true, address(orderOperator)); 129 | emit OrderExecuted(keccak256(abi.encode(order)), address(this)); 130 | evc.batch(items); 131 | 132 | assertEq(asset.balanceOf(address(alice)), 98.99e18); 133 | assertEq(vault.maxWithdraw(alice), 0); 134 | assertEq(vault.maxWithdraw(alicesSubAccount), 1e18); 135 | assertEq(vault.maxWithdraw(address(this)), 0.01e18); // tip 136 | 137 | // it's neither possible to cancel the order now nor to submit/execute it again 138 | vm.prank(alice); 139 | vm.expectRevert(LightweightOrderOperator.InvalidOrderState.selector); 140 | evc.call( 141 | address(orderOperator), alice, 0, abi.encodeWithSelector(LightweightOrderOperator.cancel.selector, order) 142 | ); 143 | 144 | vm.prank(alice); 145 | vm.expectRevert(LightweightOrderOperator.InvalidOrderState.selector); 146 | evc.call( 147 | address(orderOperator), alice, 0, abi.encodeWithSelector(LightweightOrderOperator.submit.selector, order) 148 | ); 149 | 150 | vm.expectRevert(LightweightOrderOperator.InvalidOrderState.selector); 151 | evc.batch(items); 152 | 153 | // alice submits an identical order but with a different salt value 154 | order.salt = 1; 155 | vm.prank(alice); 156 | vm.expectEmit(false, false, false, true, address(orderOperator)); 157 | emit OrderPending(order); 158 | evc.call( 159 | address(orderOperator), alice, 0, abi.encodeWithSelector(LightweightOrderOperator.submit.selector, order) 160 | ); 161 | 162 | // and she cancels it right away 163 | vm.prank(alice); 164 | vm.expectEmit(false, false, false, true, address(orderOperator)); 165 | emit OrderCancelled(keccak256(abi.encode(order))); 166 | evc.call( 167 | address(orderOperator), alice, 0, abi.encodeWithSelector(LightweightOrderOperator.cancel.selector, order) 168 | ); 169 | 170 | // so that no one can execute it 171 | items[1].data = abi.encodeWithSelector(LightweightOrderOperator.execute.selector, order); 172 | 173 | vm.expectRevert(LightweightOrderOperator.InvalidOrderState.selector); 174 | evc.batch(items); 175 | 176 | // alice signs a permit message so that anyone can submit the order on her behalf 177 | // and get tipped. 178 | // first update salt and the tip 179 | order.salt = 2; 180 | order.submissionTipToken = ERC20(address(vault)); 181 | 182 | // then prepare the data for the permit 183 | items[0] = IEVC.BatchItem({ 184 | targetContract: address(vault), 185 | onBehalfOfAccount: alice, 186 | value: 0, 187 | data: abi.encodeWithSelector( 188 | VaultSimple.deposit.selector, 189 | 0.01e18, 190 | address(orderOperator) // deposit into the operator's account in order to payout the submission tip in the 191 | // vault's shares 192 | ) 193 | }); 194 | items[1] = IEVC.BatchItem({ 195 | targetContract: address(orderOperator), 196 | onBehalfOfAccount: alice, 197 | value: 0, 198 | data: abi.encodeWithSelector(LightweightOrderOperator.submit.selector, order) 199 | }); 200 | 201 | bytes memory data = abi.encodeWithSelector(IEVC.batch.selector, items); 202 | bytes memory signature = permitSigner.signPermit(alice, address(0), 0, 0, type(uint256).max, 0, data); 203 | 204 | // having the signature, anyone can submit the order and get tipped 205 | items[0] = IEVC.BatchItem({ 206 | targetContract: address(orderOperator), 207 | onBehalfOfAccount: address(this), 208 | value: 0, 209 | data: abi.encodeWithSelector( 210 | LightweightOrderOperator.setTipReceiver.selector, 211 | address(this) // set the tip receiver to this contract 212 | ) 213 | }); 214 | items[1] = IEVC.BatchItem({ 215 | targetContract: address(evc), 216 | onBehalfOfAccount: address(0), 217 | value: 0, 218 | data: abi.encodeWithSelector( 219 | IEVC.permit.selector, alice, address(0), 0, 0, type(uint256).max, 0, data, signature 220 | ) 221 | }); 222 | 223 | vm.expectEmit(false, false, false, true, address(orderOperator)); 224 | emit OrderPending(order); 225 | evc.batch(items); 226 | 227 | assertEq(asset.balanceOf(address(alice)), 98.98e18); 228 | assertEq(vault.maxWithdraw(alice), 0); 229 | assertEq(vault.maxWithdraw(alicesSubAccount), 1e18); 230 | assertEq(vault.maxWithdraw(address(this)), 0.02e18); // tips accumulating! 231 | 232 | // then anyone can execute the order as long as the condition is met 233 | items[1] = IEVC.BatchItem({ 234 | targetContract: address(orderOperator), 235 | onBehalfOfAccount: address(this), 236 | value: 0, 237 | data: abi.encodeWithSelector(LightweightOrderOperator.execute.selector, order) 238 | }); 239 | 240 | vm.warp(99); 241 | vm.expectRevert(SimpleConditionsEnforcer.ConditionNotMet.selector); 242 | evc.batch(items); 243 | 244 | // now it succeeds 245 | vm.warp(100); 246 | vm.expectEmit(false, false, false, true, address(orderOperator)); 247 | emit OrderExecuted(keccak256(abi.encode(order)), address(this)); 248 | evc.batch(items); 249 | 250 | assertEq(asset.balanceOf(address(alice)), 97.97e18); 251 | assertEq(vault.maxWithdraw(alice), 0); 252 | assertEq(vault.maxWithdraw(alicesSubAccount), 2e18); 253 | assertEq(vault.maxWithdraw(address(this)), 0.03e18); // tips accumulating! 254 | 255 | // anyone can also execute the signed order directly if they have an appropriate signature 256 | order.salt = 3; 257 | order.submissionTipToken = ERC20(address(0)); 258 | 259 | data = abi.encodeWithSelector( 260 | IEVC.call.selector, 261 | address(orderOperator), 262 | alice, 263 | 0, 264 | abi.encodeWithSelector(LightweightOrderOperator.execute.selector, order) 265 | ); 266 | signature = permitSigner.signPermit(alice, address(0), 0, 1, type(uint256).max, 0, data); 267 | 268 | items[1] = IEVC.BatchItem({ 269 | targetContract: address(evc), 270 | onBehalfOfAccount: address(0), 271 | value: 0, 272 | data: abi.encodeWithSelector( 273 | IEVC.permit.selector, alice, address(0), 0, 1, type(uint256).max, 0, data, signature 274 | ) 275 | }); 276 | 277 | vm.expectEmit(false, false, false, true, address(orderOperator)); 278 | emit OrderExecuted(keccak256(abi.encode(order)), address(this)); 279 | evc.batch(items); 280 | 281 | assertEq(asset.balanceOf(address(alice)), 96.96e18); 282 | assertEq(vault.maxWithdraw(alice), 0); 283 | assertEq(vault.maxWithdraw(alicesSubAccount), 3e18); 284 | assertEq(vault.maxWithdraw(address(this)), 0.04e18); // tips accumulating! 285 | } 286 | } 287 | --------------------------------------------------------------------------------