155 lines
5.1 KiB
Solidity
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);
|
|
}
|
|
}
|