- Expand token-aggregation API (report routes), canonical tokens, pools - Add flash vault contracts + tests (indexed, DODO cwUSDC, XAUT borrow) - PMM pools JSON, deploy/export scripts, metamask verified list Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
6.5 KiB
Solidity
180 lines
6.5 KiB
Solidity
// 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");
|
|
}
|
|
}
|