// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./interfaces/GPv2Order.sol"; import "./interfaces/IConditionalOrder.sol"; import "./BondingCurveAdapter.sol"; /** * @title MycoConditionalOrder * @notice ComposableCoW handler for $MYCO bonding curve trades. * * Implements IConditionalOrder.getTradeableOrder() to generate GPv2Order.Data * from static input (order parameters) and offchain input (live quotes from * the cow-service Watch Tower integration). * * Stateless — all order parameters are encoded in staticInput. * The Watch Tower polls this handler to discover executable orders. */ contract MycoConditionalOrder is IConditionalOrder { // =================== // Types // =================== enum TradeDirection { BUY, // USDC → MYCO SELL // MYCO → USDC } /// @dev Static input encoded per-order. Set once when creating the conditional order. struct OrderData { TradeDirection direction; uint256 inputAmount; // USDC amount (buy) or MYCO amount (sell) uint256 minOutputAmount; // Minimum output (slippage floor) address receiver; // Recipient of output tokens uint256 validityDuration; // Seconds from now the order remains valid } /// @dev Offchain input provided by the Watch Tower at poll time. struct OffchainData { uint256 quotedOutput; // Live quote from adapter.quoteBuy/quoteSell uint32 validTo; // Timestamp when this quote expires } // =================== // Immutables // =================== BondingCurveAdapter public immutable adapter; address public immutable usdcToken; address public immutable mycoTokenAddr; /// @dev App data hash identifying orders from this handler. bytes32 public constant APP_DATA = keccak256("myco-bonding-curve-v1"); // =================== // Constructor // =================== constructor( address _adapter, address _usdcToken, address _mycoToken ) { adapter = BondingCurveAdapter(_adapter); usdcToken = _usdcToken; mycoTokenAddr = _mycoToken; } // =================== // IConditionalOrder // =================== /// @inheritdoc IConditionalOrder function getTradeableOrder( address owner, address, bytes calldata staticInput, bytes calldata offchainInput ) external view override returns (GPv2Order.Data memory order) { OrderData memory params = abi.decode(staticInput, (OrderData)); OffchainData memory quote = abi.decode(offchainInput, (OffchainData)); // Validate quote hasn't expired if (block.timestamp > quote.validTo) { revert OrderNotValid("Quote expired"); } // Validate quoted output meets minimum if (quote.quotedOutput < params.minOutputAmount) { revert OrderNotValid("Quote below minimum output"); } // Determine receiver (default to owner if not set) address receiver = params.receiver == address(0) ? owner : params.receiver; if (params.direction == TradeDirection.BUY) { // Buy: sell USDC, buy MYCO uint256 balance = IERC20(usdcToken).balanceOf(owner); if (balance < params.inputAmount) { revert PollTryNextBlock("Insufficient USDC balance"); } order = GPv2Order.Data({ sellToken: usdcToken, buyToken: mycoTokenAddr, receiver: receiver, sellAmount: params.inputAmount, buyAmount: quote.quotedOutput, validTo: quote.validTo, appData: APP_DATA, feeAmount: 0, kind: GPv2Order.KIND_SELL, partiallyFillable: false, sellTokenBalance: GPv2Order.BALANCE_ERC20, buyTokenBalance: GPv2Order.BALANCE_ERC20 }); } else { // Sell: sell MYCO, buy USDC uint256 balance = IERC20(mycoTokenAddr).balanceOf(owner); if (balance < params.inputAmount) { revert PollTryNextBlock("Insufficient MYCO balance"); } order = GPv2Order.Data({ sellToken: mycoTokenAddr, buyToken: usdcToken, receiver: receiver, sellAmount: params.inputAmount, buyAmount: quote.quotedOutput, validTo: quote.validTo, appData: APP_DATA, feeAmount: 0, kind: GPv2Order.KIND_SELL, partiallyFillable: false, sellTokenBalance: GPv2Order.BALANCE_ERC20, buyTokenBalance: GPv2Order.BALANCE_ERC20 }); } } /// @inheritdoc IConditionalOrder function verify( address owner, address sender, bytes32, bytes32, bytes calldata staticInput, bytes calldata offchainInput, GPv2Order.Data calldata ) external view override { this.getTradeableOrder(owner, sender, staticInput, offchainInput); } }