myco-bonding-curve/reference/MycoConditionalOrder.sol

155 lines
5.1 KiB
Solidity

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