├── src ├── workshop_3 │ ├── PositionManager.sol │ ├── questionnaire.md │ └── VaultRegularBorrowable.sol ├── interfaces │ ├── IIRM.sol │ └── IPriceOracle.sol └── workshop_2 │ ├── IWorkshopVault.sol │ └── WorkshopVault.sol ├── .gitignore ├── .gitmodules ├── foundry.toml ├── .github └── workflows │ └── test.yml ├── README.md └── test └── workshop_2 └── WorkshopVault.t.sol /src/workshop_3/PositionManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "evc-playground/vaults/VaultRegularBorrowable.sol"; 6 | 7 | contract PositionManager {} 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | .DS_Store -------------------------------------------------------------------------------- /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 | /// @notice Computes the interest rate for a given vault, asset and utilisation. 7 | /// @param vault The address of the vault. 8 | /// @param asset The address of the asset. 9 | /// @param utilisation The utilisation rate. 10 | /// @return The computed interest rate in SPY (Second Percentage Yield). 11 | function computeInterestRate(address vault, address asset, uint32 utilisation) external returns (uint96); 12 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | [submodule "lib/ethereum-vault-connector"] 8 | path = lib/ethereum-vault-connector 9 | url = https://github.com/euler-xyz/ethereum-vault-connector 10 | [submodule "lib/erc4626-tests"] 11 | path = lib/erc4626-tests 12 | url = https://github.com/a16z/erc4626-tests 13 | [submodule "lib/evc-playground"] 14 | path = lib/evc-playground 15 | url = https://github.com/euler-xyz/evc-playground 16 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc = "0.8.20" 6 | remappings = [ 7 | "forge-std/=lib/forge-std/src/", 8 | "openzeppelin/=lib/openzeppelin-contracts/contracts/", 9 | "evc/=lib/ethereum-vault-connector/src/", 10 | "evc-playground/=lib/evc-playground/src/", 11 | "a16z-erc4626-tests/=lib/erc4626-tests/" 12 | ] 13 | 14 | [profile.default.fmt] 15 | line_length = 120 16 | tab_width = 4 17 | bracket_spacing = false 18 | int_types = "long" 19 | multiline_func_header = "params_first" 20 | quote_style = "double" 21 | number_underscore = "preserve" 22 | override_spacing = true 23 | wrap_comments = true -------------------------------------------------------------------------------- /.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@v4 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/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 | } -------------------------------------------------------------------------------- /src/workshop_2/IWorkshopVault.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 | 7 | interface IWorkshopVault is IVault { 8 | // [ASSIGNMENT]: add borrowing functionality by implementing the following functions: 9 | function borrow(uint256 assets, address receiver) external; 10 | function repay(uint256 assets, address receiver) external; 11 | function pullDebt(address from, uint256 assets) external returns (bool); 12 | function liquidate(address violator, address collateral) external; 13 | 14 | // [ASSIGNMENT]: don't forget that the following functions must be overridden in order to support borrowing: 15 | // [ASSIGNMENT]: - disableController() 16 | // [ASSIGNMENT]: - checkAccountStatus() 17 | // [ASSIGNMENT]: - maxWithdraw() 18 | // [ASSIGNMENT]: - maxRedeem() 19 | // [ASSIGNMENT]: - _convertToShares() 20 | // [ASSIGNMENT]: - _convertToAssets() 21 | 22 | // [ASSIGNMENT]: don't forget about implementing and using modified version of the _msgSender() function for the 23 | // borrowing purposes 24 | 25 | // [ASSIGNMENT] optional: add interest accrual 26 | // [ASSIGNMENT] optional: integrate with an oracle of choice in checkAccountStatus() and liquidate() 27 | // [ASSIGNMENT] optional: implement a circuit breaker in checkVaultStatus(), may be EIP-7265 inspired 28 | // [ASSIGNMENT] optional: add EIP-7540 compatibility for RWAs 29 | } 30 | -------------------------------------------------------------------------------- /src/workshop_3/questionnaire.md: -------------------------------------------------------------------------------- 1 | ## Workshop 3 Assignment: 2 | 3 | 1. How many sub-accounts does an Ethereum address have on the EVC? How are their addresses calculated? 4 | 5 | ``` 6 | Answer: EVC gives every Ethereum account 256 addresses. Sub account addresses are created by XORing the owning address with the sub-account ID. 7 | ``` 8 | 9 | 1. Does the sub-account system decrease the security of user accounts? 10 | 11 | ``` 12 | Answer: No, the su-account system does not decrease the security of the user accounts. 13 | The EVC handles Authentication. Vaults don't need to know anything about sub-accounts. 14 | ``` 15 | 16 | 1. Provide a couple of use cases for the operator functionality of the EVC. 17 | 18 | ``` 19 | Answer: Here are a couple of use-cases for the operator fucntionality: 20 | 1. It can allow a hot-wallet to perform trades but not withdrawals. 21 | 2. It can allow external users to perfrom specific actions on your account based on market conditions. 22 | ``` 23 | 24 | 1. What is the main difference between the operator and the controller? 25 | 26 | ``` 27 | Answer: Controllers are similar to operators, but they can call users associated collateral vaults for liquidations. 28 | ``` 29 | 30 | 1. What does it mean to defer the account and vault status checks? What is the purpose of this deferral? 31 | 32 | ``` 33 | Answer: If a vault is called directly, requiring a vault status check will verify the check is immidiately satisfied. If a vault is called from an EVC batch, all checks are put into a queue and verified later. 34 | The purpose of this deferral is for the checks to be performed at the end of batch. 35 | ``` 36 | 37 | 1. Why is it useful to allow re-entrancy for `call` and `batch` functions? 38 | 39 | ``` 40 | Answer: It helps in crafting a much better user experience by batching multiple and complex interaction into one seamless step. 41 | ``` 42 | 43 | 1. How does the simulation feature of the EVC work? 44 | 45 | ``` 46 | Answer: User add operations to the builder and only when the conditions are satisfied is an actual transaction executed. 47 | ``` 48 | 49 | 1. Provide a couple of use cases for the `permit` functionality of the EVC. 50 | 51 | ``` 52 | Answer: The permit method allows users to sign the batch and have another entitty execute it on their behalf. 53 | Use case 1: It can provide a more general permitting system than allowance in ERC-20 tokens 54 | Use case 2: It can delegate authority to smart contract wallets like with ERC-1271 55 | ``` 56 | 57 | 1. What is the purpose of the nonce namespace? 58 | 59 | ``` 60 | Answer: It's a way to segment streams of execution orders. From the presentation with Doug the nonce namesapces are tracked by the EVC itself. 61 | ``` 62 | 63 | 1. Why should the EVC neither be given any privileges nor hold any tokens? 64 | 65 | ``` 66 | Answer: The EVC should not be given any privileges or hold any tokens due to the risks associated with its ability to execute arbitrary calldata, the control exerted by the Controller Vault, and the diverse nature of assets that can be used as collateral within the protocol. 67 | ``` 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Euler <> Encode Educate 2 | 3 | ## Usage 4 | 5 | Install Foundry: 6 | 7 | ```sh 8 | curl -L https://foundry.paradigm.xyz | bash 9 | ``` 10 | 11 | This will download foundryup. To start Foundry, run: 12 | 13 | ```sh 14 | foundryup 15 | ``` 16 | 17 | Clone the repo and install dependencies: 18 | 19 | ```sh 20 | git clone https://github.com/euler-xyz/euler-encode-workshop.git && cd euler-encode-workshop && forge install && forge update 21 | ``` 22 | 23 | ## Slides 24 | 25 | Workshop presentation slides can be found here: 26 | 27 | * [Workshop 1](https://docs.google.com/presentation/d/1nQfDXEJFMHLgT8JYrPZxzeVS3b5mPBwLhJOuTntjzyo/edit?usp=sharing) 28 | * [Workshop 2](https://docs.google.com/presentation/d/1cYceiIXRDbtpzzimj0QuOh4wY53ZfSjKYaugQz_cql0/edit?usp=sharing) 29 | * [Workshop 3](https://docs.google.com/presentation/d/1RvB05rKljiRSf9Dl-FJYot8Ct1iLWpGmP3fwGYxGCrQ/edit?usp=sharing) 30 | 31 | ## Assignments 32 | 33 | Fork this repository and complete the assignments. Create a PR to merge your solution with the `master` branch of this repository. To do that, follow the instructions: 34 | 35 | |Assignment|`branch-name`|Prize Pool|Rules|Deadline| 36 | |---|---|---|---|---| 37 | |Workshop 2 Questionnaire|`assignment-q2`|$500|FCFS for 10 participants|No deadline| 38 | |Workshop 3 Questionnaire|`assignment-q3`|$500|FCFS for 10 participants|No deadline| 39 | |Workshop 2/3 Coding Assignment|`assignment-c`|$2000|5 best submissions; both Workshops judged together|Jan 31st midnight UTC| 40 | 41 | 1. Fork the Repository 42 | 43 | First, you need to fork this repository on GitHub. Go to the [repository](https://github.com/euler-xyz/euler-encode-workshop.git) and click the "Fork" button in the upper right corner. 44 | 45 | 2. Clone and navigate to the Forked Repository 46 | 47 | Now, clone the forked repository to your local machine. Replace `your-username` with your GitHub username. 48 | 49 | ```sh 50 | git clone https://github.com/your-username/euler-encode-workshop.git && cd euler-encode-workshop && forge install && forge update 51 | ``` 52 | 53 | 3. Create a New Branch 54 | 55 | Create a new branch for your assignment. Replace `branch-name` with the name relevant to the assignment you wish to complete as per the table above. 56 | 57 | ```sh 58 | git checkout master && git checkout -b branch-name 59 | ``` 60 | 61 | 4. Complete the Assignment 62 | 63 | At this point, you can start working on the assignment. Make changes to the files as necessary. For details look below. 64 | 65 | 5. Stage, Commit and Push Your Changes 66 | 67 | Once you've completed the assignments, stage and commit your changes. Push your changes to your forked repository on GitHub. Replace `branch-name` accordingly. 68 | 69 | ```sh 70 | git add . && git commit -m "assignment completed" && git push origin branch-name 71 | ``` 72 | 73 | 6. Create a Pull Request 74 | 75 | Finally, go back to your forked repository on the GitHub website and click "Pull requests" at the top and then click "New pull request". From the dropdown menu, select the relevant branch of your forked repository and `master` branch of the original repository, then click "Create pull request". The PR title should be as in the Assignment column from the table above. 76 | 77 | 7. Repeat 78 | 79 | If you are completing more than one assignment, repeat steps 3-6 for each assignment using different branch names and creating new PRs. If you wish to complete all the assignments, you should have at most 3 PRs. Coding Assignment from both Workshop 2 and 3 should be submitted in the same PR. 80 | 81 | ### Workshop 2 82 | 83 | #### Questionnaire 84 | Answer the EVC related questions tagged with `[ASSIGNMENT]` which can be found in the source [file](./src/workshop_2/WorkshopVault.sol). The questions should be answered inline in the source file. 85 | 86 | #### Coding Assignment 87 | Add borrowing functionality to the workshop [vault](./src/workshop_2/WorkshopVault.sol) as per additional instructions in the interface [file](./src/workshop_2/IWorkshopVault.sol). You should not modify the vault constructor, otherwise the tests will not compile. Run `forge compile` or `forge test` before submitting to check if everything's in order. 88 | 89 | ### Workshop 3 90 | 91 | #### Questionnaire 92 | Answer the EVC related questions which can be found in the assignment [file](./src/workshop_3/questionnaire.md). The questions should be answered inline in the file. 93 | 94 | #### Coding Assignment 95 | Taking from the EVC operator concept, and using `VaultRegularBorrowable` [contract](https://github.com/euler-xyz/evc-playground/blob/master/src/vaults/VaultRegularBorrowable.sol) from the [evc-playground repository](https://github.com/euler-xyz/evc-playground), build a simple position manager that allows keepers to rebalance assets between multiple vaults of user's choice. Whether the assets should be rebalanced or not should be determined based on a predefined condition, i.e. deposit everything into a vault with the highest APY at the moment, but rebalance no more often than every day. The solution should be provided in the dedicated source [file](./src/workshop_3/PositionManager.sol). 96 | 97 | ### Resources 98 | 99 | 1. [EVC docs](https://www.evc.wtf) 100 | 1. [EVC repository](https://github.com/euler-xyz/ethereum-vault-connector) 101 | 1. [EVC playground repository](https://github.com/euler-xyz/evc-playground) 102 | -------------------------------------------------------------------------------- /src/workshop_2/WorkshopVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "openzeppelin/token/ERC20/extensions/ERC4626.sol"; 6 | import "evc/interfaces/IEthereumVaultConnector.sol"; 7 | import "evc/interfaces/IVault.sol"; 8 | import "./IWorkshopVault.sol"; 9 | 10 | contract WorkshopVault is ERC4626, IVault, IWorkshopVault { 11 | IEVC internal immutable evc; 12 | 13 | constructor( 14 | IEVC _evc, 15 | IERC20 _asset, 16 | string memory _name, 17 | string memory _symbol 18 | ) ERC4626(_asset) ERC20(_name, _symbol) { 19 | evc = _evc; 20 | } 21 | 22 | modifier callThroughEVC() { 23 | if (msg.sender == address(evc)) { 24 | _; 25 | } else { 26 | bytes memory result = evc.call(address(this), msg.sender, 0, msg.data); 27 | 28 | assembly { 29 | return(add(32, result), mload(result)) 30 | } 31 | } 32 | } 33 | 34 | modifier withChecks(address account) { 35 | _; 36 | 37 | if (account == address(0)) { 38 | evc.requireVaultStatusCheck(); 39 | } else { 40 | evc.requireAccountAndVaultStatusCheck(account); 41 | } 42 | } 43 | 44 | function _msgSender() internal view virtual override returns (address) { 45 | if (msg.sender == address(evc)) { 46 | (address onBehalfOfAccount,) = evc.getCurrentOnBehalfOfAccount(address(0)); 47 | return onBehalfOfAccount; 48 | } else { 49 | return msg.sender; 50 | } 51 | } 52 | 53 | // IVault 54 | function disableController() external override{ 55 | evc.disableController(_msgSender()); 56 | } 57 | 58 | function checkAccountStatus( 59 | address account, 60 | address[] calldata collaterals 61 | ) public virtual override returns (bytes4 magicValue) { 62 | require(msg.sender == address(evc), "only evc can call this"); 63 | require(evc.areChecksInProgress(), "can only be called when checks in progress"); 64 | 65 | // [TODO]: Write some custom logic evaluating the account health 66 | // [TODO]: Check if it stays above a minimum threshold after considering the intended borrow amount. 67 | // [TODO]: Compare the existing debt to the maximum allowed debt ratio or absolute limit. 68 | // [TODO]: Consider factors like past repayment behavior and delinquencies. 69 | 70 | return IVault.checkAccountStatus.selector; 71 | } 72 | 73 | function checkVaultStatus() public virtual returns (bytes4 magicValue) { 74 | require(msg.sender == address(evc), "only evc can call this"); 75 | require(evc.areChecksInProgress(), "can only be called when checks in progress"); 76 | 77 | // [TODO]: Write some custom logic evaluating the vault health 78 | 79 | return IVault.checkVaultStatus.selector; 80 | } 81 | 82 | function deposit( 83 | uint256 assets, 84 | address receiver 85 | ) public virtual override callThroughEVC withChecks(address(0)) returns (uint256 shares) { 86 | return super.deposit(assets, receiver); 87 | } 88 | 89 | function mint( 90 | uint256 shares, 91 | address receiver 92 | ) public virtual override callThroughEVC withChecks(address(0)) returns (uint256 assets) { 93 | return super.mint(shares, receiver); 94 | } 95 | 96 | function withdraw( 97 | uint256 assets, 98 | address receiver, 99 | address owner 100 | ) public virtual override callThroughEVC withChecks(owner) returns (uint256 shares) { 101 | return super.withdraw(assets, receiver, owner); 102 | } 103 | 104 | function redeem( 105 | uint256 shares, 106 | address receiver, 107 | address owner 108 | ) public virtual override callThroughEVC withChecks(owner) returns (uint256 assets) { 109 | return super.redeem(shares, receiver, owner); 110 | } 111 | 112 | function transfer( 113 | address to, 114 | uint256 value 115 | ) public virtual override (ERC20, IERC20) callThroughEVC withChecks(_msgSender()) returns (bool) { 116 | return super.transfer(to, value); 117 | } 118 | 119 | function transferFrom( 120 | address from, 121 | address to, 122 | uint256 value 123 | ) public virtual override (ERC20, IERC20) callThroughEVC withChecks(from) returns (bool) { 124 | return super.transferFrom(from, to, value); 125 | } 126 | 127 | // IWorkshopVault 128 | function borrow(uint256 assets, address receiver) external { 129 | require(isBorrowingAllowed(_msgSender(), assets), "Borrowing not allowed"); 130 | 131 | updateCollateralizationRatio(_msgSender(), assets); 132 | 133 | _asset.safeTransfer(receiver, assets); 134 | _borrowed[_msgSender()] += assets; 135 | // [TODO:] add interest accrual logic here 136 | 137 | emit Borrowed(_msgSender(), assets); 138 | } 139 | 140 | function repay(uint256 assets, address receiver) external { 141 | require(_asset.balanceOf(_msgSender()) >= assets, "Insufficient assets for repayment"); 142 | 143 | updateCollateralizationRatio(_msgSender(), -assets); // Negative value for repayment 144 | 145 | _asset.safeTransferFrom(_msgSender(), receiver, assets); 146 | _borrowed[_msgSender()] -= assets; 147 | // [TODO] Calculate interest accrued 148 | 149 | emit Repaid(_msgSender(), assets); 150 | } 151 | 152 | function pullDebt(address from, uint256 assets) external returns (bool) { 153 | require(_borrowed[from] >= assets, "Insufficient debt to pull"); 154 | 155 | // [TODO]: Transfer collateral to caller to cover debt 156 | 157 | _borrowed[from] -= assets; 158 | updateCollateralizationRatio(from, -assets); 159 | 160 | emit DebtPulled(from, assets); 161 | 162 | return true; 163 | } 164 | 165 | function liquidate(address violator, address collateral) external { 166 | require(isLiquidationAllowed(violator), "Liquidation not allowed"); 167 | 168 | // [TODO]: Seize collateral 169 | 170 | // [TODO]: Use seized collateral to repay debt 171 | 172 | // [TODO]: Distribute any surplus to liquidator or other stakeholders 173 | 174 | // 5. Emit event for liquidation 175 | emit Liquidated(violator, collateral); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /test/workshop_2/WorkshopVault.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import {ERC4626Test} from "a16z-erc4626-tests/ERC4626.test.sol"; 6 | import "openzeppelin/mocks/token/ERC20Mock.sol"; 7 | import "evc/EthereumVaultConnector.sol"; 8 | import "../../src/workshop_2/WorkshopVault.sol"; 9 | 10 | contract TestVault is WorkshopVault { 11 | bool internal shouldRunOriginalAccountStatusCheck; 12 | bool internal shouldRunOriginalVaultStatusCheck; 13 | 14 | constructor( 15 | IEVC _evc, 16 | IERC20 _asset, 17 | string memory _name, 18 | string memory _symbol 19 | ) WorkshopVault(_evc, _asset, _name, _symbol) {} 20 | 21 | function setShouldRunOriginalAccountStatusCheck(bool _shouldRunOriginalAccountStatusCheck) external { 22 | shouldRunOriginalAccountStatusCheck = _shouldRunOriginalAccountStatusCheck; 23 | } 24 | 25 | function setShouldRunOriginalVaultStatusCheck(bool _shouldRunOriginalVaultStatusCheck) external { 26 | shouldRunOriginalVaultStatusCheck = _shouldRunOriginalVaultStatusCheck; 27 | } 28 | 29 | function checkAccountStatus( 30 | address account, 31 | address[] calldata collaterals 32 | ) public override returns (bytes4 magicValue) { 33 | return shouldRunOriginalAccountStatusCheck 34 | ? super.checkAccountStatus(account, collaterals) 35 | : this.checkAccountStatus.selector; 36 | } 37 | 38 | function checkVaultStatus() public override returns (bytes4 magicValue) { 39 | return shouldRunOriginalVaultStatusCheck ? super.checkVaultStatus() : this.checkVaultStatus.selector; 40 | } 41 | } 42 | 43 | contract VaultTest is ERC4626Test { 44 | IEVC _evc_; 45 | 46 | function setUp() public override { 47 | _evc_ = new EthereumVaultConnector(); 48 | _underlying_ = address(new ERC20Mock()); 49 | _delta_ = 0; 50 | _vaultMayBeEmpty = false; 51 | _unlimitedAmount = false; 52 | _vault_ = address( 53 | new TestVault( 54 | _evc_, 55 | IERC20(_underlying_), 56 | "Vault", 57 | "VLT" 58 | ) 59 | ); 60 | } 61 | 62 | function test_DepositWithEVC(address alice, uint64 amount) public { 63 | vm.assume(alice != address(0) && alice != address(_evc_) && alice != address(_vault_)); 64 | vm.assume(amount > 0); 65 | 66 | ERC20 underlying = ERC20(_underlying_); 67 | WorkshopVault vault = WorkshopVault(_vault_); 68 | 69 | // mint some assets to alice 70 | ERC20Mock(_underlying_).mint(alice, amount); 71 | 72 | // alice approves the vault to spend her assets 73 | vm.prank(alice); 74 | underlying.approve(address(vault), type(uint256).max); 75 | 76 | // alice deposits assets through the EVC 77 | vm.prank(alice); 78 | _evc_.call(address(vault), alice, 0, abi.encodeWithSelector(IERC4626.deposit.selector, amount, alice)); 79 | 80 | // verify alice's balance 81 | assertEq(underlying.balanceOf(alice), 0); 82 | assertEq(vault.convertToAssets(vault.balanceOf(alice)), amount); 83 | } 84 | 85 | function test_assignment_BasicFlow(address alice, address bob, address charlie, uint64 amount) public { 86 | vm.assume(alice != address(0) && alice != address(_evc_) && alice != address(_vault_)); 87 | vm.assume(bob != address(0) && bob != address(_evc_) && bob != address(_vault_)); 88 | vm.assume(charlie != address(0) && charlie != address(_evc_) && charlie != address(_vault_)); 89 | vm.assume( 90 | !_evc_.haveCommonOwner(alice, bob) && !_evc_.haveCommonOwner(alice, charlie) 91 | && !_evc_.haveCommonOwner(bob, charlie) 92 | ); 93 | vm.assume(amount > 100); 94 | 95 | uint256 amountToBorrow = amount / 2; 96 | ERC20 underlying = ERC20(_underlying_); 97 | WorkshopVault vault = WorkshopVault(_vault_); 98 | 99 | // mint some assets to alice 100 | ERC20Mock(_underlying_).mint(alice, amount); 101 | 102 | // make charlie an operator of alice's account 103 | vm.prank(alice); 104 | _evc_.setAccountOperator(alice, charlie, true); 105 | 106 | // alice approves the vault to spend her assets 107 | vm.prank(alice); 108 | underlying.approve(address(vault), type(uint256).max); 109 | 110 | // charlie deposits assets on alice's behalf 111 | vm.prank(charlie); 112 | _evc_.call(address(vault), alice, 0, abi.encodeWithSelector(IERC4626.deposit.selector, amount, alice)); 113 | 114 | // verify alice's balance 115 | assertEq(underlying.balanceOf(alice), 0); 116 | assertEq(vault.convertToAssets(vault.balanceOf(alice)), amount); 117 | 118 | // alice tries to borrow assets from the vault, should fail due to controller disabled 119 | vm.prank(alice); 120 | vm.expectRevert(); 121 | vault.borrow(amount, alice); 122 | 123 | // alice enables controller 124 | vm.prank(alice); 125 | _evc_.enableController(alice, address(vault)); 126 | 127 | // alice tries to borrow again, now it should succeed 128 | vm.prank(alice); 129 | vault.borrow(amountToBorrow, alice); 130 | 131 | // varify alice's balance. despite amount borrowed, she should still hold shares worth the full amount 132 | assertEq(underlying.balanceOf(alice), amountToBorrow); 133 | assertEq(vault.convertToAssets(vault.balanceOf(alice)), amount); 134 | 135 | // verify maxWithdraw and maxRedeem functions 136 | assertEq(vault.maxWithdraw(alice), amount - amountToBorrow); 137 | assertEq(vault.convertToAssets(vault.maxRedeem(alice)), amount - amountToBorrow); 138 | 139 | // verify conversion functions 140 | assertEq(vault.convertToShares(amount), vault.balanceOf(alice)); 141 | assertEq(vault.convertToAssets(vault.balanceOf(alice)), amount); 142 | 143 | // alice tries to disable controller, it should fail due to outstanding debt 144 | vm.prank(alice); 145 | vm.expectRevert(); 146 | vault.disableController(); 147 | 148 | // bob tries to pull some debt from alice's account, it should fail due to disabled controller 149 | vm.prank(bob); 150 | vm.expectRevert(); 151 | vault.pullDebt(alice, amountToBorrow / 2); 152 | 153 | // bob enables controller 154 | vm.prank(bob); 155 | _evc_.enableController(bob, address(vault)); 156 | 157 | // bob tries again to pull some debt from alice's account, it should succeed now 158 | vm.prank(bob); 159 | vault.pullDebt(alice, amountToBorrow / 2); 160 | 161 | // charlie repays part of alice's debt using her assets 162 | vm.prank(charlie); 163 | _evc_.call( 164 | address(vault), 165 | alice, 166 | 0, 167 | abi.encodeWithSelector(WorkshopVault.repay.selector, amountToBorrow - amountToBorrow / 2, alice) 168 | ); 169 | 170 | // verify alice's balance 171 | assertEq(underlying.balanceOf(alice), amountToBorrow / 2); 172 | assertEq(vault.convertToAssets(vault.balanceOf(alice)), amount); 173 | 174 | // alice can disable the controller now 175 | vm.prank(alice); 176 | vault.disableController(); 177 | 178 | // bob tries to disable the controller, it should fail due to outstanding debt 179 | vm.prank(bob); 180 | vm.expectRevert(); 181 | vault.disableController(); 182 | 183 | // alice repays bob's debt 184 | vm.prank(alice); 185 | vault.repay(amountToBorrow / 2, bob); 186 | 187 | // verify bob's balance 188 | assertEq(underlying.balanceOf(bob), 0); 189 | 190 | // bob can disable the controller now 191 | vm.prank(bob); 192 | vault.disableController(); 193 | } 194 | 195 | function test_assigment_InterestAccrual(address alice, uint64 amount) public { 196 | vm.assume(alice != address(0) && alice != address(_evc_) && alice != address(_vault_)); 197 | vm.assume(amount > 1e18); 198 | 199 | ERC20 underlying = ERC20(_underlying_); 200 | WorkshopVault vault = WorkshopVault(_vault_); 201 | 202 | // mint some assets to alice 203 | ERC20Mock(_underlying_).mint(alice, amount); 204 | 205 | // alice approves the vault to spend her assets 206 | vm.prank(alice); 207 | underlying.approve(address(vault), type(uint256).max); 208 | 209 | // alice deposits the assets 210 | vm.prank(alice); 211 | vault.deposit(amount, alice); 212 | 213 | // verify alice's balance 214 | assertEq(underlying.balanceOf(alice), 0); 215 | assertEq(vault.convertToAssets(vault.balanceOf(alice)), amount); 216 | 217 | // alice enables controller 218 | vm.prank(alice); 219 | _evc_.enableController(alice, address(vault)); 220 | 221 | // alice borrows assets from the vault 222 | vm.prank(alice); 223 | vault.borrow(amount, alice); 224 | 225 | // allow some time to pass to check if interest accrues 226 | vm.roll(365 days); 227 | vm.warp(365 days / 12); 228 | 229 | // repay the amount borrowed. if interest accrues, alice should still have outstanding debt 230 | vm.prank(alice); 231 | vault.repay(amount, alice); 232 | 233 | // try to disable controller, it should fail due to outstanding debt if interest accrues 234 | vm.prank(alice); 235 | vm.expectRevert(); 236 | vault.disableController(); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/workshop_3/VaultRegularBorrowable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import "../interfaces/IIRM.sol"; 6 | import "../interfaces/IPriceOracle.sol"; 7 | import "./VaultSimpleBorrowable.sol"; 8 | 9 | /// @title VaultRegularBorrowable 10 | /// @notice This contract extends VaultSimpleBorrowable with additional features like interest rate accrual and 11 | /// recognition of external collateral vaults and liquidations. 12 | contract VaultRegularBorrowable is VaultSimpleBorrowable { 13 | using FixedPointMathLib for uint256; 14 | 15 | uint256 internal constant COLLATERAL_FACTOR_SCALE = 100; 16 | uint256 internal constant MAX_LIQUIDATION_INCENTIVE = 20; 17 | uint256 internal constant TARGET_HEALTH_FACTOR = 125; 18 | uint256 internal constant ONE = 1e27; 19 | 20 | uint96 internal interestRate; 21 | uint256 internal lastInterestUpdate; 22 | uint256 internal interestAccumulator; 23 | mapping(address account => uint256) internal userInterestAccumulator; 24 | mapping(ERC4626 vault => uint256) internal collateralFactor; 25 | 26 | // IRM 27 | IIRM public irm; 28 | 29 | // oracle 30 | ERC20 public referenceAsset; // This is the asset that we use to calculate the value of all other assets 31 | IPriceOracle public oracle; 32 | 33 | error InvalidCollateralFactor(); 34 | error SelfLiquidation(); 35 | error VaultStatusCheckDeferred(); 36 | error ViolatorStatusCheckDeferred(); 37 | error NoLiquidationOpportunity(); 38 | error RepayAssetsInsufficient(); 39 | error RepayAssetsExceeded(); 40 | error CollateralDisabled(); 41 | 42 | constructor( 43 | IEVC _evc, 44 | ERC20 _asset, 45 | IIRM _irm, 46 | IPriceOracle _oracle, 47 | ERC20 _referenceAsset, 48 | string memory _name, 49 | string memory _symbol 50 | ) VaultSimpleBorrowable(_evc, _asset, _name, _symbol) { 51 | irm = _irm; 52 | oracle = _oracle; 53 | referenceAsset = _referenceAsset; 54 | lastInterestUpdate = block.timestamp; 55 | interestAccumulator = ONE; 56 | } 57 | 58 | /// @notice Sets the IRM of the vault. 59 | /// @param _irm The new IRM. 60 | function setIRM(IIRM _irm) external onlyOwner { 61 | irm = _irm; 62 | } 63 | 64 | /// @notice Sets the reference asset of the vault. 65 | /// @param _referenceAsset The new reference asset. 66 | function setReferenceAsset(ERC20 _referenceAsset) external onlyOwner { 67 | referenceAsset = _referenceAsset; 68 | } 69 | 70 | /// @notice Sets the price oracle of the vault. 71 | /// @param _oracle The new price oracle. 72 | function setOracle(IPriceOracle _oracle) external onlyOwner { 73 | oracle = _oracle; 74 | } 75 | 76 | /// @notice Sets the collateral factor of a vault. 77 | /// @param vault The vault. 78 | /// @param _collateralFactor The new collateral factor. 79 | function setCollateralFactor(ERC4626 vault, uint256 _collateralFactor) external onlyOwner { 80 | if (_collateralFactor > COLLATERAL_FACTOR_SCALE) { 81 | revert InvalidCollateralFactor(); 82 | } 83 | 84 | collateralFactor[vault] = _collateralFactor; 85 | } 86 | 87 | /// @notice Gets the current interest rate of the vault. 88 | /// @dev Reverts if the vault status check is deferred because the interest rate is calculated in the 89 | /// checkVaultStatus(). 90 | /// @return The current interest rate. 91 | function getInterestRate() external view nonReentrantRO returns (uint256) { 92 | if (isVaultStatusCheckDeferred(address(this))) { 93 | revert VaultStatusCheckDeferred(); 94 | } 95 | 96 | return interestRate; 97 | } 98 | 99 | /// @notice Gets the collateral factor of a vault. 100 | /// @param vault The vault. 101 | /// @return The collateral factor. 102 | function getCollateralFactor(ERC4626 vault) external view nonReentrantRO returns (uint256) { 103 | return collateralFactor[vault]; 104 | } 105 | 106 | /// @notice Checks the status of an account. 107 | /// @param account The account. 108 | /// @param collaterals The collaterals of the account. 109 | function doCheckAccountStatus(address account, address[] calldata collaterals) internal view virtual override { 110 | if (_debtOf(account) > 0) { 111 | (, uint256 liabilityValue, uint256 collateralValue) = _calculateLiabilityAndCollateral(account, collaterals); 112 | 113 | if (liabilityValue > collateralValue) { 114 | revert AccountUnhealthy(); 115 | } 116 | } 117 | } 118 | 119 | /// @notice Liquidates a violator account. 120 | /// @param violator The violator account. 121 | /// @param collateral The collateral of the violator. 122 | /// @param repayAssets The assets to repay. 123 | function liquidate( 124 | address violator, 125 | address collateral, 126 | uint256 repayAssets 127 | ) external callThroughEVC nonReentrant { 128 | address msgSender = _msgSenderForBorrow(); 129 | 130 | if (msgSender == violator) { 131 | revert SelfLiquidation(); 132 | } 133 | 134 | if (repayAssets == 0) { 135 | revert RepayAssetsInsufficient(); 136 | } 137 | 138 | // due to later violator's account check forgiveness, 139 | // the violator's account must be fully settled when liquidating 140 | if (isAccountStatusCheckDeferred(violator)) { 141 | revert ViolatorStatusCheckDeferred(); 142 | } 143 | 144 | // sanity check: the violator must be under control of the EVC 145 | if (!isControllerEnabled(violator, address(this))) { 146 | revert ControllerDisabled(); 147 | } 148 | 149 | createVaultSnapshot(); 150 | 151 | uint256 seizeShares = _calculateSharesToSeize(violator, collateral, repayAssets); 152 | 153 | _decreaseOwed(violator, repayAssets); 154 | _increaseOwed(msgSender, repayAssets); 155 | 156 | emit Repay(msgSender, violator, repayAssets); 157 | emit Borrow(msgSender, msgSender, repayAssets); 158 | 159 | if (collateral == address(this)) { 160 | // if the liquidator tries to seize the assets from this vault, 161 | // we need to be sure that the violator has enabled this vault as collateral 162 | if (!isCollateralEnabled(violator, collateral)) { 163 | revert CollateralDisabled(); 164 | } 165 | 166 | balanceOf[violator] -= seizeShares; 167 | balanceOf[msgSender] += seizeShares; 168 | 169 | emit Transfer(violator, msgSender, seizeShares); 170 | } else { 171 | // if external assets are being seized, the EVC will take care of safety 172 | // checks during the collateral control 173 | liquidateCollateralShares(collateral, violator, msgSender, seizeShares); 174 | 175 | // there's a possibility that the liquidation does not bring the violator back to 176 | // a healthy state or the liquidator chooses not to repay enough to bring the violator 177 | // back to health. hence, the account status check that is scheduled during the 178 | // controlCollateral may fail reverting the liquidation. hence, as a controller, we 179 | // can forgive the account status check for the violator allowing it to end up in 180 | // an unhealthy state after the liquidation. 181 | // IMPORTANT: the account status check forgiveness must be done with care! 182 | // a malicious collateral could do some funky stuff during the controlCollateral 183 | // leading to withdrawal of more collateral than specified, or withdrawal of other 184 | // collaterals, leaving us with bad debt. to prevent that, we ensure that only 185 | // collaterals with cf > 0 can be seized which means that only vetted collaterals 186 | // are seizable and cannot do any harm during the controlCollateral. 187 | // the other option would be to snapshot the balances of all the collaterals 188 | // before the controlCollateral and compare them with expected balances after the 189 | // controlCollateral. however, this is out of scope for this playground. 190 | forgiveAccountStatusCheck(violator); 191 | } 192 | 193 | requireAccountAndVaultStatusCheck(msgSender); 194 | } 195 | 196 | /// @notice Calculates the liability and collateral of an account. 197 | /// @param account The account. 198 | /// @param collaterals The collaterals of the account. 199 | /// @return liabilityAssets The liability assets. 200 | /// @return liabilityValue The liability value. 201 | /// @return collateralValue The risk-adjusted collateral value. 202 | function _calculateLiabilityAndCollateral( 203 | address account, 204 | address[] memory collaterals 205 | ) internal view returns (uint256 liabilityAssets, uint256 liabilityValue, uint256 collateralValue) { 206 | liabilityAssets = _debtOf(account); 207 | 208 | // Calculate the value of the liability in terms of the reference asset 209 | liabilityValue = IPriceOracle(oracle).getQuote(liabilityAssets, address(asset), address(referenceAsset)); 210 | 211 | // Calculate the aggregated value of the collateral in terms of the reference asset 212 | for (uint256 i = 0; i < collaterals.length; ++i) { 213 | ERC4626 collateral = ERC4626(collaterals[i]); 214 | uint256 cf = collateralFactor[collateral]; 215 | 216 | // Collaterals with a collateral factor of 0 are worthless 217 | if (cf != 0) { 218 | uint256 collateralShares = collateral.balanceOf(account); 219 | 220 | if (collateralShares > 0) { 221 | uint256 collateralAssets = collateral.convertToAssets(collateralShares); 222 | 223 | collateralValue += ( 224 | IPriceOracle(oracle).getQuote( 225 | collateralAssets, address(collateral.asset()), address(referenceAsset) 226 | ) * cf 227 | ) / COLLATERAL_FACTOR_SCALE; 228 | } 229 | } 230 | } 231 | } 232 | 233 | /// @notice Calculates the amount of shares to seize from a violator's account during a liquidation event. 234 | /// @dev This function is used during the liquidation process to determine the amount of collateral to seize. 235 | /// @param violator The address of the violator's account. 236 | /// @param collateral The address of the collateral to be seized. 237 | /// @param repayAssets The amount of assets the liquidator is attempting to repay. 238 | /// @return The amount of collateral shares to seize from the violator's account. 239 | function _calculateSharesToSeize( 240 | address violator, 241 | address collateral, 242 | uint256 repayAssets 243 | ) internal view returns (uint256) { 244 | // do not allow to seize the assets for collateral without a collateral factor. 245 | // note that a user can enable any address as collateral, even if it's not recognized 246 | // as such (cf == 0) 247 | uint256 cf = collateralFactor[ERC4626(collateral)]; 248 | if (cf == 0) { 249 | revert CollateralDisabled(); 250 | } 251 | 252 | (uint256 liabilityAssets, uint256 liabilityValue, uint256 collateralValue) = 253 | _calculateLiabilityAndCollateral(violator, getCollaterals(violator)); 254 | 255 | // trying to repay more than the violator owes 256 | if (repayAssets > liabilityAssets) { 257 | revert RepayAssetsExceeded(); 258 | } 259 | 260 | // check if violator's account is unhealthy 261 | if (collateralValue >= liabilityValue) { 262 | revert NoLiquidationOpportunity(); 263 | } 264 | 265 | // calculate dynamic liquidation incentive 266 | uint256 liquidationIncentive = 100 - (100 * collateralValue) / liabilityValue; 267 | 268 | if (liquidationIncentive > MAX_LIQUIDATION_INCENTIVE) { 269 | liquidationIncentive = MAX_LIQUIDATION_INCENTIVE; 270 | } 271 | 272 | // calculate the max repay value that will bring the violator back to target health factor 273 | uint256 maxRepayValue = (TARGET_HEALTH_FACTOR * liabilityValue - 100 * collateralValue) 274 | / (TARGET_HEALTH_FACTOR - (cf * (100 + liquidationIncentive)) / 100); 275 | 276 | // get the desired value of repay assets 277 | uint256 repayValue = IPriceOracle(oracle).getQuote(repayAssets, address(asset), address(referenceAsset)); 278 | 279 | // check if the liquidator is not trying to repay too much. 280 | // this prevents the liquidator from liquidating entire position if not necessary. 281 | // if the at least half of the debt needs to be repaid to bring the account back to target health factor, 282 | // the liquidator can repay the entire debt. 283 | if (repayValue > maxRepayValue && maxRepayValue < liabilityValue / 2) { 284 | revert RepayAssetsExceeded(); 285 | } 286 | 287 | // the liquidator will be transferred the collateral value of the repaid debt + the liquidation incentive 288 | address collateralAsset = address(ERC4626(collateral).asset()); 289 | uint256 collateralUnit = 10 ** ERC20(collateralAsset).decimals(); 290 | 291 | uint256 seizeValue = (repayValue * (100 + liquidationIncentive)) / 100; 292 | 293 | uint256 seizeAssets = (seizeValue * collateralUnit) 294 | / IPriceOracle(oracle).getQuote(collateralUnit, collateralAsset, address(referenceAsset)); 295 | 296 | uint256 seizeShares = ERC4626(collateral).convertToShares(seizeAssets); 297 | 298 | if (seizeShares == 0) { 299 | revert RepayAssetsInsufficient(); 300 | } 301 | 302 | return seizeShares; 303 | } 304 | 305 | /// @notice Increases the owed amount of an account. 306 | /// @dev This function is overridden to snapshot the interest accumulator for the account. 307 | /// @param account The account. 308 | /// @param assets The assets. 309 | function _increaseOwed(address account, uint256 assets) internal virtual override { 310 | super._increaseOwed(account, assets); 311 | userInterestAccumulator[account] = interestAccumulator; 312 | } 313 | 314 | /// @notice Decreases the owed amount of an account. 315 | /// @dev This function is overridden to snapshot the interest accumulator for the account. 316 | /// @param account The account. 317 | /// @param assets The assets. 318 | function _decreaseOwed(address account, uint256 assets) internal virtual override { 319 | super._decreaseOwed(account, assets); 320 | userInterestAccumulator[account] = interestAccumulator; 321 | } 322 | 323 | /// @notice Returns the debt of an account. 324 | /// @dev This function is overridden to take into account the interest rate accrual. 325 | /// @param account The account. 326 | /// @return The debt of the account. 327 | function _debtOf(address account) internal view virtual override returns (uint256) { 328 | uint256 debt = owed[account]; 329 | 330 | if (debt == 0) return 0; 331 | 332 | (, uint256 currentInterestAccumulator,) = _accrueInterestCalculate(); 333 | return (debt * currentInterestAccumulator) / userInterestAccumulator[account]; 334 | } 335 | 336 | /// @notice Accrues interest. 337 | /// @return The current values of total borrowed and interest accumulator. 338 | function _accrueInterest() internal virtual override returns (uint256, uint256) { 339 | (uint256 currentTotalBorrowed, uint256 currentInterestAccumulator, bool shouldUpdate) = 340 | _accrueInterestCalculate(); 341 | 342 | if (shouldUpdate) { 343 | totalBorrowed = currentTotalBorrowed; 344 | interestAccumulator = currentInterestAccumulator; 345 | lastInterestUpdate = block.timestamp; 346 | } 347 | 348 | return (currentTotalBorrowed, currentInterestAccumulator); 349 | } 350 | 351 | /// @notice Calculates the accrued interest. 352 | /// @return The total borrowed amount, the interest accumulator and a boolean value that indicates whether the data 353 | /// should be updated. 354 | function _accrueInterestCalculate() internal view virtual override returns (uint256, uint256, bool) { 355 | uint256 timeElapsed = block.timestamp - lastInterestUpdate; 356 | uint256 oldTotalBorrowed = totalBorrowed; 357 | uint256 oldInterestAccumulator = interestAccumulator; 358 | 359 | if (timeElapsed == 0) { 360 | return (oldTotalBorrowed, oldInterestAccumulator, false); 361 | } 362 | 363 | uint256 newInterestAccumulator = 364 | (FixedPointMathLib.rpow(uint256(interestRate) + ONE, timeElapsed, ONE) * oldInterestAccumulator) / ONE; 365 | 366 | uint256 newTotalBorrowed = (oldTotalBorrowed * newInterestAccumulator) / oldInterestAccumulator; 367 | 368 | return (newTotalBorrowed, newInterestAccumulator, true); 369 | } 370 | 371 | /// @notice Updates the interest rate. 372 | function _updateInterest() internal virtual override { 373 | uint256 borrowed = totalBorrowed; 374 | uint256 poolAssets = _totalAssets + borrowed; 375 | 376 | uint32 utilisation; 377 | if (poolAssets != 0) { 378 | utilisation = uint32((borrowed * type(uint32).max) / poolAssets); 379 | } 380 | 381 | interestRate = irm.computeInterestRate(address(this), address(asset), utilisation); 382 | } 383 | } --------------------------------------------------------------------------------