├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── addresses └── 1.json ├── foundry.toml ├── remappings.txt ├── src ├── IVault.sol └── exercises │ ├── 00 │ ├── REQUIREMENTS.md │ ├── SIP00.sol │ └── Vault00.sol │ ├── 01 │ ├── REQUIREMENTS.md │ ├── SIP01.sol │ ├── TESTS.md │ └── Vault01.sol │ ├── 02 │ ├── REQUIREMENTS.md │ ├── SIP02.sol │ ├── TESTS.md │ └── Vault02.sol │ ├── 03 │ ├── REQUIREMENTS.md │ ├── SIP03.sol │ └── Vault03.sol │ ├── 04 │ ├── REQUIREMENTS.md │ ├── SIP04.sol │ └── Vault04.sol │ └── storage │ ├── Authorized.sol │ ├── Balances.sol │ ├── Supply.sol │ ├── VaultStorageOwnable.sol │ └── VaultStoragePausable.sol └── test ├── TestVault00.t.sol ├── TestVault01.t.sol ├── TestVault02.t.sol ├── TestVault03.t.sol ├── TestVault04.t.sol └── utils └── Forks.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | ETH_RPC_URL: ${{secrets.ETH_RPC_URL}} 11 | 12 | jobs: 13 | check: 14 | strategy: 15 | fail-fast: true 16 | 17 | name: Foundry project 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | 24 | - name: Install Foundry 25 | uses: foundry-rs/foundry-toolchain@v1 26 | with: 27 | version: nightly 28 | 29 | - name: Show Forge version 30 | run: | 31 | forge --version 32 | 33 | - name: Run Forge fmt 34 | run: | 35 | forge fmt --check 36 | id: fmt 37 | 38 | - name: Run Forge build 39 | run: | 40 | forge build --sizes 41 | id: build 42 | 43 | - name: Run Forge tests 44 | run: | 45 | forge test -vvv 46 | id: test 47 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-proposal-simulator"] 2 | path = lib/forge-proposal-simulator 3 | url = https://github.com/solidity-labs-io/forge-proposal-simulator 4 | [submodule "lib/forge-std"] 5 | path = lib/forge-std 6 | url = https://github.com/foundry-rs/forge-std 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 10 | [submodule "lib/openzeppelin-contracts-upgradeable"] 11 | path = lib/openzeppelin-contracts-upgradeable 12 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure Development Workshop 2 | 3 | This workshop will walk you through the basics of securing smart contracts by testing your deployment scripts and governance proposals. We will be using the [forge proposal simulator](https://github.com/solidity-labs-io/forge-proposal-simulator) to make this process easier. 4 | 5 | ## Overview 6 | 7 | In this workshop we will be protecting a governance heavy application from the consequences of malicious upgrades and or deployment scripts. 8 | 9 | ### Further Reading 10 | 11 | For governance safety assistance, refer to our [forge proposal simulator](https://github.com/solidity-labs-io/forge-proposal-simulator) tool. See the [security checklist](https://github.com/solidity-labs-io/code-review-checklist) and [security](https://medium.com/@elliotfriedman3/a-security-stack-4aedd8617e8b) [stack](https://medium.com/@elliotfriedman3/a-security-stack-part-2-aaacbbf77346) for a list of items to consider when building a smart contract system. 12 | 13 | ## Environment Setup 14 | 15 | Set the `ETH_RPC_URL` environment variable to the URL of an Ethereum node. For example, to use the Alchemy mainnet node, run: 16 | 17 | ```bash 18 | export ETH_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/your-api-key 19 | ``` 20 | 21 | Make sure the latest version of foundry is installed. If not, run: 22 | 23 | ```bash 24 | foundryup 25 | ``` 26 | -------------------------------------------------------------------------------- /addresses/1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 4 | "name": "DEPLOYER_EOA", 5 | "isContract": false 6 | }, 7 | { 8 | "addr": "0xc0da02939e1441f497fd74f78ce7decb17b66529", 9 | "isContract": true, 10 | "name": "COMPOUND_GOVERNOR_BRAVO" 11 | }, 12 | { 13 | "addr": "0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3", 14 | "isContract": true, 15 | "name": "COMPOUND_CONFIGURATOR" 16 | }, 17 | { 18 | "addr": "0xf603265f91f58F1EfA4fAd57694Fb3B77b25fC18", 19 | "isContract": false, 20 | "name": "COMPOUND_PROPOSER" 21 | }, 22 | { 23 | "addr": "0xa17581a9e3356d9a858b789d68b4d866e593ae94", 24 | "isContract": true, 25 | "name": "COMPOUND_COMET" 26 | }, 27 | { 28 | "addr": "0xc00e94cb662c3520282e6f5717214004a7f26888", 29 | "isContract": true, 30 | "name": "COMP_TOKEN" 31 | }, 32 | { 33 | "addr": "0x6B175474E89094C44Da98b954EedeAC495271d0F", 34 | "isContract": true, 35 | "name": "DAI" 36 | }, 37 | { 38 | "addr": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 39 | "isContract": true, 40 | "name": "USDC" 41 | }, 42 | { 43 | "addr": "0xdAC17F958D2ee523a2206206994597C13D831ec7", 44 | "isContract": true, 45 | "name": "USDT" 46 | }, 47 | { 48 | "addr": "0x6d903f6003cca6255D85CcA4D3B5E5146dC33925", 49 | "isContract": true, 50 | "name": "COMPOUND_TIMELOCK_BRAVO" 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | remappings = [ 6 | "@forge-proposal-simulator=lib/forge-proposal-simulator/", 7 | "@addresses/=lib/forge-proposal-simulator/addresses/", 8 | "@examples/=lib/forge-proposal-simulator/examples/", 9 | "@forge-std/=lib/forge-proposal-simulator/lib/forge-std/src/", 10 | "@interface/=lib/forge-proposal-simulator/src/interface/", 11 | "@mocks/=lib/forge-proposal-simulator/mocks/", 12 | "@proposals/=lib/forge-proposal-simulator/src/proposals/", 13 | "@script/=lib/forge-proposal-simulator/script/", 14 | "@test/=test/", 15 | "@utils/=lib/forge-proposal-simulator/utils/", 16 | "forge-proposal-simulator/=lib/forge-proposal-simulator/", 17 | "forge-std/=lib/forge-proposal-simulator/lib/forge-std/src/", 18 | "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", 19 | ] 20 | fs_permissions = [{ access = "read", path = "./addresses/"}] 21 | 22 | [fmt] 23 | line_length = 70 24 | 25 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 26 | 27 | [rpc_endpoints] 28 | localhost = "http://127.0.0.1:8545" 29 | sepolia = "${SEPOLIA_RPC_URL}" 30 | ethereum = "${ETH_RPC_URL}" 31 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @forge-proposal-simulator=lib/forge-proposal-simulator/ 2 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ 3 | @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ 4 | -------------------------------------------------------------------------------- /src/IVault.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | /// @notice interface for a vault where a user can deposit authorized tokens 4 | /// and then withdraw tokens. 5 | /// Acts as a Peg Stability Module of tokens of the same value. Users can 6 | /// deposit tokens of type A and withdraw tokens of type B. 7 | interface IVault { 8 | function authorizedToken(address) external view returns (bool); 9 | function balanceOf(address) external view returns (uint256); 10 | function totalSupplied() external view returns (uint256); 11 | 12 | /// @notice depositing increases balanceOf and totalSupplied by amount 13 | /// deposited 14 | function deposit(address token, uint256 amount) external; 15 | 16 | /// @notice withdrawing decreases balanceOf and totalSupplied by amount 17 | /// withdrawn 18 | function withdraw(address token, uint256 amount) external; 19 | } 20 | -------------------------------------------------------------------------------- /src/exercises/00/REQUIREMENTS.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The business team needs a simple contract that allows swapping between various different tokens of the same value. This is a peg stability module that allows users to deposit one asset, and withdraw another of the same value. It does not use chainlink and assumes price parity of all assets. There are no swap fees, and LP's deposit then withdraw to swap. 4 | 5 | Create a contract that allows users to deposit any of the supported tokens, and withdraw any of the supported tokens. 6 | 7 | User balances should be tracked, a user's balance should go up when they deposit a token and go down when they withdraw a token. A user should only be able to withdraw up to the amount of tokens they have deposited. -------------------------------------------------------------------------------- /src/exercises/00/SIP00.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {GovernorBravoProposal} from 5 | "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; 6 | import {Addresses} from 7 | "@forge-proposal-simulator/addresses/Addresses.sol"; 8 | 9 | import {Vault} from "src/exercises/00/Vault00.sol"; 10 | import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; 11 | 12 | /// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/exercises/00/SIP00.sol:SIP00 -vvvv 13 | contract SIP00 is GovernorBravoProposal { 14 | using ForkSelector for uint256; 15 | 16 | constructor() { 17 | primaryForkId = ETHEREUM_FORK_ID; 18 | } 19 | 20 | function setupProposal() public { 21 | ETHEREUM_FORK_ID.createForksAndSelect(); 22 | 23 | string memory addressesFolderPath = "./addresses"; 24 | uint256[] memory chainIds = new uint256[](1); 25 | chainIds[0] = 1; 26 | 27 | setAddresses(new Addresses(addressesFolderPath, chainIds)); 28 | } 29 | 30 | function name() public pure override returns (string memory) { 31 | return "SIP-00 System Deploy"; 32 | } 33 | 34 | function description() 35 | public 36 | pure 37 | override 38 | returns (string memory) 39 | { 40 | return name(); 41 | } 42 | 43 | function run() public override { 44 | setupProposal(); 45 | 46 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 47 | 48 | super.run(); 49 | } 50 | 51 | function deploy() public override { 52 | if (!addresses.isAddressSet("V0_VAULT")) { 53 | address[] memory tokens = new address[](3); 54 | tokens[0] = addresses.getAddress("USDC"); 55 | tokens[1] = addresses.getAddress("DAI"); 56 | tokens[2] = addresses.getAddress("USDT"); 57 | 58 | Vault vault = new Vault(tokens); 59 | 60 | addresses.addAddress("V0_VAULT", address(vault), true); 61 | } 62 | } 63 | 64 | function validate() public view override { 65 | Vault vault = Vault(addresses.getAddress("V0_VAULT")); 66 | 67 | assertEq( 68 | vault.authorizedToken(addresses.getAddress("USDC")), 69 | true, 70 | "USDC should be authorized" 71 | ); 72 | assertEq( 73 | vault.authorizedToken(addresses.getAddress("DAI")), 74 | true, 75 | "DAI should be authorized" 76 | ); 77 | assertEq( 78 | vault.authorizedToken(addresses.getAddress("USDT")), 79 | true, 80 | "USDT should be authorized" 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/exercises/00/Vault00.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | import {SafeERC20} from 4 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | contract Vault { 8 | using SafeERC20 for IERC20; 9 | 10 | /// @notice Mapping of authorized tokens 11 | mapping(address => bool) public authorizedToken; 12 | 13 | /// @notice User's balance of all tokens deposited in the vault 14 | mapping(address => uint256) public balanceOf; 15 | 16 | /// @notice Total amount of tokens supplied to the vault 17 | /// 18 | /// invariants: 19 | /// totalSupplied = sum(balanceOf all users) 20 | /// sum(balanceOf(vault) authorized tokens) >= totalSupplied 21 | /// 22 | uint256 public totalSupplied; 23 | 24 | /// @notice Deposit event 25 | /// @param token The token deposited 26 | /// @param sender The address that deposited the token 27 | /// @param amount The amount deposited 28 | event Deposit( 29 | address indexed token, address indexed sender, uint256 amount 30 | ); 31 | 32 | /// @notice Withdraw event 33 | /// @param token The token withdrawn 34 | /// @param sender The address that withdrew the token 35 | /// @param amount The amount withdrawn 36 | event Withdraw( 37 | address indexed token, address indexed sender, uint256 amount 38 | ); 39 | 40 | /// @notice Construct the vault with a list of authorized tokens 41 | /// @param _tokens The list of authorized tokens 42 | constructor(address[] memory _tokens) { 43 | for (uint256 i = 0; i < _tokens.length; i++) { 44 | authorizedToken[_tokens[i]] = true; 45 | } 46 | } 47 | 48 | /// @notice Deposit tokens into the vault 49 | /// @param token The token to deposit, only authorized tokens allowed 50 | /// @param amount The amount to deposit 51 | function deposit(address token, uint256 amount) external { 52 | require(authorizedToken[token], "Vault: token not authorized"); 53 | 54 | /// save on gas by using unchecked, no need to check for overflow 55 | /// as all deposited tokens are whitelisted 56 | unchecked { 57 | balanceOf[msg.sender] += amount; 58 | } 59 | 60 | totalSupplied += amount; 61 | 62 | IERC20(token).safeTransferFrom( 63 | msg.sender, address(this), amount 64 | ); 65 | 66 | emit Deposit(token, msg.sender, amount); 67 | } 68 | 69 | /// @notice Withdraw tokens from the vault 70 | /// @param token The token to withdraw, only authorized tokens are allowed 71 | /// this is implicitly checked because a user can only have a balance of an 72 | /// authorized token 73 | /// @param amount The amount to withdraw 74 | function withdraw(address token, uint256 amount) external { 75 | /// both a check and an effect, ensures user has sufficient funds for withdrawal 76 | balanceOf[msg.sender] -= amount; 77 | 78 | /// save on gas by using unchecked, no need to check for underflow 79 | /// as all deposited tokens are whitelisted 80 | unchecked { 81 | /// implicitly checks for balance 82 | totalSupplied -= amount; 83 | } 84 | 85 | IERC20(token).safeTransfer(msg.sender, amount); 86 | 87 | emit Withdraw(token, msg.sender, amount); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/exercises/01/REQUIREMENTS.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The first round of security reviews was conducted by an external security firm. The results were pretty bad for such a simple contract. They refused to give you the findings, so you're on your own. 4 | 5 | Good luck... 6 | -------------------------------------------------------------------------------- /src/exercises/01/SIP01.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {GovernorBravoProposal} from 5 | "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; 6 | import {Addresses} from 7 | "@forge-proposal-simulator/addresses/Addresses.sol"; 8 | 9 | import {Vault} from "src/exercises/01/Vault01.sol"; 10 | import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; 11 | 12 | /// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/exercises/01/SIP01.sol:SIP01 -vvvv 13 | contract SIP01 is GovernorBravoProposal { 14 | using ForkSelector for uint256; 15 | 16 | constructor() { 17 | primaryForkId = ETHEREUM_FORK_ID; 18 | } 19 | 20 | function setupProposal() public { 21 | ETHEREUM_FORK_ID.createForksAndSelect(); 22 | 23 | string memory addressesFolderPath = "./addresses"; 24 | uint256[] memory chainIds = new uint256[](1); 25 | chainIds[0] = 1; 26 | 27 | setAddresses(new Addresses(addressesFolderPath, chainIds)); 28 | } 29 | 30 | function name() public pure override returns (string memory) { 31 | return "SIP-01 System Deploy"; 32 | } 33 | 34 | function description() 35 | public 36 | pure 37 | override 38 | returns (string memory) 39 | { 40 | return name(); 41 | } 42 | 43 | function run() public override { 44 | setupProposal(); 45 | 46 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 47 | 48 | super.run(); 49 | } 50 | 51 | function deploy() public override { 52 | if (!addresses.isAddressSet("V1_VAULT")) { 53 | address[] memory tokens = new address[](2); 54 | tokens[0] = addresses.getAddress("USDC"); 55 | tokens[1] = addresses.getAddress("USDT"); 56 | 57 | Vault vault = new Vault(tokens); 58 | 59 | addresses.addAddress("V1_VAULT", address(vault), true); 60 | } 61 | } 62 | 63 | function validate() public view override { 64 | Vault vault = Vault(addresses.getAddress("V1_VAULT")); 65 | 66 | assertEq( 67 | vault.authorizedToken(addresses.getAddress("USDC")), 68 | true, 69 | "USDC should be authorized" 70 | ); 71 | assertEq( 72 | vault.authorizedToken(addresses.getAddress("USDT")), 73 | true, 74 | "USDT should be authorized" 75 | ); 76 | assertEq( 77 | vault.authorizedToken(addresses.getAddress("DAI")), 78 | true, 79 | "DAI should be authorized" 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/exercises/01/TESTS.md: -------------------------------------------------------------------------------- 1 | # Vault Test Cases 2 | 3 | - decimal scaling logic is correct 4 | - multiple users deposit, each with different tokens, and withdraw different tokens than deposited 5 | -------------------------------------------------------------------------------- /src/exercises/01/Vault01.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | import {IERC20Metadata} from 4 | "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 5 | import {SafeERC20} from 6 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | 9 | contract Vault { 10 | using SafeERC20 for IERC20; 11 | 12 | /// @notice Mapping of authorized tokens 13 | mapping(address => bool) public authorizedToken; 14 | 15 | /// @notice User's balance of all tokens deposited in the vault 16 | mapping(address => uint256) public balanceOf; 17 | 18 | /// @notice Total amount of tokens supplied to the vault 19 | /// 20 | /// invariants: 21 | /// totalSupplied = sum(balanceOf all users) 22 | /// sum(balanceOf(vault) authorized tokens) >= totalSupplied 23 | /// 24 | uint256 public totalSupplied; 25 | 26 | /// @notice Deposit event 27 | /// @param token The token deposited 28 | /// @param sender The address that deposited the token 29 | /// @param amount The amount deposited 30 | event Deposit( 31 | address indexed token, address indexed sender, uint256 amount 32 | ); 33 | 34 | /// @notice Withdraw event 35 | /// @param token The token withdrawn 36 | /// @param sender The address that withdrew the token 37 | /// @param amount The amount withdrawn 38 | event Withdraw( 39 | address indexed token, address indexed sender, uint256 amount 40 | ); 41 | 42 | /// @notice Construct the vault with a list of authorized tokens 43 | /// @param _tokens The list of authorized tokens 44 | constructor(address[] memory _tokens) { 45 | for (uint256 i = 0; i < _tokens.length; i++) { 46 | require( 47 | IERC20Metadata(_tokens[i]).decimals() <= 18, 48 | "unsupported decimals" 49 | ); 50 | authorizedToken[_tokens[i]] = true; 51 | } 52 | } 53 | 54 | /// @notice Deposit tokens into the vault 55 | /// @param token The token to deposit, only authorized tokens allowed 56 | /// @param amount The amount to deposit 57 | function deposit(address token, uint256 amount) external { 58 | require(authorizedToken[token], "Vault: token not authorized"); 59 | 60 | uint256 normalizedAmount = getNormalizedAmount(token, amount); 61 | 62 | /// save on gas by using unchecked, no need to check for overflow 63 | /// as all deposited tokens are whitelisted 64 | unchecked { 65 | balanceOf[msg.sender] += normalizedAmount; 66 | } 67 | 68 | totalSupplied += normalizedAmount; 69 | 70 | IERC20(token).safeTransferFrom( 71 | msg.sender, address(this), amount 72 | ); 73 | 74 | emit Deposit(token, msg.sender, amount); 75 | } 76 | 77 | /// @notice Withdraw tokens from the vault 78 | /// @param token The token to withdraw, only authorized tokens are allowed 79 | /// this is implicitly checked because a user can only have a balance of an 80 | /// authorized token 81 | /// @param amount The amount to withdraw 82 | function withdraw(address token, uint256 amount) external { 83 | require(authorizedToken[token], "Vault: token not authorized"); 84 | 85 | uint256 normalizedAmount = getNormalizedAmount(token, amount); 86 | 87 | /// both a check and an effect, ensures user has sufficient funds for 88 | /// withdrawal 89 | /// must be checked for underflow as a user can only withdraw what they 90 | /// have deposited 91 | balanceOf[msg.sender] -= normalizedAmount; 92 | 93 | /// save on gas by using unchecked, no need to check for underflow 94 | /// as all deposited tokens are whitelisted, plus we know our invariant 95 | /// always holds 96 | unchecked { 97 | totalSupplied -= normalizedAmount; 98 | } 99 | 100 | IERC20(token).safeTransfer(msg.sender, amount); 101 | 102 | emit Withdraw(token, msg.sender, amount); 103 | } 104 | 105 | /// @notice public for testing purposes, returns the normalized amount of 106 | /// tokens scaled to 18 decimals 107 | /// @param token The token to deposit 108 | /// @param amount The amount to deposit 109 | function getNormalizedAmount(address token, uint256 amount) 110 | public 111 | view 112 | returns (uint256 normalizedAmount) 113 | { 114 | uint8 decimals = IERC20Metadata(token).decimals(); 115 | if (decimals < 18) { 116 | /// scale the amount to 18 decimals 117 | /// unchecked because we know that the product will always be less 118 | /// than 2^256-1 as 1e18 = $1 119 | unchecked { 120 | normalizedAmount = amount * (10 ** (18 - decimals)); 121 | } 122 | } else if (decimals > 18) { 123 | revert("Vault: unsupported decimals over 18"); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/exercises/02/REQUIREMENTS.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | New requirements have been added. 4 | 5 | The contract needs a way for an owner to add and remove tokens from the whitelisted tokens list. You counter and say that the complexity of adding removal logic would greatly increase the attack surface of the contract. The business team agrees to cut the removal logic, but they still want to be able to add tokens to the whitelist after deployment. 6 | 7 | -------------------------------------------------------------------------------- /src/exercises/02/SIP02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {GovernorBravoProposal} from 5 | "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; 6 | import {Addresses} from 7 | "@forge-proposal-simulator/addresses/Addresses.sol"; 8 | import { 9 | ProxyAdmin, 10 | TransparentUpgradeableProxy, 11 | ITransparentUpgradeableProxy 12 | } from 13 | "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 14 | import {ERC1967Utils} from 15 | "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; 16 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 17 | 18 | import {Vault} from "src/exercises/02/Vault02.sol"; 19 | import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; 20 | 21 | /// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/exercises/02/SIP02.sol:SIP02 -vvvv 22 | contract SIP02 is GovernorBravoProposal { 23 | using ForkSelector for uint256; 24 | 25 | constructor() { 26 | primaryForkId = ETHEREUM_FORK_ID; 27 | } 28 | 29 | function setupProposal() public { 30 | ETHEREUM_FORK_ID.createForksAndSelect(); 31 | 32 | string memory addressesFolderPath = "./addresses"; 33 | uint256[] memory chainIds = new uint256[](1); 34 | chainIds[0] = 1; 35 | 36 | setAddresses(new Addresses(addressesFolderPath, chainIds)); 37 | } 38 | 39 | function name() public pure override returns (string memory) { 40 | return "SIP-02 Upgrade"; 41 | } 42 | 43 | function description() 44 | public 45 | pure 46 | override 47 | returns (string memory) 48 | { 49 | return name(); 50 | } 51 | 52 | function run() public override { 53 | setupProposal(); 54 | 55 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 56 | 57 | super.run(); 58 | } 59 | 60 | function deploy() public override { 61 | if (!addresses.isAddressSet("V2_VAULT")) { 62 | address[] memory tokens = new address[](3); 63 | tokens[0] = addresses.getAddress("USDC"); 64 | tokens[1] = addresses.getAddress("DAI"); 65 | tokens[2] = addresses.getAddress("USDT"); 66 | 67 | address owner = addresses.getAddress("DEPLOYER_EOA"); 68 | 69 | address vaultImpl = address(new Vault(tokens, owner)); 70 | addresses.addAddress("V2_VAULT", vaultImpl, true); 71 | } 72 | } 73 | 74 | function validate() public view override { 75 | Vault vault = Vault(addresses.getAddress("V2_VAULT")); 76 | 77 | assertEq( 78 | vault.authorizedToken(addresses.getAddress("USDC")), 79 | true, 80 | "USDC should be authorized" 81 | ); 82 | assertEq( 83 | vault.authorizedToken(addresses.getAddress("DAI")), 84 | true, 85 | "DAI should be authorized" 86 | ); 87 | assertEq( 88 | vault.authorizedToken(addresses.getAddress("USDT")), 89 | true, 90 | "USDT should be authorized" 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/exercises/02/TESTS.md: -------------------------------------------------------------------------------- 1 | # Vault Test Cases 2 | 3 | - decimal scaling logic is correct 4 | - multiple users deposit, each with different tokens, and withdraw different tokens than deposited 5 | -------------------------------------------------------------------------------- /src/exercises/02/Vault02.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | import {IERC20Metadata} from 4 | "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 5 | import {SafeERC20} from 6 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 8 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | 10 | contract Vault is Ownable { 11 | using SafeERC20 for IERC20; 12 | 13 | /// @notice Mapping of authorized tokens 14 | mapping(address => bool) public authorizedToken; 15 | 16 | /// @notice User's balance of all tokens deposited in the vault 17 | mapping(address => uint256) public balanceOf; 18 | 19 | /// @notice Total amount of tokens supplied to the vault 20 | /// 21 | /// invariants: 22 | /// totalSupplied = sum(balanceOf all users) 23 | /// sum(balanceOf(vault) authorized tokens) >= totalSupplied 24 | /// 25 | uint256 public totalSupplied; 26 | 27 | /// @notice Deposit event 28 | /// @param token The token deposited 29 | /// @param sender The address that deposited the token 30 | /// @param amount The amount deposited 31 | event Deposit( 32 | address indexed token, address indexed sender, uint256 amount 33 | ); 34 | 35 | /// @notice Withdraw event 36 | /// @param token The token withdrawn 37 | /// @param sender The address that withdrew the token 38 | /// @param amount The amount withdrawn 39 | event Withdraw( 40 | address indexed token, address indexed sender, uint256 amount 41 | ); 42 | 43 | event TokenAdded(address indexed token); 44 | 45 | /// @notice Construct the vault with a list of authorized tokens 46 | /// @param _tokens The list of authorized tokens 47 | constructor(address[] memory _tokens, address _owner) 48 | Ownable(_owner) 49 | { 50 | for (uint256 i = 0; i < _tokens.length; i++) { 51 | require( 52 | IERC20Metadata(_tokens[i]).decimals() <= 18, 53 | "Vault: unsupported decimals" 54 | ); 55 | 56 | authorizedToken[_tokens[i]] = true; 57 | 58 | emit TokenAdded(_tokens[i]); 59 | } 60 | } 61 | 62 | /// ------------------------------------------------------------- 63 | /// ------------------------------------------------------------- 64 | /// -------------------- ONLY OWNER FUNCTION -------------------- 65 | /// ------------------------------------------------------------- 66 | /// ------------------------------------------------------------- 67 | 68 | /// @notice Add a token to the list of authorized tokens 69 | /// only callable by the owner 70 | /// @param token to add 71 | function addToken(address token) external onlyOwner { 72 | require( 73 | IERC20Metadata(token).decimals() <= 18, 74 | "Vault: unsupported decimals" 75 | ); 76 | require( 77 | !authorizedToken[token], "Vault: token already authorized" 78 | ); 79 | 80 | authorizedToken[token] = true; 81 | 82 | emit TokenAdded(token); 83 | } 84 | 85 | /// ------------------------------------------------------------- 86 | /// ------------------------------------------------------------- 87 | /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- 88 | /// ------------------------------------------------------------- 89 | /// ------------------------------------------------------------- 90 | 91 | /// @notice Deposit tokens into the vault 92 | /// @param token The token to deposit, only authorized tokens allowed 93 | /// @param amount The amount to deposit 94 | function deposit(address token, uint256 amount) external { 95 | require(authorizedToken[token], "Vault: token not authorized"); 96 | 97 | uint256 normalizedAmount = getNormalizedAmount(token, amount); 98 | 99 | /// save on gas by using unchecked, no need to check for overflow 100 | /// as all deposited tokens are whitelisted 101 | unchecked { 102 | balanceOf[msg.sender] += normalizedAmount; 103 | } 104 | 105 | totalSupplied += normalizedAmount; 106 | 107 | IERC20(token).safeTransferFrom( 108 | msg.sender, address(this), amount 109 | ); 110 | 111 | emit Deposit(token, msg.sender, amount); 112 | } 113 | 114 | /// @notice Withdraw tokens from the vault 115 | /// @param token The token to withdraw, only authorized tokens are allowed 116 | /// this is implicitly checked because a user can only have a balance of an 117 | /// authorized token 118 | /// @param amount The amount to withdraw 119 | function withdraw(address token, uint256 amount) external { 120 | require(authorizedToken[token], "Vault: token not authorized"); 121 | 122 | uint256 normalizedAmount = getNormalizedAmount(token, amount); 123 | 124 | /// both a check and an effect, ensures user has sufficient funds for 125 | /// withdrawal 126 | /// must be checked for underflow as a user can only withdraw what they 127 | /// have deposited 128 | balanceOf[msg.sender] -= normalizedAmount; 129 | 130 | /// save on gas by using unchecked, no need to check for underflow 131 | /// as all deposited tokens are whitelisted, plus we know our invariant 132 | /// always holds 133 | unchecked { 134 | totalSupplied -= normalizedAmount; 135 | } 136 | 137 | IERC20(token).safeTransfer(msg.sender, amount); 138 | 139 | emit Withdraw(token, msg.sender, amount); 140 | } 141 | 142 | /// -------------------------------------------------------- 143 | /// -------------------------------------------------------- 144 | /// ----------------- PUBLIC VIEW FUNCTION ----------------- 145 | /// -------------------------------------------------------- 146 | /// -------------------------------------------------------- 147 | 148 | /// @notice public for testing purposes, returns the normalized amount of 149 | /// tokens scaled to 18 decimals 150 | /// @param token The token to deposit 151 | /// @param amount The amount to deposit 152 | function getNormalizedAmount(address token, uint256 amount) 153 | public 154 | view 155 | returns (uint256 normalizedAmount) 156 | { 157 | uint8 decimals = IERC20Metadata(token).decimals(); 158 | normalizedAmount = amount; 159 | if (decimals < 18) { 160 | normalizedAmount = amount * (10 ** (18 - decimals)); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/exercises/03/REQUIREMENTS.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Make the contract upgradeable while preserving all previous functionality. Users can migrate to this contract via opt-in by removing their liquidity from the previous contract and depositing here. -------------------------------------------------------------------------------- /src/exercises/03/SIP03.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {GovernorBravoProposal} from 5 | "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; 6 | import {Addresses} from 7 | "@forge-proposal-simulator/addresses/Addresses.sol"; 8 | 9 | import { 10 | ProxyAdmin, 11 | TransparentUpgradeableProxy, 12 | ITransparentUpgradeableProxy 13 | } from 14 | "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 15 | import {ERC1967Utils} from 16 | "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; 17 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 18 | 19 | import {Vault} from "src/exercises/03/Vault03.sol"; 20 | import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; 21 | 22 | /// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/exercises/03/SIP03.sol:SIP03 -vvvv 23 | contract SIP03 is GovernorBravoProposal { 24 | using ForkSelector for uint256; 25 | 26 | constructor() { 27 | primaryForkId = ETHEREUM_FORK_ID; 28 | } 29 | 30 | function setupProposal() public { 31 | ETHEREUM_FORK_ID.createForksAndSelect(); 32 | 33 | string memory addressesFolderPath = "./addresses"; 34 | uint256[] memory chainIds = new uint256[](1); 35 | chainIds[0] = 1; 36 | 37 | setAddresses(new Addresses(addressesFolderPath, chainIds)); 38 | } 39 | 40 | function name() public pure override returns (string memory) { 41 | return "SIP-03 Upgrade"; 42 | } 43 | 44 | function description() 45 | public 46 | pure 47 | override 48 | returns (string memory) 49 | { 50 | return name(); 51 | } 52 | 53 | function run() public override { 54 | setupProposal(); 55 | 56 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 57 | 58 | super.run(); 59 | } 60 | 61 | function deploy() public override { 62 | address vaultProxy; 63 | if (!addresses.isAddressSet("V3_VAULT_IMPL")) { 64 | address vaultImpl = address(new Vault()); 65 | addresses.addAddress("V3_VAULT_IMPL", vaultImpl, true); 66 | 67 | address[] memory tokens = new address[](3); 68 | tokens[0] = addresses.getAddress("USDC"); 69 | tokens[1] = addresses.getAddress("DAI"); 70 | tokens[2] = addresses.getAddress("USDT"); 71 | 72 | address owner = 73 | addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"); 74 | 75 | // Generate calldata for initialize function of vault 76 | bytes memory data = abi.encodeWithSignature( 77 | "initialize(address[],address)", tokens, owner 78 | ); 79 | 80 | /// proxy admin contract is created by the Transparent Upgradeable Proxy 81 | vaultProxy = address( 82 | new TransparentUpgradeableProxy( 83 | vaultImpl, owner, data 84 | ) 85 | ); 86 | addresses.addAddress("VAULT_PROXY", vaultProxy, true); 87 | 88 | address proxyAdmin = address( 89 | uint160( 90 | uint256( 91 | vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT) 92 | ) 93 | ) 94 | ); 95 | addresses.addAddress("PROXY_ADMIN", proxyAdmin, true); 96 | } 97 | } 98 | 99 | function validate() public view override { 100 | Vault vault = Vault(addresses.getAddress("VAULT_PROXY")); 101 | 102 | assertEq( 103 | vault.authorizedToken(addresses.getAddress("USDC")), 104 | true, 105 | "USDC should be authorized" 106 | ); 107 | assertEq( 108 | vault.authorizedToken(addresses.getAddress("DAI")), 109 | true, 110 | "DAI should be authorized" 111 | ); 112 | assertEq( 113 | vault.authorizedToken(addresses.getAddress("USDT")), 114 | true, 115 | "USDT should be authorized" 116 | ); 117 | 118 | address vaultProxy = addresses.getAddress("VAULT_PROXY"); 119 | bytes32 adminSlot = 120 | vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); 121 | 122 | address proxyAdmin = address(uint160(uint256(adminSlot))); 123 | 124 | assertEq( 125 | ProxyAdmin(proxyAdmin).owner(), 126 | addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"), 127 | "owner not set" 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/exercises/03/Vault03.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | import {IERC20Metadata} from 4 | "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 5 | import {SafeERC20} from 6 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | 9 | import {VaultStorageOwnable} from 10 | "src/exercises/storage/VaultStorageOwnable.sol"; 11 | 12 | /// @notice Add maxsupply to the vault and update getNormalizedAmount logic 13 | /// add integration tests 14 | contract Vault is VaultStorageOwnable { 15 | using SafeERC20 for IERC20; 16 | 17 | /// @notice Deposit event 18 | /// @param token The token deposited 19 | /// @param sender The address that deposited the token 20 | /// @param amount The amount deposited 21 | event Deposit( 22 | address indexed token, address indexed sender, uint256 amount 23 | ); 24 | 25 | /// @notice Withdraw event 26 | /// @param token The token withdrawn 27 | /// @param sender The address that withdrew the token 28 | /// @param amount The amount withdrawn 29 | event Withdraw( 30 | address indexed token, address indexed sender, uint256 amount 31 | ); 32 | 33 | /// @notice emitted when new token is whitelisted 34 | /// @param token newly added 35 | event TokenAdded(address indexed token); 36 | 37 | /// @notice max supply updated event 38 | /// @param previousMaxSupply value of previous max supply 39 | /// @param currentMaxSupply new max supply value 40 | event MaxSupplyUpdated( 41 | uint256 previousMaxSupply, uint256 currentMaxSupply 42 | ); 43 | 44 | constructor() { 45 | _disableInitializers(); 46 | } 47 | 48 | /// @notice Initialize the vault with a list of authorized tokens 49 | /// @param tokens The list of authorized tokens 50 | /// @param vaultOwner The owner address to set for the contract 51 | function initialize(address[] memory tokens, address vaultOwner) 52 | external 53 | initializer 54 | { 55 | __Ownable_init(vaultOwner); 56 | 57 | for (uint256 i = 0; i < tokens.length; i++) { 58 | require( 59 | IERC20Metadata(tokens[i]).decimals() <= 18, 60 | "Vault: unsupported decimals" 61 | ); 62 | 63 | authorizedToken[tokens[i]] = true; 64 | 65 | emit TokenAdded(tokens[i]); 66 | } 67 | } 68 | 69 | /// ------------------------------------------------------------- 70 | /// ------------------------------------------------------------- 71 | /// -------------------- ONLY OWNER FUNCTION -------------------- 72 | /// ------------------------------------------------------------- 73 | /// ------------------------------------------------------------- 74 | 75 | /// @notice Add a token to the list of authorized tokens 76 | /// only callable by the owner 77 | /// @param token to add 78 | function addToken(address token) external onlyOwner { 79 | require( 80 | IERC20Metadata(token).decimals() <= 18, 81 | "Vault: unsupported decimals" 82 | ); 83 | require( 84 | !authorizedToken[token], "Vault: token already authorized" 85 | ); 86 | 87 | authorizedToken[token] = true; 88 | 89 | emit TokenAdded(token); 90 | } 91 | 92 | /// @notice Set the maximum supply of the vault 93 | /// @param newMaxSupply The new maximum supply of the vault 94 | function setMaxSupply(uint256 newMaxSupply) external onlyOwner { 95 | uint256 previousMaxSupply = maxSupply; 96 | maxSupply = newMaxSupply; 97 | 98 | /// only read stack variables, save a warm SLOAD 99 | emit MaxSupplyUpdated(previousMaxSupply, newMaxSupply); 100 | } 101 | 102 | /// ------------------------------------------------------------- 103 | /// ------------------------------------------------------------- 104 | /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- 105 | /// ------------------------------------------------------------- 106 | /// ------------------------------------------------------------- 107 | 108 | /// @notice Deposit tokens into the vault 109 | /// @param token The token to deposit, only authorized tokens allowed 110 | /// @param amount The amount to deposit 111 | function deposit(address token, uint256 amount) external { 112 | require(authorizedToken[token], "Vault: token not authorized"); 113 | 114 | uint256 normalizedAmount = getNormalizedAmount(token, amount); 115 | 116 | require( 117 | totalSupplied + normalizedAmount <= maxSupply, 118 | "Vault: supply cap reached" 119 | ); 120 | 121 | /// save on gas by using unchecked, no need to check for overflow 122 | /// as all deposited tokens are whitelisted 123 | unchecked { 124 | balanceOf[msg.sender] += normalizedAmount; 125 | } 126 | 127 | totalSupplied += normalizedAmount; 128 | 129 | IERC20(token).safeTransferFrom( 130 | msg.sender, address(this), amount 131 | ); 132 | 133 | emit Deposit(token, msg.sender, amount); 134 | } 135 | 136 | /// @notice Withdraw tokens from the vault 137 | /// @param token The token to withdraw, only authorized tokens are allowed 138 | /// this is implicitly checked because a user can only have a balance of an 139 | /// authorized token 140 | /// @param amount The amount to withdraw 141 | function withdraw(address token, uint256 amount) external { 142 | require(authorizedToken[token], "Vault: token not authorized"); 143 | 144 | uint256 normalizedAmount = getNormalizedAmount(token, amount); 145 | 146 | /// both a check and an effect, ensures user has sufficient funds for 147 | /// withdrawal 148 | /// must be checked for underflow as a user can only withdraw what they 149 | /// have deposited 150 | balanceOf[msg.sender] -= normalizedAmount; 151 | 152 | /// save on gas by using unchecked, no need to check for underflow 153 | /// as all deposited tokens are whitelisted, plus we know our invariant 154 | /// always holds 155 | unchecked { 156 | totalSupplied -= normalizedAmount; 157 | } 158 | 159 | IERC20(token).safeTransfer(msg.sender, amount); 160 | 161 | emit Withdraw(token, msg.sender, amount); 162 | } 163 | 164 | /// -------------------------------------------------------- 165 | /// -------------------------------------------------------- 166 | /// ----------------- PUBLIC VIEW FUNCTION ----------------- 167 | /// -------------------------------------------------------- 168 | /// -------------------------------------------------------- 169 | 170 | /// @notice public for testing purposes, returns the normalized amount of 171 | /// tokens scaled to 18 decimals 172 | /// @param token The token to deposit 173 | /// @param amount The amount to deposit 174 | function getNormalizedAmount(address token, uint256 amount) 175 | public 176 | view 177 | returns (uint256 normalizedAmount) 178 | { 179 | uint8 decimals = IERC20Metadata(token).decimals(); 180 | normalizedAmount = amount; 181 | if (decimals < 18) { 182 | normalizedAmount = amount * (10 ** (18 - decimals)); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/exercises/04/REQUIREMENTS.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Make the contract pauseable, while preserving all previous functionality. Users should not be able to deposit when the contract is paused. Users should be able to withdraw their liquidity when the contract is paused. Only the owner can pause and unpause the contract. 4 | 5 | Then upgrade the existing contract's implementation to this new contract. This migration is forced, and users not wishing to stay for this new contract upgrade must withdraw their liquidity. 6 | -------------------------------------------------------------------------------- /src/exercises/04/SIP04.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {GovernorBravoProposal} from 5 | "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; 6 | import {Addresses} from 7 | "@forge-proposal-simulator/addresses/Addresses.sol"; 8 | import { 9 | ProxyAdmin, 10 | TransparentUpgradeableProxy, 11 | ITransparentUpgradeableProxy 12 | } from 13 | "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 14 | import {ERC1967Utils} from 15 | "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; 16 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 17 | 18 | import {Vault} from "src/exercises/04/Vault04.sol"; 19 | import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; 20 | 21 | /// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/exercises/04/SIP04.sol:SIP04 -vvvv 22 | contract SIP04 is GovernorBravoProposal { 23 | using ForkSelector for uint256; 24 | 25 | constructor() { 26 | primaryForkId = ETHEREUM_FORK_ID; 27 | } 28 | 29 | function setupProposal() public { 30 | ETHEREUM_FORK_ID.createForksAndSelect(); 31 | 32 | string memory addressesFolderPath = "./addresses"; 33 | uint256[] memory chainIds = new uint256[](1); 34 | chainIds[0] = 1; 35 | 36 | setAddresses(new Addresses(addressesFolderPath, chainIds)); 37 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 38 | } 39 | 40 | function name() public pure override returns (string memory) { 41 | return "SIP-04"; 42 | } 43 | 44 | function description() 45 | public 46 | pure 47 | override 48 | returns (string memory) 49 | { 50 | return "Upgrade to V4 Vault Implementation"; 51 | } 52 | 53 | function run() public override { 54 | setupProposal(); 55 | 56 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 57 | 58 | super.run(); 59 | } 60 | 61 | function deploy() public override { 62 | if (!addresses.isAddressSet("V4_VAULT_IMPL")) { 63 | address vaultImpl = address(new Vault()); 64 | addresses.addAddress("V4_VAULT_IMPL", vaultImpl, true); 65 | } 66 | } 67 | 68 | function build() 69 | public 70 | override 71 | buildModifier(addresses.getAddress("COMPOUND_TIMELOCK_BRAVO")) 72 | { 73 | /// static calls - filtered out 74 | address vaultProxy = addresses.getAddress("VAULT_PROXY"); 75 | bytes32 adminSlot = 76 | vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); 77 | 78 | address proxyAdmin = address(uint160(uint256(adminSlot))); 79 | 80 | /// recorded calls 81 | 82 | // upgrade to new implementation 83 | ProxyAdmin(proxyAdmin).upgradeAndCall( 84 | ITransparentUpgradeableProxy(vaultProxy), 85 | addresses.getAddress("V4_VAULT_IMPL"), 86 | "" 87 | ); 88 | 89 | Vault(vaultProxy).setMaxSupply(1_000_000e18); 90 | } 91 | 92 | function validate() public view override { 93 | Vault vault = Vault(addresses.getAddress("VAULT_PROXY")); 94 | 95 | assertEq( 96 | vault.authorizedToken(addresses.getAddress("USDC")), 97 | true, 98 | "USDC should be authorized" 99 | ); 100 | assertEq( 101 | vault.authorizedToken(addresses.getAddress("DAI")), 102 | true, 103 | "DAI should be authorized" 104 | ); 105 | assertEq( 106 | vault.authorizedToken(addresses.getAddress("USDT")), 107 | true, 108 | "USDT should be authorized" 109 | ); 110 | assertEq( 111 | vault.maxSupply(), 1_000_000e18, "max supply not set" 112 | ); 113 | 114 | address vaultProxy = addresses.getAddress("VAULT_PROXY"); 115 | bytes32 adminSlot = 116 | vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); 117 | address proxyAdmin = address(uint160(uint256(adminSlot))); 118 | 119 | /// check not paused 120 | /// check logic contract address is v4 impl 121 | 122 | assertEq( 123 | ProxyAdmin(proxyAdmin).owner(), 124 | addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"), 125 | "owner not set" 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/exercises/04/Vault04.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | import {IERC20Metadata} from 4 | "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 5 | import {SafeERC20} from 6 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import {OwnableUpgradeable} from 8 | "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 9 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 10 | 11 | import {VaultStoragePausable} from 12 | "src/exercises/storage/VaultStoragePausable.sol"; 13 | 14 | /// @notice Add maxsupply to the vault and update getNormalizedAmount logic 15 | /// allows pausing by owner 16 | contract Vault is VaultStoragePausable { 17 | using SafeERC20 for IERC20; 18 | 19 | /// @notice Deposit event 20 | /// @param token The token deposited 21 | /// @param sender The address that deposited the token 22 | /// @param amount The amount deposited 23 | event Deposit( 24 | address indexed token, address indexed sender, uint256 amount 25 | ); 26 | 27 | /// @notice Withdraw event 28 | /// @param token The token withdrawn 29 | /// @param sender The address that withdrew the token 30 | /// @param amount The amount withdrawn 31 | event Withdraw( 32 | address indexed token, address indexed sender, uint256 amount 33 | ); 34 | 35 | /// @notice emitted when new token is whitelisted 36 | /// @param token newly added 37 | event TokenAdded(address indexed token); 38 | 39 | /// @notice max supply updated event 40 | /// @param previousMaxSupply value of previous max supply 41 | /// @param currentMaxSupply new max supply value 42 | event MaxSupplyUpdated( 43 | uint256 previousMaxSupply, uint256 currentMaxSupply 44 | ); 45 | 46 | constructor() { 47 | _disableInitializers(); 48 | } 49 | 50 | /// @notice Initialize the vault with a list of authorized tokens 51 | /// @param tokens The list of authorized tokens 52 | /// @param vaultOwner The owner address to set for the contract 53 | function initialize(address[] memory tokens, address vaultOwner) 54 | external 55 | initializer 56 | { 57 | __Ownable_init(vaultOwner); 58 | 59 | for (uint256 i = 0; i < tokens.length; i++) { 60 | require( 61 | IERC20Metadata(tokens[i]).decimals() <= 18, 62 | "Vault: unsupported decimals" 63 | ); 64 | 65 | authorizedToken[tokens[i]] = true; 66 | 67 | emit TokenAdded(tokens[i]); 68 | } 69 | } 70 | 71 | /// ------------------------------------------------------------- 72 | /// ------------------------------------------------------------- 73 | /// -------------------- ONLY OWNER FUNCTION -------------------- 74 | /// ------------------------------------------------------------- 75 | /// ------------------------------------------------------------- 76 | 77 | /// @notice Add a token to the list of authorized tokens 78 | /// only callable by the owner 79 | /// @param token to add 80 | function addToken(address token) external onlyOwner { 81 | require( 82 | IERC20Metadata(token).decimals() <= 18, 83 | "Vault: unsupported decimals" 84 | ); 85 | require( 86 | !authorizedToken[token], "Vault: token already authorized" 87 | ); 88 | 89 | authorizedToken[token] = true; 90 | 91 | emit TokenAdded(token); 92 | } 93 | 94 | /// @notice Set the maximum supply of the vault 95 | /// @param newMaxSupply The new maximum supply of the vault 96 | function setMaxSupply(uint256 newMaxSupply) external onlyOwner { 97 | uint256 previousMaxSupply = maxSupply; 98 | maxSupply = newMaxSupply; 99 | 100 | /// only read stack variables, save a warm SLOAD 101 | emit MaxSupplyUpdated(previousMaxSupply, newMaxSupply); 102 | } 103 | 104 | /// @notice pauses the contract, callable only by the owner 105 | /// and when the contract is unpaused 106 | function pause() external onlyOwner whenNotPaused { 107 | _pause(); 108 | } 109 | 110 | /// @notice unpauses the contract, callable only by the owner 111 | /// and when the contract is paused 112 | function unpause() external onlyOwner whenPaused { 113 | _unpause(); 114 | } 115 | 116 | /// ------------------------------------------------------------- 117 | /// ------------------------------------------------------------- 118 | /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- 119 | /// ------------------------------------------------------------- 120 | /// ------------------------------------------------------------- 121 | 122 | /// @notice Deposit tokens into the vault 123 | /// @param token The token to deposit, only authorized tokens allowed 124 | /// @param amount The amount to deposit 125 | function deposit(address token, uint256 amount) 126 | external 127 | whenNotPaused 128 | { 129 | require(authorizedToken[token], "Vault: token not authorized"); 130 | 131 | uint256 normalizedAmount = getNormalizedAmount(token, amount); 132 | 133 | require( 134 | totalSupplied + normalizedAmount <= maxSupply, 135 | "Vault: supply cap reached" 136 | ); 137 | 138 | /// save on gas by using unchecked, no need to check for overflow 139 | /// as all deposited tokens are whitelisted 140 | unchecked { 141 | balanceOf[msg.sender] += normalizedAmount; 142 | } 143 | 144 | totalSupplied += normalizedAmount; 145 | 146 | IERC20(token).safeTransferFrom( 147 | msg.sender, address(this), amount 148 | ); 149 | 150 | emit Deposit(token, msg.sender, amount); 151 | } 152 | 153 | /// @notice Withdraw tokens from the vault 154 | /// @param token The token to withdraw, only authorized tokens are allowed 155 | /// this is implicitly checked because a user can only have a balance of an 156 | /// authorized token 157 | /// @param amount The amount to withdraw 158 | function withdraw(address token, uint256 amount) external { 159 | require(authorizedToken[token], "Vault: token not authorized"); 160 | 161 | uint256 normalizedAmount = getNormalizedAmount(token, amount); 162 | 163 | /// both a check and an effect, ensures user has sufficient funds for 164 | /// withdrawal 165 | /// must be checked for underflow as a user can only withdraw what they 166 | /// have deposited 167 | balanceOf[msg.sender] -= normalizedAmount; 168 | 169 | /// save on gas by using unchecked, no need to check for underflow 170 | /// as all deposited tokens are whitelisted, plus we know our invariant 171 | /// always holds 172 | unchecked { 173 | totalSupplied -= normalizedAmount; 174 | } 175 | 176 | IERC20(token).safeTransfer(msg.sender, amount); 177 | 178 | emit Withdraw(token, msg.sender, amount); 179 | } 180 | 181 | /// -------------------------------------------------------- 182 | /// -------------------------------------------------------- 183 | /// ----------------- PUBLIC VIEW FUNCTION ----------------- 184 | /// -------------------------------------------------------- 185 | /// -------------------------------------------------------- 186 | 187 | /// @notice public for testing purposes, returns the normalized amount of 188 | /// tokens scaled to 18 decimals 189 | /// @param token The token to deposit 190 | /// @param amount The amount to deposit 191 | function getNormalizedAmount(address token, uint256 amount) 192 | public 193 | view 194 | returns (uint256 normalizedAmount) 195 | { 196 | uint8 decimals = IERC20Metadata(token).decimals(); 197 | normalizedAmount = amount; 198 | if (decimals < 18) { 199 | normalizedAmount = amount * (10 ** (18 - decimals)); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/exercises/storage/Authorized.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | /// inherit ownable, switch around the order o 4 | contract Authorized { 5 | /// @notice Mapping of authorized tokens 6 | mapping(address => bool) public authorizedToken; 7 | } 8 | -------------------------------------------------------------------------------- /src/exercises/storage/Balances.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | /// inherit ownable, switch around the order o 4 | contract Balances { 5 | /// @notice User's balance of all tokens deposited in the vault 6 | mapping(address => uint256) public balanceOf; 7 | } 8 | -------------------------------------------------------------------------------- /src/exercises/storage/Supply.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | /// inherit ownable, switch around the order o 4 | contract Supply { 5 | /// @notice Maximum amount of tokens that can be supplied to the vault 6 | uint256 public maxSupply; 7 | 8 | /// @notice Total amount of tokens supplied to the vault 9 | /// 10 | /// invariants: 11 | /// totalSupplied = sum(balanceOf all users) 12 | /// sum(balanceOf(vault) authorized tokens) >= totalSupplied 13 | /// 14 | uint256 public totalSupplied; 15 | } 16 | -------------------------------------------------------------------------------- /src/exercises/storage/VaultStorageOwnable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | import {OwnableUpgradeable} from 4 | "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | 6 | import {Supply} from "src/exercises/storage/Supply.sol"; 7 | import {Balances} from "src/exercises/storage/Balances.sol"; 8 | import {Authorized} from "src/exercises/storage/Authorized.sol"; 9 | 10 | contract VaultStorageOwnable is 11 | OwnableUpgradeable, 12 | Supply, 13 | Balances, 14 | Authorized 15 | {} 16 | -------------------------------------------------------------------------------- /src/exercises/storage/VaultStoragePausable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.25; 2 | 3 | import {PausableUpgradeable} from 4 | "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; 5 | import {OwnableUpgradeable} from 6 | "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 7 | 8 | import {Supply} from "src/exercises/storage/Supply.sol"; 9 | import {Balances} from "src/exercises/storage/Balances.sol"; 10 | import {Authorized} from "src/exercises/storage/Authorized.sol"; 11 | 12 | contract VaultStoragePausable is 13 | OwnableUpgradeable, 14 | Supply, 15 | Authorized, 16 | Balances, 17 | PausableUpgradeable 18 | {} 19 | -------------------------------------------------------------------------------- /test/TestVault00.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {SafeERC20} from 4 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {console} from "@forge-std/console.sol"; 6 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | import {Test} from "@forge-std/Test.sol"; 9 | 10 | import {Vault} from "src/exercises/00/Vault00.sol"; 11 | import {SIP00} from "src/exercises/00/SIP00.sol"; 12 | 13 | contract TestVault00 is Test, SIP00 { 14 | using SafeERC20 for IERC20; 15 | 16 | Vault public vault; 17 | 18 | /// @notice user addresses 19 | address public immutable userA = address(1111); 20 | address public immutable userB = address(2222); 21 | address public immutable userC = address(3333); 22 | 23 | /// @notice token addresses 24 | address public dai; 25 | address public usdc; 26 | address public usdt; 27 | 28 | function setUp() public { 29 | /// set the environment variables 30 | vm.setEnv("DO_RUN", "false"); 31 | vm.setEnv("DO_BUILD", "false"); 32 | vm.setEnv("DO_DEPLOY", "true"); 33 | vm.setEnv("DO_SIMULATE", "false"); 34 | vm.setEnv("DO_PRINT", "false"); 35 | vm.setEnv("DO_VALIDATE", "true"); 36 | 37 | /// setup the proposal 38 | setupProposal(); 39 | 40 | /// run the proposal 41 | deploy(); 42 | 43 | dai = addresses.getAddress("DAI"); 44 | usdc = addresses.getAddress("USDC"); 45 | usdt = addresses.getAddress("USDT"); 46 | vault = Vault(addresses.getAddress("V0_VAULT")); 47 | } 48 | 49 | function testValidate() public view { 50 | /// validate the proposal 51 | validate(); 52 | } 53 | 54 | function testVaultDepositDai() public { 55 | uint256 daiDepositAmount = 1_000e18; 56 | 57 | _vaultDeposit(dai, address(this), daiDepositAmount); 58 | } 59 | 60 | function testVaultDepositUsdc() public { 61 | uint256 usdcDepositAmount = 1_000e6; 62 | 63 | _vaultDeposit(usdc, address(this), usdcDepositAmount); 64 | } 65 | 66 | function testMultipleUsersDepositUsdc() public { 67 | uint256 usdcDepositAmount = 1_000e6; 68 | 69 | _vaultDeposit(usdc, userA, usdcDepositAmount); 70 | _vaultDeposit(usdc, userB, usdcDepositAmount); 71 | _vaultDeposit(usdc, userC, usdcDepositAmount); 72 | } 73 | 74 | function testVaultDepositUsdt() public { 75 | uint256 usdtDepositAmount = 1_000e6; 76 | 77 | deal(usdt, address(this), usdtDepositAmount); 78 | 79 | USDT(usdt).approve( 80 | addresses.getAddress("V0_VAULT"), usdtDepositAmount 81 | ); 82 | 83 | /// this executes 3 state transitions: 84 | /// 1. deposit dai into the vault 85 | /// 2. increase the user's balance in the vault 86 | /// 3. increase the total supplied amount in the vault 87 | vault.deposit(usdt, usdtDepositAmount); 88 | 89 | assertEq( 90 | vault.balanceOf(address(this)), 91 | usdtDepositAmount, 92 | "vault token balance not increased" 93 | ); 94 | assertEq( 95 | vault.totalSupplied(), 96 | usdtDepositAmount, 97 | "vault total supplied not increased" 98 | ); 99 | assertEq( 100 | IERC20(usdt).balanceOf(address(vault)), 101 | usdtDepositAmount, 102 | "token balance not increased" 103 | ); 104 | } 105 | 106 | function testVaultWithdrawalDai() public { 107 | uint256 daiDepositAmount = 1_000e18; 108 | 109 | _vaultDeposit(dai, address(this), daiDepositAmount); 110 | 111 | vault.withdraw(dai, daiDepositAmount); 112 | 113 | assertEq( 114 | vault.balanceOf(address(this)), 115 | 0, 116 | "vault dai balance not 0" 117 | ); 118 | assertEq( 119 | vault.totalSupplied(), 0, "vault total supplied not 0" 120 | ); 121 | assertEq( 122 | IERC20(dai).balanceOf(address(this)), 123 | daiDepositAmount, 124 | "user's dai balance not increased" 125 | ); 126 | } 127 | 128 | function testSwapTwoUsers() public { 129 | uint256 usdcDepositAmount = 1_000e6; 130 | uint256 usdtDepositAmount = 1_000e8; 131 | 132 | _vaultDeposit(usdc, userA, usdcDepositAmount); 133 | _vaultDeposit(usdt, userB, usdtDepositAmount); 134 | 135 | vm.prank(userA); 136 | vault.withdraw(usdt, usdcDepositAmount); 137 | assertEq( 138 | IERC20(usdt).balanceOf(userA), 139 | usdcDepositAmount, 140 | "userA usdt balance not increased" 141 | ); 142 | 143 | vm.prank(userB); 144 | vault.withdraw(usdc, usdcDepositAmount); 145 | assertEq( 146 | IERC20(usdt).balanceOf(userA), 147 | usdcDepositAmount, 148 | "userB usdc balance not increased" 149 | ); 150 | } 151 | 152 | function _vaultDeposit( 153 | address token, 154 | address sender, 155 | uint256 amount 156 | ) private { 157 | uint256 startingTotalSupplied = vault.totalSupplied(); 158 | uint256 startingTotalBalance = 159 | IERC20(token).balanceOf(address(vault)); 160 | uint256 startingUserBalance = vault.balanceOf(sender); 161 | 162 | deal(token, sender, amount); 163 | 164 | vm.startPrank(sender); 165 | IERC20(token).safeIncreaseAllowance( 166 | addresses.getAddress("V0_VAULT"), amount 167 | ); 168 | 169 | /// this executes 3 state transitions: 170 | /// 1. deposit dai into the vault 171 | /// 2. increase the user's balance in the vault 172 | /// 3. increase the total supplied amount in the vault 173 | vault.deposit(token, amount); 174 | vm.stopPrank(); 175 | 176 | assertEq( 177 | vault.balanceOf(sender), 178 | startingUserBalance + amount, 179 | "user vault balance not increased" 180 | ); 181 | assertEq( 182 | vault.totalSupplied(), 183 | startingTotalSupplied + amount, 184 | "vault total supplied not increased by deposited amount" 185 | ); 186 | assertEq( 187 | IERC20(token).balanceOf(address(vault)), 188 | startingTotalBalance + amount, 189 | "token balance not increased" 190 | ); 191 | } 192 | } 193 | 194 | interface USDT { 195 | function approve(address, uint256) external; 196 | function transferFrom(address, address, uint256) external; 197 | } 198 | -------------------------------------------------------------------------------- /test/TestVault01.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {SafeERC20} from 4 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {console} from "@forge-std/console.sol"; 6 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import {Test} from "@forge-std/Test.sol"; 8 | 9 | import {Vault} from "src/exercises/01/Vault01.sol"; 10 | import {SIP01} from "src/exercises/01/SIP01.sol"; 11 | 12 | contract TestVault01 is Test, SIP01 { 13 | using SafeERC20 for IERC20; 14 | 15 | Vault public vault; 16 | 17 | /// @notice user addresses 18 | address public immutable userA = address(1111); 19 | address public immutable userB = address(2222); 20 | address public immutable userC = address(3333); 21 | 22 | /// @notice token addresses 23 | address public usdc; 24 | address public usdt; 25 | 26 | function setUp() public { 27 | /// set the environment variables 28 | vm.setEnv("DO_RUN", "false"); 29 | vm.setEnv("DO_BUILD", "false"); 30 | vm.setEnv("DO_DEPLOY", "true"); 31 | vm.setEnv("DO_SIMULATE", "false"); 32 | vm.setEnv("DO_PRINT", "false"); 33 | vm.setEnv("DO_VALIDATE", "true"); 34 | 35 | /// setup the proposal 36 | setupProposal(); 37 | 38 | /// run the proposal 39 | deploy(); 40 | 41 | usdc = addresses.getAddress("USDC"); 42 | usdt = addresses.getAddress("USDT"); 43 | vault = Vault(addresses.getAddress("V1_VAULT")); 44 | } 45 | 46 | function testVaultDepositUsdc() public { 47 | uint256 usdcDepositAmount = 1_000e6; 48 | 49 | _vaultDeposit(usdc, address(this), usdcDepositAmount); 50 | } 51 | 52 | function testMultipleUsersDepositUsdc() public { 53 | uint256 usdcDepositAmount = 1_000e6; 54 | 55 | _vaultDeposit(usdc, userA, usdcDepositAmount); 56 | _vaultDeposit(usdc, userB, usdcDepositAmount); 57 | _vaultDeposit(usdc, userC, usdcDepositAmount); 58 | } 59 | 60 | function testVaultWithdrawalUsdc() public { 61 | uint256 usdcDepositAmount = 1_000e6; 62 | 63 | _vaultDeposit(usdc, address(this), usdcDepositAmount); 64 | 65 | vault.withdraw(usdc, usdcDepositAmount); 66 | 67 | assertEq( 68 | vault.balanceOf(address(this)), 69 | 0, 70 | "vault usdc balance not 0" 71 | ); 72 | assertEq( 73 | vault.totalSupplied(), 0, "vault total supplied not 0" 74 | ); 75 | assertEq( 76 | IERC20(usdc).balanceOf(address(this)), 77 | usdcDepositAmount, 78 | "user's usdc balance not increased" 79 | ); 80 | } 81 | 82 | function testVaultDepositUsdt() public { 83 | uint256 usdtDepositAmount = 1_000e8; 84 | 85 | _vaultDeposit(usdt, address(this), usdtDepositAmount); 86 | } 87 | 88 | function testVaultWithdrawalUsdt() public { 89 | uint256 usdtDepositAmount = 1_000e8; 90 | 91 | _vaultDeposit(usdt, address(this), usdtDepositAmount); 92 | vault.withdraw(usdt, usdtDepositAmount); 93 | 94 | assertEq( 95 | vault.balanceOf(address(this)), 96 | 0, 97 | "vault usdt balance not 0" 98 | ); 99 | assertEq( 100 | vault.totalSupplied(), 0, "vault total supplied not 0" 101 | ); 102 | assertEq( 103 | IERC20(usdt).balanceOf(address(this)), 104 | usdtDepositAmount, 105 | "user's usdt balance not increased" 106 | ); 107 | } 108 | 109 | function testSwapTwoUsers() public { 110 | uint256 usdcDepositAmount = 1_000e6; 111 | uint256 usdtDepositAmount = 1_000e8; 112 | 113 | _vaultDeposit(usdc, userA, usdcDepositAmount); 114 | _vaultDeposit(usdt, userB, usdtDepositAmount); 115 | 116 | vm.prank(userA); 117 | vault.withdraw(usdt, usdcDepositAmount); 118 | assertEq( 119 | IERC20(usdt).balanceOf(userA), 120 | usdcDepositAmount, 121 | "userA usdt balance not increased" 122 | ); 123 | 124 | vm.prank(userB); 125 | vault.withdraw(usdc, usdcDepositAmount); 126 | assertEq( 127 | IERC20(usdc).balanceOf(userB), 128 | usdcDepositAmount, 129 | "userB usdc balance not increased" 130 | ); 131 | assertEq( 132 | IERC20(usdt).balanceOf(userA), 133 | usdcDepositAmount, 134 | "userB usdt balance remains unchanged" 135 | ); 136 | } 137 | 138 | function _vaultDeposit( 139 | address token, 140 | address sender, 141 | uint256 amount 142 | ) private { 143 | uint256 startingTotalSupplied = vault.totalSupplied(); 144 | uint256 startingTotalBalance = 145 | IERC20(token).balanceOf(address(vault)); 146 | uint256 startingUserBalance = vault.balanceOf(sender); 147 | 148 | deal(token, sender, amount); 149 | 150 | vm.startPrank(sender); 151 | IERC20(token).safeIncreaseAllowance( 152 | addresses.getAddress("V1_VAULT"), amount 153 | ); 154 | 155 | /// this executes 3 state transitions: 156 | /// 1. deposit dai into the vault 157 | /// 2. increase the user's balance in the vault 158 | /// 3. increase the total supplied amount in the vault 159 | vault.deposit(token, amount); 160 | vm.stopPrank(); 161 | 162 | uint256 normalizedAmount = 163 | vault.getNormalizedAmount(token, amount); 164 | 165 | assertEq( 166 | vault.balanceOf(sender), 167 | startingUserBalance + normalizedAmount, 168 | "user vault balance not increased" 169 | ); 170 | assertEq( 171 | vault.totalSupplied(), 172 | startingTotalSupplied + normalizedAmount, 173 | "vault total supplied not increased by deposited amount" 174 | ); 175 | assertEq( 176 | IERC20(token).balanceOf(address(vault)), 177 | startingTotalBalance + amount, 178 | "token balance not increased" 179 | ); 180 | } 181 | } 182 | 183 | interface USDT { 184 | function approve(address, uint256) external; 185 | function transferFrom(address, address, uint256) external; 186 | } 187 | -------------------------------------------------------------------------------- /test/TestVault02.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {SafeERC20} from 4 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import {Test} from "@forge-std/Test.sol"; 7 | 8 | import {Vault} from "src/exercises/02/Vault02.sol"; 9 | import {SIP02} from "src/exercises/02/SIP02.sol"; 10 | 11 | contract TestVault02 is Test, SIP02 { 12 | using SafeERC20 for IERC20; 13 | 14 | Vault public vault; 15 | 16 | /// @notice user addresses 17 | address public immutable userA = address(1111); 18 | address public immutable userB = address(2222); 19 | address public immutable userC = address(3333); 20 | 21 | /// @notice token addresses 22 | address public dai; 23 | address public usdc; 24 | address public usdt; 25 | 26 | function setUp() public { 27 | /// set the environment variables 28 | vm.setEnv("DO_RUN", "false"); 29 | vm.setEnv("DO_BUILD", "false"); 30 | vm.setEnv("DO_DEPLOY", "true"); 31 | vm.setEnv("DO_SIMULATE", "false"); 32 | vm.setEnv("DO_PRINT", "false"); 33 | vm.setEnv("DO_VALIDATE", "true"); 34 | 35 | /// setup the proposal 36 | setupProposal(); 37 | 38 | /// run the proposal 39 | deploy(); 40 | 41 | dai = addresses.getAddress("DAI"); 42 | usdc = addresses.getAddress("USDC"); 43 | usdt = addresses.getAddress("USDT"); 44 | vault = Vault(addresses.getAddress("V2_VAULT")); 45 | } 46 | 47 | function testValidate() public view { 48 | /// validate the proposal 49 | validate(); 50 | } 51 | 52 | function testVaultDepositDai() public { 53 | uint256 daiDepositAmount = 1_000e18; 54 | 55 | _vaultDeposit(dai, address(this), daiDepositAmount); 56 | } 57 | 58 | function testVaultDepositUsdc() public { 59 | uint256 usdcDepositAmount = 1_000e6; 60 | 61 | _vaultDeposit(usdc, address(this), usdcDepositAmount); 62 | } 63 | 64 | function testMultipleUsersDepositUsdc() public { 65 | uint256 usdcDepositAmount = 1_000e6; 66 | 67 | _vaultDeposit(usdc, userA, usdcDepositAmount); 68 | _vaultDeposit(usdc, userB, usdcDepositAmount); 69 | _vaultDeposit(usdc, userC, usdcDepositAmount); 70 | } 71 | 72 | function testVaultDepositUsdt() public { 73 | uint256 usdtDepositAmount = 1_000e6; 74 | 75 | deal(usdt, address(this), usdtDepositAmount); 76 | 77 | USDT(usdt).approve( 78 | addresses.getAddress("V2_VAULT"), usdtDepositAmount 79 | ); 80 | 81 | vault.deposit(usdt, usdtDepositAmount); 82 | 83 | assertEq( 84 | vault.balanceOf(address(this)), 85 | vault.getNormalizedAmount(usdt, usdtDepositAmount), 86 | "vault token balance not increased" 87 | ); 88 | assertEq( 89 | vault.totalSupplied(), 90 | vault.getNormalizedAmount(usdt, usdtDepositAmount), 91 | "vault total supplied not increased" 92 | ); 93 | assertEq( 94 | IERC20(usdt).balanceOf(address(vault)), 95 | usdtDepositAmount, 96 | "token balance not increased" 97 | ); 98 | } 99 | 100 | function testVaultWithdrawalDai() public { 101 | uint256 daiDepositAmount = 1_000e18; 102 | 103 | _vaultDeposit(dai, address(this), daiDepositAmount); 104 | 105 | vault.withdraw(dai, daiDepositAmount); 106 | 107 | assertEq( 108 | vault.balanceOf(address(this)), 109 | 0, 110 | "vault dai balance not 0" 111 | ); 112 | assertEq( 113 | vault.totalSupplied(), 0, "vault total supplied not 0" 114 | ); 115 | assertEq( 116 | IERC20(dai).balanceOf(address(this)), 117 | daiDepositAmount, 118 | "user's dai balance not increased" 119 | ); 120 | } 121 | 122 | function testSwapTwoUsers() public { 123 | uint256 usdcDepositAmount = 1_000e6; 124 | uint256 usdtDepositAmount = 1_000e8; 125 | 126 | _vaultDeposit(usdc, userA, usdcDepositAmount); 127 | _vaultDeposit(usdt, userB, usdtDepositAmount); 128 | 129 | vm.prank(userA); 130 | vault.withdraw(usdt, usdcDepositAmount); 131 | assertEq( 132 | IERC20(usdt).balanceOf(userA), 133 | usdcDepositAmount, 134 | "userA usdt balance not increased" 135 | ); 136 | 137 | vm.prank(userB); 138 | vault.withdraw(usdc, usdcDepositAmount); 139 | assertEq( 140 | IERC20(usdc).balanceOf(userB), 141 | usdcDepositAmount, 142 | "userB usdc balance not increased" 143 | ); 144 | assertEq( 145 | IERC20(usdt).balanceOf(userA), 146 | usdcDepositAmount, 147 | "userB usdc balance not increased" 148 | ); 149 | } 150 | 151 | function _vaultDeposit( 152 | address token, 153 | address sender, 154 | uint256 amount 155 | ) private { 156 | uint256 startingTotalSupplied = vault.totalSupplied(); 157 | uint256 startingTotalBalance = 158 | IERC20(token).balanceOf(address(vault)); 159 | uint256 startingUserBalance = vault.balanceOf(sender); 160 | 161 | deal(token, sender, amount); 162 | 163 | vm.startPrank(sender); 164 | IERC20(token).safeIncreaseAllowance( 165 | addresses.getAddress("V2_VAULT"), amount 166 | ); 167 | 168 | /// this executes 3 state transitions: 169 | /// 1. deposit dai into the vault 170 | /// 2. increase the user's balance in the vault 171 | /// 3. increase the total supplied amount in the vault 172 | vault.deposit(token, amount); 173 | vm.stopPrank(); 174 | 175 | uint256 normalizedAmount = 176 | vault.getNormalizedAmount(token, amount); 177 | 178 | assertEq( 179 | vault.balanceOf(sender), 180 | startingUserBalance + normalizedAmount, 181 | "user vault balance not increased" 182 | ); 183 | assertEq( 184 | vault.totalSupplied(), 185 | startingTotalSupplied + normalizedAmount, 186 | "vault total supplied not increased by deposited amount" 187 | ); 188 | assertEq( 189 | IERC20(token).balanceOf(address(vault)), 190 | startingTotalBalance + amount, 191 | "token balance not increased" 192 | ); 193 | } 194 | } 195 | 196 | interface USDT { 197 | function approve(address, uint256) external; 198 | function transferFrom(address, address, uint256) external; 199 | } 200 | -------------------------------------------------------------------------------- /test/TestVault03.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {SafeERC20} from 4 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import {Test, console} from "@forge-std/Test.sol"; 7 | 8 | import {SIP03} from "src/exercises/03/SIP03.sol"; 9 | import {Vault} from "src/exercises/03/Vault03.sol"; 10 | 11 | contract TestVault03 is Test, SIP03 { 12 | using SafeERC20 for IERC20; 13 | 14 | Vault public vault; 15 | 16 | /// @notice user addresses 17 | address public immutable userA = address(1111); 18 | address public immutable userB = address(2222); 19 | address public immutable userC = address(3333); 20 | 21 | /// @notice token addresses 22 | address public dai; 23 | address public usdc; 24 | address public usdt; 25 | 26 | function _loadUsers() private { 27 | address[] memory users = new address[](3); 28 | users[0] = userA; 29 | users[1] = userB; 30 | users[2] = userC; 31 | 32 | for (uint256 i = 0; i < users.length; i++) { 33 | uint256 daiDepositAmount = 1_000e18; 34 | uint256 usdtDepositAmount = 1_000e8; 35 | uint256 usdcDepositAmount = 1_000e6; 36 | 37 | _vaultDeposit(dai, users[i], daiDepositAmount); 38 | _vaultDeposit(usdc, users[i], usdcDepositAmount); 39 | _vaultDeposit(usdt, users[i], usdtDepositAmount); 40 | } 41 | } 42 | 43 | function setUp() public { 44 | /// set the environment variables 45 | vm.setEnv("DO_RUN", "false"); 46 | vm.setEnv("DO_BUILD", "false"); 47 | vm.setEnv("DO_DEPLOY", "true"); 48 | vm.setEnv("DO_SIMULATE", "false"); 49 | vm.setEnv("DO_PRINT", "false"); 50 | vm.setEnv("DO_VALIDATE", "false"); 51 | 52 | setupProposal(); 53 | 54 | deploy(); 55 | 56 | dai = addresses.getAddress("DAI"); 57 | usdc = addresses.getAddress("USDC"); 58 | usdt = addresses.getAddress("USDT"); 59 | vault = Vault(addresses.getAddress("VAULT_PROXY")); 60 | 61 | vm.prank(vault.owner()); 62 | vault.setMaxSupply(100_000_000e18); 63 | 64 | /// load data into newly deployed contract 65 | _loadUsers(); 66 | } 67 | 68 | function testSetup() public view { 69 | validate(); 70 | assertEq( 71 | vault.maxSupply(), 100_000_000e18, "max supply not set" 72 | ); 73 | assertEq( 74 | vault.totalSupplied(), 75 | ( 76 | vault.getNormalizedAmount(dai, 1_000e18) 77 | + vault.getNormalizedAmount(usdc, 1_000e6) 78 | + vault.getNormalizedAmount(usdt, 1_000e8) 79 | ) * 3, 80 | "total supplied not set" 81 | ); 82 | } 83 | 84 | function testVaultDepositDai() public { 85 | uint256 daiDepositAmount = 1_000e18; 86 | 87 | _vaultDeposit(dai, address(this), daiDepositAmount); 88 | } 89 | 90 | function testVaultWithdrawalDai() public { 91 | uint256 daiDepositAmount = 1_000e18; 92 | 93 | _vaultDeposit(dai, address(this), daiDepositAmount); 94 | uint256 startingVaultBalance = vault.balanceOf(address(this)); 95 | uint256 startingTotalSupplied = vault.totalSupplied(); 96 | 97 | vault.withdraw(dai, daiDepositAmount); 98 | 99 | assertEq( 100 | vault.balanceOf(address(this)), 101 | startingVaultBalance - daiDepositAmount, 102 | "vault dai balance not 0" 103 | ); 104 | assertEq( 105 | vault.totalSupplied(), 106 | startingTotalSupplied - daiDepositAmount, 107 | "vault total supplied not 0" 108 | ); 109 | assertEq( 110 | IERC20(dai).balanceOf(address(this)), 111 | daiDepositAmount, 112 | "user's dai balance not increased" 113 | ); 114 | } 115 | 116 | function testVaultWithdrawUSDC() public { 117 | uint256 usdcDepositAmount = 1_000e6; 118 | 119 | _vaultDeposit(usdc, address(this), usdcDepositAmount); 120 | uint256 startingVaultBalance = vault.balanceOf(address(this)); 121 | uint256 startingTotalSupplied = vault.totalSupplied(); 122 | 123 | vault.withdraw(usdc, usdcDepositAmount); 124 | 125 | assertEq( 126 | vault.balanceOf(address(this)), 127 | startingVaultBalance 128 | - vault.getNormalizedAmount(usdc, usdcDepositAmount), 129 | "vault usdc balance not 0" 130 | ); 131 | assertEq( 132 | vault.totalSupplied(), 133 | startingTotalSupplied 134 | - vault.getNormalizedAmount(usdc, usdcDepositAmount), 135 | "vault total supplied not 0" 136 | ); 137 | assertEq( 138 | IERC20(usdc).balanceOf(address(this)), 139 | usdcDepositAmount, 140 | "user's usdc balance not increased" 141 | ); 142 | } 143 | 144 | function testVaultWithdrawUSDT() public { 145 | uint256 usdtDepositAmount = 1_000e8; 146 | 147 | _vaultDeposit(usdt, address(this), usdtDepositAmount); 148 | uint256 startingVaultBalance = vault.balanceOf(address(this)); 149 | uint256 startingTotalSupplied = vault.totalSupplied(); 150 | 151 | vault.withdraw(usdt, usdtDepositAmount); 152 | 153 | assertEq( 154 | vault.balanceOf(address(this)), 155 | startingVaultBalance 156 | - vault.getNormalizedAmount(usdt, usdtDepositAmount), 157 | "vault usdt balance not 0" 158 | ); 159 | assertEq( 160 | vault.totalSupplied(), 161 | startingTotalSupplied 162 | - vault.getNormalizedAmount(usdt, usdtDepositAmount), 163 | "vault total supplied not 0" 164 | ); 165 | assertEq( 166 | IERC20(usdt).balanceOf(address(this)), 167 | usdtDepositAmount, 168 | "user's usdt balance not increased" 169 | ); 170 | } 171 | 172 | function _vaultDeposit( 173 | address token, 174 | address sender, 175 | uint256 amount 176 | ) private { 177 | uint256 startingTotalSupplied = vault.totalSupplied(); 178 | uint256 startingTotalBalance = 179 | IERC20(token).balanceOf(address(vault)); 180 | uint256 startingUserBalance = vault.balanceOf(sender); 181 | 182 | deal(token, sender, amount); 183 | 184 | vm.startPrank(sender); 185 | IERC20(token).safeIncreaseAllowance( 186 | addresses.getAddress("VAULT_PROXY"), amount 187 | ); 188 | 189 | vault.deposit(token, amount); 190 | vm.stopPrank(); 191 | 192 | uint256 normalizedAmount = 193 | vault.getNormalizedAmount(token, amount); 194 | 195 | assertEq( 196 | vault.balanceOf(sender), 197 | startingUserBalance + normalizedAmount, 198 | "user vault balance not increased" 199 | ); 200 | assertEq( 201 | vault.totalSupplied(), 202 | startingTotalSupplied + normalizedAmount, 203 | "vault total supplied not increased by deposited amount" 204 | ); 205 | assertEq( 206 | IERC20(token).balanceOf(address(vault)), 207 | startingTotalBalance + amount, 208 | "token balance not increased" 209 | ); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /test/TestVault04.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {SafeERC20} from 4 | "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import {Test, console} from "@forge-std/Test.sol"; 7 | import {ERC1967Utils} from 8 | "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; 9 | import {ProxyAdmin} from 10 | "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 11 | 12 | import {SIP03} from "src/exercises/03/SIP03.sol"; 13 | import {SIP04} from "src/exercises/04/SIP04.sol"; 14 | import {Vault} from "src/exercises/04/Vault04.sol"; 15 | 16 | contract TestVault04 is Test, SIP04 { 17 | using SafeERC20 for IERC20; 18 | 19 | Vault public vault; 20 | 21 | /// @notice user addresses 22 | address public immutable userA = address(1111); 23 | address public immutable userB = address(2222); 24 | address public immutable userC = address(3333); 25 | 26 | /// @notice token addresses 27 | address public dai; 28 | address public usdc; 29 | address public usdt; 30 | 31 | function setUp() public { 32 | /// set the environment variables 33 | vm.setEnv("DO_RUN", "false"); 34 | vm.setEnv("DO_BUILD", "false"); 35 | vm.setEnv("DO_DEPLOY", "true"); 36 | vm.setEnv("DO_SIMULATE", "false"); 37 | vm.setEnv("DO_PRINT", "false"); 38 | vm.setEnv("DO_VALIDATE", "false"); 39 | 40 | SIP03 sip03 = new SIP03(); 41 | 42 | sip03.setupProposal(); 43 | sip03.deploy(); 44 | 45 | /// set the addresses contrac to the SIP03 addresses for integration testing 46 | setAddresses(sip03.addresses()); 47 | dai = addresses.getAddress("DAI"); 48 | usdc = addresses.getAddress("USDC"); 49 | usdt = addresses.getAddress("USDT"); 50 | vault = Vault(addresses.getAddress("VAULT_PROXY")); 51 | 52 | vm.prank(vault.owner()); 53 | vault.setMaxSupply(100_000_000e18); 54 | 55 | /// setup the proposal 56 | setupProposal(); 57 | 58 | /// overwrite the newly created proposal Addresses contract 59 | setAddresses(sip03.addresses()); 60 | 61 | /// deploy contracts from MIP-04 62 | deploy(); 63 | 64 | /// build and run proposal 65 | build(); 66 | simulate(); 67 | } 68 | 69 | function testValidate() public view { 70 | assertEq( 71 | vault.maxSupply(), 1_000_000e18, "max supply not set" 72 | ); 73 | 74 | bytes32 adminSlot = 75 | vm.load(address(vault), ERC1967Utils.ADMIN_SLOT); 76 | address proxyAdmin = address(uint160(uint256(adminSlot))); 77 | 78 | assertEq( 79 | ProxyAdmin(proxyAdmin).owner(), 80 | addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"), 81 | "owner not set" 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/utils/Forks.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {Vm} from "@forge-std/Vm.sol"; 4 | 5 | uint256 constant ETHEREUM_FORK_ID = 0; 6 | 7 | library ForkSelector { 8 | Vm internal constant vmContract = 9 | Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); 10 | 11 | function createForksAndSelect(uint256 selectFork) internal { 12 | (bool success,) = address(vmContract).call( 13 | abi.encodeWithSignature("activeFork()") 14 | ); 15 | (bool successSwitchFork,) = address(vmContract).call( 16 | abi.encodeWithSignature("selectFork(uint256)", selectFork) 17 | ); 18 | 19 | if (!successSwitchFork || !success) { 20 | vmContract.createSelectFork("ethereum"); 21 | } else { 22 | vmContract.selectFork(selectFork); 23 | } 24 | } 25 | } 26 | --------------------------------------------------------------------------------