myco-bonding-curve/reference/BondingCurveAdapter.sol

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);
}
}