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