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: 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/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 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..fa52531 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +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/ 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/proposals/sips/SIP00.sol b/src/exercises/00/SIP00.sol similarity index 77% rename from src/proposals/sips/SIP00.sol rename to src/exercises/00/SIP00.sol index df231bb..5156b9c 100644 --- a/src/proposals/sips/SIP00.sol +++ b/src/exercises/00/SIP00.sol @@ -3,18 +3,17 @@ 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 {MockToken} from "@mocks/MockToken.sol"; +import {Vault} from "src/exercises/00/Vault00.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; constructor() { - // ETHEREUM_FORK_ID.createForksAndSelect(); primaryForkId = ETHEREUM_FORK_ID; } @@ -32,7 +31,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(); } @@ -45,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"); @@ -53,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/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/exercises/01/SIP01.sol b/src/exercises/01/SIP01.sol new file mode 100644 index 0000000..3b695a8 --- /dev/null +++ b/src/exercises/01/SIP01.sol @@ -0,0 +1,82 @@ +// 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/exercises/01/Vault01.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 +contract SIP01 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-01 System Deploy"; + } + + 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[](2); + tokens[0] = addresses.getAddress("USDC"); + tokens[1] = 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("USDT")), + true, + "USDT should be authorized" + ); + assertEq( + vault.authorizedToken(addresses.getAddress("DAI")), + true, + "DAI should be authorized" + ); + } +} 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/exercises/02/SIP02.sol b/src/exercises/02/SIP02.sol new file mode 100644 index 0000000..bda8de2 --- /dev/null +++ b/src/exercises/02/SIP02.sol @@ -0,0 +1,93 @@ +// 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/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; + + 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-02 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("V2_VAULT")) { + 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"); + + address vaultImpl = address(new Vault(tokens, owner)); + addresses.addAddress("V2_VAULT", vaultImpl, true); + } + } + + function validate() public view override { + Vault vault = Vault(addresses.getAddress("V2_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" + ); + } +} 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/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/SIP03.sol b/src/exercises/03/SIP03.sol new file mode 100644 index 0000000..0489a47 --- /dev/null +++ b/src/exercises/03/SIP03.sol @@ -0,0 +1,130 @@ +// 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/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() { + 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 { + address vaultProxy; + 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"); + + // 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 { + 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" + ); + + 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/Vault03.sol b/src/exercises/03/Vault03.sol new file mode 100644 index 0000000..43d4aa5 --- /dev/null +++ b/src/exercises/03/Vault03.sol @@ -0,0 +1,186 @@ +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 {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 +/// deploy Vault 03 to mainnet +/// add integration tests +contract Vault is VaultStorageOwnable { + 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 { + 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/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..0aaade3 --- /dev/null +++ b/src/exercises/04/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 { + 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)); + setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); + } + + function name() public pure override returns (string memory) { + return "SIP-04"; + } + + function description() + public + pure + override + returns (string memory) + { + return "Upgrade to V4 Vault Implementation"; + } + + 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_IMPL"), + "" + ); + + 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/04/Vault04.sol b/src/exercises/04/Vault04.sol new file mode 100644 index 0000000..6f0d243 --- /dev/null +++ b/src/exercises/04/Vault04.sol @@ -0,0 +1,202 @@ +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 +/// allows pausing by owner +contract Vault 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); + } + + /// @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 ----------------- + /// ------------------------------------------------------------- + /// ------------------------------------------------------------- + + /// @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/IVault.sol b/src/exercises/IVault.sol new file mode 100644 index 0000000..c000cc6 --- /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; +} 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/test/00/Test_SIP00.t.sol b/test/TestVault00.t.sol similarity index 85% rename from test/00/Test_SIP00.t.sol rename to test/TestVault00.t.sol index 8897bc9..3210aac 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/examples/00/Vault00.sol"; -import {SIP00} from "src/proposals/sips/SIP00.sol"; +import {Vault} from "src/exercises/00/Vault00.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,13 +40,15 @@ 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")); + vault = Vault(addresses.getAddress("V0_VAULT")); + } + + function testValidate() public view { + /// validate the proposal + validate(); } function testVaultDepositDai() public { @@ -73,7 +76,9 @@ contract TestSIP00 is Test, SIP00 { deal(usdt, address(this), usdtDepositAmount); - USDT(usdt).approve(addresses.getAddress("V1_VAULT"), usdtDepositAmount); + USDT(usdt).approve( + addresses.getAddress("V0_VAULT"), usdtDepositAmount + ); /// this executes 3 state transitions: /// 1. deposit dai into the vault @@ -105,8 +110,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,18 +149,21 @@ 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); 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/02/TestVault02.t.sol b/test/TestVault02.t.sol similarity index 80% rename from test/02/TestVault02.t.sol rename to test/TestVault02.t.sol index dbdba0b..6ca5bf3 100644 --- a/test/02/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/examples/00/Vault00.sol"; -import {SIP00} from "src/proposals/sips/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; @@ -39,13 +39,15 @@ 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")); + vault = Vault(addresses.getAddress("V2_VAULT")); + } + + function testValidate() public view { + /// validate the proposal + validate(); } function testVaultDepositDai() public { @@ -73,7 +75,9 @@ contract TestVault02 is Test, SIP00 { deal(usdt, address(this), usdtDepositAmount); - USDT(usdt).approve(addresses.getAddress("V1_VAULT"), usdtDepositAmount); + USDT(usdt).approve( + addresses.getAddress("V2_VAULT"), usdtDepositAmount + ); /// this executes 3 state transitions: /// 1. deposit dai into the vault @@ -83,12 +87,12 @@ contract TestVault02 is Test, SIP00 { 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( @@ -105,8 +109,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,18 +148,21 @@ 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); vm.startPrank(sender); IERC20(token).safeIncreaseAllowance( - addresses.getAddress("V1_VAULT"), amount + addresses.getAddress("V2_VAULT"), amount ); /// this executes 3 state transitions: @@ -159,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 new file mode 100644 index 0000000..6ecc0a6 --- /dev/null +++ b/test/TestVault04.t.sol @@ -0,0 +1,139 @@ +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, console} from "@forge-std/Test.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, SIP04 { + using SafeERC20 for IERC20; + + Vault 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"); + + SIP03 sip03 = new SIP03(); + + sip03.setupProposal(); + sip03.deploy(); + + /// setup the proposal + setupProposal(); + + /// copy SIP03 addresses into this contract for integration testing + setAddresses(sip03.addresses()); + + /// deploy contracts from MIP-04 + deploy(); + + /// build and run proposal + build(); + simulate(); + + dai = addresses.getAddress("DAI"); + usdc = addresses.getAddress("USDC"); + usdt = addresses.getAddress("USDT"); + vault = Vault(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; + + _vaultDeposit(usdc, address(this), usdcDepositAmount); + + 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(); + + uint256 normalizedAmount = + vault.getNormalizedAmount(token, amount); + + assertEq( + vault.balanceOf(sender), + startingUserBalance + normalizedAmount, + "user vault balance not increased" + ); + assertEq( + vault.totalSupplied(), + startingTotalSupplied + normalizedAmount, + "vault total supplied not increased by deposited amount" + ); + assertEq( + IERC20(token).balanceOf(address(vault)), + startingTotalBalance + amount, + "token balance not increased" + ); + } +} 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(); - } -}