Contract API
OrderVault
Limit and stop orders that execute on SaucerSwap V2 with no off-chain keeper.
Each order is an HTS NFT minted by this vault; whoever holds it owns the order. Each market runs one self-rescheduling sweep through the Hedera Schedule Service, and the sweep's cost is split across the orders it checks. Scheduling a call is a fixed network fee, so the next sweep waits as long as the nearest trigger allows. A fill needs the pool's TWAP to agree with Chainlink, so a manipulated or stale market cannot drain an order.
contract OrderVault is Ownable2Step, ReentrancyGuard · packages/foundry/contracts/OrderVault.sol
Functions
fund
function fund() external payableEndow the vault with liquid HBAR to back the payer float, so its scheduled sweeps can always pay their gas at execution. Anyone may fund it; only the owner may withdraw the surplus above the float. Kept separate from receive, which only accepts swap proceeds from the router.
placeOrder
function placeOrder(PlaceParams calldata p) external payable nonReentrant returns (uint256 orderId)Escrow amountIn, mint the order NFT to the caller and make sure the market sweep is running.
msg.value is the check budget, plus amountIn when the input is HBAR. The caller needs auto-association slots or an association with collection.
cancel
function cancel(uint256 orderId) external nonReentrantCancel an open order; escrow and unused budget go back to the NFT holder.
topUp
function topUp(uint256 orderId) external payable nonReentrantAdd HBAR to an order's check budget. Anyone may top up any order.
executeOrder
function executeOrder(uint256 orderId) external nonReentrant returns (bool settled)Check one order now and fill or expire it if due. The caller pays gas; the budget is untouched.
claim
function claim(address token) external nonReentrantPull a payout that could not be delivered at settlement.
sweep
function sweep(uint256 marketId, uint32 epoch) external nonReentrantCheck up to maxOrders open orders of a market, fill the ones whose trigger is met, and, when called by the Hedera Schedule Service, schedule the next sweep.
Scheduled calls arrive with msg.sender == this vault and carry the epoch they were scheduled under; a stale epoch means an earlier sweep replaced this one, so it returns at once. Scheduled calls debit each checked order's budget because the vault pays their fees. Manual calls are paid by the caller, debit nothing and ignore epoch.
restartSweep
function restartSweep(uint256 marketId) external nonReentrantRestart a market's sweep chain if it stopped, e.g. because the vault could not pay a schedule. Anyone may call it; the caller pays for scheduling the next sweep.
fillFromVault
function fillFromVault(uint256 orderId, uint256 oraclePrice, uint256 poolPrice) external returns (uint256 amountOut)Settle a fill. Only callable by the vault itself, so a failed swap can be caught.
View functions
getMarket
function getMarket(uint256 marketId) external view returns (Market memory)getOrder
function getOrder(uint256 orderId) external view returns (Order memory)openOrders
function openOrders(uint256 marketId) external view returns (uint256[] memory)holderOf
function holderOf(uint256 orderId) public view returns (address)Current holder of an order's NFT: the account that can cancel it and receives its proceeds.
guardReading
function guardReading(uint256 marketId) public view returns (GuardReading memory)Chainlink price, pool TWAP price and the guard verdict for a market.
checkCost
function checkCost(uint256 marketId) public view returns (uint256)HBAR (tinybar) charged to an order per scheduled check if it were the only funded order.
checkCostShared
function checkCostShared(uint256 fundedOrders) external view returns (uint256)HBAR (tinybar) charged per check when fundedOrders orders share the sweep.
fillCost
function fillCost(uint256 marketId, Side side) public view returns (uint256)HBAR (tinybar) an order always keeps back: its fill, plus its part of a final sweep.
minBudget
function minBudget(uint256 marketId, Side side) public view returns (uint256)Smallest budget accepted at placement: the reserve plus MIN_CHECKS_FUNDED solo checks.
nextCheckDelay
function nextCheckDelay(uint256 marketId, uint8 orderType, Side side, uint128 typeParam, uint256 expiry) external view returns (uint256)Seconds until an order of orderType with typeParam would next be checked, at today's price.
The same rule a sweep applies; the frontend uses it to size budgets. A fresh order carries no state, so this reads the first-check delay. Held or triggered orders get minInterval.
sweepStatus
function sweepStatus(uint256 marketId) external view returns (SweepStatus status, uint256 nextSweepAt)Whether a market's checks are running. Stalled means funded orders are waiting but no schedule will fire (the vault could not pay one, or it was missed); anyone may restartSweep.
sweepGasLimit
function sweepGasLimit(uint256 marketId) public view returns (uint256)Gas limit given to the next scheduled sweep: enough for the orders it will check today.
Hedera bills gas used, but the payer must hold gasLimit x price up front, so keep it tight.
payerFloat
function payerFloat() public view returns (uint256 float)HBAR the vault keeps liquid to pay one scheduled sweep at execution, so a surplus withdrawal can never leave a funded market unable to pay its own keeper. Sized to the costliest funded market's sweep at the configured price. A residual stall (a gas-price spike past this reserve, or HSS capacity saturation) is still possible and is what restartSweep recovers.
surplus
function surplus() public view returns (uint256)HBAR the vault holds beyond what it owes: escrow, budgets, credits and the payer float.
Owner functions
initialize
function initialize(string calldata name, string calldata symbol) external payable onlyOwnerCreate the order NFT collection. The vault is treasury and holds the supply and wipe keys.
Send the HTS creation fee as value (about 15 HBAR on testnet); any excess stays as surplus.
listMarket
function listMarket(Market calldata market) external onlyOwner returns (uint256 marketId)List a market and associate the vault with its HTS tokens.
updateMarket
function updateMarket(uint256 marketId, GuardParams calldata guard, SweepParams calldata sweepParams, bool active) external onlyOwnerTune a market's guard and sweep. Bounds keep the guard from being switched off.
setCosts
function setCosts(Costs calldata costs_) external onlyOwnerregisterOrderType
function registerOrderType(address impl) external onlyOwner returns (uint8 id)Register a view-only order-type strategy and return its id. Append-only: ids are never reused, so an order's bound type can never change under it. The new type is active for new orders at once.
setOrderTypeActive
function setOrderTypeActive(uint8 id, bool active) external onlyOwnerPause or resume an order type for NEW orders. Existing orders of that type keep running and can always be cancelled; only placement is gated.
withdrawSurplus
function withdrawSurplus(address payable to) external onlyOwner nonReentrantWithdraw HBAR the vault holds beyond escrow, budgets, credits and the payer float.
Public state
| Declaration | Description |
|---|---|
ISaucerSwapV2Router public immutable ROUTER | |
address public immutable WHBAR | |
address public collection | HTS NFT collection whose serial numbers are order ids. |
Costs public costs | |
uint32 public marketCount | |
mapping(uint256 marketId => SweepState) public sweeps | |
mapping(uint8 id => address impl) public orderTypes | The order-type strategy registry. orderTypes[id] is a view-only IOrderType; orderTypeActive gates whether NEW orders may use it. Append-only ids, so an order's bound type never changes. |
mapping(uint8 id => bool active) public orderTypeActive | |
uint8 public orderTypeCount | |
mapping(address token => uint256) public escrowed | Tokens held on behalf of open orders, keyed by token (address(0) is HBAR). |
uint256 public totalBudgets | HBAR prepaid for scheduled checks and fills of open orders. |
mapping(address account => mapping(address token => uint256)) public credits | Payouts that could not be delivered and wait for claim. |
mapping(address token => uint256) public totalCredits |
Events
| Event | Description |
|---|---|
CollectionCreated(address collection) | |
MarketListed(uint256 indexed marketId, address base, address quote, address pool, uint24 poolFee) | |
MarketUpdated(uint256 indexed marketId) | |
CostsUpdated(Costs costs) | |
OrderPlaced( uint256 indexed orderId, uint256 indexed marketId, address indexed maker, Side side, uint8 orderType, uint256 amountIn, uint256 typeParam, uint256 slippageBps, uint256 expiry, uint256 budget ) | |
OrderTypeRegistered(uint8 indexed id, address impl) | |
OrderTypeActiveSet(uint8 indexed id, bool active) | |
OrderStateUpdated(uint256 indexed orderId, bytes32 state) | A strategy returned new per-order state (e.g. a trailing stop raised its peak); the vault stored it. |
OrderEvalSkipped(uint256 indexed orderId) | A strategy reverted or ran out of its gas stipend during a sweep; the order was skipped, not filled. |
OrderFilled( uint256 indexed orderId, address indexed holder, uint256 amountIn, uint256 amountOut, uint256 minAmountOut, uint256 oraclePrice, uint256 poolPrice, uint256 budgetRefund ) | |
OrderCancelled(uint256 indexed orderId, address indexed holder, uint256 refund, uint256 budgetRefund) | |
OrderExpired(uint256 indexed orderId, address indexed holder, uint256 refund, uint256 budgetRefund) | |
OrderChecked(uint256 indexed orderId, uint256 charged, uint256 budgetLeft) | |
FillHeld(uint256 indexed orderId, GuardState reason, uint256 oraclePrice, uint256 poolPrice) | |
FillFailed(uint256 indexed orderId, bytes reason) | |
BudgetExhausted(uint256 indexed orderId, uint256 budgetLeft) | |
BudgetToppedUp(uint256 indexed orderId, address indexed from, uint256 amount, uint256 budget) | |
SweepScheduled(uint256 indexed marketId, address schedule, uint256 executeAt, uint256 epoch) | |
SweepSuperseded(uint256 indexed marketId, uint256 epoch) | |
SweepBroughtForward(uint256 indexed orderId, uint256 charged, uint256 budgetLeft) | An order needed a check sooner than the pending sweep and paid for the superseded run. |
SweepScheduleFailed(uint256 indexed marketId, int64 responseCode) | |
SweepExecuted( uint256 indexed marketId, bool scheduled, GuardState guard, uint256 checked, uint256 filled, uint256 openOrders ) | |
Credited(address indexed account, address indexed token, uint256 amount) | |
Claimed(address indexed account, address indexed token, uint256 amount) | |
NftSettlementFailed(uint256 indexed orderId, int64 responseCode) | |
SurplusWithdrawn(address indexed to, uint256 amount) | |
Funded(address indexed from, uint256 amount) |
Errors
| Error | Description |
|---|---|
AlreadyInitialized() | |
NotInitialized() | |
UnknownMarket(uint256 marketId) | |
MarketInactive(uint256 marketId) | |
InvalidMarket() | |
InvalidGuard() | |
InvalidSweep() | |
InvalidCosts() | |
InvalidAmount() | |
InvalidTrigger() | |
UnknownOrderType(uint8 id) | |
OrderTypeInactive(uint8 id) | |
InvalidOrderParams() | |
InvalidSlippage(uint256 slippageBps, uint256 minBps, uint256 maxBps) | |
InvalidExpiry(uint256 expiry) | |
InsufficientBudget(uint256 provided, uint256 required) | |
WrongValue(uint256 sent, uint256 expected) | |
OrderNotOpen(uint256 orderId) | |
NotHolder(uint256 orderId, address caller) | |
OnlySelf() | |
NothingToClaim() | |
TransferFailed() | |
SweepAlive(uint256 marketId, uint256 nextSweepAt) | |
NoFundedOrders(uint256 marketId) | |
NotRouter(address sender) |
IOrderType
A stateless, view-only strategy for one kind of order (limit, stop-loss, trailing stop, …). The vault owns all order state and all funds; a strategy only reads what the vault passes and returns decisions. Because every function is view and the vault reaches it with staticcall, a strategy can never write storage, move value, or reenter. The vault always enforces the guard and its own Chainlink-priced slippage floor on top of whatever a strategy returns, so a buggy or hostile strategy cannot fill an order at a bad price or past the guard — see minOut.
interface IOrderType · packages/foundry/contracts/interfaces/IOrderType.sol
Functions
validate
function validate(Side side, uint128 amountIn, uint128 param, uint16 slippageBps, uint40 expiry, uint40 nowTs) external pure returns (bool ok)Validate placement parameters for this type. Returns false (or reverts) if invalid.
| Parameter | Description |
|---|---|
side | Sell the base, or buy it. |
amountIn | Amount escrowed. |
param | The type's parameter: a trigger price for limit/stop, a trail in bps for trailing. |
slippageBps | Maker's slippage tolerance. |
expiry | Order expiry (unix seconds). |
nowTs | Current block timestamp, passed in so the strategy stays pure. |
evaluate
function evaluate(Side side, uint128 param, bytes32 state, uint256 oraclePrice) external pure returns (uint256 distanceBps, bytes32 newState)The whole per-check decision, in one call so a check costs one staticcall.
| Parameter | Description |
|---|---|
side | Sell or buy. |
param | The type parameter (see validate). |
state | This order's opaque per-type state (e.g. a trailing peak); zero for a new order. |
oraclePrice | The Chainlink cross price for the market, in the market's price units. |
| Returns | Description |
|---|---|
distanceBps | 0 when the trigger is met (fill now); otherwise how far the price is from the trigger, which the vault turns into the next-check delay. |
newState | The state to persist; the vault writes it only when it differs from state. |
minOut
function minOut(Side side, uint128 amountIn, uint128 param, uint256 oraclePrice) external pure returns (uint256)An optional extra minimum-out floor, priced from Chainlink only. The vault takes the MAXIMUM of this and its own Chainlink floor (Chainlink value less the maker's slippage), so a strategy can only ever ask for MORE protection, never less. Return 0 to rely entirely on the vault's floor.
| Parameter | Description |
|---|---|
oraclePrice | The Chainlink cross price for the market. |
LimitOrderType
Sell at or above a price, or buy at or below it. Stateless. Behaviour is identical to the original fixed Sell+AtOrAbove / Buy+AtOrBelow logic, so existing orders and proofs are unchanged.
contract LimitOrderType is IOrderType · packages/foundry/contracts/ordertypes/LimitOrderType.sol
Functions
validate
function validate(Side, uint128 amountIn, uint128 param, uint16, uint40, uint40) external pure returns (bool)evaluate
function evaluate(Side side, uint128 param, bytes32 state, uint256 oraclePrice) external pure returns (uint256 distanceBps, bytes32 newState)minOut
function minOut(Side, uint128, uint128, uint256) external pure returns (uint256)StopOrderType
A stop-loss (sell at or below a price) or a stop-buy (buy at or above it). Stateless. Behaviour is identical to the original fixed Sell+AtOrBelow / Buy+AtOrAbove logic.
contract StopOrderType is IOrderType · packages/foundry/contracts/ordertypes/StopOrderType.sol
Functions
validate
function validate(Side, uint128 amountIn, uint128 param, uint16, uint40, uint40) external pure returns (bool)evaluate
function evaluate(Side side, uint128 param, bytes32 state, uint256 oraclePrice) external pure returns (uint256 distanceBps, bytes32 newState)minOut
function minOut(Side, uint128, uint128, uint256) external pure returns (uint256)TrailingStopType
Rides the price up and fires on a pullback: it tracks the highest Chainlink price the order has seen at a check (the "peak"), and the trigger is peak × (1 − trail). As the peak rises the trigger rises with it; it never moves down.
The peak is the maximum of the oracle sampled at scheduled checks, not the true continuous high, so a spike that reverses entirely between two checks is not captured. This is inherent to a pay-per-check, on-chain design and is stated in the UI and docs. Per-order state is the peak, returned to the vault which stores it; this contract is pure and cannot itself write anything.
contract TrailingStopType is IOrderType · packages/foundry/contracts/ordertypes/TrailingStopType.sol
Functions
validate
function validate(Side side, uint128 amountIn, uint128 param, uint16, uint40, uint40) external pure returns (bool)Sell-side only, with the trail in [MIN_TRAIL_BPS, MAX_TRAIL_BPS].
evaluate
function evaluate(Side, uint128 param, bytes32 state, uint256 oraclePrice) external pure returns (uint256 distanceBps, bytes32 newState)minOut
function minOut(Side, uint128, uint128, uint256) external pure returns (uint256)Public state
| Declaration | Description |
|---|---|
uint128 public constant MIN_TRAIL_BPS = 50 | |
uint128 public constant MAX_TRAIL_BPS = 5000 |
MarketGuard
Decides whether a market is safe to fill: both Chainlink feeds must be valid and fresh, and the pool's TWAP must sit within the market's deviation limit of the Chainlink cross price.
An external library so the check is linked rather than inlined, keeping OrderVault under 24 KiB.
library MarketGuard · packages/foundry/contracts/libraries/MarketGuard.sol
Functions
paramsValid
function paramsValid(GuardParams calldata g, uint24 poolFee) external pure returns (bool)Whether guard parameters keep the guard meaningful. The owner can tune a market but can't switch its protection off: no near-spot TWAP, no unlimited oracle age, no unbounded deviation, and slippage must exceed the pool fee (or no order could ever fill) without exceeding 10%.
read
function read(Market storage m) external view returns (GuardReading memory r)Shared types
File-level declarations in packages/foundry/contracts/types/OrderTypes.sol.
Enums
Side
enum Side {
SellBase,
BuyBase
}Trigger
Sell + AtOrAbove is a limit sell, Sell + AtOrBelow a stop-loss, Buy + AtOrBelow a limit buy, Buy + AtOrAbove a stop-buy.
enum Trigger {
AtOrAbove,
AtOrBelow
}Status
enum Status {
None,
Open,
Filled,
Cancelled,
Expired
}HtsOperation
Which HTS operation failed, for the shared HtsError.
enum HtsOperation {
CreateCollection,
Mint,
TransferNft,
Associate
}GuardState
enum GuardState {
Open,
OracleInvalid,
OracleStale,
TwapUnavailable,
DeviationTooHigh
}SweepStatus
enum SweepStatus {
Idle,
Scheduled,
Stalled
}Structs
Market
A tradable pair. Prices are quote per 1 base, 8 decimals.
struct Market {
address base;
address quote;
uint8 baseDecimals;
uint8 quoteDecimals;
bool baseIsHbar;
bool quoteIsHbar;
IAggregatorV3 baseFeed;
IAggregatorV3 quoteFeed;
uint8 baseFeedDecimals;
uint8 quoteFeedDecimals;
ISaucerSwapV2Pool pool;
uint24 poolFee;
bool baseIsToken0;
bool active;
GuardParams guard;
SweepParams sweep;
}GuardParams
struct GuardParams {
uint32 twapWindow;
uint16 maxDeviationBps;
uint32 maxOracleAge;
uint16 maxSlippageBps;
}SweepParams
How often a market's sweep runs. The next sweep waits roughly as long as the price would need to reach the nearest trigger at maxMoveBpsPerHour, clamped to [minInterval, maxInterval].
struct SweepParams {
uint32 minInterval;
uint32 maxInterval;
uint16 maxMoveBpsPerHour;
uint16 maxOrders;
uint8 maxFills;
}SweepState
struct SweepState {
address pendingSchedule;
uint40 nextSweepAt;
uint32 cursor;
uint32 fundedOrders;
/// @dev Bumped on every new schedule; a scheduled sweep carrying an older epoch has been superseded.
uint32 epoch;
/// @dev Consecutive sweeps where a triggered order could not fill; drives the back-off.
uint8 heldStreak;
}Costs
Gas used by each piece of work, measured on Hedera testnet, and the network gas price.
struct Costs {
uint32 scheduleGas;
uint32 sweepBaseGas;
uint32 checkGas;
uint32 fillGasHbarIn;
uint32 fillGasTokenIn;
uint32 settleGas;
/// @dev A sweep that finds nothing to do: superseded by an earlier one, or left with no funded orders.
uint32 idleSweepGas;
/// @dev Hedera prices gas in USD; converted to tinybar through the 0x168 exchange-rate contract.
uint32 gasPriceTinycents;
uint16 safetyBps;
}Order
struct Order {
uint32 marketId;
Side side;
uint8 orderType; // index into the vault's order-type registry (limit, stop, trailing, …)
Status status;
bool funded;
uint16 slippageBps;
uint40 createdAt;
uint40 expiry;
uint128 amountIn;
uint128 typeParam; // the type's parameter: a trigger price for limit/stop, a trail in bps for trailing
uint128 budget;
bytes32 typeState; // the type's opaque per-order state (e.g. a trailing peak); zero for stateless types
}PlaceParams
struct PlaceParams {
uint32 marketId;
Side side;
uint8 orderType;
uint128 amountIn;
uint128 typeParam;
uint16 slippageBps;
uint40 expiry;
}GuardReading
struct GuardReading {
GuardState state;
uint256 oraclePrice;
uint256 poolPrice;
uint256 deviationBps;
uint256 oracleUpdatedAt;
}Errors
| Error | Description |
|---|---|
HtsError(HtsOperation operation, int64 responseCode) | An HTS system-contract call returned a non-success response code. Shared by the vault and the Settlement library so both can revert and callers can catch the same error. |
From packages/foundry/contracts (NatSpec) at v1.1 (6ddd9a3) · View source