diff --git a/contracts/interfaces/vault/IERC4626.sol b/contracts/interfaces/vault/IERC4626.sol new file mode 100644 index 0000000..aa2af28 --- /dev/null +++ b/contracts/interfaces/vault/IERC4626.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0 +// ███╗ ███╗ █████╗ ██╗ ██╗ █████╗ +// ████╗ ████║██╔══██╗██║ ██║██╔══██╗ +// ██╔████╔██║███████║███████║███████║ +// ██║╚██╔╝██║██╔══██║██╔══██║██╔══██║ +// ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██║ +// ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ +// Website: https://maha.xyz +// Discord: https://discord.gg/mahadao +// Twitter: https://twitter.com/mahaxyz_ +pragma solidity 0.8.21; + +interface IERC4626 { + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + event Withdraw( + address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares + ); + /** + * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing. + * + * - MUST be an ERC-20 token contract. + * - MUST NOT revert. + */ + + function asset() external view returns (address assetTokenAddress); + // /** + // * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given + // * current on-chain conditions. + // * + // * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit + // * call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called + // * in the same transaction. + // * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the + // * deposit would be accepted, regardless if the user has enough tokens approved, etc. + // * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + // * - MUST NOT revert. + // * + // * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in + // * share price or some other type of condition, meaning the depositor will lose assets by depositing. + // */ + function previewDeposit( + uint256 assets + ) external view returns (uint256 shares); + /** + * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * deposit execution, and are accounted for during deposit. + * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + // /** + // * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, + // * given current on-chain conditions. + // * + // * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a + // withdraw + // * call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if + // * called + // * in the same transaction. + // * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though + // * the withdrawal would be accepted, regardless if the user has enough shares, etc. + // * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + // * - MUST NOT revert. + // * + // * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in + // * share price or some other type of condition, meaning the depositor will lose assets by depositing. + // */ + function previewWithdraw( + uint256 assets + ) external view returns (uint256 shares); + // /** + // * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver. + // * + // * - MUST emit the Withdraw event. + // * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + // * withdraw execution, and are accounted for during withdraw. + // * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner + // * not having enough shares, etc). + // * + // * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + // * Those methods should be performed separately. + // */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); +} diff --git a/contracts/vault/USDCVault.sol b/contracts/vault/USDCVault.sol new file mode 100644 index 0000000..02fbe2d --- /dev/null +++ b/contracts/vault/USDCVault.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-3.0 +// ███╗ ███╗ █████╗ ██╗ ██╗ █████╗ +// ████╗ ████║██╔══██╗██║ ██║██╔══██╗ +// ██╔████╔██║███████║███████║███████║ +// ██║╚██╔╝██║██╔══██║██╔══██║██╔══██║ +// ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██║ +// ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ +// Website: https://maha.xyz +// Discord: https://discord.gg/mahadao +// Twitter: https://twitter.com/mahaxyz_ + +pragma solidity 0.8.21; + +import {IERC4626} from "../interfaces/vault/IERC4626.sol"; +import {IPegStabilityModule, ZapSafetyPool} from "../periphery/zaps/implementations/ethereum/ZapSafetyPool.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {console} from "lib/forge-std/src/console.sol"; + +interface StableSwap { + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external; + function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256); + function get_dx(int128 i, int128 j, uint256 dy) external view returns (uint256); +} + +contract USDCVault is ERC20Upgradeable, Ownable2StepUpgradeable, IERC4626 { + using SafeERC20 for IERC20; + + // State variables for different tokens, vaults, and external contracts + IERC20 public USDC; // USDC token + IERC20 public USDE; // USDe token + IERC4626 public SZAI; // SZAI vault interface + IERC4626 public SUSDE; // SUSDe vault interface + IPegStabilityModule public PSM; // Peg Stability Module (for USDC-to-USDe conversion) + ZapSafetyPool public zapContract; // Zap contract for interacting with safety pool + StableSwap public stableSwap; // StableSwap interface for exchanging assets between pools + + /** + * @dev Initializes the contract with necessary addresses and configurations. + * @param _name The name of the vault token. + * @param _symbol The symbol of the vault token. + * @param _usdc The address of the USDC token contract. + * @param _usde The address of the USDe token contract. + * @param _sZAI The address of the SZAI vault contract. + * @param _sUSDe The address of the SUSDe vault contract. + * @param _psm The address of the Peg Stability Module (PSM). + * @param _zapContract The address of the ZapSafetyPool contract. + * @param _stableSwapPool The address of the StableSwap contract. + */ + function initialize( + string memory _name, + string memory _symbol, + address _usdc, + address _usde, + address _sZAI, + address _sUSDe, + address _psm, + address _zapContract, + address _stableSwapPool + ) external initializer { + USDC = IERC20(_usdc); + USDE = IERC20(_usde); + SZAI = IERC4626(_sZAI); + SUSDE = IERC4626(_sUSDe); + PSM = IPegStabilityModule(_psm); + zapContract = ZapSafetyPool(_zapContract); + stableSwap = StableSwap(_stableSwapPool); + __Ownable_init(msg.sender); + __ERC20_init(_name, _symbol); + } + + /** + * @dev Deposits the given amount of USDC into the vault, swaps for USDe, deposits it into the SUSDe vault, + * then zaps into SZAI and mints shares for the receiver. + * @param assets The amount of USDC to deposit. + * @param receiver The address that will receive the minted shares. + * @return shares The amount of shares minted and assigned to the receiver. + */ + function deposit(uint256 assets, address receiver) external returns (uint256) { + // 1. Take the USDC from the User as collateral + USDC.safeTransferFrom(msg.sender, address(this), assets); + + // 2. Convert USDC to USDe + uint256 balanceUSDe = _swapUSDCtoUSDe(assets); + console.log("USDe balance this contract have after swap", balanceUSDe); + + // 3. Deposit the USDe into SUSDe Vault + USDE.approve(address(SUSDE), balanceUSDe); + uint256 susdeShares = SUSDE.deposit(balanceUSDe, address(this)); + console.log("After depositing in sUSDe vault get shares", susdeShares); + // 4. Zap into SZAI from SUSDe shares + uint256 SZAIBalanceBefore = IERC20(address(SZAI)).balanceOf(address(this)); // Before Zap + IERC20(address(SUSDE)).approve(address(zapContract), susdeShares); + zapContract.zapIntoSafetyPool(PSM, susdeShares); + uint256 SZAIBalanceAfter = IERC20(address(SZAI)).balanceOf(address(this)); // After Zap + console.log("After Zap we get SZAI", SZAIBalanceAfter); + require(SZAIBalanceAfter > SZAIBalanceBefore, "Zap Failed"); + + uint256 sZAIMinted = SZAIBalanceAfter - SZAIBalanceBefore; + + console.log("SZAI Minted : ", sZAIMinted); + // 5. Calculate the number of shares to mint for the receiver based on the assets + uint256 sharesToMint = previewDeposit(sZAIMinted); + + console.log("Shares to Mint", sharesToMint); + + // 6. Mint the calculated number of shares to the receiver + _mint(receiver, sharesToMint); + + return sharesToMint; + } + + /** + * @dev Withdraws assets (SZAI) from the vault by burning the user's shares. + * @param assets The amount of assets (SZAI) to withdraw. + * @param receiver The address to receive the withdrawn assets. + * @param owner The owner of the shares to burn. + * @return shares The amount of shares burned. + */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) { + // 1. Calculate the number of shares the user needs to burn to withdraw the requested assets (SZAI) + shares = previewWithdraw(assets); + console.log("Shares to Burn", shares); + + // 3. Check if the vault has enough SZAI for the withdrawal + uint256 vaultSZAI = IERC20(address(SZAI)).balanceOf(address(this)); + console.log("SZAI Balance in Vault: ", vaultSZAI); + require(vaultSZAI >= assets, "Insufficient SZAI in vault for withdrawal"); + + // 4. Transfer the SZAI to the receiver + IERC20(address(SZAI)).safeTransfer(receiver, shares); + + // 5. Burn the shares from the owner's balance + _burn(owner, assets); + + return shares; // Return the number of shares burned + } + /** + * @dev Converts a given amount of assets into the equivalent number of shares. + * @param assets The amount of assets to convert. + * @return shares The equivalent number of shares. + */ + + function previewDeposit( + uint256 assets + ) public view returns (uint256 shares) { + return _convertSZAIToUSDC(assets); + } + + function previewWithdraw( + uint256 assets + ) public view returns (uint256 shares) { + return _convertUSDCToSZAI(assets); + } + + /** + * @dev Returns the address of the asset held by the vault (USDC in this case). + * @return The address of the asset. + */ + function asset() external view returns (address) { + return address(USDC); + } + + function _convertSZAIToUSDC( + uint256 assets + ) internal view returns (uint256) { + int128 indexTokenIn = 1; // USDC + int128 indexTokenOut = 0; // USDe + uint256 SzaiBalance = IERC20(address(SZAI)).balanceOf(address(this)); + console.log("SZAI Balance in contract", SzaiBalance); + // convert SZAI to ZAI First + uint256 ZaiBalance = SZAI.previewWithdraw(SzaiBalance); + console.log("ZAI Assets", ZaiBalance); + // Convert ZAI to SUSDe + uint256 sUsdeBalance = PSM.toCollateralAmount(ZaiBalance); + console.log("SUSDE Balance", sUsdeBalance); + // Convert SUSDE to USDe + uint256 usdeBalance = SUSDE.previewWithdraw(sUsdeBalance); + console.log("USDE Balance", usdeBalance); + // Convert USDE to USDC + uint256 usdcBalance = stableSwap.get_dx(indexTokenIn, indexTokenOut, usdeBalance); + return usdcBalance; + } + + function _convertUSDCToSZAI( + uint256 shares + ) internal view returns (uint256) { + int128 indexTokenIn = 1; // USDC + int128 indexTokenOut = 0; // USDe + // USDC to USDe + uint256 usdeAfterSwap = stableSwap.get_dy(indexTokenIn, indexTokenOut, shares); + console.log("USDE Swap Balance", usdeAfterSwap); + // USDe to SUSDe + uint256 susdeAfterDeposit = SUSDE.previewDeposit(usdeAfterSwap); + console.log("SUSDE shares after deposit", susdeAfterDeposit); + // Call PSM to get the ZAI Mint Amount + uint256 zaiToMint = PSM.mintAmountIn(susdeAfterDeposit); + console.log("ZAI to mint", zaiToMint); + // Call preview deposit to get SZAI + uint256 sZaiAfterDeposit = SZAI.previewDeposit(zaiToMint); + console.log("SZAI After Deposit", sZaiAfterDeposit); + return sZaiAfterDeposit; + } + + /** + * @dev Swaps USDC for USDe using the StableSwap contract. + * @param _amount The amount of USDC to swap. + * @return The amount of USDe obtained from the swap. + */ + function _swapUSDCtoUSDe( + uint256 _amount + ) internal returns (uint256) { + int128 indexTokenIn = 1; // USDC + int128 indexTokenOut = 0; // USDe + // Approve Curve Pool to use USDC + USDC.approve(address(stableSwap), _amount); + uint256 beforeBalanceUSDE = USDE.balanceOf(address(this)); // Before Balance + StableSwap(stableSwap).exchange(indexTokenIn, indexTokenOut, _amount, 1); + uint256 afterBalanceUSDE = USDE.balanceOf(address(this)); // After Balance + require(afterBalanceUSDE > beforeBalanceUSDE, "Swap Failed"); + USDE.approve(address(SUSDE), afterBalanceUSDE); + return afterBalanceUSDE; + } +} diff --git a/test/foundry/USDCVaultFork.sol b/test/foundry/USDCVaultFork.sol new file mode 100644 index 0000000..7348a53 --- /dev/null +++ b/test/foundry/USDCVaultFork.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.21; + +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +import {IPegStabilityModule} from "contracts/interfaces/core/IPegStabilityModule.sol"; +import {IERC4626} from "contracts/interfaces/vault/IERC4626.sol"; + +import {ZapSafetyPool} from "contracts/periphery/zaps/implementations/ethereum/ZapSafetyPool.sol"; +import {USDCVault} from "contracts/vault/USDCVault.sol"; +import {Test, console} from "lib/forge-std/src/Test.sol"; + +interface StableSwap { + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external; +} + +contract USDCVaultFork is Test { + USDCVault public vault; + IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 public constant USDE = IERC20(0x4c9EDD5852cd905f086C759E8383e09bff1E68B3); + IERC4626 public constant SUSDE = IERC4626(0x9D39A5DE30e57443BfF2A8307A4256c8797A3497); + IERC4626 public constant SZAI = IERC4626(0x69000195D5e3201Cf73C9Ae4a1559244DF38D47C); + IPegStabilityModule public constant PSM = IPegStabilityModule(0x7DCdE153e4cACe9Ca852590d9654c7694388Db54); + ZapSafetyPool public constant ZAP = ZapSafetyPool(0x7e8503b58f7B734431569A0D3c2Db77c1dbae6e8); + StableSwap public constant POOL = StableSwap(0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72); + string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); + uint256 public mainnetFork; + + function setUp() external { + // Create Mainnet Fork here + mainnetFork = vm.createFork(MAINNET_RPC_URL); + vm.selectFork(mainnetFork); + vault = new USDCVault(); + vault.initialize( + "USDC-SZAI Vault", + "USDC-SZAI", + address(USDC), + address(USDE), + address(SZAI), + address(SUSDE), + address(PSM), + address(ZAP), + address(POOL) + ); + } + + function testInitValuesVault() external view { + address vaultAddress = vault.asset(); + assertEq(vaultAddress, address(USDC)); + } + + function testVaultDeposit() external { + // Define the addresses for the whale and the recipient + address USDCWHALE = 0x412Dd3F282b1FA20d3232d86aE060dEC644249f6; + address bob = makeAddr("1"); + uint256 depositAmount = 100_000_000; // USDC to deposit + // Start the prank for the whale (USDCWHALE) + vm.startPrank(USDCWHALE); + // Approve Vault to transfer USDC + USDC.approve(address(vault), depositAmount); + // Perform the deposit of 100 million USDC into the vault for 'bob' + vault.deposit(depositAmount, bob); + vm.stopPrank(); + } + + function testWithdrawVault() external { + // Define the addresses for the whale and the recipient + address USDCWHALE = 0x412Dd3F282b1FA20d3232d86aE060dEC644249f6; + address bob = makeAddr("1"); + address alice = makeAddr("2"); + uint256 depositAmount = 100_000_000; // USDC to deposit + uint256 withdrawAmount = 50_000_000; + // Start the prank for the whale (USDCWHALE) + vm.startPrank(USDCWHALE); + // Approve Vault to transfer USDC + USDC.approve(address(vault), depositAmount); + // Perform the deposit of 100 million USDC into the vault for 'bob' + vault.deposit(depositAmount, bob); + vault.withdraw(withdrawAmount, alice, bob); + vm.stopPrank(); + } +}