208 lines
6.9 KiB
Solidity
208 lines
6.9 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.24;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
import "../MycoBondingCurve.sol";
|
|
import "./interfaces/ISettlement.sol";
|
|
|
|
/**
|
|
* @title BondingCurveAdapter
|
|
* @notice Bridge between CoW Protocol settlement and MycoBondingCurve.
|
|
*
|
|
* Called as a pre-interaction hook by solvers during batch settlement.
|
|
* The adapter receives tokens from settlement, executes the curve operation,
|
|
* and holds output tokens for settlement accounting.
|
|
*
|
|
* Token flow (buy):
|
|
* User USDC → VaultRelayer → Adapter.executeBuyForCoW() → BondingCurve.buy()
|
|
* → MYCO minted to Adapter → Settlement → User
|
|
*
|
|
* Token flow (sell):
|
|
* User MYCO → VaultRelayer → Adapter.executeSellForCoW() → BondingCurve.sell()
|
|
* → USDC to Adapter → Settlement → User
|
|
*/
|
|
contract BondingCurveAdapter is ReentrancyGuard, Ownable {
|
|
using SafeERC20 for IERC20;
|
|
|
|
// ===================
|
|
// State
|
|
// ===================
|
|
|
|
MycoBondingCurve public immutable bondingCurve;
|
|
IERC20 public immutable usdc;
|
|
IERC20 public immutable mycoToken;
|
|
address public immutable settlement;
|
|
address public immutable vaultRelayer;
|
|
|
|
// ===================
|
|
// Events
|
|
// ===================
|
|
|
|
event CowBuyExecuted(uint256 usdcIn, uint256 mycoOut);
|
|
event CowSellExecuted(uint256 mycoIn, uint256 usdcOut);
|
|
event TokensRecovered(address token, uint256 amount, address to);
|
|
|
|
// ===================
|
|
// Errors
|
|
// ===================
|
|
|
|
error OnlySettlement();
|
|
error ZeroAmount();
|
|
error SlippageExceeded(uint256 got, uint256 minExpected);
|
|
error InsufficientBalance(uint256 available, uint256 required);
|
|
|
|
// ===================
|
|
// Modifiers
|
|
// ===================
|
|
|
|
modifier onlySettlement() {
|
|
if (msg.sender != settlement) revert OnlySettlement();
|
|
_;
|
|
}
|
|
|
|
// ===================
|
|
// Constructor
|
|
// ===================
|
|
|
|
/**
|
|
* @param _bondingCurve Address of MycoBondingCurve
|
|
* @param _settlement Address of GPv2Settlement on Base
|
|
* @param _usdc Address of USDC
|
|
* @param _mycoToken Address of MYCO token
|
|
*/
|
|
constructor(
|
|
address _bondingCurve,
|
|
address _settlement,
|
|
address _usdc,
|
|
address _mycoToken
|
|
) Ownable(msg.sender) {
|
|
bondingCurve = MycoBondingCurve(_bondingCurve);
|
|
settlement = _settlement;
|
|
usdc = IERC20(_usdc);
|
|
mycoToken = IERC20(_mycoToken);
|
|
vaultRelayer = ISettlement(_settlement).vaultRelayer();
|
|
|
|
// Pre-approve bonding curve to spend our USDC (for buy operations)
|
|
IERC20(_usdc).approve(_bondingCurve, type(uint256).max);
|
|
// Pre-approve bonding curve to burn our MYCO (for sell operations)
|
|
IERC20(_mycoToken).approve(_bondingCurve, type(uint256).max);
|
|
// Pre-approve vault relayer to pull output tokens for settlement
|
|
IERC20(_usdc).approve(vaultRelayer, type(uint256).max);
|
|
IERC20(_mycoToken).approve(vaultRelayer, type(uint256).max);
|
|
}
|
|
|
|
// ===================
|
|
// Execute Functions (called by solvers as pre-interaction)
|
|
// ===================
|
|
|
|
/**
|
|
* @notice Execute a buy on the bonding curve for CoW settlement.
|
|
* @dev Called by GPv2Settlement as a pre-interaction hook.
|
|
* USDC must already be in this contract (transferred by settlement).
|
|
* @param usdcAmount Amount of USDC to spend
|
|
* @param minMycoOut Minimum MYCO tokens to receive (slippage protection)
|
|
* @return mycoOut Amount of MYCO minted
|
|
*/
|
|
function executeBuyForCoW(
|
|
uint256 usdcAmount,
|
|
uint256 minMycoOut
|
|
) external onlySettlement nonReentrant returns (uint256 mycoOut) {
|
|
if (usdcAmount == 0) revert ZeroAmount();
|
|
|
|
uint256 usdcBalance = usdc.balanceOf(address(this));
|
|
if (usdcBalance < usdcAmount) {
|
|
revert InsufficientBalance(usdcBalance, usdcAmount);
|
|
}
|
|
|
|
// Execute buy on bonding curve — mints MYCO to this adapter
|
|
mycoOut = bondingCurve.buy(usdcAmount, minMycoOut);
|
|
|
|
if (mycoOut < minMycoOut) {
|
|
revert SlippageExceeded(mycoOut, minMycoOut);
|
|
}
|
|
|
|
emit CowBuyExecuted(usdcAmount, mycoOut);
|
|
}
|
|
|
|
/**
|
|
* @notice Execute a sell on the bonding curve for CoW settlement.
|
|
* @dev Called by GPv2Settlement as a pre-interaction hook.
|
|
* MYCO must already be in this contract (transferred by settlement).
|
|
* @param mycoAmount Amount of MYCO to sell
|
|
* @param minUsdcOut Minimum USDC to receive (slippage protection)
|
|
* @return usdcOut Amount of USDC received
|
|
*/
|
|
function executeSellForCoW(
|
|
uint256 mycoAmount,
|
|
uint256 minUsdcOut
|
|
) external onlySettlement nonReentrant returns (uint256 usdcOut) {
|
|
if (mycoAmount == 0) revert ZeroAmount();
|
|
|
|
uint256 mycoBalance = mycoToken.balanceOf(address(this));
|
|
if (mycoBalance < mycoAmount) {
|
|
revert InsufficientBalance(mycoBalance, mycoAmount);
|
|
}
|
|
|
|
// Execute sell on bonding curve — returns USDC to this adapter
|
|
usdcOut = bondingCurve.sell(mycoAmount, minUsdcOut);
|
|
|
|
if (usdcOut < minUsdcOut) {
|
|
revert SlippageExceeded(usdcOut, minUsdcOut);
|
|
}
|
|
|
|
emit CowSellExecuted(mycoAmount, usdcOut);
|
|
}
|
|
|
|
// ===================
|
|
// View Functions (used by Watch Tower for quotes)
|
|
// ===================
|
|
|
|
/**
|
|
* @notice Quote a buy: how much MYCO for a given USDC amount
|
|
* @param usdcAmount Amount of USDC to spend
|
|
* @return mycoOut Expected MYCO output
|
|
*/
|
|
function quoteBuy(uint256 usdcAmount) external view returns (uint256 mycoOut) {
|
|
return bondingCurve.calculateBuyReturn(usdcAmount);
|
|
}
|
|
|
|
/**
|
|
* @notice Quote a sell: how much USDC for a given MYCO amount
|
|
* @param mycoAmount Amount of MYCO to sell
|
|
* @return usdcOut Expected USDC output (after fees)
|
|
*/
|
|
function quoteSell(uint256 mycoAmount) external view returns (uint256 usdcOut) {
|
|
return bondingCurve.calculateSellReturn(mycoAmount);
|
|
}
|
|
|
|
/**
|
|
* @notice Get the current token price from the bonding curve
|
|
* @return price Current price in USDC (6 decimals)
|
|
*/
|
|
function currentPrice() external view returns (uint256 price) {
|
|
return bondingCurve.getCurrentPrice();
|
|
}
|
|
|
|
// ===================
|
|
// Admin (emergency recovery)
|
|
// ===================
|
|
|
|
/**
|
|
* @notice Recover tokens stuck in the adapter (emergency only)
|
|
* @param token Token to recover
|
|
* @param amount Amount to recover
|
|
* @param to Recipient
|
|
*/
|
|
function recoverTokens(
|
|
address token,
|
|
uint256 amount,
|
|
address to
|
|
) external onlyOwner {
|
|
IERC20(token).safeTransfer(to, amount);
|
|
emit TokensRecovered(token, amount, to);
|
|
}
|
|
}
|