├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── src ├── workshop_2 │ ├── IWorkshopVault.sol │ └── WorkshopVault.sol └── workshop_3 │ ├── PositionManager.sol │ └── questionnaire.md └── test └── workshop_2 └── WorkshopVault.t.sol /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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_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 | // [ASSIGNMENT]: what is the purpose of this modifier? 23 | modifier callThroughEVC() { 24 | if (msg.sender == address(evc)) { 25 | _; 26 | } else { 27 | bytes memory result = evc.call(address(this), msg.sender, 0, msg.data); 28 | 29 | assembly { 30 | return(add(32, result), mload(result)) 31 | } 32 | } 33 | } 34 | 35 | // [ASSIGNMENT]: why the account status check might not be necessary in certain situations? 36 | // [ASSIGNMENT]: is the vault status check always necessary? why? 37 | modifier withChecks(address account) { 38 | _; 39 | 40 | if (account == address(0)) { 41 | evc.requireVaultStatusCheck(); 42 | } else { 43 | evc.requireAccountAndVaultStatusCheck(account); 44 | } 45 | } 46 | 47 | // [ASSIGNMENT]: can this function be used to authenticate the account for the sake of the borrow-related 48 | // operations? why? 49 | // [ASSIGNMENT]: if the answer to the above is "no", how this function could be modified to allow safe borrowing? 50 | function _msgSender() internal view virtual override returns (address) { 51 | if (msg.sender == address(evc)) { 52 | (address onBehalfOfAccount,) = evc.getCurrentOnBehalfOfAccount(address(0)); 53 | return onBehalfOfAccount; 54 | } else { 55 | return msg.sender; 56 | } 57 | } 58 | 59 | // IVault 60 | // [ASSIGNMENT]: why this function is necessary? is it safe to unconditionally disable the controller? 61 | function disableController() external { 62 | evc.disableController(_msgSender()); 63 | } 64 | 65 | // [ASSIGNMENT]: provide a couple use cases for this function 66 | function checkAccountStatus( 67 | address account, 68 | address[] calldata collaterals 69 | ) public virtual returns (bytes4 magicValue) { 70 | require(msg.sender == address(evc), "only evc can call this"); 71 | require(evc.areChecksInProgress(), "can only be called when checks in progress"); 72 | 73 | // some custom logic evaluating the account health 74 | 75 | return IVault.checkAccountStatus.selector; 76 | } 77 | 78 | // [ASSIGNMENT]: provide a couple use cases for this function 79 | function checkVaultStatus() public virtual returns (bytes4 magicValue) { 80 | require(msg.sender == address(evc), "only evc can call this"); 81 | require(evc.areChecksInProgress(), "can only be called when checks in progress"); 82 | 83 | // some custom logic evaluating the vault health 84 | 85 | // [ASSIGNMENT]: what can be done if the vault status check needs access to the initial state of the vault in 86 | // order to evaluate the vault health? 87 | 88 | return IVault.checkVaultStatus.selector; 89 | } 90 | 91 | function deposit( 92 | uint256 assets, 93 | address receiver 94 | ) public virtual override callThroughEVC withChecks(address(0)) returns (uint256 shares) { 95 | return super.deposit(assets, receiver); 96 | } 97 | 98 | function mint( 99 | uint256 shares, 100 | address receiver 101 | ) public virtual override callThroughEVC withChecks(address(0)) returns (uint256 assets) { 102 | return super.mint(shares, receiver); 103 | } 104 | 105 | function withdraw( 106 | uint256 assets, 107 | address receiver, 108 | address owner 109 | ) public virtual override callThroughEVC withChecks(owner) returns (uint256 shares) { 110 | return super.withdraw(assets, receiver, owner); 111 | } 112 | 113 | function redeem( 114 | uint256 shares, 115 | address receiver, 116 | address owner 117 | ) public virtual override callThroughEVC withChecks(owner) returns (uint256 assets) { 118 | return super.redeem(shares, receiver, owner); 119 | } 120 | 121 | function transfer( 122 | address to, 123 | uint256 value 124 | ) public virtual override (ERC20, IERC20) callThroughEVC withChecks(_msgSender()) returns (bool) { 125 | return super.transfer(to, value); 126 | } 127 | 128 | function transferFrom( 129 | address from, 130 | address to, 131 | uint256 value 132 | ) public virtual override (ERC20, IERC20) callThroughEVC withChecks(from) returns (bool) { 133 | return super.transferFrom(from, to, value); 134 | } 135 | 136 | // IWorkshopVault 137 | function borrow(uint256 assets, address receiver) external {} 138 | function repay(uint256 assets, address receiver) external {} 139 | function pullDebt(address from, uint256 assets) external returns (bool) {} 140 | function liquidate(address violator, address collateral) external {} 141 | } 142 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 1. Does the sub-account system decrease the security of user accounts? 5 | 1. Provide a couple of use cases for the operator functionality of the EVC. 6 | 1. What is the main difference between the operator and the controller? 7 | 1. What does it mean to defer the account and vault status checks? What is the purpose of this deferral? 8 | 1. Why is it useful to allow re-entrancy for `call` and `batch` functions? 9 | 1. How does the simulation feature of the EVC work? 10 | 1. Provide a couple of use cases for the `permit` functionality of the EVC. 11 | 1. What is the purpose of the nonce namespace? 12 | 1. Why should the EVC neither be given any privileges nor hold any tokens? 13 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------