// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Test} from "forge-std/Test.sol"; import {DBISEngineXSingleSidedDodoCwusdcVault} from "../../contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol"; import {MockMintableToken} from "../dbis/MockMintableToken.sol"; contract MockDodoPool { using SafeERC20 for IERC20; address public immutable base; address public immutable quote; uint256 public baseReserve; uint256 public quoteReserve; constructor(address base_, address quote_) { base = base_; quote = quote_; } function _BASE_TOKEN_() external view returns (address) { return base; } function _QUOTE_TOKEN_() external view returns (address) { return quote; } function querySellBase(address, uint256 payBaseAmount) external pure returns (uint256 receiveQuoteAmount, uint256 mtFee) { return (payBaseAmount, 0); } function querySellQuote(address, uint256 payQuoteAmount) external pure returns (uint256 receiveBaseAmount, uint256 mtFee) { return (payQuoteAmount, 0); } function getVaultReserve() external view returns (uint256, uint256) { return (baseReserve, quoteReserve); } function buyShares(address) external returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare) { uint256 baseBalance = IERC20(base).balanceOf(address(this)); uint256 quoteBalance = IERC20(quote).balanceOf(address(this)); baseShare = baseBalance - baseReserve; quoteShare = quoteBalance - quoteReserve; baseReserve = baseBalance; quoteReserve = quoteBalance; lpShare = baseShare < quoteShare ? baseShare : quoteShare; } } contract MockDodoIntegration { using SafeERC20 for IERC20; function addLiquidity(address pool, uint256 baseAmount, uint256 quoteAmount) external returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare) { require(baseAmount > 0 && quoteAmount > 0, "zero amount"); address base = MockDodoPool(pool)._BASE_TOKEN_(); address quote = MockDodoPool(pool)._QUOTE_TOKEN_(); IERC20(base).safeTransferFrom(msg.sender, pool, baseAmount); IERC20(quote).safeTransferFrom(msg.sender, pool, quoteAmount); return MockDodoPool(pool).buyShares(msg.sender); } } contract DBISEngineXSingleSidedDodoCwusdcVaultTest is Test { MockMintableToken internal cwusdc; MockMintableToken internal weth; MockDodoIntegration internal integration; MockDodoPool internal pool; DBISEngineXSingleSidedDodoCwusdcVault internal vault; address internal constant FUNDER = address(0xF00D); address internal constant OWNER = address(0xA11CE); function setUp() public { cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this)); weth = new MockMintableToken("Wrapped Ether", "WETH", 18, address(this)); integration = new MockDodoIntegration(); pool = new MockDodoPool(address(cwusdc), address(weth)); vault = new DBISEngineXSingleSidedDodoCwusdcVault(address(cwusdc), address(weth), address(integration), OWNER); cwusdc.mint(FUNDER, 100_000_000); weth.mint(FUNDER, 1 ether); vm.startPrank(FUNDER); cwusdc.approve(address(vault), type(uint256).max); weth.approve(address(vault), type(uint256).max); vm.stopPrank(); } function testAcceptsSingleSidedCwusdcAsInventoryButNotExecutable() public { vm.prank(FUNDER); vault.depositCwusdc(10_000_000); ( uint256 cwusdcBalance, uint256 quoteBalance, uint256 cwusdcInventory, uint256 quoteInventory, bool solvent, bool executable ) = vault.solvencyState(); assertEq(cwusdcBalance, 10_000_000, "cw balance"); assertEq(quoteBalance, 0, "quote balance"); assertEq(cwusdcInventory, 10_000_000, "cw inventory"); assertEq(quoteInventory, 0, "quote inventory"); assertTrue(solvent, "single-sided inventory is solvent"); assertFalse(executable, "single-sided inventory is not executable DODO liquidity"); } function testPromoteRequiresTwoSidedInventory() public { vm.prank(OWNER); vault.setDodoPool(address(pool)); vm.prank(OWNER); vault.setCanary(1_000, 1_000, 1_000, 1_000); vm.prank(FUNDER); vault.depositCwusdc(10_000_000); vm.expectRevert(bytes("two-sided required")); vm.prank(OWNER); vault.promoteToDodo(1_000_000, 0, 0, 0, 0); vm.expectRevert(bytes("insufficient quote inventory")); vm.prank(OWNER); vault.promoteToDodo(1_000_000, 1, 0, 0, 0); } function testPromotesTwoSidedInventoryAndPassesCanary() public { vm.prank(OWNER); vault.setDodoPool(address(pool)); vm.prank(OWNER); vault.setCanary(1_000, 1_000, 1_000, 1_000); vm.startPrank(FUNDER); vault.depositCwusdc(10_000_000); vault.depositQuote(1 ether); vm.stopPrank(); vm.prank(OWNER); (uint256 baseShare, uint256 quoteShare, uint256 lpShare) = vault.promoteToDodo(2_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000); assertEq(baseShare, 2_000_000, "base share"); assertEq(quoteShare, 2_000_000, "quote share"); assertEq(lpShare, 2_000_000, "lp share"); assertEq(vault.accountedCwusdcInventory(), 8_000_000, "remaining cw inventory"); assertEq(vault.accountedQuoteInventory(), 1 ether - 2_000_000, "remaining quote inventory"); assertEq(cwusdc.balanceOf(address(pool)), 2_000_000, "pool cw balance"); assertEq(weth.balanceOf(address(pool)), 2_000_000, "pool quote balance"); assertTrue(vault.canaryPasses(), "canary passes"); } function testCannotRescueAccountedInventory() public { vm.prank(FUNDER); vault.depositCwusdc(10_000_000); vm.expectRevert(bytes("cwusdc insolvent")); vm.prank(OWNER); vault.rescueUnaccountedToken(address(cwusdc), OWNER, 1); } function testOwnerCanWithdrawAccountedInventory() public { vm.prank(FUNDER); vault.depositCwusdc(10_000_000); vm.prank(OWNER); vault.withdrawCwusdcInventory(OWNER, 4_000_000); assertEq(vault.accountedCwusdcInventory(), 6_000_000, "inventory decremented"); assertEq(cwusdc.balanceOf(OWNER), 4_000_000, "owner received"); } }