From b4ad9a8f9b72df05e0009a62ef6d2bcd69d0d358 Mon Sep 17 00:00:00 2001 From: prateek Date: Mon, 4 Nov 2024 21:47:25 +0530 Subject: [PATCH 01/22] add sip with incorrect parameter fails in validate --- .gitmodules | 3 + lib/openzeppelin-contracts-upgradeable | 1 + .../sips/SIP00IncorrectParameter.sol | 80 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 160000 lib/openzeppelin-contracts-upgradeable create mode 100644 src/proposals/sips/SIP00IncorrectParameter.sol diff --git a/.gitmodules b/.gitmodules index dd23735..9aa44ff 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..ee3a7e4 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit ee3a7e4f38c62bcd22727489218b29f3b893594a diff --git a/src/proposals/sips/SIP00IncorrectParameter.sol b/src/proposals/sips/SIP00IncorrectParameter.sol new file mode 100644 index 0000000..296f9e5 --- /dev/null +++ b/src/proposals/sips/SIP00IncorrectParameter.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {GovernorBravoProposal} from + "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; +import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; + +import {Vault} from "src/examples/00/Vault00.sol"; +import {MockToken} from "@mocks/MockToken.sol"; +import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; + +/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP00IncorrectParameter.sol:SIP00IncorrectParameter -vvvv +contract SIP00IncorrectParameter is GovernorBravoProposal { + using ForkSelector for uint256; + + constructor() { + // ETHEREUM_FORK_ID.createForksAndSelect(); + primaryForkId = ETHEREUM_FORK_ID; + } + + function setupProposal() public { + ETHEREUM_FORK_ID.createForksAndSelect(); + + string memory addressesFolderPath = "./addresses"; + uint256[] memory chainIds = new uint256[](1); + chainIds[0] = 1; + + setAddresses(new Addresses(addressesFolderPath, chainIds)); + } + + function name() public pure override returns (string memory) { + return "SIP-00 System Deploy Incorrect Params"; + } + + function description() public pure override returns (string memory) { + return name(); + } + + function run() public override { + setupProposal(); + + setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); + + super.run(); + } + + function deploy() public override { + if (!addresses.isAddressSet("V1_VAULT")) { + address[] memory tokens = new address[](3); + tokens[0] = addresses.getAddress("USDC"); + // usdc added again instead of dai + tokens[1] = addresses.getAddress("USDC"); + tokens[2] = addresses.getAddress("USDT"); + + Vault vault = new Vault(tokens); + + addresses.addAddress("V1_VAULT", address(vault), true); + } + } + + function validate() public view override { + Vault vault = Vault(addresses.getAddress("V1_VAULT")); + + assertEq( + vault.authorizedToken(addresses.getAddress("USDC")), + true, + "USDC should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("DAI")), + true, + "DAI should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("USDT")), + true, + "USDT should be authorized" + ); + } +} From 7f1a3f41753a5091b1f26b11faf76161a64a9d31 Mon Sep 17 00:00:00 2001 From: prateek Date: Mon, 4 Nov 2024 21:49:30 +0530 Subject: [PATCH 02/22] add oz upgradeable library --- lib/openzeppelin-contracts-upgradeable | 2 +- remappings.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index ee3a7e4..fa52531 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit ee3a7e4f38c62bcd22727489218b29f3b893594a +Subproject commit fa525310e45f91eb20a6d3baa2644be8e0adba31 diff --git a/remappings.txt b/remappings.txt index 0c0ea9e..122a05d 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,3 @@ @forge-proposal-simulator=lib/forge-proposal-simulator/ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ From 0bb042c8dfeb765fce5af591680eb81f7fa88c6b Mon Sep 17 00:00:00 2001 From: prateek Date: Mon, 4 Nov 2024 21:49:52 +0530 Subject: [PATCH 03/22] add new upgradeable vault --- src/examples/03/Vault03.sol | 165 ++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 src/examples/03/Vault03.sol diff --git a/src/examples/03/Vault03.sol b/src/examples/03/Vault03.sol new file mode 100644 index 0000000..4768546 --- /dev/null +++ b/src/examples/03/Vault03.sol @@ -0,0 +1,165 @@ +pragma solidity 0.8.25; + +import {IERC20Metadata} from + "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from + "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Vault03 is OwnableUpgradeable { + using SafeERC20 for IERC20; + + /// @notice Mapping of authorized tokens + mapping(address => bool) public authorizedToken; + + /// @notice User's balance of all tokens deposited in the vault + mapping(address => uint256) public balanceOf; + + /// @notice Total amount of tokens supplied to the vault + /// + /// invariants: + /// totalSupplied = sum(balanceOf all users) + /// sum(balanceOf(vault) authorized tokens) >= totalSupplied + /// + uint256 public totalSupplied; + + /// @notice Deposit event + /// @param token The token deposited + /// @param sender The address that deposited the token + /// @param amount The amount deposited + event Deposit( + address indexed token, address indexed sender, uint256 amount + ); + + /// @notice Withdraw event + /// @param token The token withdrawn + /// @param sender The address that withdrew the token + /// @param amount The amount withdrawn + event Withdraw( + address indexed token, address indexed sender, uint256 amount + ); + + event TokenAdded(address indexed token); + + /// @notice Construct the vault with a list of authorized tokens + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the vault with a list of authorized tokens + /// @param _tokens The list of authorized tokens + /// @param _owner The owner address to set for the contract + function initialize(address[] memory _tokens, address _owner) external initializer { + __Ownable_init(_owner); + + for (uint256 i = 0; i < _tokens.length; i++) { + require( + IERC20Metadata(_tokens[i]).decimals() <= 18, + "Vault: unsupported decimals" + ); + + authorizedToken[_tokens[i]] = true; + + emit TokenAdded(_tokens[i]); + } + } + + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + /// -------------------- ONLY OWNER FUNCTION -------------------- + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + + /// @notice Add a token to the list of authorized tokens + /// only callable by the owner + /// @param token to add + function addToken(address token) external onlyOwner { + require( + IERC20Metadata(token).decimals() <= 18, + "Vault: unsupported decimals" + ); + require(!authorizedToken[token], "Vault: token already authorized"); + + authorizedToken[token] = true; + + emit TokenAdded(token); + } + + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + + /// @notice Deposit tokens into the vault + /// @param token The token to deposit, only authorized tokens allowed + /// @param amount The amount to deposit + function deposit(address token, uint256 amount) external { + require(authorizedToken[token], "Vault: token not authorized"); + + uint256 normalizedAmount = getNormalizedAmount(token, amount); + + /// save on gas by using unchecked, no need to check for overflow + /// as all deposited tokens are whitelisted + unchecked { + balanceOf[msg.sender] += normalizedAmount; + } + + totalSupplied += normalizedAmount; + + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + emit Deposit(token, msg.sender, amount); + } + + /// @notice Withdraw tokens from the vault + /// @param token The token to withdraw, only authorized tokens are allowed + /// this is implicitly checked because a user can only have a balance of an + /// authorized token + /// @param amount The amount to withdraw + function withdraw(address token, uint256 amount) external { + require(authorizedToken[token], "Vault: token not authorized"); + + uint256 normalizedAmount = getNormalizedAmount(token, amount); + + /// both a check and an effect, ensures user has sufficient funds for + /// withdrawal + /// must be checked for underflow as a user can only withdraw what they + /// have deposited + balanceOf[msg.sender] -= normalizedAmount; + + /// save on gas by using unchecked, no need to check for underflow + /// as all deposited tokens are whitelisted, plus we know our invariant + /// always holds + unchecked { + totalSupplied -= normalizedAmount; + } + + IERC20(token).safeTransfer(msg.sender, amount); + + emit Withdraw(token, msg.sender, amount); + } + + /// -------------------------------------------------------- + /// -------------------------------------------------------- + /// ----------------- PUBLIC VIEW FUNCTION ----------------- + /// -------------------------------------------------------- + /// -------------------------------------------------------- + + /// @notice public for testing purposes, returns the normalized amount of + /// tokens scaled to 18 decimals + /// @param token The token to deposit + /// @param amount The amount to deposit + function getNormalizedAmount(address token, uint256 amount) + public + view + returns (uint256 normalizedAmount) + { + uint8 decimals = IERC20Metadata(token).decimals(); + normalizedAmount = amount; + if (decimals < 18) { + normalizedAmount = amount * (10 ** (18 - decimals)); + } + } +} From eea26e146359992fd8524c7bf49789530388406d Mon Sep 17 00:00:00 2001 From: prateek Date: Mon, 4 Nov 2024 21:50:19 +0530 Subject: [PATCH 04/22] add faulty vault implementation for upgrade --- src/examples/04/Vault04.sol | 176 ++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/examples/04/Vault04.sol diff --git a/src/examples/04/Vault04.sol b/src/examples/04/Vault04.sol new file mode 100644 index 0000000..603d929 --- /dev/null +++ b/src/examples/04/Vault04.sol @@ -0,0 +1,176 @@ +pragma solidity 0.8.25; + +import {IERC20Metadata} from + "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from + "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + + +/// @notice Add maxsupply to the vault and update getNormalizedAmount logic +contract Vault04 is OwnableUpgradeable { + using SafeERC20 for IERC20; + + /// @notice Mapping of authorized tokens + mapping(address => bool) public authorizedToken; + + /// @notice User's balance of all tokens deposited in the vault + mapping(address => uint256) public balanceOf; + + /// @notice Maximum amount of tokens that can be supplied to the vault + uint256 public maxSupply; + + /// @notice Total amount of tokens supplied to the vault + /// + /// invariants: + /// totalSupplied = sum(balanceOf all users) + /// sum(balanceOf(vault) authorized tokens) >= totalSupplied + /// + uint256 public totalSupplied; + + /// @notice Deposit event + /// @param token The token deposited + /// @param sender The address that deposited the token + /// @param amount The amount deposited + event Deposit( + address indexed token, address indexed sender, uint256 amount + ); + + /// @notice Withdraw event + /// @param token The token withdrawn + /// @param sender The address that withdrew the token + /// @param amount The amount withdrawn + event Withdraw( + address indexed token, address indexed sender, uint256 amount + ); + + event TokenAdded(address indexed token); + + /// @notice Construct the vault with a list of authorized tokens + constructor (){ + _disableInitializers(); + } + + /// @notice Initialize the vault with a list of authorized tokens + /// @param _tokens The list of authorized tokens + /// @param _owner The owner address to set for the contract + function initialize(address[] memory _tokens, address _owner) external initializer { + __Ownable_init(_owner); + + for (uint256 i = 0; i < _tokens.length; i++) { + require( + IERC20Metadata(_tokens[i]).decimals() <= 18, + "Vault: unsupported decimals" + ); + + authorizedToken[_tokens[i]] = true; + + emit TokenAdded(_tokens[i]); + } + } + + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + /// -------------------- ONLY OWNER FUNCTION -------------------- + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + + /// @notice Add a token to the list of authorized tokens + /// only callable by the owner + /// @param token to add + function addToken(address token) external onlyOwner { + require( + IERC20Metadata(token).decimals() <= 18, + "Vault: unsupported decimals" + ); + require(!authorizedToken[token], "Vault: token already authorized"); + + authorizedToken[token] = true; + + emit TokenAdded(token); + } + + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + + /// @notice Deposit tokens into the vault + /// @param token The token to deposit, only authorized tokens allowed + /// @param amount The amount to deposit + function deposit(address token, uint256 amount) external { + require(authorizedToken[token], "Vault: token not authorized"); + + uint256 normalizedAmount = getNormalizedAmount(token, amount); + + /// save on gas by using unchecked, no need to check for overflow + /// as all deposited tokens are whitelisted + unchecked { + balanceOf[msg.sender] += normalizedAmount; + } + + totalSupplied += normalizedAmount; + + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + emit Deposit(token, msg.sender, amount); + } + + /// @notice Withdraw tokens from the vault + /// @param token The token to withdraw, only authorized tokens are allowed + /// this is implicitly checked because a user can only have a balance of an + /// authorized token + /// @param amount The amount to withdraw + function withdraw(address token, uint256 amount) external { + require(authorizedToken[token], "Vault: token not authorized"); + + uint256 normalizedAmount = getNormalizedAmount(token, amount); + + /// both a check and an effect, ensures user has sufficient funds for + /// withdrawal + /// must be checked for underflow as a user can only withdraw what they + /// have deposited + balanceOf[msg.sender] -= normalizedAmount; + + /// save on gas by using unchecked, no need to check for underflow + /// as all deposited tokens are whitelisted, plus we know our invariant + /// always holds + unchecked { + totalSupplied -= normalizedAmount; + } + + IERC20(token).safeTransfer(msg.sender, amount); + + emit Withdraw(token, msg.sender, amount); + } + + /// @notice Set the maximum supply of the vault + /// @param _maxSupply The maximum supply of the vault + function setMaxSupply(uint256 _maxSupply) external onlyOwner { + maxSupply = _maxSupply; + } + + /// -------------------------------------------------------- + /// -------------------------------------------------------- + /// ----------------- PUBLIC VIEW FUNCTION ----------------- + /// -------------------------------------------------------- + /// -------------------------------------------------------- + + /// @notice public for testing purposes, returns the normalized amount of + /// tokens scaled to 18 decimals + /// @param token The token to deposit + /// @param amount The amount to deposit + function getNormalizedAmount(address token, uint256 amount) + public + view + returns (uint256 normalizedAmount) + { + uint8 decimals = IERC20Metadata(token).decimals(); + // normalizedAmount = amount; + if (decimals < 18) { + normalizedAmount = amount * (10 ** (18 - decimals)); + } + } +} From 7b452437e1094b54b47014af33a57d76c0cfb60b Mon Sep 17 00:00:00 2001 From: prateek Date: Mon, 4 Nov 2024 21:51:08 +0530 Subject: [PATCH 05/22] add sip to upgrade vault and fails in validation --- src/proposals/sips/SIP02IncorrectUpgrade.sol | 114 +++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/proposals/sips/SIP02IncorrectUpgrade.sol diff --git a/src/proposals/sips/SIP02IncorrectUpgrade.sol b/src/proposals/sips/SIP02IncorrectUpgrade.sol new file mode 100644 index 0000000..0edba26 --- /dev/null +++ b/src/proposals/sips/SIP02IncorrectUpgrade.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {GovernorBravoProposal} from + "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; +import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; + +import {Vault03} from "src/examples/03/Vault03.sol"; +import {Vault04} from "src/examples/04/Vault04.sol"; +import {MockToken} from "@mocks/MockToken.sol"; +import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; +import {ProxyAdmin, TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + + +/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP02IncorrectUpgrade.sol:SIP02IncorrectUpgrade -vvvv +contract SIP02IncorrectUpgrade is GovernorBravoProposal { + using ForkSelector for uint256; + + constructor() { + // ETHEREUM_FORK_ID.createForksAndSelect(); + primaryForkId = ETHEREUM_FORK_ID; + } + + function setupProposal() public { + ETHEREUM_FORK_ID.createForksAndSelect(); + + string memory addressesFolderPath = "./addresses"; + uint256[] memory chainIds = new uint256[](1); + chainIds[0] = 1; + + setAddresses(new Addresses(addressesFolderPath, chainIds)); + } + + function name() public pure override returns (string memory) { + return "SIP-02 Incorrect Upgrade"; + } + + function description() public pure override returns (string memory) { + return name(); + } + + function run() public override { + setupProposal(); + + setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); + + super.run(); + } + + function deploy() public override { + address vaultProxy; + if (!addresses.isAddressSet("V3_VAULT_IMPLEMENTATION")) { + address vaultImpl = address(new Vault03()); + addresses.addAddress("V3_VAULT_IMPLEMENTATION", vaultImpl, true); + + address[] memory tokens = new address[](3); + tokens[0] = addresses.getAddress("USDC"); + tokens[1] = addresses.getAddress("DAI"); + tokens[2] = addresses.getAddress("USDT"); + + address owner = addresses.getAddress("DEPLOYER_EOA"); + + // Generate calldata for initialize function of vault + bytes memory data = abi.encodeWithSignature("initialize(address[],address)", tokens, owner); + + vaultProxy = address(new TransparentUpgradeableProxy(vaultImpl, owner, data)); + addresses.addAddress("VAULT_PROXY", vaultProxy, true); + } + + deal(addresses.getAddress("USDC"), addresses.getAddress("DEPLOYER_EOA"), 1_000e18); + IERC20(addresses.getAddress("USDC")).approve(vaultProxy, type(uint256).max); + Vault03(vaultProxy).deposit(addresses.getAddress("USDC"), uint256(1_000e18)); + + bytes32 adminSlot = vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); + address admin = address(uint160(uint256(adminSlot))); + + if (!addresses.isAddressSet("V4_VAULT_IMPLEMENTATION")) { + address vaultImpl = address(new Vault04()); + addresses.addAddress("V4_VAULT_IMPLEMENTATION", vaultImpl, true); + + // upgrade to new implementation + ProxyAdmin(admin).upgradeAndCall(ITransparentUpgradeableProxy(vaultProxy), vaultImpl, ""); + } + + Vault04(vaultProxy).setMaxSupply(1_000_000e18); + } + + function validate() public view override { + Vault04 vault = Vault04(addresses.getAddress("VAULT_PROXY")); + + assertEq( + vault.authorizedToken(addresses.getAddress("USDC")), + true, + "USDC should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("DAI")), + true, + "DAI should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("USDT")), + true, + "USDT should be authorized" + ); + + assertEq(vault.maxSupply(), 1_000_000e18, "Max supply should be 1,000,000 USDC"); + // fails as slot for totalSupplied is changed + assertEq(vault.totalSupplied(), 1_000e18, "Total supplied should be 1000 USDC"); + } +} + From bcd9cadfc56e5d1676b9c9efc29ebd92b8356cdf Mon Sep 17 00:00:00 2001 From: prateek Date: Mon, 4 Nov 2024 21:51:40 +0530 Subject: [PATCH 06/22] add tests to verify upgrade failed --- test/04/TestVault04.t.sol | 112 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 test/04/TestVault04.t.sol diff --git a/test/04/TestVault04.t.sol b/test/04/TestVault04.t.sol new file mode 100644 index 0000000..4c34e3f --- /dev/null +++ b/test/04/TestVault04.t.sol @@ -0,0 +1,112 @@ +pragma solidity ^0.8.0; + +import {SafeERC20} from + "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Test} from "@forge-std/Test.sol"; + +import {Vault04} from "src/examples/04/vault04.sol"; +import {SIP02IncorrectUpgrade} from "src/proposals/sips/SIP02IncorrectUpgrade.sol"; + +contract TestVault04 is Test, SIP02IncorrectUpgrade { + using SafeERC20 for IERC20; + + Vault04 public vault; + + /// @notice user addresses + address public immutable userA = address(1111); + address public immutable userB = address(2222); + address public immutable userC = address(3333); + + /// @notice token addresses + address public dai; + address public usdc; + address public usdt; + + function setUp() public { + /// set the environment variables + vm.setEnv("DO_RUN", "false"); + vm.setEnv("DO_BUILD", "false"); + vm.setEnv("DO_DEPLOY", "true"); + vm.setEnv("DO_SIMULATE", "false"); + vm.setEnv("DO_PRINT", "false"); + vm.setEnv("DO_VALIDATE", "false"); + + /// setup the proposal + setupProposal(); + + /// run the proposal + vm.startPrank(addresses.getAddress("DEPLOYER_EOA")); + deploy(); + vm.stopPrank(); + dai = addresses.getAddress("DAI"); + usdc = addresses.getAddress("USDC"); + usdt = addresses.getAddress("USDT"); + vault = Vault04(addresses.getAddress("VAULT_PROXY")); + } + + function testVaultDepositDai() public { + uint256 daiDepositAmount = 1_000e18; + + _vaultDeposit(dai, address(this), daiDepositAmount); + } + + function testVaultWithdrawalDai() public { + uint256 daiDepositAmount = 1_000e18; + + _vaultDeposit(dai, address(this), daiDepositAmount); + + vault.withdraw(dai, daiDepositAmount); + + assertEq(vault.balanceOf(address(this)), 0, "vault dai balance not 0"); + assertEq(vault.totalSupplied(), 0, "vault total supplied not 0"); + assertEq( + IERC20(dai).balanceOf(address(this)), + daiDepositAmount, + "user's dai balance not increased" + ); + } + + function testWithdrawAlreadyDepositedUSDC() public { + uint256 usdcDepositAmount = 1_000e6; + vault.withdraw(usdc, usdcDepositAmount); + } + + function _vaultDeposit(address token, address sender, uint256 amount) + private + { + uint256 startingTotalSupplied = vault.totalSupplied(); + uint256 startingTotalBalance = IERC20(token).balanceOf(address(vault)); + uint256 startingUserBalance = vault.balanceOf(sender); + + deal(token, sender, amount); + + vm.startPrank(sender); + IERC20(token).safeIncreaseAllowance( + addresses.getAddress("VAULT_PROXY"), amount + ); + + /// this executes 3 state transitions: + /// 1. deposit dai into the vault + /// 2. increase the user's balance in the vault + /// 3. increase the total supplied amount in the vault + vault.deposit(token, amount); + vm.stopPrank(); + + assertEq( + vault.balanceOf(sender), + startingUserBalance + amount, + "user vault balance not increased" + ); + assertEq( + vault.totalSupplied(), + startingTotalSupplied + amount, + "vault total supplied not increased by deposited amount" + ); + assertEq( + IERC20(token).balanceOf(address(vault)), + startingTotalBalance + amount, + "token balance not increased" + ); + } +} From f8c128e377b8e8cc216f614b0b05dab340593c63 Mon Sep 17 00:00:00 2001 From: prateek Date: Mon, 4 Nov 2024 22:53:42 +0530 Subject: [PATCH 07/22] rename files --- .../sips/{SIP00IncorrectParameter.sol => SIP01.sol} | 6 +++--- src/proposals/sips/{SIP02IncorrectUpgrade.sol => SIP02.sol} | 6 +++--- test/04/TestVault04.t.sol | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename src/proposals/sips/{SIP00IncorrectParameter.sol => SIP01.sol} (93%) rename src/proposals/sips/{SIP02IncorrectUpgrade.sol => SIP02.sol} (96%) diff --git a/src/proposals/sips/SIP00IncorrectParameter.sol b/src/proposals/sips/SIP01.sol similarity index 93% rename from src/proposals/sips/SIP00IncorrectParameter.sol rename to src/proposals/sips/SIP01.sol index 296f9e5..082d053 100644 --- a/src/proposals/sips/SIP00IncorrectParameter.sol +++ b/src/proposals/sips/SIP01.sol @@ -9,8 +9,8 @@ import {Vault} from "src/examples/00/Vault00.sol"; import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; -/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP00IncorrectParameter.sol:SIP00IncorrectParameter -vvvv -contract SIP00IncorrectParameter is GovernorBravoProposal { +/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP01.sol:SIP01 -vvvv +contract SIP01 is GovernorBravoProposal { using ForkSelector for uint256; constructor() { @@ -29,7 +29,7 @@ contract SIP00IncorrectParameter is GovernorBravoProposal { } function name() public pure override returns (string memory) { - return "SIP-00 System Deploy Incorrect Params"; + return "SIP-01 System Deploy"; } function description() public pure override returns (string memory) { diff --git a/src/proposals/sips/SIP02IncorrectUpgrade.sol b/src/proposals/sips/SIP02.sol similarity index 96% rename from src/proposals/sips/SIP02IncorrectUpgrade.sol rename to src/proposals/sips/SIP02.sol index 0edba26..d83553e 100644 --- a/src/proposals/sips/SIP02IncorrectUpgrade.sol +++ b/src/proposals/sips/SIP02.sol @@ -14,8 +14,8 @@ import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.s import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP02IncorrectUpgrade.sol:SIP02IncorrectUpgrade -vvvv -contract SIP02IncorrectUpgrade is GovernorBravoProposal { +/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP02.sol:SIP02 -vvvv +contract SIP02 is GovernorBravoProposal { using ForkSelector for uint256; constructor() { @@ -34,7 +34,7 @@ contract SIP02IncorrectUpgrade is GovernorBravoProposal { } function name() public pure override returns (string memory) { - return "SIP-02 Incorrect Upgrade"; + return "SIP-02 Upgrade"; } function description() public pure override returns (string memory) { diff --git a/test/04/TestVault04.t.sol b/test/04/TestVault04.t.sol index 4c34e3f..b9103fa 100644 --- a/test/04/TestVault04.t.sol +++ b/test/04/TestVault04.t.sol @@ -6,9 +6,9 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; import {Vault04} from "src/examples/04/vault04.sol"; -import {SIP02IncorrectUpgrade} from "src/proposals/sips/SIP02IncorrectUpgrade.sol"; +import {SIP02} from "src/proposals/sips/SIP02.sol"; -contract TestVault04 is Test, SIP02IncorrectUpgrade { +contract TestVault04 is Test, SIP02 { using SafeERC20 for IERC20; Vault04 public vault; From 3c022cde8778b9bc8f7f3874493fd3a5272a66b7 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 09:52:33 +0700 Subject: [PATCH 08/22] line length: 80 -> 70 Signed-off-by: Elliot --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index f29e04b..a5462e9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -20,7 +20,7 @@ remappings = [ fs_permissions = [{ access = "read", path = "./addresses/"}] [fmt] -line_length = 80 +line_length = 70 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options From a8d1ffdc56a920ac8e41b765407bcfb8dc675995 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 09:52:58 +0700 Subject: [PATCH 09/22] fmt Signed-off-by: Elliot --- src/examples/01/REQUIREMENTS.md | 5 - .../00/REQUIREMENTS.md | 0 src/{examples => exercises}/00/Vault00.sol | 4 +- src/exercises/01/REQUIREMENTS.md | 5 + src/{examples => exercises}/01/TESTS.md | 0 src/{examples => exercises}/01/Vault01.sol | 4 +- .../02/REQUIREMENTS.md | 0 src/{examples => exercises}/02/TESTS.md | 0 src/{examples => exercises}/02/Vault02.sol | 12 +- src/{examples => exercises}/03/Vault03.sol | 35 ++-- src/exercises/03/Vault03Storage.sol | 20 ++ src/{examples => exercises}/04/Vault04.sol | 93 +++++---- src/exercises/04/Vault04Storage.sol | 21 ++ src/exercises/05/Vault05.sol | 196 ++++++++++++++++++ src/exercises/storage/Authorized.sol | 7 + src/exercises/storage/Balances.sol | 7 + src/exercises/storage/Supply.sol | 15 ++ src/exercises/storage/VaultStorageOwnable.sol | 15 ++ .../storage/VaultStoragePausable.sol | 18 ++ src/proposals/sips/SIP00.sol | 13 +- src/proposals/sips/SIP01.sol | 13 +- src/proposals/sips/SIP02.sol | 88 +++++--- test/00/Test_SIP00.t.sol | 27 ++- test/02/TestVault02.t.sol | 27 ++- test/04/TestVault04.t.sol | 23 +- test/utils/Forks.sol | 5 +- test/utils/ProposalsMap.sol | 110 ---------- 27 files changed, 525 insertions(+), 238 deletions(-) delete mode 100644 src/examples/01/REQUIREMENTS.md rename src/{examples => exercises}/00/REQUIREMENTS.md (100%) rename src/{examples => exercises}/00/Vault00.sol (96%) create mode 100644 src/exercises/01/REQUIREMENTS.md rename src/{examples => exercises}/01/TESTS.md (100%) rename src/{examples => exercises}/01/Vault01.sol (97%) rename src/{examples => exercises}/02/REQUIREMENTS.md (100%) rename src/{examples => exercises}/02/TESTS.md (100%) rename src/{examples => exercises}/02/Vault02.sol (95%) rename src/{examples => exercises}/03/Vault03.sol (86%) create mode 100644 src/exercises/03/Vault03Storage.sol rename src/{examples => exercises}/04/Vault04.sol (71%) create mode 100644 src/exercises/04/Vault04Storage.sol create mode 100644 src/exercises/05/Vault05.sol create mode 100644 src/exercises/storage/Authorized.sol create mode 100644 src/exercises/storage/Balances.sol create mode 100644 src/exercises/storage/Supply.sol create mode 100644 src/exercises/storage/VaultStorageOwnable.sol create mode 100644 src/exercises/storage/VaultStoragePausable.sol delete mode 100644 test/utils/ProposalsMap.sol diff --git a/src/examples/01/REQUIREMENTS.md b/src/examples/01/REQUIREMENTS.md deleted file mode 100644 index 075e30d..0000000 --- a/src/examples/01/REQUIREMENTS.md +++ /dev/null @@ -1,5 +0,0 @@ -# Overview - -The first round of security reviews was conducted by an external security firm. The results were pretty bad for such a simple contract, and the CTO is livid. You show up to the meeting and get yelled at for writing such bad code. The CTO is demanding you fix the code immediately. They refused to give you the findings, so you're on your own. - -Good luck. diff --git a/src/examples/00/REQUIREMENTS.md b/src/exercises/00/REQUIREMENTS.md similarity index 100% rename from src/examples/00/REQUIREMENTS.md rename to src/exercises/00/REQUIREMENTS.md diff --git a/src/examples/00/Vault00.sol b/src/exercises/00/Vault00.sol similarity index 96% rename from src/examples/00/Vault00.sol rename to src/exercises/00/Vault00.sol index 1606828..c7263b9 100644 --- a/src/examples/00/Vault00.sol +++ b/src/exercises/00/Vault00.sol @@ -59,7 +59,9 @@ contract Vault { totalSupplied += amount; - IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + IERC20(token).safeTransferFrom( + msg.sender, address(this), amount + ); emit Deposit(token, msg.sender, amount); } diff --git a/src/exercises/01/REQUIREMENTS.md b/src/exercises/01/REQUIREMENTS.md new file mode 100644 index 0000000..a56bc7c --- /dev/null +++ b/src/exercises/01/REQUIREMENTS.md @@ -0,0 +1,5 @@ +# Overview + +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. + +Good luck... diff --git a/src/examples/01/TESTS.md b/src/exercises/01/TESTS.md similarity index 100% rename from src/examples/01/TESTS.md rename to src/exercises/01/TESTS.md diff --git a/src/examples/01/Vault01.sol b/src/exercises/01/Vault01.sol similarity index 97% rename from src/examples/01/Vault01.sol rename to src/exercises/01/Vault01.sol index da4fa47..b957ca2 100644 --- a/src/examples/01/Vault01.sol +++ b/src/exercises/01/Vault01.sol @@ -67,7 +67,9 @@ contract Vault { totalSupplied += normalizedAmount; - IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + IERC20(token).safeTransferFrom( + msg.sender, address(this), amount + ); emit Deposit(token, msg.sender, amount); } diff --git a/src/examples/02/REQUIREMENTS.md b/src/exercises/02/REQUIREMENTS.md similarity index 100% rename from src/examples/02/REQUIREMENTS.md rename to src/exercises/02/REQUIREMENTS.md diff --git a/src/examples/02/TESTS.md b/src/exercises/02/TESTS.md similarity index 100% rename from src/examples/02/TESTS.md rename to src/exercises/02/TESTS.md diff --git a/src/examples/02/Vault02.sol b/src/exercises/02/Vault02.sol similarity index 95% rename from src/examples/02/Vault02.sol rename to src/exercises/02/Vault02.sol index 123a6f6..4dbf4b6 100644 --- a/src/examples/02/Vault02.sol +++ b/src/exercises/02/Vault02.sol @@ -44,7 +44,9 @@ contract Vault is Ownable { /// @notice Construct the vault with a list of authorized tokens /// @param _tokens The list of authorized tokens - constructor(address[] memory _tokens, address _owner) Ownable(_owner) { + constructor(address[] memory _tokens, address _owner) + Ownable(_owner) + { for (uint256 i = 0; i < _tokens.length; i++) { require( IERC20Metadata(_tokens[i]).decimals() <= 18, @@ -71,7 +73,9 @@ contract Vault is Ownable { IERC20Metadata(token).decimals() <= 18, "Vault: unsupported decimals" ); - require(!authorizedToken[token], "Vault: token already authorized"); + require( + !authorizedToken[token], "Vault: token already authorized" + ); authorizedToken[token] = true; @@ -100,7 +104,9 @@ contract Vault is Ownable { totalSupplied += normalizedAmount; - IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + IERC20(token).safeTransferFrom( + msg.sender, address(this), amount + ); emit Deposit(token, msg.sender, amount); } diff --git a/src/examples/03/Vault03.sol b/src/exercises/03/Vault03.sol similarity index 86% rename from src/examples/03/Vault03.sol rename to src/exercises/03/Vault03.sol index 4768546..653726c 100644 --- a/src/examples/03/Vault03.sol +++ b/src/exercises/03/Vault03.sol @@ -1,28 +1,17 @@ pragma solidity 0.8.25; +import {OwnableUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Vault03 is OwnableUpgradeable { - using SafeERC20 for IERC20; - - /// @notice Mapping of authorized tokens - mapping(address => bool) public authorizedToken; +import {Vault03Storage} from "src/exercises/03/Vault03Storage.sol"; - /// @notice User's balance of all tokens deposited in the vault - mapping(address => uint256) public balanceOf; - - /// @notice Total amount of tokens supplied to the vault - /// - /// invariants: - /// totalSupplied = sum(balanceOf all users) - /// sum(balanceOf(vault) authorized tokens) >= totalSupplied - /// - uint256 public totalSupplied; +contract Vault03 is Vault03Storage { + using SafeERC20 for IERC20; /// @notice Deposit event /// @param token The token deposited @@ -42,7 +31,6 @@ contract Vault03 is OwnableUpgradeable { event TokenAdded(address indexed token); - /// @notice Construct the vault with a list of authorized tokens constructor() { _disableInitializers(); } @@ -50,7 +38,10 @@ contract Vault03 is OwnableUpgradeable { /// @notice Initialize the vault with a list of authorized tokens /// @param _tokens The list of authorized tokens /// @param _owner The owner address to set for the contract - function initialize(address[] memory _tokens, address _owner) external initializer { + function initialize(address[] memory _tokens, address _owner) + external + initializer + { __Ownable_init(_owner); for (uint256 i = 0; i < _tokens.length; i++) { @@ -79,7 +70,9 @@ contract Vault03 is OwnableUpgradeable { IERC20Metadata(token).decimals() <= 18, "Vault: unsupported decimals" ); - require(!authorizedToken[token], "Vault: token already authorized"); + require( + !authorizedToken[token], "Vault: token already authorized" + ); authorizedToken[token] = true; @@ -108,7 +101,9 @@ contract Vault03 is OwnableUpgradeable { totalSupplied += normalizedAmount; - IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + IERC20(token).safeTransferFrom( + msg.sender, address(this), amount + ); emit Deposit(token, msg.sender, amount); } diff --git a/src/exercises/03/Vault03Storage.sol b/src/exercises/03/Vault03Storage.sol new file mode 100644 index 0000000..273961d --- /dev/null +++ b/src/exercises/03/Vault03Storage.sol @@ -0,0 +1,20 @@ +pragma solidity 0.8.25; + +import {OwnableUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract Vault03Storage is OwnableUpgradeable { + /// @notice Mapping of authorized tokens + mapping(address => bool) public authorizedToken; + + /// @notice User's balance of all tokens deposited in the vault + mapping(address => uint256) public balanceOf; + + /// @notice Total amount of tokens supplied to the vault + /// + /// invariants: + /// totalSupplied = sum(balanceOf all users) + /// sum(balanceOf(vault) authorized tokens) >= totalSupplied + /// + uint256 public totalSupplied; +} diff --git a/src/examples/04/Vault04.sol b/src/exercises/04/Vault04.sol similarity index 71% rename from src/examples/04/Vault04.sol rename to src/exercises/04/Vault04.sol index 603d929..1fa143c 100644 --- a/src/examples/04/Vault04.sol +++ b/src/exercises/04/Vault04.sol @@ -4,31 +4,22 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {VaultStorageOwnable} from + "src/exercises/storage/VaultStorageOwnable.sol"; /// @notice Add maxsupply to the vault and update getNormalizedAmount logic -contract Vault04 is OwnableUpgradeable { +/// TODO make pauseable +/// make totalSupplied offsets the same +/// inherit pausable to mess up the storage slot offsets +/// add governance proposal that deploys and ugprades the existing vault from +/// proposal 03 +/// deploy Vault 03 to mainnet +/// add integration tests +contract Vault04 is VaultStorageOwnable { using SafeERC20 for IERC20; - /// @notice Mapping of authorized tokens - mapping(address => bool) public authorizedToken; - - /// @notice User's balance of all tokens deposited in the vault - mapping(address => uint256) public balanceOf; - - /// @notice Maximum amount of tokens that can be supplied to the vault - uint256 public maxSupply; - - /// @notice Total amount of tokens supplied to the vault - /// - /// invariants: - /// totalSupplied = sum(balanceOf all users) - /// sum(balanceOf(vault) authorized tokens) >= totalSupplied - /// - uint256 public totalSupplied; - /// @notice Deposit event /// @param token The token deposited /// @param sender The address that deposited the token @@ -45,28 +36,39 @@ contract Vault04 is OwnableUpgradeable { address indexed token, address indexed sender, uint256 amount ); + /// @notice emitted when new token is whitelisted + /// @param token newly added event TokenAdded(address indexed token); - /// @notice Construct the vault with a list of authorized tokens - constructor (){ + /// @notice max supply updated event + /// @param previousMaxSupply value of previous max supply + /// @param currentMaxSupply new max supply value + event MaxSupplyUpdated( + uint256 previousMaxSupply, uint256 currentMaxSupply + ); + + constructor() { _disableInitializers(); } /// @notice Initialize the vault with a list of authorized tokens - /// @param _tokens The list of authorized tokens - /// @param _owner The owner address to set for the contract - function initialize(address[] memory _tokens, address _owner) external initializer { - __Ownable_init(_owner); + /// @param tokens The list of authorized tokens + /// @param vaultOwner The owner address to set for the contract + function initialize(address[] memory tokens, address vaultOwner) + external + initializer + { + __Ownable_init(vaultOwner); - for (uint256 i = 0; i < _tokens.length; i++) { + for (uint256 i = 0; i < tokens.length; i++) { require( - IERC20Metadata(_tokens[i]).decimals() <= 18, + IERC20Metadata(tokens[i]).decimals() <= 18, "Vault: unsupported decimals" ); - authorizedToken[_tokens[i]] = true; + authorizedToken[tokens[i]] = true; - emit TokenAdded(_tokens[i]); + emit TokenAdded(tokens[i]); } } @@ -84,13 +86,25 @@ contract Vault04 is OwnableUpgradeable { IERC20Metadata(token).decimals() <= 18, "Vault: unsupported decimals" ); - require(!authorizedToken[token], "Vault: token already authorized"); + require( + !authorizedToken[token], "Vault: token already authorized" + ); authorizedToken[token] = true; emit TokenAdded(token); } + /// @notice Set the maximum supply of the vault + /// @param newMaxSupply The new maximum supply of the vault + function setMaxSupply(uint256 newMaxSupply) external onlyOwner { + uint256 previousMaxSupply = maxSupply; + maxSupply = newMaxSupply; + + /// only read stack variables, save a warm SLOAD + emit MaxSupplyUpdated(previousMaxSupply, newMaxSupply); + } + /// ------------------------------------------------------------- /// ------------------------------------------------------------- /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- @@ -105,6 +119,11 @@ contract Vault04 is OwnableUpgradeable { uint256 normalizedAmount = getNormalizedAmount(token, amount); + require( + totalSupplied + normalizedAmount <= maxSupply, + "Vault: supply cap reached" + ); + /// save on gas by using unchecked, no need to check for overflow /// as all deposited tokens are whitelisted unchecked { @@ -113,7 +132,9 @@ contract Vault04 is OwnableUpgradeable { totalSupplied += normalizedAmount; - IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + IERC20(token).safeTransferFrom( + msg.sender, address(this), amount + ); emit Deposit(token, msg.sender, amount); } @@ -146,12 +167,6 @@ contract Vault04 is OwnableUpgradeable { emit Withdraw(token, msg.sender, amount); } - /// @notice Set the maximum supply of the vault - /// @param _maxSupply The maximum supply of the vault - function setMaxSupply(uint256 _maxSupply) external onlyOwner { - maxSupply = _maxSupply; - } - /// -------------------------------------------------------- /// -------------------------------------------------------- /// ----------------- PUBLIC VIEW FUNCTION ----------------- @@ -168,9 +183,9 @@ contract Vault04 is OwnableUpgradeable { returns (uint256 normalizedAmount) { uint8 decimals = IERC20Metadata(token).decimals(); - // normalizedAmount = amount; + normalizedAmount = amount; if (decimals < 18) { - normalizedAmount = amount * (10 ** (18 - decimals)); + normalizedAmount = amount ** (10 * (18 - decimals)); } } } diff --git a/src/exercises/04/Vault04Storage.sol b/src/exercises/04/Vault04Storage.sol new file mode 100644 index 0000000..34a1c1a --- /dev/null +++ b/src/exercises/04/Vault04Storage.sol @@ -0,0 +1,21 @@ +pragma solidity 0.8.25; + +/// inherit ownable, switch around the order o +contract Vault04Storage { + /// @notice Mapping of authorized tokens + mapping(address => bool) public authorizedToken; + + /// @notice User's balance of all tokens deposited in the vault + mapping(address => uint256) public balanceOf; + + /// @notice Maximum amount of tokens that can be supplied to the vault + uint256 public maxSupply; + + /// @notice Total amount of tokens supplied to the vault + /// + /// invariants: + /// totalSupplied = sum(balanceOf all users) + /// sum(balanceOf(vault) authorized tokens) >= totalSupplied + /// + uint256 public totalSupplied; +} diff --git a/src/exercises/05/Vault05.sol b/src/exercises/05/Vault05.sol new file mode 100644 index 0000000..3dfbb48 --- /dev/null +++ b/src/exercises/05/Vault05.sol @@ -0,0 +1,196 @@ +pragma solidity 0.8.25; + +import {IERC20Metadata} from + "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from + "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {OwnableUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {VaultStoragePausable} from + "src/exercises/storage/VaultStoragePausable.sol"; + +/// @notice Add maxsupply to the vault and update getNormalizedAmount logic +/// TODO make pauseable +/// make totalSupplied offsets the same +/// inherit pausable to mess up the storage slot offsets +/// add governance proposal that deploys and ugprades the existing vault from +/// proposal 03 +/// deploy Vault 03 to mainnet +/// add integration tests +contract Vault05 is VaultStoragePausable { + using SafeERC20 for IERC20; + + /// @notice Deposit event + /// @param token The token deposited + /// @param sender The address that deposited the token + /// @param amount The amount deposited + event Deposit( + address indexed token, address indexed sender, uint256 amount + ); + + /// @notice Withdraw event + /// @param token The token withdrawn + /// @param sender The address that withdrew the token + /// @param amount The amount withdrawn + event Withdraw( + address indexed token, address indexed sender, uint256 amount + ); + + /// @notice emitted when new token is whitelisted + /// @param token newly added + event TokenAdded(address indexed token); + + /// @notice max supply updated event + /// @param previousMaxSupply value of previous max supply + /// @param currentMaxSupply new max supply value + event MaxSupplyUpdated( + uint256 previousMaxSupply, uint256 currentMaxSupply + ); + + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the vault with a list of authorized tokens + /// @param tokens The list of authorized tokens + /// @param vaultOwner The owner address to set for the contract + function initialize(address[] memory tokens, address vaultOwner) + external + initializer + { + __Ownable_init(vaultOwner); + + for (uint256 i = 0; i < tokens.length; i++) { + require( + IERC20Metadata(tokens[i]).decimals() <= 18, + "Vault: unsupported decimals" + ); + + authorizedToken[tokens[i]] = true; + + emit TokenAdded(tokens[i]); + } + } + + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + /// -------------------- ONLY OWNER FUNCTION -------------------- + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + + /// @notice Add a token to the list of authorized tokens + /// only callable by the owner + /// @param token to add + function addToken(address token) external onlyOwner { + require( + IERC20Metadata(token).decimals() <= 18, + "Vault: unsupported decimals" + ); + require( + !authorizedToken[token], "Vault: token already authorized" + ); + + authorizedToken[token] = true; + + emit TokenAdded(token); + } + + /// @notice Set the maximum supply of the vault + /// @param newMaxSupply The new maximum supply of the vault + function setMaxSupply(uint256 newMaxSupply) external onlyOwner { + uint256 previousMaxSupply = maxSupply; + maxSupply = newMaxSupply; + + /// only read stack variables, save a warm SLOAD + emit MaxSupplyUpdated(previousMaxSupply, newMaxSupply); + } + + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + + /// @notice Deposit tokens into the vault + /// @param token The token to deposit, only authorized tokens allowed + /// @param amount The amount to deposit + function deposit(address token, uint256 amount) + external + whenNotPaused + { + require(authorizedToken[token], "Vault: token not authorized"); + + uint256 normalizedAmount = getNormalizedAmount(token, amount); + + require( + totalSupplied + normalizedAmount <= maxSupply, + "Vault: supply cap reached" + ); + + /// save on gas by using unchecked, no need to check for overflow + /// as all deposited tokens are whitelisted + unchecked { + balanceOf[msg.sender] += normalizedAmount; + } + + totalSupplied += normalizedAmount; + + IERC20(token).safeTransferFrom( + msg.sender, address(this), amount + ); + + emit Deposit(token, msg.sender, amount); + } + + /// @notice Withdraw tokens from the vault + /// @param token The token to withdraw, only authorized tokens are allowed + /// this is implicitly checked because a user can only have a balance of an + /// authorized token + /// @param amount The amount to withdraw + function withdraw(address token, uint256 amount) external { + require(authorizedToken[token], "Vault: token not authorized"); + + uint256 normalizedAmount = getNormalizedAmount(token, amount); + + /// both a check and an effect, ensures user has sufficient funds for + /// withdrawal + /// must be checked for underflow as a user can only withdraw what they + /// have deposited + balanceOf[msg.sender] -= normalizedAmount; + + /// save on gas by using unchecked, no need to check for underflow + /// as all deposited tokens are whitelisted, plus we know our invariant + /// always holds + unchecked { + totalSupplied -= normalizedAmount; + } + + IERC20(token).safeTransfer(msg.sender, amount); + + emit Withdraw(token, msg.sender, amount); + } + + /// -------------------------------------------------------- + /// -------------------------------------------------------- + /// ----------------- PUBLIC VIEW FUNCTION ----------------- + /// -------------------------------------------------------- + /// -------------------------------------------------------- + + /// @notice public for testing purposes, returns the normalized amount of + /// tokens scaled to 18 decimals + /// @param token The token to deposit + /// @param amount The amount to deposit + function getNormalizedAmount(address token, uint256 amount) + public + view + returns (uint256 normalizedAmount) + { + uint8 decimals = IERC20Metadata(token).decimals(); + normalizedAmount = amount; + if (decimals < 18) { + normalizedAmount = amount ** (10 * (18 - decimals)); + } + } +} diff --git a/src/exercises/storage/Authorized.sol b/src/exercises/storage/Authorized.sol new file mode 100644 index 0000000..b909220 --- /dev/null +++ b/src/exercises/storage/Authorized.sol @@ -0,0 +1,7 @@ +pragma solidity 0.8.25; + +/// inherit ownable, switch around the order o +contract Authorized { + /// @notice Mapping of authorized tokens + mapping(address => bool) public authorizedToken; +} diff --git a/src/exercises/storage/Balances.sol b/src/exercises/storage/Balances.sol new file mode 100644 index 0000000..967d7f1 --- /dev/null +++ b/src/exercises/storage/Balances.sol @@ -0,0 +1,7 @@ +pragma solidity 0.8.25; + +/// inherit ownable, switch around the order o +contract Balances { + /// @notice User's balance of all tokens deposited in the vault + mapping(address => uint256) public balanceOf; +} diff --git a/src/exercises/storage/Supply.sol b/src/exercises/storage/Supply.sol new file mode 100644 index 0000000..37a7b69 --- /dev/null +++ b/src/exercises/storage/Supply.sol @@ -0,0 +1,15 @@ +pragma solidity 0.8.25; + +/// inherit ownable, switch around the order o +contract Supply { + /// @notice Maximum amount of tokens that can be supplied to the vault + uint256 public maxSupply; + + /// @notice Total amount of tokens supplied to the vault + /// + /// invariants: + /// totalSupplied = sum(balanceOf all users) + /// sum(balanceOf(vault) authorized tokens) >= totalSupplied + /// + uint256 public totalSupplied; +} diff --git a/src/exercises/storage/VaultStorageOwnable.sol b/src/exercises/storage/VaultStorageOwnable.sol new file mode 100644 index 0000000..953349a --- /dev/null +++ b/src/exercises/storage/VaultStorageOwnable.sol @@ -0,0 +1,15 @@ +pragma solidity 0.8.25; + +import {OwnableUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import {Supply} from "src/exercises/storage/Supply.sol"; +import {Balances} from "src/exercises/storage/Balances.sol"; +import {Authorized} from "src/exercises/storage/Authorized.sol"; + +contract VaultStorageOwnable is + OwnableUpgradeable, + Supply, + Balances, + Authorized +{} diff --git a/src/exercises/storage/VaultStoragePausable.sol b/src/exercises/storage/VaultStoragePausable.sol new file mode 100644 index 0000000..afbb67f --- /dev/null +++ b/src/exercises/storage/VaultStoragePausable.sol @@ -0,0 +1,18 @@ +pragma solidity 0.8.25; + +import {PausableUpgradeable} from + "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {OwnableUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import {Supply} from "src/exercises/storage/Supply.sol"; +import {Balances} from "src/exercises/storage/Balances.sol"; +import {Authorized} from "src/exercises/storage/Authorized.sol"; + +contract VaultStoragePausable is + OwnableUpgradeable, + Supply, + Balances, + PausableUpgradeable, + Authorized +{} diff --git a/src/proposals/sips/SIP00.sol b/src/proposals/sips/SIP00.sol index df231bb..690c62e 100644 --- a/src/proposals/sips/SIP00.sol +++ b/src/proposals/sips/SIP00.sol @@ -3,9 +3,10 @@ pragma solidity ^0.8.0; import {GovernorBravoProposal} from "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; -import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; +import {Addresses} from + "@forge-proposal-simulator/addresses/Addresses.sol"; -import {Vault} from "src/examples/00/Vault00.sol"; +import {Vault} from "src/exercises/00/Vault00.sol"; import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; @@ -14,7 +15,6 @@ contract SIP00 is GovernorBravoProposal { using ForkSelector for uint256; constructor() { - // ETHEREUM_FORK_ID.createForksAndSelect(); primaryForkId = ETHEREUM_FORK_ID; } @@ -32,7 +32,12 @@ contract SIP00 is GovernorBravoProposal { return "SIP-00 System Deploy"; } - function description() public pure override returns (string memory) { + function description() + public + pure + override + returns (string memory) + { return name(); } diff --git a/src/proposals/sips/SIP01.sol b/src/proposals/sips/SIP01.sol index 082d053..b6a09a4 100644 --- a/src/proposals/sips/SIP01.sol +++ b/src/proposals/sips/SIP01.sol @@ -3,9 +3,10 @@ pragma solidity ^0.8.0; import {GovernorBravoProposal} from "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; -import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; +import {Addresses} from + "@forge-proposal-simulator/addresses/Addresses.sol"; -import {Vault} from "src/examples/00/Vault00.sol"; +import {Vault} from "src/exercises/00/Vault00.sol"; import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; @@ -14,7 +15,6 @@ contract SIP01 is GovernorBravoProposal { using ForkSelector for uint256; constructor() { - // ETHEREUM_FORK_ID.createForksAndSelect(); primaryForkId = ETHEREUM_FORK_ID; } @@ -32,7 +32,12 @@ contract SIP01 is GovernorBravoProposal { return "SIP-01 System Deploy"; } - function description() public pure override returns (string memory) { + function description() + public + pure + override + returns (string memory) + { return name(); } diff --git a/src/proposals/sips/SIP02.sol b/src/proposals/sips/SIP02.sol index d83553e..1d86b53 100644 --- a/src/proposals/sips/SIP02.sol +++ b/src/proposals/sips/SIP02.sol @@ -3,23 +3,28 @@ pragma solidity ^0.8.0; import {GovernorBravoProposal} from "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; -import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; +import {Addresses} from + "@forge-proposal-simulator/addresses/Addresses.sol"; -import {Vault03} from "src/examples/03/Vault03.sol"; -import {Vault04} from "src/examples/04/Vault04.sol"; +import {Vault03} from "src/exercises/03/Vault03.sol"; +import {Vault04} from "src/exercises/04/Vault04.sol"; import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; -import {ProxyAdmin, TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import { + ProxyAdmin, + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ERC1967Utils} from + "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - /// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP02.sol:SIP02 -vvvv contract SIP02 is GovernorBravoProposal { using ForkSelector for uint256; constructor() { - // ETHEREUM_FORK_ID.createForksAndSelect(); primaryForkId = ETHEREUM_FORK_ID; } @@ -37,7 +42,12 @@ contract SIP02 is GovernorBravoProposal { return "SIP-02 Upgrade"; } - function description() public pure override returns (string memory) { + function description() + public + pure + override + returns (string memory) + { return name(); } @@ -53,7 +63,9 @@ contract SIP02 is GovernorBravoProposal { address vaultProxy; if (!addresses.isAddressSet("V3_VAULT_IMPLEMENTATION")) { address vaultImpl = address(new Vault03()); - addresses.addAddress("V3_VAULT_IMPLEMENTATION", vaultImpl, true); + addresses.addAddress( + "V3_VAULT_IMPLEMENTATION", vaultImpl, true + ); address[] memory tokens = new address[](3); tokens[0] = addresses.getAddress("USDC"); @@ -63,26 +75,43 @@ contract SIP02 is GovernorBravoProposal { address owner = addresses.getAddress("DEPLOYER_EOA"); // Generate calldata for initialize function of vault - bytes memory data = abi.encodeWithSignature("initialize(address[],address)", tokens, owner); - - vaultProxy = address(new TransparentUpgradeableProxy(vaultImpl, owner, data)); + bytes memory data = abi.encodeWithSignature( + "initialize(address[],address)", tokens, owner + ); + + vaultProxy = address( + new TransparentUpgradeableProxy( + vaultImpl, owner, data + ) + ); addresses.addAddress("VAULT_PROXY", vaultProxy, true); } - deal(addresses.getAddress("USDC"), addresses.getAddress("DEPLOYER_EOA"), 1_000e18); - IERC20(addresses.getAddress("USDC")).approve(vaultProxy, type(uint256).max); - Vault03(vaultProxy).deposit(addresses.getAddress("USDC"), uint256(1_000e18)); - - bytes32 adminSlot = vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); - address admin = address(uint160(uint256(adminSlot))); - if (!addresses.isAddressSet("V4_VAULT_IMPLEMENTATION")) { address vaultImpl = address(new Vault04()); - addresses.addAddress("V4_VAULT_IMPLEMENTATION", vaultImpl, true); - - // upgrade to new implementation - ProxyAdmin(admin).upgradeAndCall(ITransparentUpgradeableProxy(vaultProxy), vaultImpl, ""); + addresses.addAddress( + "V4_VAULT_IMPLEMENTATION", vaultImpl, true + ); } + } + + function build() + public + override + buildModifier(addresses.getAddress("COMPOUND_TIMELOCK_BRAVO")) + { + address vaultProxy = addresses.getAddress("VAULT_PROXY"); + bytes32 adminSlot = + vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); + + address proxyAdmin = address(uint160(uint256(adminSlot))); + + // upgrade to new implementation + ProxyAdmin(proxyAdmin).upgradeAndCall( + ITransparentUpgradeableProxy(vaultProxy), + addresses.getAddress("V4_VAULT_IMPLEMENTATION"), + "" + ); Vault04(vaultProxy).setMaxSupply(1_000_000e18); } @@ -106,9 +135,16 @@ contract SIP02 is GovernorBravoProposal { "USDT should be authorized" ); - assertEq(vault.maxSupply(), 1_000_000e18, "Max supply should be 1,000,000 USDC"); + assertEq( + vault.maxSupply(), + 1_000_000e18, + "Max supply should be 1,000,000 USDC" + ); // fails as slot for totalSupplied is changed - assertEq(vault.totalSupplied(), 1_000e18, "Total supplied should be 1000 USDC"); + assertEq( + vault.totalSupplied(), + 1_000e18, + "Total supplied should be 1000 USDC" + ); } } - diff --git a/test/00/Test_SIP00.t.sol b/test/00/Test_SIP00.t.sol index 8897bc9..8d0e162 100644 --- a/test/00/Test_SIP00.t.sol +++ b/test/00/Test_SIP00.t.sol @@ -6,7 +6,7 @@ import {console} from "@forge-std/console.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; -import {Vault} from "src/examples/00/Vault00.sol"; +import {Vault} from "src/exercises/00/Vault00.sol"; import {SIP00} from "src/proposals/sips/SIP00.sol"; contract TestSIP00 is Test, SIP00 { @@ -73,7 +73,9 @@ contract TestSIP00 is Test, SIP00 { deal(usdt, address(this), usdtDepositAmount); - USDT(usdt).approve(addresses.getAddress("V1_VAULT"), usdtDepositAmount); + USDT(usdt).approve( + addresses.getAddress("V1_VAULT"), usdtDepositAmount + ); /// this executes 3 state transitions: /// 1. deposit dai into the vault @@ -105,8 +107,14 @@ contract TestSIP00 is Test, SIP00 { vault.withdraw(dai, daiDepositAmount); - assertEq(vault.balanceOf(address(this)), 0, "vault dai balance not 0"); - assertEq(vault.totalSupplied(), 0, "vault total supplied not 0"); + assertEq( + vault.balanceOf(address(this)), + 0, + "vault dai balance not 0" + ); + assertEq( + vault.totalSupplied(), 0, "vault total supplied not 0" + ); assertEq( IERC20(dai).balanceOf(address(this)), daiDepositAmount, @@ -138,11 +146,14 @@ contract TestSIP00 is Test, SIP00 { ); } - function _vaultDeposit(address token, address sender, uint256 amount) - private - { + function _vaultDeposit( + address token, + address sender, + uint256 amount + ) private { uint256 startingTotalSupplied = vault.totalSupplied(); - uint256 startingTotalBalance = IERC20(token).balanceOf(address(vault)); + uint256 startingTotalBalance = + IERC20(token).balanceOf(address(vault)); uint256 startingUserBalance = vault.balanceOf(sender); deal(token, sender, amount); diff --git a/test/02/TestVault02.t.sol b/test/02/TestVault02.t.sol index dbdba0b..0f5ecb1 100644 --- a/test/02/TestVault02.t.sol +++ b/test/02/TestVault02.t.sol @@ -6,7 +6,7 @@ import {console} from "@forge-std/console.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; -import {Vault} from "src/examples/00/Vault00.sol"; +import {Vault} from "src/exercises/00/Vault00.sol"; import {SIP00} from "src/proposals/sips/SIP00.sol"; contract TestVault02 is Test, SIP00 { @@ -73,7 +73,9 @@ contract TestVault02 is Test, SIP00 { deal(usdt, address(this), usdtDepositAmount); - USDT(usdt).approve(addresses.getAddress("V1_VAULT"), usdtDepositAmount); + USDT(usdt).approve( + addresses.getAddress("V1_VAULT"), usdtDepositAmount + ); /// this executes 3 state transitions: /// 1. deposit dai into the vault @@ -105,8 +107,14 @@ contract TestVault02 is Test, SIP00 { vault.withdraw(dai, daiDepositAmount); - assertEq(vault.balanceOf(address(this)), 0, "vault dai balance not 0"); - assertEq(vault.totalSupplied(), 0, "vault total supplied not 0"); + assertEq( + vault.balanceOf(address(this)), + 0, + "vault dai balance not 0" + ); + assertEq( + vault.totalSupplied(), 0, "vault total supplied not 0" + ); assertEq( IERC20(dai).balanceOf(address(this)), daiDepositAmount, @@ -138,11 +146,14 @@ contract TestVault02 is Test, SIP00 { ); } - function _vaultDeposit(address token, address sender, uint256 amount) - private - { + function _vaultDeposit( + address token, + address sender, + uint256 amount + ) private { uint256 startingTotalSupplied = vault.totalSupplied(); - uint256 startingTotalBalance = IERC20(token).balanceOf(address(vault)); + uint256 startingTotalBalance = + IERC20(token).balanceOf(address(vault)); uint256 startingUserBalance = vault.balanceOf(sender); deal(token, sender, amount); diff --git a/test/04/TestVault04.t.sol b/test/04/TestVault04.t.sol index b9103fa..06c0bbb 100644 --- a/test/04/TestVault04.t.sol +++ b/test/04/TestVault04.t.sol @@ -5,7 +5,7 @@ import {SafeERC20} from import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; -import {Vault04} from "src/examples/04/vault04.sol"; +import {Vault04} from "src/exercises/04/vault04.sol"; import {SIP02} from "src/proposals/sips/SIP02.sol"; contract TestVault04 is Test, SIP02 { @@ -58,8 +58,14 @@ contract TestVault04 is Test, SIP02 { vault.withdraw(dai, daiDepositAmount); - assertEq(vault.balanceOf(address(this)), 0, "vault dai balance not 0"); - assertEq(vault.totalSupplied(), 0, "vault total supplied not 0"); + assertEq( + vault.balanceOf(address(this)), + 0, + "vault dai balance not 0" + ); + assertEq( + vault.totalSupplied(), 0, "vault total supplied not 0" + ); assertEq( IERC20(dai).balanceOf(address(this)), daiDepositAmount, @@ -72,11 +78,14 @@ contract TestVault04 is Test, SIP02 { vault.withdraw(usdc, usdcDepositAmount); } - function _vaultDeposit(address token, address sender, uint256 amount) - private - { + function _vaultDeposit( + address token, + address sender, + uint256 amount + ) private { uint256 startingTotalSupplied = vault.totalSupplied(); - uint256 startingTotalBalance = IERC20(token).balanceOf(address(vault)); + uint256 startingTotalBalance = + IERC20(token).balanceOf(address(vault)); uint256 startingUserBalance = vault.balanceOf(sender); deal(token, sender, amount); diff --git a/test/utils/Forks.sol b/test/utils/Forks.sol index bf6ce67..6dea4b5 100644 --- a/test/utils/Forks.sol +++ b/test/utils/Forks.sol @@ -9,8 +9,9 @@ library ForkSelector { Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); function createForksAndSelect(uint256 selectFork) internal { - (bool success,) = - address(vmContract).call(abi.encodeWithSignature("activeFork()")); + (bool success,) = address(vmContract).call( + abi.encodeWithSignature("activeFork()") + ); (bool successSwitchFork,) = address(vmContract).call( abi.encodeWithSignature("selectFork(uint256)", selectFork) ); diff --git a/test/utils/ProposalsMap.sol b/test/utils/ProposalsMap.sol deleted file mode 100644 index 8c224aa..0000000 --- a/test/utils/ProposalsMap.sol +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; -import {Script, stdJson} from "@forge-std/Script.sol"; - -import {Proposal} from "@proposals/Proposal.sol"; - -contract ProposalsMap is Script { - using stdJson for string; - - struct ProposalFields { - uint256 id; - string path; - } - - ProposalFields[] public proposals; - - mapping(uint256 id => uint256) private proposalIdToIndex; - - mapping(string path => uint256) private proposalPathToIndex; - - constructor() { - string memory data = vm.readFile( - string( - abi.encodePacked( - vm.projectRoot(), "/src/proposals/sips/sips.json" - ) - ) - ); - - bytes memory parsedJson = vm.parseJson(data); - - ProposalFields[] memory jsonProposals = - abi.decode(parsedJson, (ProposalFields[])); - - for (uint256 i = 0; i < jsonProposals.length; i++) { - addProposal(jsonProposals[i]); - } - } - - function addProposal(ProposalFields memory proposal) public { - uint256 index = proposals.length; - - proposals.push(); - - proposals[index].id = proposal.id; - proposals[index].path = proposal.path; - - proposalIdToIndex[proposal.id] = index + 1; - proposalPathToIndex[proposal.path] = index; - } - - function getProposalById(uint256 id) - public - view - returns (string memory path) - { - if (proposalIdToIndex[id] == 0) { - return ""; - } - - ProposalFields memory proposal = proposals[proposalIdToIndex[id] - 1]; - return proposal.path; - } - - function getProposalByPath(string memory path) - public - view - returns (uint256 proposalId) - { - ProposalFields memory proposal = proposals[proposalPathToIndex[path]]; - return proposal.id; - } - - function getAllProposalsInDevelopment() - public - view - returns (ProposalFields[] memory _proposals) - { - // filter proposals with id == 0; - uint256 count = 0; - for (uint256 i = 0; i < proposals.length; i++) { - if (proposals[i].id == 0) { - count++; - } - } - - _proposals = new ProposalFields[](count); - uint256 index = 0; - for (uint256 i = 0; i < proposals.length; i++) { - if (proposals[i].id == 0) { - _proposals[index] = proposals[i]; - index++; - } - } - } - - function runProposal(string memory proposalPath) - public - returns (Proposal proposal) - { - proposal = Proposal(deployCode(proposalPath)); - vm.makePersistent(address(proposal)); - - vm.selectFork(proposal.primaryForkId()); - - proposal.run(); - } -} From 9c038517e1029a97f9aeca856abe34c2d348fa63 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 10:06:20 +0700 Subject: [PATCH 10/22] add: interface Signed-off-by: Elliot --- src/exercises/IVault.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/exercises/IVault.sol diff --git a/src/exercises/IVault.sol b/src/exercises/IVault.sol new file mode 100644 index 0000000..be73ee7 --- /dev/null +++ b/src/exercises/IVault.sol @@ -0,0 +1,19 @@ +pragma solidity 0.8.25; + +/// @notice interface for a vault where a user can deposit authorized tokens +/// and then withdraw tokens. +/// Acts as a Peg Stability Module of tokens of the same value. Users can +/// deposit tokens of type A and withdraw tokens of type B. +interface IVault { + function authorizedToken(address) external view returns (bool); + function balanceOf(address) external view returns (uint256); + function totalSupplied() external view returns (uint256); + + /// @notice depositing increases balanceOf and totalSupplied by amount + /// deposited + function deposit(address token, uint256 amount) external; + + /// @notice withdrawing decreases balanceOf and totalSupplied by amount + /// withdrawn + function withdraw(address token, uint256 amount) external; +} \ No newline at end of file From cc135428f6bd5902da092af6361fb676f6600d44 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 10:06:40 +0700 Subject: [PATCH 11/22] fix: import case Signed-off-by: Elliot --- test/04/TestVault04.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/04/TestVault04.t.sol b/test/04/TestVault04.t.sol index 06c0bbb..e378c83 100644 --- a/test/04/TestVault04.t.sol +++ b/test/04/TestVault04.t.sol @@ -5,7 +5,7 @@ import {SafeERC20} from import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; -import {Vault04} from "src/exercises/04/vault04.sol"; +import {Vault04} from "src/exercises/04/Vault04.sol"; import {SIP02} from "src/proposals/sips/SIP02.sol"; contract TestVault04 is Test, SIP02 { From 6d17690d456804a643392abbcde1b9f74226a481 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 10:08:50 +0700 Subject: [PATCH 12/22] chore: formatting Signed-off-by: Elliot --- src/proposals/sips/SIP01.sol | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/proposals/sips/SIP01.sol b/src/proposals/sips/SIP01.sol index b6a09a4..1b35a85 100644 --- a/src/proposals/sips/SIP01.sol +++ b/src/proposals/sips/SIP01.sol @@ -51,11 +51,9 @@ contract SIP01 is GovernorBravoProposal { function deploy() public override { if (!addresses.isAddressSet("V1_VAULT")) { - address[] memory tokens = new address[](3); + address[] memory tokens = new address[](2); tokens[0] = addresses.getAddress("USDC"); - // usdc added again instead of dai - tokens[1] = addresses.getAddress("USDC"); - tokens[2] = addresses.getAddress("USDT"); + tokens[1] = addresses.getAddress("USDT"); Vault vault = new Vault(tokens); @@ -72,14 +70,14 @@ contract SIP01 is GovernorBravoProposal { "USDC should be authorized" ); assertEq( - vault.authorizedToken(addresses.getAddress("DAI")), + vault.authorizedToken(addresses.getAddress("USDT")), true, - "DAI should be authorized" + "USDT should be authorized" ); assertEq( - vault.authorizedToken(addresses.getAddress("USDT")), + vault.authorizedToken(addresses.getAddress("DAI")), true, - "USDT should be authorized" + "DAI should be authorized" ); } } From c19d59c4cfe0686048be7c58d5130558fac08de7 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 10:09:19 +0700 Subject: [PATCH 13/22] fmt Signed-off-by: Elliot --- src/exercises/IVault.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercises/IVault.sol b/src/exercises/IVault.sol index be73ee7..c000cc6 100644 --- a/src/exercises/IVault.sol +++ b/src/exercises/IVault.sol @@ -16,4 +16,4 @@ interface IVault { /// @notice withdrawing decreases balanceOf and totalSupplied by amount /// withdrawn function withdraw(address token, uint256 amount) external; -} \ No newline at end of file +} From 7b077f944ee40a56100b6e33ba943b1083dacb29 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 10:12:53 +0700 Subject: [PATCH 14/22] add: env ETH_RPC_URL Signed-off-by: Elliot --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 762a296..e5a124d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,7 @@ on: env: FOUNDRY_PROFILE: ci + ETH_RPC_URL: ${{secrets.ETH_RPC_URL}} jobs: check: From b3c0bf73b92881dee2d563037952a32b09409120 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 10:22:22 +0700 Subject: [PATCH 15/22] fix: normalize amount in _vaultDeposit helper Signed-off-by: Elliot --- test/04/TestVault04.t.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/04/TestVault04.t.sol b/test/04/TestVault04.t.sol index e378c83..cfa4355 100644 --- a/test/04/TestVault04.t.sol +++ b/test/04/TestVault04.t.sol @@ -39,6 +39,7 @@ contract TestVault04 is Test, SIP02 { vm.startPrank(addresses.getAddress("DEPLOYER_EOA")); deploy(); vm.stopPrank(); + dai = addresses.getAddress("DAI"); usdc = addresses.getAddress("USDC"); usdt = addresses.getAddress("USDT"); @@ -75,6 +76,7 @@ contract TestVault04 is Test, SIP02 { function testWithdrawAlreadyDepositedUSDC() public { uint256 usdcDepositAmount = 1_000e6; + _vaultDeposit(usdc, address(this), usdcDepositAmount); vault.withdraw(usdc, usdcDepositAmount); } @@ -102,14 +104,16 @@ contract TestVault04 is Test, SIP02 { vault.deposit(token, amount); vm.stopPrank(); + uint256 normalizedAmount = vault.getNormalizedAmount(token, amount); + assertEq( vault.balanceOf(sender), - startingUserBalance + amount, + startingUserBalance + normalizedAmount, "user vault balance not increased" ); assertEq( vault.totalSupplied(), - startingTotalSupplied + amount, + startingTotalSupplied + normalizedAmount, "vault total supplied not increased by deposited amount" ); assertEq( From 68aaa8e4dd93a3f0f635b7b92ff420821495dd73 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 10:23:12 +0700 Subject: [PATCH 16/22] fmt Signed-off-by: Elliot --- test/04/TestVault04.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/04/TestVault04.t.sol b/test/04/TestVault04.t.sol index cfa4355..3b7bd15 100644 --- a/test/04/TestVault04.t.sol +++ b/test/04/TestVault04.t.sol @@ -104,7 +104,8 @@ contract TestVault04 is Test, SIP02 { vault.deposit(token, amount); vm.stopPrank(); - uint256 normalizedAmount = vault.getNormalizedAmount(token, amount); + uint256 normalizedAmount = + vault.getNormalizedAmount(token, amount); assertEq( vault.balanceOf(sender), From 4765aa0502b9b5104b2dd1bbea73ee0e2bbd6db1 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 10:31:02 +0700 Subject: [PATCH 17/22] add: validate in its own test Signed-off-by: Elliot --- test/02/TestVault02.t.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/02/TestVault02.t.sol b/test/02/TestVault02.t.sol index 0f5ecb1..e7c4104 100644 --- a/test/02/TestVault02.t.sol +++ b/test/02/TestVault02.t.sol @@ -39,15 +39,17 @@ contract TestVault02 is Test, SIP00 { /// run the proposal deploy(); - /// validate the proposal - validate(); - dai = addresses.getAddress("DAI"); usdc = addresses.getAddress("USDC"); usdt = addresses.getAddress("USDT"); vault = Vault(addresses.getAddress("V1_VAULT")); } + function testValidate() public view { + /// validate the proposal + validate(); + } + function testVaultDepositDai() public { uint256 daiDepositAmount = 1_000e18; From 50648e6cfcc40be767fa50e98c8f626a69f2acba Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 12:02:39 +0700 Subject: [PATCH 18/22] checkpoint Signed-off-by: Elliot --- .../sips => exercises/00}/SIP00.sol | 2 +- .../sips => exercises/01}/SIP01.sol | 4 +- .../sips => exercises/02}/SIP02.sol | 5 +- src/exercises/03/SIP04.sol | 124 ++++++++++++++ src/exercises/03/Vault03.sol | 160 ------------------ src/exercises/03/Vault03Storage.sol | 20 --- src/exercises/{04 => 03}/Vault04.sol | 0 src/exercises/{04 => 03}/Vault04Storage.sol | 0 src/exercises/{05 => 04}/Vault05.sol | 0 .../Test_SIP00.t.sol => TestVault00.t.sol} | 13 +- test/{02 => }/TestVault02.t.sol | 2 +- test/{04 => }/TestVault04.t.sol | 2 +- 12 files changed, 139 insertions(+), 193 deletions(-) rename src/{proposals/sips => exercises/00}/SIP00.sol (96%) rename src/{proposals/sips => exercises/01}/SIP01.sol (94%) rename src/{proposals/sips => exercises/02}/SIP02.sol (95%) create mode 100644 src/exercises/03/SIP04.sol delete mode 100644 src/exercises/03/Vault03.sol delete mode 100644 src/exercises/03/Vault03Storage.sol rename src/exercises/{04 => 03}/Vault04.sol (100%) rename src/exercises/{04 => 03}/Vault04Storage.sol (100%) rename src/exercises/{05 => 04}/Vault05.sol (100%) rename test/{00/Test_SIP00.t.sol => TestVault00.t.sol} (97%) rename test/{02 => }/TestVault02.t.sol (99%) rename test/{04 => }/TestVault04.t.sol (98%) diff --git a/src/proposals/sips/SIP00.sol b/src/exercises/00/SIP00.sol similarity index 96% rename from src/proposals/sips/SIP00.sol rename to src/exercises/00/SIP00.sol index 690c62e..3dbca96 100644 --- a/src/proposals/sips/SIP00.sol +++ b/src/exercises/00/SIP00.sol @@ -10,7 +10,7 @@ import {Vault} from "src/exercises/00/Vault00.sol"; import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; -/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP00.sol:SIP00 -vvvv +/// 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 contract SIP00 is GovernorBravoProposal { using ForkSelector for uint256; diff --git a/src/proposals/sips/SIP01.sol b/src/exercises/01/SIP01.sol similarity index 94% rename from src/proposals/sips/SIP01.sol rename to src/exercises/01/SIP01.sol index 1b35a85..5a6a8f3 100644 --- a/src/proposals/sips/SIP01.sol +++ b/src/exercises/01/SIP01.sol @@ -6,11 +6,11 @@ import {GovernorBravoProposal} from import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; -import {Vault} from "src/exercises/00/Vault00.sol"; +import {Vault} from "src/exercises/01/Vault01.sol"; import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; -/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP01.sol:SIP01 -vvvv +/// 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 contract SIP01 is GovernorBravoProposal { using ForkSelector for uint256; diff --git a/src/proposals/sips/SIP02.sol b/src/exercises/02/SIP02.sol similarity index 95% rename from src/proposals/sips/SIP02.sol rename to src/exercises/02/SIP02.sol index 1d86b53..5bcae40 100644 --- a/src/proposals/sips/SIP02.sol +++ b/src/exercises/02/SIP02.sol @@ -6,7 +6,7 @@ import {GovernorBravoProposal} from import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; -import {Vault03} from "src/exercises/03/Vault03.sol"; +import {Vault03} from "src/exercises/03/Vault.sol"; import {Vault04} from "src/exercises/04/Vault04.sol"; import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; @@ -20,7 +20,7 @@ import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/proposals/sips/SIP02.sol:SIP02 -vvvv +/// 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 contract SIP02 is GovernorBravoProposal { using ForkSelector for uint256; @@ -140,7 +140,6 @@ contract SIP02 is GovernorBravoProposal { 1_000_000e18, "Max supply should be 1,000,000 USDC" ); - // fails as slot for totalSupplied is changed assertEq( vault.totalSupplied(), 1_000e18, diff --git a/src/exercises/03/SIP04.sol b/src/exercises/03/SIP04.sol new file mode 100644 index 0000000..e3c46f3 --- /dev/null +++ b/src/exercises/03/SIP04.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {GovernorBravoProposal} from + "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; +import {Addresses} from + "@forge-proposal-simulator/addresses/Addresses.sol"; + +import {Vault04} from "src/exercises/04/Vault04.sol"; +import {MockToken} from "@mocks/MockToken.sol"; +import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; +import { + ProxyAdmin, + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ERC1967Utils} from + "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/exercises/SIP04.sol:SIP04 -vvvv +contract SIP04 is GovernorBravoProposal { + using ForkSelector for uint256; + + constructor() { + primaryForkId = ETHEREUM_FORK_ID; + } + + function setupProposal() public { + ETHEREUM_FORK_ID.createForksAndSelect(); + + string memory addressesFolderPath = "./addresses"; + uint256[] memory chainIds = new uint256[](1); + chainIds[0] = 1; + + setAddresses(new Addresses(addressesFolderPath, chainIds)); + } + + function name() public pure override returns (string memory) { + return "SIP-04 Upgrade"; + } + + function description() + public + pure + override + returns (string memory) + { + return name(); + } + + function run() public override { + setupProposal(); + + setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); + + super.run(); + } + + function deploy() public override { + if (!addresses.isAddressSet("PROXY_ADMIN")) { + ProxyAdmin proxyAdmin = new ProxyAdmin(); + proxyAdmin.transferOwnership( + addresses.getAddress("COMPOUND_TIMELOCK_BRAVO") + ); + + addresses.addAddress("PROXY_ADMIN", address(proxyAdmin), true); + } + + address vaultProxy; + if (!addresses.isAddressSet("V4_VAULT_IMPLEMENTATION")) { + address vaultImpl = address(new Vault04()); + addresses.addAddress( + "V4_VAULT_IMPLEMENTATION", vaultImpl, true + ); + + address[] memory tokens = new address[](3); + tokens[0] = addresses.getAddress("USDC"); + tokens[1] = addresses.getAddress("DAI"); + tokens[2] = addresses.getAddress("USDT"); + + address owner = addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"); + + // Generate calldata for initialize function of vault + bytes memory data = abi.encodeWithSignature( + "initialize(address[],address)", tokens, owner + ); + + vaultProxy = address( + new TransparentUpgradeableProxy( + vaultImpl, owner, data + ) + ); + addresses.addAddress("VAULT_PROXY", vaultProxy, true); + } + } + + function validate() public view override { + Vault04 vault = Vault04(addresses.getAddress("VAULT_PROXY")); + + assertEq( + vault.authorizedToken(addresses.getAddress("USDC")), + true, + "USDC should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("DAI")), + true, + "DAI should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("USDT")), + true, + "USDT should be authorized" + ); + + address vaultProxy = addresses.getAddress("VAULT_PROXY"); + bytes32 adminSlot = + vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); + + address proxyAdmin = address(uint160(uint256(adminSlot))); + } +} diff --git a/src/exercises/03/Vault03.sol b/src/exercises/03/Vault03.sol deleted file mode 100644 index 653726c..0000000 --- a/src/exercises/03/Vault03.sol +++ /dev/null @@ -1,160 +0,0 @@ -pragma solidity 0.8.25; - -import {OwnableUpgradeable} from - "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {IERC20Metadata} from - "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {SafeERC20} from - "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Vault03Storage} from "src/exercises/03/Vault03Storage.sol"; - -contract Vault03 is Vault03Storage { - using SafeERC20 for IERC20; - - /// @notice Deposit event - /// @param token The token deposited - /// @param sender The address that deposited the token - /// @param amount The amount deposited - event Deposit( - address indexed token, address indexed sender, uint256 amount - ); - - /// @notice Withdraw event - /// @param token The token withdrawn - /// @param sender The address that withdrew the token - /// @param amount The amount withdrawn - event Withdraw( - address indexed token, address indexed sender, uint256 amount - ); - - event TokenAdded(address indexed token); - - constructor() { - _disableInitializers(); - } - - /// @notice Initialize the vault with a list of authorized tokens - /// @param _tokens The list of authorized tokens - /// @param _owner The owner address to set for the contract - function initialize(address[] memory _tokens, address _owner) - external - initializer - { - __Ownable_init(_owner); - - for (uint256 i = 0; i < _tokens.length; i++) { - require( - IERC20Metadata(_tokens[i]).decimals() <= 18, - "Vault: unsupported decimals" - ); - - authorizedToken[_tokens[i]] = true; - - emit TokenAdded(_tokens[i]); - } - } - - /// ------------------------------------------------------------- - /// ------------------------------------------------------------- - /// -------------------- ONLY OWNER FUNCTION -------------------- - /// ------------------------------------------------------------- - /// ------------------------------------------------------------- - - /// @notice Add a token to the list of authorized tokens - /// only callable by the owner - /// @param token to add - function addToken(address token) external onlyOwner { - require( - IERC20Metadata(token).decimals() <= 18, - "Vault: unsupported decimals" - ); - require( - !authorizedToken[token], "Vault: token already authorized" - ); - - authorizedToken[token] = true; - - emit TokenAdded(token); - } - - /// ------------------------------------------------------------- - /// ------------------------------------------------------------- - /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- - /// ------------------------------------------------------------- - /// ------------------------------------------------------------- - - /// @notice Deposit tokens into the vault - /// @param token The token to deposit, only authorized tokens allowed - /// @param amount The amount to deposit - function deposit(address token, uint256 amount) external { - require(authorizedToken[token], "Vault: token not authorized"); - - uint256 normalizedAmount = getNormalizedAmount(token, amount); - - /// save on gas by using unchecked, no need to check for overflow - /// as all deposited tokens are whitelisted - unchecked { - balanceOf[msg.sender] += normalizedAmount; - } - - totalSupplied += normalizedAmount; - - IERC20(token).safeTransferFrom( - msg.sender, address(this), amount - ); - - emit Deposit(token, msg.sender, amount); - } - - /// @notice Withdraw tokens from the vault - /// @param token The token to withdraw, only authorized tokens are allowed - /// this is implicitly checked because a user can only have a balance of an - /// authorized token - /// @param amount The amount to withdraw - function withdraw(address token, uint256 amount) external { - require(authorizedToken[token], "Vault: token not authorized"); - - uint256 normalizedAmount = getNormalizedAmount(token, amount); - - /// both a check and an effect, ensures user has sufficient funds for - /// withdrawal - /// must be checked for underflow as a user can only withdraw what they - /// have deposited - balanceOf[msg.sender] -= normalizedAmount; - - /// save on gas by using unchecked, no need to check for underflow - /// as all deposited tokens are whitelisted, plus we know our invariant - /// always holds - unchecked { - totalSupplied -= normalizedAmount; - } - - IERC20(token).safeTransfer(msg.sender, amount); - - emit Withdraw(token, msg.sender, amount); - } - - /// -------------------------------------------------------- - /// -------------------------------------------------------- - /// ----------------- PUBLIC VIEW FUNCTION ----------------- - /// -------------------------------------------------------- - /// -------------------------------------------------------- - - /// @notice public for testing purposes, returns the normalized amount of - /// tokens scaled to 18 decimals - /// @param token The token to deposit - /// @param amount The amount to deposit - function getNormalizedAmount(address token, uint256 amount) - public - view - returns (uint256 normalizedAmount) - { - uint8 decimals = IERC20Metadata(token).decimals(); - normalizedAmount = amount; - if (decimals < 18) { - normalizedAmount = amount * (10 ** (18 - decimals)); - } - } -} diff --git a/src/exercises/03/Vault03Storage.sol b/src/exercises/03/Vault03Storage.sol deleted file mode 100644 index 273961d..0000000 --- a/src/exercises/03/Vault03Storage.sol +++ /dev/null @@ -1,20 +0,0 @@ -pragma solidity 0.8.25; - -import {OwnableUpgradeable} from - "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract Vault03Storage is OwnableUpgradeable { - /// @notice Mapping of authorized tokens - mapping(address => bool) public authorizedToken; - - /// @notice User's balance of all tokens deposited in the vault - mapping(address => uint256) public balanceOf; - - /// @notice Total amount of tokens supplied to the vault - /// - /// invariants: - /// totalSupplied = sum(balanceOf all users) - /// sum(balanceOf(vault) authorized tokens) >= totalSupplied - /// - uint256 public totalSupplied; -} diff --git a/src/exercises/04/Vault04.sol b/src/exercises/03/Vault04.sol similarity index 100% rename from src/exercises/04/Vault04.sol rename to src/exercises/03/Vault04.sol diff --git a/src/exercises/04/Vault04Storage.sol b/src/exercises/03/Vault04Storage.sol similarity index 100% rename from src/exercises/04/Vault04Storage.sol rename to src/exercises/03/Vault04Storage.sol diff --git a/src/exercises/05/Vault05.sol b/src/exercises/04/Vault05.sol similarity index 100% rename from src/exercises/05/Vault05.sol rename to src/exercises/04/Vault05.sol diff --git a/test/00/Test_SIP00.t.sol b/test/TestVault00.t.sol similarity index 97% rename from test/00/Test_SIP00.t.sol rename to test/TestVault00.t.sol index 8d0e162..bf3e612 100644 --- a/test/00/Test_SIP00.t.sol +++ b/test/TestVault00.t.sol @@ -4,12 +4,13 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {console} from "@forge-std/console.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + import {Test} from "@forge-std/Test.sol"; import {Vault} from "src/exercises/00/Vault00.sol"; -import {SIP00} from "src/proposals/sips/SIP00.sol"; +import {SIP00} from "src/exercises/00/SIP00.sol"; -contract TestSIP00 is Test, SIP00 { +contract TestVault00 is Test, SIP00 { using SafeERC20 for IERC20; Vault public vault; @@ -39,15 +40,17 @@ contract TestSIP00 is Test, SIP00 { /// run the proposal deploy(); - /// validate the proposal - validate(); - dai = addresses.getAddress("DAI"); usdc = addresses.getAddress("USDC"); usdt = addresses.getAddress("USDT"); vault = Vault(addresses.getAddress("V1_VAULT")); } + function testValidate() public view { + /// validate the proposal + validate(); + } + function testVaultDepositDai() public { uint256 daiDepositAmount = 1_000e18; diff --git a/test/02/TestVault02.t.sol b/test/TestVault02.t.sol similarity index 99% rename from test/02/TestVault02.t.sol rename to test/TestVault02.t.sol index e7c4104..a4584b8 100644 --- a/test/02/TestVault02.t.sol +++ b/test/TestVault02.t.sol @@ -7,7 +7,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; import {Vault} from "src/exercises/00/Vault00.sol"; -import {SIP00} from "src/proposals/sips/SIP00.sol"; +import {SIP00} from "src/exercises/00/SIP00.sol"; contract TestVault02 is Test, SIP00 { using SafeERC20 for IERC20; diff --git a/test/04/TestVault04.t.sol b/test/TestVault04.t.sol similarity index 98% rename from test/04/TestVault04.t.sol rename to test/TestVault04.t.sol index 3b7bd15..196a22d 100644 --- a/test/04/TestVault04.t.sol +++ b/test/TestVault04.t.sol @@ -5,8 +5,8 @@ import {SafeERC20} from import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; +import {SIP02} from "src/exercises/02/SIP02.sol"; import {Vault04} from "src/exercises/04/Vault04.sol"; -import {SIP02} from "src/proposals/sips/SIP02.sol"; contract TestVault04 is Test, SIP02 { using SafeERC20 for IERC20; From aecfda8fd226f1056c115cdb7f9210a2764b2896 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 13:30:20 +0700 Subject: [PATCH 19/22] checkpoint: organization, clean up TODO's Signed-off-by: Elliot --- src/exercises/00/SIP00.sol | 7 +- src/exercises/01/SIP01.sol | 1 - src/exercises/02/SIP02.sol | 70 +--------- src/exercises/03/REQUIREMENTS.md | 3 + src/exercises/03/{SIP04.sol => SIP03.sol} | 50 +++---- .../{04/Vault05.sol => 03/Vault03.sol} | 18 +-- src/exercises/03/Vault04Storage.sol | 21 --- src/exercises/04/REQUIREMENTS.md | 5 + src/exercises/04/SIP04.sol | 123 ++++++++++++++++++ src/exercises/{03 => 04}/Vault04.sol | 33 +++-- test/TestVault00.t.sol | 6 +- test/TestVault02.t.sol | 19 +-- test/TestVault04.t.sol | 6 +- 13 files changed, 212 insertions(+), 150 deletions(-) create mode 100644 src/exercises/03/REQUIREMENTS.md rename src/exercises/03/{SIP04.sol => SIP03.sol} (73%) rename src/exercises/{04/Vault05.sol => 03/Vault03.sol} (92%) delete mode 100644 src/exercises/03/Vault04Storage.sol create mode 100644 src/exercises/04/REQUIREMENTS.md create mode 100644 src/exercises/04/SIP04.sol rename src/exercises/{03 => 04}/Vault04.sol (89%) diff --git a/src/exercises/00/SIP00.sol b/src/exercises/00/SIP00.sol index 3dbca96..5156b9c 100644 --- a/src/exercises/00/SIP00.sol +++ b/src/exercises/00/SIP00.sol @@ -7,7 +7,6 @@ import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; import {Vault} from "src/exercises/00/Vault00.sol"; -import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; /// 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 @@ -50,7 +49,7 @@ contract SIP00 is GovernorBravoProposal { } function deploy() public override { - if (!addresses.isAddressSet("V1_VAULT")) { + if (!addresses.isAddressSet("V0_VAULT")) { address[] memory tokens = new address[](3); tokens[0] = addresses.getAddress("USDC"); tokens[1] = addresses.getAddress("DAI"); @@ -58,12 +57,12 @@ contract SIP00 is GovernorBravoProposal { Vault vault = new Vault(tokens); - addresses.addAddress("V1_VAULT", address(vault), true); + addresses.addAddress("V0_VAULT", address(vault), true); } } function validate() public view override { - Vault vault = Vault(addresses.getAddress("V1_VAULT")); + Vault vault = Vault(addresses.getAddress("V0_VAULT")); assertEq( vault.authorizedToken(addresses.getAddress("USDC")), diff --git a/src/exercises/01/SIP01.sol b/src/exercises/01/SIP01.sol index 5a6a8f3..3b695a8 100644 --- a/src/exercises/01/SIP01.sol +++ b/src/exercises/01/SIP01.sol @@ -7,7 +7,6 @@ import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; import {Vault} from "src/exercises/01/Vault01.sol"; -import {MockToken} from "@mocks/MockToken.sol"; import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; /// 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 diff --git a/src/exercises/02/SIP02.sol b/src/exercises/02/SIP02.sol index 5bcae40..bda8de2 100644 --- a/src/exercises/02/SIP02.sol +++ b/src/exercises/02/SIP02.sol @@ -5,11 +5,6 @@ import {GovernorBravoProposal} from "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; - -import {Vault03} from "src/exercises/03/Vault.sol"; -import {Vault04} from "src/exercises/04/Vault04.sol"; -import {MockToken} from "@mocks/MockToken.sol"; -import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; import { ProxyAdmin, TransparentUpgradeableProxy, @@ -20,6 +15,9 @@ import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Vault} from "src/exercises/02/Vault02.sol"; +import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; + /// 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 contract SIP02 is GovernorBravoProposal { using ForkSelector for uint256; @@ -60,13 +58,7 @@ contract SIP02 is GovernorBravoProposal { } function deploy() public override { - address vaultProxy; - if (!addresses.isAddressSet("V3_VAULT_IMPLEMENTATION")) { - address vaultImpl = address(new Vault03()); - addresses.addAddress( - "V3_VAULT_IMPLEMENTATION", vaultImpl, true - ); - + if (!addresses.isAddressSet("V2_VAULT")) { address[] memory tokens = new address[](3); tokens[0] = addresses.getAddress("USDC"); tokens[1] = addresses.getAddress("DAI"); @@ -74,50 +66,13 @@ contract SIP02 is GovernorBravoProposal { address owner = addresses.getAddress("DEPLOYER_EOA"); - // Generate calldata for initialize function of vault - bytes memory data = abi.encodeWithSignature( - "initialize(address[],address)", tokens, owner - ); - - vaultProxy = address( - new TransparentUpgradeableProxy( - vaultImpl, owner, data - ) - ); - addresses.addAddress("VAULT_PROXY", vaultProxy, true); - } - - if (!addresses.isAddressSet("V4_VAULT_IMPLEMENTATION")) { - address vaultImpl = address(new Vault04()); - addresses.addAddress( - "V4_VAULT_IMPLEMENTATION", vaultImpl, true - ); + address vaultImpl = address(new Vault(tokens, owner)); + addresses.addAddress("V2_VAULT", vaultImpl, true); } } - function build() - public - override - buildModifier(addresses.getAddress("COMPOUND_TIMELOCK_BRAVO")) - { - address vaultProxy = addresses.getAddress("VAULT_PROXY"); - bytes32 adminSlot = - vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); - - address proxyAdmin = address(uint160(uint256(adminSlot))); - - // upgrade to new implementation - ProxyAdmin(proxyAdmin).upgradeAndCall( - ITransparentUpgradeableProxy(vaultProxy), - addresses.getAddress("V4_VAULT_IMPLEMENTATION"), - "" - ); - - Vault04(vaultProxy).setMaxSupply(1_000_000e18); - } - function validate() public view override { - Vault04 vault = Vault04(addresses.getAddress("VAULT_PROXY")); + Vault vault = Vault(addresses.getAddress("V2_VAULT")); assertEq( vault.authorizedToken(addresses.getAddress("USDC")), @@ -134,16 +89,5 @@ contract SIP02 is GovernorBravoProposal { true, "USDT should be authorized" ); - - assertEq( - vault.maxSupply(), - 1_000_000e18, - "Max supply should be 1,000,000 USDC" - ); - assertEq( - vault.totalSupplied(), - 1_000e18, - "Total supplied should be 1000 USDC" - ); } } diff --git a/src/exercises/03/REQUIREMENTS.md b/src/exercises/03/REQUIREMENTS.md new file mode 100644 index 0000000..e423ad2 --- /dev/null +++ b/src/exercises/03/REQUIREMENTS.md @@ -0,0 +1,3 @@ +# Overview + +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. \ No newline at end of file diff --git a/src/exercises/03/SIP04.sol b/src/exercises/03/SIP03.sol similarity index 73% rename from src/exercises/03/SIP04.sol rename to src/exercises/03/SIP03.sol index e3c46f3..0489a47 100644 --- a/src/exercises/03/SIP04.sol +++ b/src/exercises/03/SIP03.sol @@ -6,9 +6,6 @@ import {GovernorBravoProposal} from import {Addresses} from "@forge-proposal-simulator/addresses/Addresses.sol"; -import {Vault04} from "src/exercises/04/Vault04.sol"; -import {MockToken} from "@mocks/MockToken.sol"; -import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; import { ProxyAdmin, TransparentUpgradeableProxy, @@ -19,8 +16,11 @@ import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -/// DO_RUN=false DO_BUILD=false DO_DEPLOY=true DO_SIMULATE=false DO_PRINT=false DO_VALIDATE=true forge script src/exercises/SIP04.sol:SIP04 -vvvv -contract SIP04 is GovernorBravoProposal { +import {Vault} from "src/exercises/03/Vault03.sol"; +import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; + +/// 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 +contract SIP03 is GovernorBravoProposal { using ForkSelector for uint256; constructor() { @@ -38,7 +38,7 @@ contract SIP04 is GovernorBravoProposal { } function name() public pure override returns (string memory) { - return "SIP-04 Upgrade"; + return "SIP-03 Upgrade"; } function description() @@ -59,45 +59,45 @@ contract SIP04 is GovernorBravoProposal { } function deploy() public override { - if (!addresses.isAddressSet("PROXY_ADMIN")) { - ProxyAdmin proxyAdmin = new ProxyAdmin(); - proxyAdmin.transferOwnership( - addresses.getAddress("COMPOUND_TIMELOCK_BRAVO") - ); - - addresses.addAddress("PROXY_ADMIN", address(proxyAdmin), true); - } - address vaultProxy; - if (!addresses.isAddressSet("V4_VAULT_IMPLEMENTATION")) { - address vaultImpl = address(new Vault04()); - addresses.addAddress( - "V4_VAULT_IMPLEMENTATION", vaultImpl, true - ); + if (!addresses.isAddressSet("V3_VAULT_IMPL")) { + address vaultImpl = address(new Vault()); + addresses.addAddress("V3_VAULT_IMPL", vaultImpl, true); address[] memory tokens = new address[](3); tokens[0] = addresses.getAddress("USDC"); tokens[1] = addresses.getAddress("DAI"); tokens[2] = addresses.getAddress("USDT"); - address owner = addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"); + address owner = + addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"); // Generate calldata for initialize function of vault bytes memory data = abi.encodeWithSignature( "initialize(address[],address)", tokens, owner ); + /// proxy admin contract is created by the Transparent Upgradeable Proxy vaultProxy = address( new TransparentUpgradeableProxy( vaultImpl, owner, data ) ); addresses.addAddress("VAULT_PROXY", vaultProxy, true); + + address proxyAdmin = address( + uint160( + uint256( + vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT) + ) + ) + ); + addresses.addAddress("PROXY_ADMIN", proxyAdmin, true); } } function validate() public view override { - Vault04 vault = Vault04(addresses.getAddress("VAULT_PROXY")); + Vault vault = Vault(addresses.getAddress("VAULT_PROXY")); assertEq( vault.authorizedToken(addresses.getAddress("USDC")), @@ -120,5 +120,11 @@ contract SIP04 is GovernorBravoProposal { vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); address proxyAdmin = address(uint160(uint256(adminSlot))); + + assertEq( + ProxyAdmin(proxyAdmin).owner(), + addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"), + "owner not set" + ); } } diff --git a/src/exercises/04/Vault05.sol b/src/exercises/03/Vault03.sol similarity index 92% rename from src/exercises/04/Vault05.sol rename to src/exercises/03/Vault03.sol index 3dfbb48..43d4aa5 100644 --- a/src/exercises/04/Vault05.sol +++ b/src/exercises/03/Vault03.sol @@ -4,22 +4,15 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {OwnableUpgradeable} from - "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {VaultStoragePausable} from - "src/exercises/storage/VaultStoragePausable.sol"; +import {VaultStorageOwnable} from + "src/exercises/storage/VaultStorageOwnable.sol"; /// @notice Add maxsupply to the vault and update getNormalizedAmount logic -/// TODO make pauseable -/// make totalSupplied offsets the same -/// inherit pausable to mess up the storage slot offsets -/// add governance proposal that deploys and ugprades the existing vault from -/// proposal 03 /// deploy Vault 03 to mainnet /// add integration tests -contract Vault05 is VaultStoragePausable { +contract Vault is VaultStorageOwnable { using SafeERC20 for IERC20; /// @notice Deposit event @@ -116,10 +109,7 @@ contract Vault05 is VaultStoragePausable { /// @notice Deposit tokens into the vault /// @param token The token to deposit, only authorized tokens allowed /// @param amount The amount to deposit - function deposit(address token, uint256 amount) - external - whenNotPaused - { + function deposit(address token, uint256 amount) external { require(authorizedToken[token], "Vault: token not authorized"); uint256 normalizedAmount = getNormalizedAmount(token, amount); diff --git a/src/exercises/03/Vault04Storage.sol b/src/exercises/03/Vault04Storage.sol deleted file mode 100644 index 34a1c1a..0000000 --- a/src/exercises/03/Vault04Storage.sol +++ /dev/null @@ -1,21 +0,0 @@ -pragma solidity 0.8.25; - -/// inherit ownable, switch around the order o -contract Vault04Storage { - /// @notice Mapping of authorized tokens - mapping(address => bool) public authorizedToken; - - /// @notice User's balance of all tokens deposited in the vault - mapping(address => uint256) public balanceOf; - - /// @notice Maximum amount of tokens that can be supplied to the vault - uint256 public maxSupply; - - /// @notice Total amount of tokens supplied to the vault - /// - /// invariants: - /// totalSupplied = sum(balanceOf all users) - /// sum(balanceOf(vault) authorized tokens) >= totalSupplied - /// - uint256 public totalSupplied; -} diff --git a/src/exercises/04/REQUIREMENTS.md b/src/exercises/04/REQUIREMENTS.md new file mode 100644 index 0000000..c9b193b --- /dev/null +++ b/src/exercises/04/REQUIREMENTS.md @@ -0,0 +1,5 @@ +# Overview + +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. + +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. diff --git a/src/exercises/04/SIP04.sol b/src/exercises/04/SIP04.sol new file mode 100644 index 0000000..94cd5fa --- /dev/null +++ b/src/exercises/04/SIP04.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {GovernorBravoProposal} from + "@forge-proposal-simulator/src/proposals/GovernorBravoProposal.sol"; +import {Addresses} from + "@forge-proposal-simulator/addresses/Addresses.sol"; +import { + ProxyAdmin, + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ERC1967Utils} from + "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {Vault} from "src/exercises/04/Vault04.sol"; +import {ForkSelector, ETHEREUM_FORK_ID} from "@test/utils/Forks.sol"; + +/// 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 +contract SIP04 is GovernorBravoProposal { + using ForkSelector for uint256; + + constructor() { + primaryForkId = ETHEREUM_FORK_ID; + } + + function setupProposal() public { + ETHEREUM_FORK_ID.createForksAndSelect(); + + string memory addressesFolderPath = "./addresses"; + uint256[] memory chainIds = new uint256[](1); + chainIds[0] = 1; + + setAddresses(new Addresses(addressesFolderPath, chainIds)); + } + + function name() public pure override returns (string memory) { + return "SIP-03 Upgrade"; + } + + function description() + public + pure + override + returns (string memory) + { + return name(); + } + + function run() public override { + setupProposal(); + + setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); + + super.run(); + } + + function deploy() public override { + if (!addresses.isAddressSet("V4_VAULT_IMPL")) { + address vaultImpl = address(new Vault()); + addresses.addAddress("V4_VAULT_IMPL", vaultImpl, true); + } + } + + function build() + public + override + buildModifier(addresses.getAddress("COMPOUND_TIMELOCK_BRAVO")) + { + address vaultProxy = addresses.getAddress("VAULT_PROXY"); + bytes32 adminSlot = + vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); + + address proxyAdmin = address(uint160(uint256(adminSlot))); + + /// recorded calls + + // upgrade to new implementation + ProxyAdmin(proxyAdmin).upgradeAndCall( + ITransparentUpgradeableProxy(vaultProxy), + addresses.getAddress("V4_VAULT_IMPLEMENTATION"), + "" + ); + + Vault(vaultProxy).setMaxSupply(1_000_000e18); + } + + function validate() public view override { + Vault vault = Vault(addresses.getAddress("VAULT_PROXY")); + + assertEq( + vault.authorizedToken(addresses.getAddress("USDC")), + true, + "USDC should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("DAI")), + true, + "DAI should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("USDT")), + true, + "USDT should be authorized" + ); + assertEq( + vault.maxSupply(), 1_000_000e18, "max supply not set" + ); + + address vaultProxy = addresses.getAddress("VAULT_PROXY"); + bytes32 adminSlot = + vm.load(vaultProxy, ERC1967Utils.ADMIN_SLOT); + address proxyAdmin = address(uint160(uint256(adminSlot))); + + assertEq( + ProxyAdmin(proxyAdmin).owner(), + addresses.getAddress("COMPOUND_TIMELOCK_BRAVO"), + "owner not set" + ); + } +} diff --git a/src/exercises/03/Vault04.sol b/src/exercises/04/Vault04.sol similarity index 89% rename from src/exercises/03/Vault04.sol rename to src/exercises/04/Vault04.sol index 1fa143c..52fec78 100644 --- a/src/exercises/03/Vault04.sol +++ b/src/exercises/04/Vault04.sol @@ -4,20 +4,16 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {OwnableUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {VaultStorageOwnable} from - "src/exercises/storage/VaultStorageOwnable.sol"; +import {VaultStoragePausable} from + "src/exercises/storage/VaultStoragePausable.sol"; /// @notice Add maxsupply to the vault and update getNormalizedAmount logic -/// TODO make pauseable -/// make totalSupplied offsets the same -/// inherit pausable to mess up the storage slot offsets -/// add governance proposal that deploys and ugprades the existing vault from -/// proposal 03 -/// deploy Vault 03 to mainnet -/// add integration tests -contract Vault04 is VaultStorageOwnable { +/// allows pausing by owner +contract Vault is VaultStoragePausable { using SafeERC20 for IERC20; /// @notice Deposit event @@ -105,6 +101,18 @@ contract Vault04 is VaultStorageOwnable { emit MaxSupplyUpdated(previousMaxSupply, newMaxSupply); } + /// @notice pauses the contract, callable only by the owner + /// and when the contract is unpaused + function pause() external onlyOwner whenNotPaused { + _pause(); + } + + /// @notice unpauses the contract, callable only by the owner + /// and when the contract is paused + function unpause() external onlyOwner whenPaused { + _unpause(); + } + /// ------------------------------------------------------------- /// ------------------------------------------------------------- /// ----------------- PUBLIC MUTATIVE FUNCTIONS ----------------- @@ -114,7 +122,10 @@ contract Vault04 is VaultStorageOwnable { /// @notice Deposit tokens into the vault /// @param token The token to deposit, only authorized tokens allowed /// @param amount The amount to deposit - function deposit(address token, uint256 amount) external { + function deposit(address token, uint256 amount) + external + whenNotPaused + { require(authorizedToken[token], "Vault: token not authorized"); uint256 normalizedAmount = getNormalizedAmount(token, amount); diff --git a/test/TestVault00.t.sol b/test/TestVault00.t.sol index bf3e612..3210aac 100644 --- a/test/TestVault00.t.sol +++ b/test/TestVault00.t.sol @@ -43,7 +43,7 @@ contract TestVault00 is Test, SIP00 { dai = addresses.getAddress("DAI"); usdc = addresses.getAddress("USDC"); usdt = addresses.getAddress("USDT"); - vault = Vault(addresses.getAddress("V1_VAULT")); + vault = Vault(addresses.getAddress("V0_VAULT")); } function testValidate() public view { @@ -77,7 +77,7 @@ contract TestVault00 is Test, SIP00 { deal(usdt, address(this), usdtDepositAmount); USDT(usdt).approve( - addresses.getAddress("V1_VAULT"), usdtDepositAmount + addresses.getAddress("V0_VAULT"), usdtDepositAmount ); /// this executes 3 state transitions: @@ -163,7 +163,7 @@ contract TestVault00 is Test, SIP00 { vm.startPrank(sender); IERC20(token).safeIncreaseAllowance( - addresses.getAddress("V1_VAULT"), amount + addresses.getAddress("V0_VAULT"), amount ); /// this executes 3 state transitions: diff --git a/test/TestVault02.t.sol b/test/TestVault02.t.sol index a4584b8..3deaea4 100644 --- a/test/TestVault02.t.sol +++ b/test/TestVault02.t.sol @@ -6,10 +6,10 @@ import {console} from "@forge-std/console.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; -import {Vault} from "src/exercises/00/Vault00.sol"; -import {SIP00} from "src/exercises/00/SIP00.sol"; +import {Vault} from "src/exercises/02/Vault02.sol"; +import {SIP02} from "src/exercises/02/SIP02.sol"; -contract TestVault02 is Test, SIP00 { +contract TestVault02 is Test, SIP02 { using SafeERC20 for IERC20; Vault public vault; @@ -42,7 +42,7 @@ contract TestVault02 is Test, SIP00 { dai = addresses.getAddress("DAI"); usdc = addresses.getAddress("USDC"); usdt = addresses.getAddress("USDT"); - vault = Vault(addresses.getAddress("V1_VAULT")); + vault = Vault(addresses.getAddress("V2_VAULT")); } function testValidate() public view { @@ -76,7 +76,7 @@ contract TestVault02 is Test, SIP00 { deal(usdt, address(this), usdtDepositAmount); USDT(usdt).approve( - addresses.getAddress("V1_VAULT"), usdtDepositAmount + addresses.getAddress("V2_VAULT"), usdtDepositAmount ); /// this executes 3 state transitions: @@ -162,7 +162,7 @@ contract TestVault02 is Test, SIP00 { vm.startPrank(sender); IERC20(token).safeIncreaseAllowance( - addresses.getAddress("V1_VAULT"), amount + addresses.getAddress("V2_VAULT"), amount ); /// this executes 3 state transitions: @@ -172,14 +172,17 @@ contract TestVault02 is Test, SIP00 { vault.deposit(token, amount); vm.stopPrank(); + uint256 normalizedAmount = + vault.getNormalizedAmount(token, amount); + assertEq( vault.balanceOf(sender), - startingUserBalance + amount, + startingUserBalance + normalizedAmount, "user vault balance not increased" ); assertEq( vault.totalSupplied(), - startingTotalSupplied + amount, + startingTotalSupplied + normalizedAmount, "vault total supplied not increased by deposited amount" ); assertEq( diff --git a/test/TestVault04.t.sol b/test/TestVault04.t.sol index 196a22d..fa8b058 100644 --- a/test/TestVault04.t.sol +++ b/test/TestVault04.t.sol @@ -6,12 +6,12 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test} from "@forge-std/Test.sol"; import {SIP02} from "src/exercises/02/SIP02.sol"; -import {Vault04} from "src/exercises/04/Vault04.sol"; +import {Vault} from "src/exercises/04/Vault04.sol"; contract TestVault04 is Test, SIP02 { using SafeERC20 for IERC20; - Vault04 public vault; + Vault public vault; /// @notice user addresses address public immutable userA = address(1111); @@ -43,7 +43,7 @@ contract TestVault04 is Test, SIP02 { dai = addresses.getAddress("DAI"); usdc = addresses.getAddress("USDC"); usdt = addresses.getAddress("USDT"); - vault = Vault04(addresses.getAddress("VAULT_PROXY")); + vault = Vault(addresses.getAddress("VAULT_PROXY")); } function testVaultDepositDai() public { From 19df21b512ff35fadd23b1b6b2fd0ee908ed3dfa Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 13:58:06 +0700 Subject: [PATCH 20/22] fix: failing tests Signed-off-by: Elliot --- src/exercises/04/SIP04.sol | 7 ++++--- src/exercises/04/Vault04.sol | 2 +- test/TestVault04.t.sol | 21 ++++++++++++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/exercises/04/SIP04.sol b/src/exercises/04/SIP04.sol index 94cd5fa..0aaade3 100644 --- a/src/exercises/04/SIP04.sol +++ b/src/exercises/04/SIP04.sol @@ -34,10 +34,11 @@ contract SIP04 is GovernorBravoProposal { chainIds[0] = 1; setAddresses(new Addresses(addressesFolderPath, chainIds)); + setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); } function name() public pure override returns (string memory) { - return "SIP-03 Upgrade"; + return "SIP-04"; } function description() @@ -46,7 +47,7 @@ contract SIP04 is GovernorBravoProposal { override returns (string memory) { - return name(); + return "Upgrade to V4 Vault Implementation"; } function run() public override { @@ -80,7 +81,7 @@ contract SIP04 is GovernorBravoProposal { // upgrade to new implementation ProxyAdmin(proxyAdmin).upgradeAndCall( ITransparentUpgradeableProxy(vaultProxy), - addresses.getAddress("V4_VAULT_IMPLEMENTATION"), + addresses.getAddress("V4_VAULT_IMPL"), "" ); diff --git a/src/exercises/04/Vault04.sol b/src/exercises/04/Vault04.sol index 52fec78..6f0d243 100644 --- a/src/exercises/04/Vault04.sol +++ b/src/exercises/04/Vault04.sol @@ -196,7 +196,7 @@ contract Vault is VaultStoragePausable { uint8 decimals = IERC20Metadata(token).decimals(); normalizedAmount = amount; if (decimals < 18) { - normalizedAmount = amount ** (10 * (18 - decimals)); + normalizedAmount = amount * (10 ** (18 - decimals)); } } } diff --git a/test/TestVault04.t.sol b/test/TestVault04.t.sol index fa8b058..0a536c1 100644 --- a/test/TestVault04.t.sol +++ b/test/TestVault04.t.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.0; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Test} from "@forge-std/Test.sol"; +import {Test, console} from "@forge-std/Test.sol"; -import {SIP02} from "src/exercises/02/SIP02.sol"; +import {SIP03} from "src/exercises/03/SIP03.sol"; +import {SIP04} from "src/exercises/04/SIP04.sol"; import {Vault} from "src/exercises/04/Vault04.sol"; -contract TestVault04 is Test, SIP02 { +contract TestVault04 is Test, SIP04 { using SafeERC20 for IERC20; Vault public vault; @@ -32,14 +33,26 @@ contract TestVault04 is Test, SIP02 { vm.setEnv("DO_PRINT", "false"); vm.setEnv("DO_VALIDATE", "false"); + SIP03 sip03 = new SIP03(); + + sip03.setupProposal(); + sip03.deploy(); + /// setup the proposal setupProposal(); + /// copy SIP03 addresses into this contract for integration testing + setAddresses(sip03.addresses()); + /// run the proposal vm.startPrank(addresses.getAddress("DEPLOYER_EOA")); deploy(); vm.stopPrank(); + /// build and run proposal + build(); + simulate(); + dai = addresses.getAddress("DAI"); usdc = addresses.getAddress("USDC"); usdt = addresses.getAddress("USDT"); @@ -76,7 +89,9 @@ contract TestVault04 is Test, SIP02 { function testWithdrawAlreadyDepositedUSDC() public { uint256 usdcDepositAmount = 1_000e6; + _vaultDeposit(usdc, address(this), usdcDepositAmount); + vault.withdraw(usdc, usdcDepositAmount); } From 2598085f1b35f2cc37620c90163052932998398c Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 14:00:29 +0700 Subject: [PATCH 21/22] fix: 02 integration test Signed-off-by: Elliot --- test/TestVault02.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TestVault02.t.sol b/test/TestVault02.t.sol index 3deaea4..6ca5bf3 100644 --- a/test/TestVault02.t.sol +++ b/test/TestVault02.t.sol @@ -87,12 +87,12 @@ contract TestVault02 is Test, SIP02 { assertEq( vault.balanceOf(address(this)), - usdtDepositAmount, + vault.getNormalizedAmount(usdt, usdtDepositAmount), "vault token balance not increased" ); assertEq( vault.totalSupplied(), - usdtDepositAmount, + vault.getNormalizedAmount(usdt, usdtDepositAmount), "vault total supplied not increased" ); assertEq( From 57b92b3b4bff85ac12ab9b8c3087c0c53cb26349 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Nov 2024 16:37:25 +0700 Subject: [PATCH 22/22] fix: remove prank when deploying SIP-04 Signed-off-by: Elliot --- test/TestVault04.t.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/TestVault04.t.sol b/test/TestVault04.t.sol index 0a536c1..6ecc0a6 100644 --- a/test/TestVault04.t.sol +++ b/test/TestVault04.t.sol @@ -44,10 +44,8 @@ contract TestVault04 is Test, SIP04 { /// copy SIP03 addresses into this contract for integration testing setAddresses(sip03.addresses()); - /// run the proposal - vm.startPrank(addresses.getAddress("DEPLOYER_EOA")); + /// deploy contracts from MIP-04 deploy(); - vm.stopPrank(); /// build and run proposal build();