diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1342175..0d4b0a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,11 @@ jobs: forge build --sizes id: build + - uses: actions/upload-artifact@v3 + with: + name: interfaces-abi + path: out/I**sol/I**json + - name: Run Forge tests env: POLYGON_RPC_URL: ${{ secrets.POLYGON_RPC_URL }} diff --git a/README.md b/README.md index 023910d..bcf7012 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Our ETHOnline2023 focus: integration with Uniswap and harnessing Safe smart wall Monorepo setup: smart contracts in root, frontend in its folder. We integrate Foundry/Forge with GitHub Actions for streamlined CI/CD. ### Deployed instances -* [ProfitPalsVaultFactory](https://polygonscan.com/address/0x5b8c9cab6cb6461c97e651fb603946228d66942e#code) -* [ProfitPalsVault(USDC, [WBTC, WETH])](https://polygonscan.com/address/0xd783b002954df35a02bfa3f1e8e0462078d27f84#code) +* [ProfitPalsVaultFactory](https://polygonscan.com/address/0xf33096dB1f341C0249aEdd164B4DeA5E2FaBecdE#code) +* [ProfitPalsVault(USDC, [WBTC, WETH])](https://polygonscan.com/address/0xd95556ce580e8b7f923cb739e6b0291734fef437#code) ### Clone/Checkout diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index 1a47b40..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console2} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/src/Constants.sol b/src/Constants.sol index 6c740e6..9a66aec 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -1,13 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -uint256 constant SHARE_DECIMALS = 6; -address constant SAFE_PROXY_FACTORY_130_POLYGON = 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2; -address constant SAFE_LOGIC_SINGLETON_POLYGON = 0x3E5c63644E683549055b9Be8653de26E0B4CD36E; -address constant USDC_POLYGON = 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; -address constant WBTC_POLYGON = 0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6; -address constant WETH_POLYGON = 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; - address constant UNISWAP_PERMIT2_POLYGON = 0x000000000022D473030F116dDEE9F6B43aC78BA3; //Goerli @@ -36,4 +29,13 @@ address constant UV3_UNIVERSAL_ROUTER = 0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7F address constant UNIVERSAL_ROUTER2 = 0x643770E279d5D0733F21d6DC03A8efbABf3255B4; // Used from Safe App UI -address constant SAFE_SIGN_MESSAGE_LIB = 0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2; \ No newline at end of file +//Polygon +address constant SAFE_SIGN_MESSAGE_LIB = 0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2; +address constant SAFE_COMPATIBILITY_FALLBACK_HANDLER = 0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4; +address constant SAFE_PROXY_FACTORY_130_POLYGON = 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2; +address constant SAFE_LOGIC_SINGLETON_POLYGON = 0x3E5c63644E683549055b9Be8653de26E0B4CD36E; + +//Deploy scripts and tests +address constant USDC_POLYGON = 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; +address constant WBTC_POLYGON = 0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6; +address constant WETH_POLYGON = 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; diff --git a/src/ProfitPalsVault.sol b/src/ProfitPalsVault.sol index f22bc0d..caca73d 100644 --- a/src/ProfitPalsVault.sol +++ b/src/ProfitPalsVault.sol @@ -2,11 +2,15 @@ pragma solidity ^0.8.0; import "@openzeppelin/token/ERC20/extensions/ERC4626.sol"; +import "@openzeppelin/token/ERC721/extensions/IERC721Enumerable.sol"; import "@openzeppelin/proxy/utils/Initializable.sol"; +import "@openzeppelin/utils/math/Math.sol"; import "@safe-contracts/GnosisSafeL2.sol"; import "@safe-contracts/proxies/GnosisSafeProxyFactory.sol"; import {Guard} from "@safe-contracts/base/GuardManager.sol"; +import "@safe-contracts/interfaces/ISignatureValidator.sol"; +import "@safe-contracts/examples/guards/ReentrancyTransactionGuard.sol"; import "./Constants.sol"; import "./interfaces/IProfitPalsVault.sol"; @@ -21,7 +25,13 @@ import "./interfaces/IProfitPalsVault.sol"; @author Gene A. Tsvigun - @author Denise Epstein - */ -contract ProfitPalsVault is IProfitPalsVault, ERC4626, Guard, Initializable { +contract ProfitPalsVault is IProfitPalsVault, ISignatureValidator, ERC4626, Guard, Initializable { + bytes32 internal constant GUARD_STORAGE_SLOT = keccak256("profit_pals_vault.guard.struct"); + + struct PPGuardValue { + bool active; + } + address[17] ALLOWED_CONTRACTS = [ //TODO make this list shorter, not all of thesea addresses are necessary UV3_UNISWAP_V3_FACTORY, UV3_MULTICALL2, @@ -42,34 +52,17 @@ contract ProfitPalsVault is IProfitPalsVault, ERC4626, Guard, Initializable { SAFE_SIGN_MESSAGE_LIB ]; - struct Action { - address to; - uint256 value; - bytes data; - Enum.Operation operation; - uint256 safeTxGas; - uint256 baseGas; - uint256 gasPrice; - address gasToken; - address payable refundReceiver; - bytes signatures; - address msgSender; - } - - event UnauthorizedActionDetected( - address to, - uint256 value, - bytes data, - Enum.Operation operation, - uint256 safeTxGas, - uint256 baseGas, - uint256 gasPrice, - address gasToken, - address payable refundReceiver, - bytes signatures, - address msgSender); +// https://docs.safe.global/safe-smart-account/signatures#contract-signature-eip-1271 +// {32-bytes signature verifier}{32-bytes data position}{1-byte signature type} +// {32-bytes signature length}{bytes signature data} + bytes nopSignature = bytes.concat( + abi.encode(address(this)), + abi.encode(uint8(65)), + bytes1(0), //static part ends here + abi.encode(uint8(1)), //signature length + bytes1(0) //signature data + ); - event ActionLog(Action action); IERC20 public immutable anchorCurrency; address public immutable operator; @@ -80,6 +73,13 @@ contract ProfitPalsVault is IProfitPalsVault, ERC4626, Guard, Initializable { mapping(address => bool) isTokenAllowed; mapping(address => bool) isContractAllowed; + //[POC limitations] https://github.com/chainhackers/ethonline-2023/issues/39 + //Hackathon version - the only position + uint256 position; + uint256 positionsBalanceBeforeTx; + uint256 anchorCurrencyBalanceBeforeTx; + address currentTxSender; + /** * @param anchorCurrency_ - The main or anchor ERC20 token that the vault will manage. * @param name_ - Name of the shares token @@ -109,6 +109,17 @@ contract ProfitPalsVault is IProfitPalsVault, ERC4626, Guard, Initializable { isTokenAllowed[0x0000000000000000000000000000000000001010] = true; // MATIC //TODO } + fallback() external {} + + function getGuard() internal pure returns (PPGuardValue storage guard) { + bytes32 slot = GUARD_STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + guard.slot := slot + } + } + + function initialize( GnosisSafeL2 safe_ ) public initializer { @@ -116,26 +127,17 @@ contract ProfitPalsVault is IProfitPalsVault, ERC4626, Guard, Initializable { //TODO send unlimited approvals for all allowedTokens } - function totalAssets() public view override(IERC4626, ERC4626) returns (uint256) { - //TODO add overall Uniswap positions here - return anchorCurrency.balanceOf(address(this)); - } - - function deposit(uint256 amount) external { - - } - - function withdraw(uint256 amount) external { - + uint256 anchorCurrencyBalance = anchorCurrency.balanceOf(address(safe)); + return anchorCurrencyBalance + positionValueInAnchorCurrency(position); } function pause() external { - + //TODO } function unpause() external { - + //TODO } function allowedTokensList() external view override returns (address[] memory) { @@ -159,6 +161,10 @@ contract ProfitPalsVault is IProfitPalsVault, ERC4626, Guard, Initializable { bytes memory signatures, address msgSender ) external override { + PPGuardValue storage guard = getGuard(); + require(!guard.active, "Reentrancy detected"); + guard.active = true; + // require(isUniswapContract[to] || isTokenAllowed[to], "Only approvals of allowed tokens to Uniswap and Uniswap contracts allowed"); //TODO iterate over allowed tokens if (!isContractAllowed[to] && !isTokenAllowed[to]) { emit UnauthorizedActionDetected(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, signatures, msgSender); @@ -176,12 +182,117 @@ contract ProfitPalsVault is IProfitPalsVault, ERC4626, Guard, Initializable { refundReceiver, signatures, msgSender)); - //TODO keep record of mint txs //TODO keep record of burn ( decrease liquidity ) txs + + positionsBalanceBeforeTx = getPositionsBalanceInSafe(); + anchorCurrencyBalanceBeforeTx = anchorCurrency.balanceOf(address(safe)); + currentTxSender = msgSender; } - function checkAfterExecution(bytes32 txHash, bool success) external override { + function checkAfterExecution(bytes32 txHash, bool) external override { + getGuard().active = false; //TODO get minted position IDs + if (currentTxSender == operator) { +// require(currentTxSender == tx.origin, "ProfitPalsVault: [POC limitations] manual operation only"); +// require(_isAnchorBalanceChanged(), "ProfitPalsVault: every opertor action must change anchor currency balance must change "); + if (currentTxSender != tx.origin || !_isAnchorBalanceChanged()) { + emit UnauthorizedActionOperatorMustChangeAnchorBalance(txHash); + } + } + + uint256 balanceAfterTx = getPositionsBalanceInSafe(); + if (positionsBalanceBeforeTx != balanceAfterTx) { +// require(positionValueInAnchorCurrency(position) == 0, "ProfitPalsVault: [POC limitations] only one open position at a time is allowed"); + if(positionValueInAnchorCurrency(position) > 0){ + emit UnauthorizedActionOnlyOneOpenPositionAllowed(txHash); + } + + uint256 positionIndex = balanceAfterTx - 1; + position = IERC721Enumerable(UV3_NONFUNGIBLE_POSITION_MANAGER).tokenOfOwnerByIndex( + address(safe), + positionIndex + ); + emit PositionAcquired(positionIndex); + } + } + + /** + * @dev Deposit/mint common workflow. + */ + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { + SafeERC20.safeTransferFrom(anchorCurrency, caller, address(safe), assets); + _mint(receiver, shares); + + emit Deposit(caller, receiver, assets, shares); + } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + _burn(owner, shares); + + bytes memory withdrawData = abi.encodeCall( + IERC20.transfer, + (receiver, assets) + ); + + safe.execTransaction( + address(anchorCurrency), //to + 0, //value + withdrawData, + Enum.Operation.Call, + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(address(this)), // refundReceiver + nopSignature + ); + + emit Withdraw(caller, receiver, owner, assets, shares); + } + + function getPositionsBalanceInSafe() private view returns (uint256 balance){ + balance = IERC721Enumerable(UV3_NONFUNGIBLE_POSITION_MANAGER).balanceOf(address(safe)); + } + + function positionValueInAnchorCurrency(uint256 positionId) private view returns (uint256 value) { + //TODO @silvesterdrago #33 get UniswapV3 position estimate + if (position > 0) { + uint256 debugFakePositionEstimateStub = 3 * 10 ** 4; + value = debugFakePositionEstimateStub; + } + value = 0; + } + + /** @dev See {IERC4626-maxWithdraw}. */ + function maxWithdraw(address owner) public view override(ERC4626, IERC4626) returns (uint256) { + return Math.min( + _convertToAssets(balanceOf(owner), Math.Rounding.Floor), + anchorCurrency.balanceOf(address(safe))); + } + + function anchorCurrencyShare() private view returns (uint256) { + return anchorCurrency.balanceOf(address(safe)) / totalAssets(); } + function isValidSignature(bytes memory, bytes memory _signature) public view override returns (bytes4){ + if (keccak256(_signature) == keccak256(hex"00")) {//TODO do some actual checking + return bytes4(EIP1271_MAGIC_VALUE); + } + return bytes4(0); + } + + function _isAnchorBalanceChanged() private view returns (bool) { + uint256 anchorBalance = anchorCurrency.balanceOf(address(safe)); + return anchorBalance != anchorCurrencyBalanceBeforeTx; + } } diff --git a/src/ProfitPalsVaultFactory.sol b/src/ProfitPalsVaultFactory.sol index dac4efd..7549cc2 100644 --- a/src/ProfitPalsVaultFactory.sol +++ b/src/ProfitPalsVaultFactory.sol @@ -11,7 +11,18 @@ import {GnosisSafeL2} from "@safe-contracts/GnosisSafeL2.sol"; import "@safe-contracts/common/Enum.sol"; import "@safe-contracts/interfaces/ISignatureValidator.sol"; - +/** + @title ProfitPalsVaultFactory + @notice ProfitPalsVaultFactory creates a new Vault with settings that stay immutable during + @notice the vault existence, namely Operator address, anchor currency, allowed tokens, operator fee + @notice creates a new `GnosisSafeProxy` using `GnosisSafeProxyFactory` + @notice approves infinite spending limit to UniswapV3 Permit2 for all allowed tokens + @notice sets the vault as one of the new Safe owners, and the operator as the other one + @notice It filters operator interactions using predefined contracts list and allowed tokens. + @dev Signs every tx to Safe with an EIP1271 signature + @author Gene A. Tsvigun - + @author Denise Epstein - +*/ contract ProfitPalsVaultFactory is IProfitPalsVaultFactory, ISignatureValidator { address public immutable safeLogicSingleton; address public immutable safeProxyFactory; @@ -61,7 +72,7 @@ contract ProfitPalsVaultFactory is IProfitPalsVaultFactory, ISignatureValidator address[] memory owners = new address[](3); owners[0] = address(vault); - owners[1] = tx.origin; //TODO think about this + owners[1] = tx.origin; //TODO drop deployer address from vault owners owners[2] = address(this); bytes memory safeInitializerData = abi.encodeCall( @@ -69,8 +80,8 @@ contract ProfitPalsVaultFactory is IProfitPalsVaultFactory, ISignatureValidator (owners, 1, address(0), - "", //abi.encodeCall(GnosisSafe.setGuard,(guard)), - address(0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4), + "", + address(SAFE_COMPATIBILITY_FALLBACK_HANDLER), address(0), 0, payable(0)) @@ -82,6 +93,10 @@ contract ProfitPalsVaultFactory is IProfitPalsVaultFactory, ISignatureValidator ); GnosisSafeL2 safe = GnosisSafeL2(payable(address(proxy))); + for (uint256 i = 0; i < tokens.length; i++) { + approveToken(safe, IERC20(tokens[i])); + } + bytes memory setGuardData = abi.encodeCall( GuardManager.setGuard, address(vault) @@ -100,13 +115,9 @@ contract ProfitPalsVaultFactory is IProfitPalsVaultFactory, ISignatureValidator nopSignature ); - for (uint256 i = 0; i < tokens.length; i++) { - approveToken(safe, IERC20(tokens[i])); - } - vault.initialize(safe); - emit ProfitPalsVaultCreated(vault, anchorCurrency, operatorFee, name, symbol); + emit ProfitPalsVaultCreated(vault, anchorCurrency, tokens, operatorFee, name, symbol, address(safe)); return vault; } @@ -114,8 +125,7 @@ contract ProfitPalsVaultFactory is IProfitPalsVaultFactory, ISignatureValidator function approveToken(GnosisSafeL2 safe, IERC20 token) private { bytes memory approveTokenData = abi.encodeCall( IERC20.approve, - (address(0x000000000022D473030F116dDEE9F6B43aC78BA3), //Uniswap Permit2 - type(uint256).max) + (address(UV3_PERMIT2), type(uint256).max) ); safe.execTransaction( @@ -132,7 +142,7 @@ contract ProfitPalsVaultFactory is IProfitPalsVaultFactory, ISignatureValidator ); } - function isValidSignature(bytes memory _data, bytes memory _signature) public view override returns (bytes4){ + function isValidSignature(bytes memory, bytes memory _signature) public view override returns (bytes4){ if (keccak256(_signature) == keccak256(hex"00")) {//TODO do some actual checking return bytes4(EIP1271_MAGIC_VALUE); } diff --git a/src/interfaces/IProfitPalsVault.sol b/src/interfaces/IProfitPalsVault.sol index ebd7559..d0bc48b 100644 --- a/src/interfaces/IProfitPalsVault.sol +++ b/src/interfaces/IProfitPalsVault.sol @@ -6,7 +6,7 @@ import "@openzeppelin/token/ERC20/IERC20.sol"; import "@safe-contracts/GnosisSafeL2.sol"; /** - @title ProfitPalsVault + @title IProfitPalsVault @notice ProfitPalsVault acts as the primary vault for the ProfitPals project, holding and managing all assets. @notice The main asset, or anchor currency, that is managed within this vault can be any ERC20 token. @notice It filters operator interactions using predefined contracts list and allowed tokens. @@ -16,6 +16,46 @@ import "@safe-contracts/GnosisSafeL2.sol"; @author Denise Epstein - */ interface IProfitPalsVault is IERC4626 { + struct Action { + address to; + uint256 value; + bytes data; + Enum.Operation operation; + uint256 safeTxGas; + uint256 baseGas; + uint256 gasPrice; + address gasToken; + address payable refundReceiver; + bytes signatures; + address msgSender; + } + + // debug events: the following events are for manual testing with relaxed limitations ------------------------------ + // instead of reverting, log unauthorized operator actions + event UnauthorizedActionDetected( + address to, + uint256 value, + bytes data, + Enum.Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes signatures, + address msgSender); + + //[POC limitations] https://github.com/chainhackers/ethonline-2023/issues/39 + event UnauthorizedActionOperatorMustChangeAnchorBalance(bytes32 txHash); + event UnauthorizedActionOnlyOneOpenPositionAllowed(bytes32 txHash); + // debug events block end: events above are for manual testing with relaxed limitations ---------------------------- + + + event ActionLog(Action action); + + event PositionAcquired(uint256 indexed tokenId); + event FungibleTokenAcquired(uint256 indexed tokenId, uint amount); + function safe() external view returns (GnosisSafeL2); function operator() external view returns (address); @@ -30,10 +70,6 @@ interface IProfitPalsVault is IERC4626 { function allowedTokensCount() external view returns (uint256); - function deposit(uint256 amount) external; - - function withdraw(uint256 amount) external; - function pause() external; function unpause() external; diff --git a/src/interfaces/IProfitPalsVaultFactory.sol b/src/interfaces/IProfitPalsVaultFactory.sol index 6e66fd3..097ef44 100644 --- a/src/interfaces/IProfitPalsVaultFactory.sol +++ b/src/interfaces/IProfitPalsVaultFactory.sol @@ -4,13 +4,27 @@ pragma solidity ^0.8.0; import "@openzeppelin/token/ERC20/IERC20.sol"; import "./IProfitPalsVault.sol"; +/** + @title IProfitPalsVaultFactory + @notice ProfitPalsVaultFactory creates a new Vault with settings that stay immutable during + @notice the vault existence, namely Operator address, anchor currency, allowed tokens, operator fee + @notice creates a new `GnosisSafeProxy` using `GnosisSafeProxyFactory` + @notice approves infinite spending limit to UniswapV3 Permit2 for all allowed tokens + @notice sets the vault as one of the new Safe owners, and the operator as the other one + @notice It filters operator interactions using predefined contracts list and allowed tokens. + @dev Signs every tx to Safe with an EIP1271 signature + @author Gene A. Tsvigun - + @author Denise Epstein - +*/ interface IProfitPalsVaultFactory { event ProfitPalsVaultCreated( IProfitPalsVault indexed vault, IERC20 indexed anchorCurrency, + address[] allowedTokens, uint256 operatorFee, string name, - string symbol + string symbol, + address safe ); function createVault( diff --git a/test/ProfitPalsVault.t.sol b/test/ProfitPalsVault.t.sol index 51947fa..33a0fe0 100644 --- a/test/ProfitPalsVault.t.sol +++ b/test/ProfitPalsVault.t.sol @@ -12,8 +12,9 @@ import {GuardManager} from "@safe-contracts/base/GuardManager.sol"; import "../src/Constants.sol"; import "../src/ProfitPalsVaultFactory.sol"; -contract SetupSafeGuard is Test { +contract ProfitPalsVaultTest is Test { address USDC_BIG_HOLDER = 0xe7804c37c13166fF0b37F5aE0BB07A3aEbb6e245; + IERC20 usdc = IERC20(USDC_POLYGON); address[] public allowedTokens; IERC20 anchorCurrency; @@ -23,6 +24,9 @@ contract SetupSafeGuard is Test { IProfitPalsVault vault; GnosisSafeProxy proxy; + address investorA; + address investorB; + function setUp() public { string memory rpcURL = vm.envString("POLYGON_RPC_URL"); uint256 forkId = vm.createFork(rpcURL); @@ -30,10 +34,18 @@ contract SetupSafeGuard is Test { anchorCurrency = IERC20(USDC_POLYGON); - vm.prank(USDC_BIG_HOLDER); - anchorCurrency.transfer(address(this), 1000 * 10 ** 6); - vm.deal(address(this), 10 ** 18); + vm.deal(investorA, 10 ** 18); + vm.deal(investorB, 10 ** 18); + + investorA = vm.createWallet("investorA").addr; + investorB = vm.createWallet("investorB").addr; + + vm.startPrank(USDC_BIG_HOLDER); + anchorCurrency.transfer(address(this), 10000 * 10 ** 6); + anchorCurrency.transfer(address(investorA), 10000 * 10 ** 6); + anchorCurrency.transfer(address(investorB), 10000 * 10 ** 6); + vm.stopPrank(); allowedTokens.push(WBTC_POLYGON); allowedTokens.push(WETH_POLYGON); @@ -61,4 +73,59 @@ contract SetupSafeGuard is Test { assertEq(vault.totalAssets(), 0); assertEq(vault.asset(), address(anchorCurrency)); } -} + + function test_deposit() public { + uint256 investment2k = 2000 * 10 ** 6; + + usdc.approve(address(vault), investment2k); + vault.deposit(investment2k, address(this)); + assertEq(vault.balanceOf(address(this)), investment2k); + assertEq(vault.totalAssets(), investment2k); + assertEq(usdc.balanceOf(address(vault.safe())), investment2k); + assertEq(usdc.balanceOf(address(vault)), 0); + + vm.startPrank(address(investorA)); + usdc.approve(address(vault), investment2k); + vault.deposit(investment2k, investorA); + vm.stopPrank(); + assertEq(vault.balanceOf(investorA), investment2k); + assertEq(vault.totalAssets(), 4000 * 10 ** 6); + assertEq(usdc.balanceOf(address(vault.safe())), 4000 * 10 ** 6); + assertEq(usdc.balanceOf(address(vault)), 0); + + vm.startPrank(address(investorB)); + usdc.approve(address(vault), investment2k); + vault.deposit(investment2k, investorB); + vm.stopPrank(); + assertEq(vault.balanceOf(investorB), investment2k); + assertEq(vault.totalAssets(), 6000 * 10 ** 6); + assertEq(usdc.balanceOf(address(vault.safe())), 6000 * 10 ** 6); + assertEq(usdc.balanceOf(address(vault)), 0); + } + + function test_withdraw() public { + uint256 investment10k = 10000 * 10 ** 6; + + usdc.approve(address(vault), investment10k); + vault.deposit(investment10k, address(this)); + assertEq(vault.balanceOf(address(this)), investment10k); + assertEq(vault.totalAssets(), investment10k); + assertEq(usdc.balanceOf(address(vault.safe())), investment10k); + assertEq(usdc.balanceOf(address(vault)), 0); + + vault.withdraw(5555 * 10 ** 6, address(this), address(this)); + assertEq(vault.balanceOf(address(this)), 4445 * 10 ** 6); + assertEq(vault.totalAssets(), 4445 * 10 ** 6); + assertEq(usdc.balanceOf(address(vault.safe())), 4445 * 10 ** 6); + assertEq(usdc.balanceOf(address(vault)), 0); + assertEq(usdc.balanceOf(address(this)), 5555 * 10 ** 6); + } + + function test_swap() public { + uint256 investment10k = 10000 * 10 ** 6; + usdc.approve(address(vault), investment10k); + vault.deposit(investment10k, address(this)); + assertEq(usdc.balanceOf(address(vault.safe())), investment10k); + //TODO use Uniswap Universal Router SDK to send swaps + } +} \ No newline at end of file