- 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>
235 lines
9.6 KiB
Solidity
235 lines
9.6 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 {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");
|
|
}
|
|
}
|