SIP-54: Limit Orders
Author | |
---|---|
Discussions-To | Discord - limit-orders |
Status | Implemented |
Created | 2020-04-27 |
Simple Summary
This SIP adds limit order functionality to the Synthetix exchange, without modifying the core exchange contracts.
Abstract
To keep the integrity of the core Synthetix contracts in place, we propose the creation of a separate layer of “advanced mode” trading contracts to enable additional functionality. The primary contract is a limit order contract. The exchange users can place limit orders on it and send the order source amount to it. Additionally, they specify the parameters of limit orders, including destination asset price and execution fees. Limit orders set by the user are executed at the right time by "execution nodes" that recieve the execution fee in exchange.
Motivation
To increase the flexibility of the Synthetix exchange, limit order functionality is needed so users can effectively trade synthetic assets. While limit orders can be trivial to implement in the case of centralized exchanges, in the case of a DEX such as Synthetix, limit orders can be challenging in terms of security guarantees and trustlessness due to client unavailability.
Specification
SynthLimitOrder Contracts Spec
NOTES:
- The following specifications use syntax from Solidity
^0.5.16
. - In order for these contracts to be able to access user SNX tokens, they must approve the proxy contract address for each token individually using the ERC20 approve() function. We recommend a max uint (2^256 - 1) approval amount.
Order of deployment:
- The
Implementation
contract is deployed - The
ImplementationResolver
contract is deployed where the address of theImplementation
contract is provided to the constructor as aninitialImplementation
- The
Proxy
contract is deployed with the address of theImplementationResolver
provided to the constructor. Implementation.initialize()
is called on theProxy
address.
Implementation Contract
The Implementation contract stores no state and no funds and is never called directly by users. It is meant to only receive forwarded contract calls from the Proxy contract.
All state read and write operations on this contract are stored in the Proxy contract state. In the event of an implementation upgrade, the Proxy state would become automatically accessible to the new Implementation
.
Public Variables
latestID
A uin256 variable that tracks the highest orderID stored.
synthetix
An address variable that contains the address of the Synthetix exchange contract
orders
A mapping between uint256 orderIDs and LimitOrder
structs
Structs
LimitOrder
struct LimitOrder {
address payable submitter;
bytes32 sourceCurrencyKey;
uint256 sourceAmount;
bytes32 destinationCurrencyKey;
uint256 minDestinationAmount;
uint256 weiDeposit;
uint256 executionFee;
uint256 executionTimestamp;
uint256 destinationAmount;
bool executed;
}
Methods
initialize
function initialize(address synthetixAddress, address _addressResolver) public;
This method can only be called once to initialize the proxy instance.
newOrder
function newOrder(bytes32 sourceCurrencyKey, uint sourceAmount, bytes32 destinationCurrencyKey, uint minDestinationAmount, uint executionFee) payable public returns (uint orderID);
This function allows a msg.sender
who has already given the Proxy
contract an allowance of sourceCurrencyKey
to submit a new limit order.
- Transfers
sourceAmount
of thesourceCurrencyKey
Synth from themsg.sender
to this contract viatransferFrom
. - Adds a new limit order using to the
orders
mapping where the key islatestID + 1
. The order'sexecuted
property is set tofalse
. - Increments the global
latestID
variable. - Requires a deposited
msg.value
to be more than theexecutionFee
in order to refund node operators for their exact gas wei cost in addition to theexecutionFee
amount. The remainder will be transferred back to the user at the end of the trade. - Emits an
Order
event for node operators including order data andorderID
. - Returns the
orderID
.
cancelOrder
function cancelOrder(uint orderID) public;
This function cancels a previously submitted and unexecuted order by the same msg.sender
of the input orderID
.
- Requires the order
submitter
property to be equal tomsg.sender
. - Requires the order
executed
property to be equal to befalse
. - Refunds the order's
sourceAmount
and depositedmsg.value
- Deletes the order using from the
orders
mapping. - Emits a
Cancel
event for node operators including theorderID
executeOrder
function executeOrder(uint orderID) public;
This function allows anyone to execute a limit order.
It fetches the order data from the orders
mapping and attempts to execute it using Synthetix.exchange()
, if the amount received is larger than or equal to the order's minDestinationAmount
:
- This transaction's submitter address is refunded for this transaction's gas cost + the
executionFee
amount from the user's wei deposit. - The remainder of the wei deposit is forwarded to the order submitter's address
- The order's
executed
property is changed totrue
, theexecutionTimestamp
property set toblock.timestamp
anddestinationAmount
set to the received amount. Execute
event is emitted with theorderID
for node operators
If the amount received is smaller than the order's minDestinationAmount
, the transaction reverts.
withdrawOrders
function withdrawOrders(uint[] orderID) public;
This function allows the sender to withdraw funds associated with an array of executed orders as soon as the Synthetix fee reclamation window for each of the order has elapsed.
It iterates over each order's data from the orders
mapping, if each order's submitter
is equal to msg.sender
, has the executed
property equal to true
and executionTimestamp + Exchanger.waitingPeriodSecs() < block.timestamp
:
- The
destinationAmount
of thedestinationCurrencyKey
is transferred tomsg.sender
usingSynth.transferAndSettle()
- The order is deleted using from the
orders
mapping. Withdraw
event is emitted with theorderID
.
Events
Order
event Order(uint indexed orderID, address indexed submitter, bytes32 sourceCurrencyKey, uint sourceAmount, bytes32 destinationCurrencyKey, uint minDestinationAmount uint executionFee, uint weiDeposit)
This event is emitted on each new submitted order. Its primary purpose is to alert node operators of newly submitted orders in realtime.
Cancel
event Cancel(uint indexed orderID);
This event is emitted on each cancelled order. Its primary purpose is to alert node operators that a previously submitted order should no longer be watched.
Execute
event Execute(uint indexed orderID, address executor);
This event is emitted on each successfully executed order. Its primary purpose is to alert node operators that a previously submitted order should no longer be watched.
Withdraw
event Withdraw(uint indexed orderID, address indexed submitter);
This event is emitted on each successfully withdrawn order.
Proxy Contract
The proxy contract receives all incoming user transactions to its address and forwards them to the current implementation contract. It also holds all deposited token funds at all times including future upgrades.
All calls to this contract address must follow the ABI of the current Implementation
contract.
Constructor
constructor(address ImplementationResolver) public;
The constructor sets the ImplementationResolver
to an internal global variable to be accessed by the fallback function on each future contract call.
Fallback function
function() public;
Following any incoming contract call, the fallback function calls the ImplementationResolver
's getImplementation()
function in order to query the address of the current Implementation
contract and then uses DELEGATECALL
opcode to forward the incoming contract call to the queried contract address.
ImplementationResolver Contract
This contract provides the current Implementation
contract address to the Proxy
contract.
It is the only contract that is controlled by an owner
address. It is also responsible for upgrading the current implementation address in case new features are to be added in the future.
Constructor
constructor(address initialImplementation, address initialOwner) public;
The constructor sets the initialImplementation
address to an internal global variable to be access later by the getImplementation()
method. It also sets the initialOwner
address to the owner
public global variable.
Public Variables
owner
An address variable that stores the contract owner address responsible for upgrading the implementation contract address.
Methods
changeOwnership
function changeOwnership(address newOwner) public;
This function allows only the current owner
address to change the contract owner to a new address. It sets the owner
global variable to the newOwner
argument value and emits a NewOwner
event.
upgrade
function upgrade(address newImplementation) public;
This function allows only the current owner
address to upgrade the contract implementation address.
It sets the internal implementation global variable to the newImplementation
global variable.
Following the upgrade, the Upgraded
event is emitted.
Events
NewOwner
event NewOwner(address newOwner);
This event is emitted when the contract owner
address is changed.
Upgraded
event Upgraded(address newImplementation);
This event is emitted when finalizeUpgrade()
is called.
Limit Order Execution Node Spec
Introduction
The Limit Order Execution Node is an always-running node that collects NewOrder
events from the proxy contract and executes each order when its minDestinationAmount
condition is met by the Synthetix oracle (Synthetix ExchangeRates.sol
contract).
Requirements
- The node collects executable limit orders as soon as their execution conditions are met.
- The node only executes orders that can refund the entire gas cost of the transaction in addition to an
executionFee
equal to or larger than a pre-configured minimum. - The node attempts to re-execute failing order transactions as soon as their order conditions are met again
Components
Watcher Service
This service listens for new Ethereum blocks in realtime. On each new block, the service queries the contract for currently executable orders.
If any executable limit orders are found, they are passed to the Execution Service
Execution Service
In order to determine whether an orderID
should be immediately executed, the service follows the following steps:
- Checks a local database for any existing pending transaction previously submitted by the node that executes the same
orderID
. If a record is found, this order will not be submitted. - Attempts to estimate gas cost for calling the
executeOrder
contract function while passing theorderID
. If the attempt fails, this is likely because the order's depositedwei
funds are insufficient to fully recover the gas cost of this transaction + theexecutionFee
. - Checks if the node wallet address owns sufficient
wei
to cover this transaction cost. If the balance is insufficient, an email notification must be sent to the node operator.
After the checks have passed, the order is executed:
- The
orderID
is submitted to theexecuteOrder
function in a new transaction - The
orderID
is temporarily stored in the local database to prevent future duplicate transactions until the transaction is included in a block.
The execution service then listens for the transaction status. If the transaction is successfully mined and but the EVM execution has failed:
- The service removes both the mapped
orderID
from the local database - This
orderID
will then be collected again by the Watcher Service as soon as its conditions are met, starting from the next block.
Client-side Javascript Library Spec
Introduction
This library is proposed in order to provide a simple Javascript interface to the limit order functionality of the Synthetix limit order contract.
Requirements
The library must allow a simple interface to the following operations:
- Submit a new limit order
- Query the execution status of an active order
- Cancel an active order
- List all active orders submitted by the user's address
API
Constructor
const instance = new SynthLimitOrder(ethereumProvider)
The library must expose a SynthLimitOrder
class. The user instantiates a class instance by passing a valid ethereum provider with signing capabilities (e.g. Metamask) to the constructor.
submitOrder
const orderID = await instance.submitOrder({
sourceCurrencyKey, // bytes32 string
sourceAmount, // base unit string amount
destinationCurrencyKey, // bytes32 string
minDestinationAmount, // base unit string amount
weiDeposit, // wei string amount
executionFee // wei string amount
})
This method allows the user to submit a new limit order on the contract by calling the newOrder
contract function.
This method also automatically attempts to sign an ERC20 approve transaction for the sourceCurrencyKey
token if a sufficient allowance is not already present for the contract.
It returns a Promise<string>
as soon as the transaction is confirmed where the string contains the new order ID.
withdraw
await instance.withdraw()
This method allows the user to withdraw funds from all executed orders on the contract that have passed the fee reclamantion duration.
The method fetches all historical Executed
events from the contract filtered by the user's address as the submitter
and queries each order's current state using StateStorage.getOrder
. If the order's executed
property is true
and executedTimestamp + 3 minutes
is larger than the current timestamp, the order is is added to the array of order IDs sent to the Proxy contract's withdrawOrders
function.
It returns a Promise<void>
as soon as the transaction is confirmed.
cancelOrder
await instance.cancelOrder(orderID)
This method cancels an active order by calling the cancelOrder
contract function.
It returns a Promise<void>
as soon as the transaction is confirmed.
getOrder
const order = await instance.getOrder(orderID)
This method allows the user to query the contract for a specific order number by querying the StateStorage
contract getOrder
function.
It returns a promise that resolves with an Order
object:
interface Order {
submitter: string;
sourceCurrencyKey: string;
sourceAmount: string;
destinationCurrencyKey: string;
minDestinationAmount: string;
weiDeposit:string;
executionFee:string;
executionTimestamp:string;
destinationAmount:string;
executed:boolean;
}
getAllActiveOrders
const order = await instance.getAllActiveOrders()
This method allows the user to query the contract for an array all active orders submitted by the user's address. This array is constructed by querying a list of all past Order
contract events filtered by the user's wallet address as the submitter
. Each orderID
is passed to the getOrder
javascript method before being included in the returned array in order to ensure that the order is still active.
It returns a Promise<Order[]>
where an Order
follows the interface above.
Rationale
Limit Order Execution Nodes
By allowing anyone to run “limit order execution nodes” and compete for limit order execution fees, we achieve order execution reliability and censorship-resistance through permissionless-ness. These are especially important in the context of limit orders, where censorship or execution delays might cause trading losses.
Upgradability
We use an upgradable proxy pattern in order to allow a present owner address to upgrade the core implementation contract at any time.
Test Cases
Test cases to be provided with implemented code.
Implementation
Not started. Spec complete and ready for community review.
Copyright
Copyright and related rights waived via CC0.