// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import "../../contracts/dex/DODOPMMIntegration.sol"; import "../../contracts/liquidity/providers/DODOPMMProvider.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockProviderERC20 is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) { _mint(msg.sender, 1_000_000 ether); } } contract MockQuotePool { address public immutable baseToken; address public immutable quoteToken; uint256 public immutable midPrice; uint256 public immutable baseToQuoteOut; uint256 public immutable quoteToBaseOut; constructor( address baseToken_, address quoteToken_, uint256 midPrice_, uint256 baseToQuoteOut_, uint256 quoteToBaseOut_ ) { baseToken = baseToken_; quoteToken = quoteToken_; midPrice = midPrice_; baseToQuoteOut = baseToQuoteOut_; quoteToBaseOut = quoteToBaseOut_; } function _BASE_TOKEN_() external view returns (address) { return baseToken; } function _QUOTE_TOKEN_() external view returns (address) { return quoteToken; } function querySellBase(address, uint256) external view returns (uint256, uint256) { return (baseToQuoteOut, 0); } function querySellQuote(address, uint256) external view returns (uint256, uint256) { return (quoteToBaseOut, 0); } function sellBase(address) external pure returns (uint256) { return 0; } function sellQuote(address) external pure returns (uint256) { return 0; } function buyShares(address) external pure returns (uint256, uint256, uint256) { return (0, 0, 0); } function getVaultReserve() external pure returns (uint256, uint256) { return (1_000_000, 1_000_000); } function getMidPrice() external view returns (uint256) { return midPrice; } function _QUOTE_RESERVE_() external pure returns (uint256) { return 1_000_000; } function _BASE_RESERVE_() external pure returns (uint256) { return 1_000_000; } } contract MockFallbackQuotePool { address public immutable baseToken; address public immutable quoteToken; uint256 public immutable baseReserve; uint256 public immutable quoteReserve; constructor( address baseToken_, address quoteToken_, uint256 baseReserve_, uint256 quoteReserve_ ) { baseToken = baseToken_; quoteToken = quoteToken_; baseReserve = baseReserve_; quoteReserve = quoteReserve_; } function _BASE_TOKEN_() external view returns (address) { return baseToken; } function _QUOTE_TOKEN_() external view returns (address) { return quoteToken; } function querySellBase(address, uint256) external pure returns (uint256, uint256) { revert("base quote disabled"); } function querySellQuote(address, uint256) external pure returns (uint256, uint256) { revert("quote quote disabled"); } function sellBase(address) external pure returns (uint256) { return 0; } function sellQuote(address) external pure returns (uint256) { return 0; } function buyShares(address) external pure returns (uint256, uint256, uint256) { return (0, 0, 0); } function getVaultReserve() external view returns (uint256, uint256) { return (baseReserve, quoteReserve); } function getMidPrice() external pure returns (uint256) { return 1e18; } function _QUOTE_RESERVE_() external view returns (uint256) { return quoteReserve; } function _BASE_RESERVE_() external view returns (uint256) { return baseReserve; } } contract DODOPMMProviderTest is Test { DODOPMMIntegration internal integration; DODOPMMProvider internal provider; MockProviderERC20 internal officialUSDT; MockProviderERC20 internal officialUSDC; MockProviderERC20 internal compliantUSDT; MockProviderERC20 internal compliantUSDC; address internal constant ADMIN = address(0xAD); address internal constant DVM = address(0xD0D0); address internal constant DODO_APPROVE = address(0xA11CE); function setUp() public { officialUSDT = new MockProviderERC20("USDT", "USDT"); officialUSDC = new MockProviderERC20("USDC", "USDC"); compliantUSDT = new MockProviderERC20("cUSDT", "cUSDT"); compliantUSDC = new MockProviderERC20("cUSDC", "cUSDC"); integration = new DODOPMMIntegration( ADMIN, DVM, DODO_APPROVE, address(officialUSDT), address(officialUSDC), address(compliantUSDT), address(compliantUSDC) ); provider = new DODOPMMProvider(address(integration), ADMIN); } function testGetQuoteUsesPoolQueryForBaseSell() public { MockQuotePool pool = new MockQuotePool( address(compliantUSDT), address(compliantUSDC), 2e18, 990_000, 995_000 ); vm.prank(ADMIN); integration.importExistingPool(address(pool), address(compliantUSDT), address(compliantUSDC), 3, 1e18, 0.5e18, false); vm.startPrank(ADMIN); provider.registerPool(address(compliantUSDT), address(compliantUSDC), address(pool)); provider.registerPool(address(compliantUSDC), address(compliantUSDT), address(pool)); vm.stopPrank(); (uint256 amountOut, uint256 slippageBps) = provider.getQuote( address(compliantUSDT), address(compliantUSDC), 1_000_000 ); assertEq(amountOut, 990_000); assertEq(slippageBps, 30); } function testGetQuoteUsesPoolQueryForQuoteSell() public { MockQuotePool pool = new MockQuotePool( address(compliantUSDT), address(compliantUSDC), 2e18, 990_000, 995_000 ); vm.prank(ADMIN); integration.importExistingPool(address(pool), address(compliantUSDT), address(compliantUSDC), 3, 1e18, 0.5e18, false); vm.startPrank(ADMIN); provider.registerPool(address(compliantUSDT), address(compliantUSDC), address(pool)); provider.registerPool(address(compliantUSDC), address(compliantUSDT), address(pool)); vm.stopPrank(); (uint256 amountOut, uint256 slippageBps) = provider.getQuote( address(compliantUSDC), address(compliantUSDT), 1_000_000 ); assertEq(amountOut, 995_000); assertEq(slippageBps, 30); } function testGetQuoteReturnsZeroForUnsupportedPair() public view { (uint256 amountOut, uint256 slippageBps) = provider.getQuote( address(compliantUSDT), address(officialUSDC), 1_000_000 ); assertEq(amountOut, 0); assertEq(slippageBps, 10000); } function testGetQuoteFallsBackToReservesForBaseSell() public { uint256 amountIn = 100_000; uint256 lpFeeRate = 30; MockFallbackQuotePool pool = new MockFallbackQuotePool( address(compliantUSDT), address(compliantUSDC), 1_000_000, 2_000_000 ); vm.prank(ADMIN); integration.importExistingPool(address(pool), address(compliantUSDT), address(compliantUSDC), lpFeeRate, 1e18, 0.5e18, false); vm.startPrank(ADMIN); provider.registerPool(address(compliantUSDT), address(compliantUSDC), address(pool)); provider.registerPool(address(compliantUSDC), address(compliantUSDT), address(pool)); vm.stopPrank(); uint256 netAmountIn = (amountIn * (10_000 - lpFeeRate)) / 10_000; uint256 expectedOut = (netAmountIn * 2_000_000) / (1_000_000 + netAmountIn); (uint256 amountOut, uint256 slippageBps) = provider.getQuote( address(compliantUSDT), address(compliantUSDC), amountIn ); assertEq(amountOut, expectedOut); assertEq(slippageBps, 100); } function testGetQuoteFallsBackToReservesForQuoteSell() public { uint256 amountIn = 100_000; uint256 lpFeeRate = 25; MockFallbackQuotePool pool = new MockFallbackQuotePool( address(compliantUSDT), address(compliantUSDC), 2_500_000, 1_500_000 ); vm.prank(ADMIN); integration.importExistingPool(address(pool), address(compliantUSDT), address(compliantUSDC), lpFeeRate, 1e18, 0.5e18, false); vm.startPrank(ADMIN); provider.registerPool(address(compliantUSDT), address(compliantUSDC), address(pool)); provider.registerPool(address(compliantUSDC), address(compliantUSDT), address(pool)); vm.stopPrank(); uint256 netAmountIn = (amountIn * (10_000 - lpFeeRate)) / 10_000; uint256 expectedOut = (netAmountIn * 2_500_000) / (1_500_000 + netAmountIn); (uint256 amountOut, uint256 slippageBps) = provider.getQuote( address(compliantUSDC), address(compliantUSDT), amountIn ); assertEq(amountOut, expectedOut); assertEq(slippageBps, 100); } }