// 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 {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; interface IEngineXDodoPoolLike { function _BASE_TOKEN_() external view returns (address); function _QUOTE_TOKEN_() external view returns (address); function querySellBase(address trader, uint256 payBaseAmount) external view returns (uint256 receiveQuoteAmount, uint256 mtFee); function querySellQuote(address trader, uint256 payQuoteAmount) external view returns (uint256 receiveBaseAmount, uint256 mtFee); function getVaultReserve() external view returns (uint256 baseReserve, uint256 quoteReserve); } interface IEngineXDodoIntegrationLike { function addLiquidity(address pool, uint256 baseAmount, uint256 quoteAmount) external returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare); } /// @notice Engine X single-sided cWUSDC inventory wrapper for later DODO PMM promotion. /// @dev cWUSDC-only inventory is accounting/support inventory, not executable public DODO liquidity. /// DODO promotion is allowed only with nonzero base and quote amounts and passing canary guards. contract DBISEngineXSingleSidedDodoCwusdcVault is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; IERC20 public immutable cWUSDC; IERC20 public immutable quoteToken; IEngineXDodoIntegrationLike public immutable dodoIntegration; address public dodoPool; bool public paused; uint256 public accountedCwusdcInventory; uint256 public accountedQuoteInventory; uint256 public totalCwusdcPromotedToDodo; uint256 public totalQuotePromotedToDodo; uint256 public sampleBaseIn; uint256 public minQuoteOut; uint256 public sampleQuoteIn; uint256 public minBaseOut; event Paused(address indexed operator); event Unpaused(address indexed operator); event DodoPoolUpdated(address indexed pool); event CanaryUpdated(uint256 sampleBaseIn, uint256 minQuoteOut, uint256 sampleQuoteIn, uint256 minBaseOut); event CwusdcInventoryDeposited(address indexed from, uint256 amount); event QuoteInventoryDeposited(address indexed from, uint256 amount); event InventoryWithdrawn(address indexed token, address indexed to, uint256 amount); event DodoLiquidityPromoted( address indexed pool, uint256 baseAmount, uint256 quoteAmount, uint256 baseShare, uint256 quoteShare, uint256 lpShare ); event UnaccountedTokenWithdrawn(address indexed token, address indexed to, uint256 amount); modifier whenNotPaused() { require(!paused, "paused"); _; } constructor(address cWUSDC_, address quoteToken_, address dodoIntegration_, address owner_) Ownable(owner_) { require(cWUSDC_ != address(0) && quoteToken_ != address(0), "zero token"); require(dodoIntegration_ != address(0), "zero integration"); require(owner_ != address(0), "zero owner"); require(cWUSDC_ != quoteToken_, "same token"); cWUSDC = IERC20(cWUSDC_); quoteToken = IERC20(quoteToken_); dodoIntegration = IEngineXDodoIntegrationLike(dodoIntegration_); } function pause() external onlyOwner { paused = true; emit Paused(msg.sender); } function unpause() external onlyOwner { paused = false; emit Unpaused(msg.sender); } function setDodoPool(address pool) external onlyOwner { _validatePool(pool); dodoPool = pool; emit DodoPoolUpdated(pool); } function setCanary(uint256 sampleBaseIn_, uint256 minQuoteOut_, uint256 sampleQuoteIn_, uint256 minBaseOut_) external onlyOwner { require(sampleBaseIn_ > 0 || sampleQuoteIn_ > 0, "zero canary"); require((sampleBaseIn_ == 0) == (minQuoteOut_ == 0), "base canary mismatch"); require((sampleQuoteIn_ == 0) == (minBaseOut_ == 0), "quote canary mismatch"); sampleBaseIn = sampleBaseIn_; minQuoteOut = minQuoteOut_; sampleQuoteIn = sampleQuoteIn_; minBaseOut = minBaseOut_; emit CanaryUpdated(sampleBaseIn_, minQuoteOut_, sampleQuoteIn_, minBaseOut_); } function depositCwusdc(uint256 amount) external nonReentrant whenNotPaused { require(amount > 0, "zero deposit"); cWUSDC.safeTransferFrom(msg.sender, address(this), amount); accountedCwusdcInventory += amount; emit CwusdcInventoryDeposited(msg.sender, amount); } function depositQuote(uint256 amount) external nonReentrant whenNotPaused { require(amount > 0, "zero deposit"); quoteToken.safeTransferFrom(msg.sender, address(this), amount); accountedQuoteInventory += amount; emit QuoteInventoryDeposited(msg.sender, amount); } function withdrawCwusdcInventory(address to, uint256 amount) external onlyOwner nonReentrant { require(to != address(0), "zero to"); require(amount > 0, "zero withdraw"); require(amount <= accountedCwusdcInventory, "insufficient cwusdc inventory"); accountedCwusdcInventory -= amount; cWUSDC.safeTransfer(to, amount); _requireSolvent(); emit InventoryWithdrawn(address(cWUSDC), to, amount); } function withdrawQuoteInventory(address to, uint256 amount) external onlyOwner nonReentrant { require(to != address(0), "zero to"); require(amount > 0, "zero withdraw"); require(amount <= accountedQuoteInventory, "insufficient quote inventory"); accountedQuoteInventory -= amount; quoteToken.safeTransfer(to, amount); _requireSolvent(); emit InventoryWithdrawn(address(quoteToken), to, amount); } function promoteToDodo( uint256 baseAmount, uint256 quoteAmount, uint256 minBaseShare, uint256 minQuoteShare, uint256 minLpShare ) external onlyOwner nonReentrant whenNotPaused returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare) { require(dodoPool != address(0), "pool not set"); require(baseAmount > 0 && quoteAmount > 0, "two-sided required"); require(baseAmount <= accountedCwusdcInventory, "insufficient cwusdc inventory"); require(quoteAmount <= accountedQuoteInventory, "insufficient quote inventory"); accountedCwusdcInventory -= baseAmount; accountedQuoteInventory -= quoteAmount; cWUSDC.forceApprove(address(dodoIntegration), baseAmount); quoteToken.forceApprove(address(dodoIntegration), quoteAmount); (baseShare, quoteShare, lpShare) = dodoIntegration.addLiquidity(dodoPool, baseAmount, quoteAmount); cWUSDC.forceApprove(address(dodoIntegration), 0); quoteToken.forceApprove(address(dodoIntegration), 0); require(baseShare >= minBaseShare, "base share too low"); require(quoteShare >= minQuoteShare, "quote share too low"); require(lpShare >= minLpShare, "lp share too low"); require(canaryPasses(), "canary failed"); totalCwusdcPromotedToDodo += baseAmount; totalQuotePromotedToDodo += quoteAmount; _requireSolvent(); emit DodoLiquidityPromoted(dodoPool, baseAmount, quoteAmount, baseShare, quoteShare, lpShare); } function canaryPasses() public view returns (bool) { if (dodoPool == address(0)) return false; IEngineXDodoPoolLike pool = IEngineXDodoPoolLike(dodoPool); (uint256 baseReserve, uint256 quoteReserve) = pool.getVaultReserve(); if (baseReserve == 0 || quoteReserve == 0) return false; if (sampleBaseIn > 0) { (uint256 quoteOut,) = pool.querySellBase(address(this), sampleBaseIn); if (quoteOut < minQuoteOut) return false; } if (sampleQuoteIn > 0) { (uint256 baseOut,) = pool.querySellQuote(address(this), sampleQuoteIn); if (baseOut < minBaseOut) return false; } return true; } function solvencyState() external view returns ( uint256 cwusdcBalance, uint256 quoteBalance, uint256 cwusdcInventory, uint256 quoteInventory, bool solvent, bool executable ) { cwusdcBalance = cWUSDC.balanceOf(address(this)); quoteBalance = quoteToken.balanceOf(address(this)); cwusdcInventory = accountedCwusdcInventory; quoteInventory = accountedQuoteInventory; solvent = cwusdcBalance >= cwusdcInventory && quoteBalance >= quoteInventory; executable = canaryPasses(); } function rescueUnaccountedToken(address token, address to, uint256 amount) external onlyOwner nonReentrant { require(to != address(0), "zero to"); require(amount > 0, "zero withdraw"); IERC20(token).safeTransfer(to, amount); _requireSolvent(); emit UnaccountedTokenWithdrawn(token, to, amount); } function _validatePool(address pool) internal view { require(pool != address(0), "zero pool"); require(IEngineXDodoPoolLike(pool)._BASE_TOKEN_() == address(cWUSDC), "unexpected base"); require(IEngineXDodoPoolLike(pool)._QUOTE_TOKEN_() == address(quoteToken), "unexpected quote"); } function _requireSolvent() internal view { require(cWUSDC.balanceOf(address(this)) >= accountedCwusdcInventory, "cwusdc insolvent"); require(quoteToken.balanceOf(address(this)) >= accountedQuoteInventory, "quote insolvent"); } }