Keepers

Perps markets rely on keepers to fetch offchain price data to fill orders and liquidate accounts. Polynomial Perps use a delayed order mechanism to fill orders. Orders are committed by the users and keepers are responsible for filling them after a delay. The minimum delay is set to one block, i.e. 2 seconds. The keepers are rewarded for their services with a portion of the fees paid by the users. Order execution keepers receive their gas cost and a tip for their work. The tip is set to $0.1 per order. Similarly, each liquidation yields a reward around $5.

To get ETH on Polynomial chain, you can use https://refuel.exchange.

Multicall

Keepers are expected to batch their actions using TrustedMulticallForwarder contract as they’re expected to update price feeds and settle orders / liquidate accounts in the same transaction. You can call aggregate3Value method on the TrustedMulticallForwarder contract to batch multiple calls.

struct Call3Value {
    address target;
    bool requireSuccess;
    uint256 value;
    bytes callData;
}

function aggregate3Value(Call3Value[] calldata calls) public returns (bytes[] memory returnData);

TrustedMulticallForwarder contract will return any rewards given to the caller address from the Polynomial Perps system.

Price Updates

Both Order Settlement and Liquidation keepers are responsible for updating oracle price data from Pyth. The keepers are expected to call Pyth Hermes APIs to fetch the latest price data, and push this data onchain.

Latest price update data can be fetched from /v2/updates/price/latest endpoint and the price update data for a specific time can be fetched from /v2/updates/price/{time} endpoint. For more details, refer to the Pyth Hermes API Documentation.

The price update data returned by Pyth needs to be further encoded to be used in Polynomial Perps. The encoding schema is as follows:

The price update data returned by Pyth needs to be further encoded to be used in Polynomial Perps. The encoding schema is as follows:

bytes signedOffchainData = abi.encode(uint8 updateType, uint64 stalenessTolerance, bytes32[] priceIds, bytes[] updateData)

The updateType is set to 1 for latest price updates and 2 for price snapshots (fixed time). The stalenessTolerance is the maximum time in seconds that the system is willing to accept for the price data to be stale (Maximum allowed value is 60 for most actions). The priceIds is the list of price IDs that the keeper is responsible for. The updateData is the list of price update data for the corresponding price IDs returned by the Pyth Hermes API.

The keepers need to call fulfillOracleQuery method on the PythERC7412Wrapper contract to push the price update data onchain and need to pay the required Pyth update fee in ETH. Currently this is 1wei for each price feed.

function fulfillOracleQuery(bytes memory signedOffchainData) external;

Order Settlement

Whenever a user commits an order, OrderCommitted event is emitted by the PerpsMarketProxy contract. This event contains the details of the order to be settled.

event OrderCommitted(
    uint128 indexed marketId,
    uint128 indexed accountId,
    SettlementStrategy.Type orderType,
    int128 sizeDelta,
    uint256 acceptablePrice,
    uint256 commitmentTime,
    uint256 expectedPriceTime,
    uint256 settlementTime,
    uint256 expirationTime,
    bytes32 indexed trackingCode,
    address sender
);

After an order is committed, the keepers can settle the order starting from the next block. This can be done by calling settleOrder method on the PerpsMarketProxy contract.

function settleOrder(uint128 accountId) external;

The keeper is expected to update price feeds before settling the order for the market they’re executing the order for, and any other markets the user have a position in. Price update is of type 2 and time should be set to expectedPriceTime of the order.

Liquidation

Liquidation keepers are responsible for liquidating accounts with bad balances. Whenever a user’s available margin goes below the maintenance margin additional to liquidation rewards, keepers can liquidate the account by calling liquidate method on the PerpsMarketProxy contract.

function liquidate(uint128 accountId) external;

The keeper is expected to update price feeds before liquidating the account. Price update is of type 1 and can fetch the latest price data from the Pyth network.

There are 3 helper methods on the PerpsMarketProxy contract that keepers can use to get information about an account. All of them would need a price update of type 1 as well.

  • canLiquidate for checking if an account is eligible for liquidation

  • getRequiredMargins for getting the required initial and maintenance margins for an account

  • getAvailableMargin for getting the available margin of an account

function canLiquidate(uint128 accountId) external view returns (bool isEligible);

function getRequiredMargins(uint128 accountId) external view returns (uint256 requiredInitialMargin, uint256 requiredMaintenanceMargin, uint256 maxLiquidationReward);

function getAvailableMargin(uint128 accountId) external view returns (int256 availableMargin);

Known Revert Reasons

  • OracleDataRequired: At least one of the price update requirement was not met

  • ERC2771ForwarderMismatchedValue: The value sent in the transaction does not match the sum of the values sent in the calls

  • NotEligibleForLiquidation: The account is not eligible for liquidation (or it might have been liquidated already)

  • SettlementWindowNotOpen: The settlement window is not open yet

  • SettlementWindowExpired: The settlement window has expired

  • OrderNotValid: The Account does not have any pending orders (or it might have been settled already)

error OracleDataRequired(address oracleContract, bytes oracleQuery);
error ERC2771ForwarderMismatchedValue(uint256 expectedValue, uint256 actualValue);
error NotEligibleForLiquidation(uint128 accountId);
error SettlementWindowNotOpen(uint256 timestamp, uint256 settlementTime);
error SettlementWindowExpired(uint256 timestamp, uint256 settlementTime, uint256 settlementExpiration);
error OrderNotValid();

The oracleQuery in the OracleDataRequired specifies the missing price update requirement. It can be decoded as follows:

(uint8 updateType, uint64 stalenessTolerance, bytes32[] priceIds) = abi.decode(oracleQuery, (uint8, uint64, bytes32[]));

If you have more than one OracleDataRequired error in the same transaction, you will receive an Errors error instead.

error Errors(bytes[] revertReasons);

Contract Addresses

Last updated