Open jjgonecrypto opened 5 years ago
A good point was raised on Twitter today that atomic swaps - such as the Uniswap synth exchange contract - would also be impacted under this proposal. For example, if a user wanted to use that contract to swap ETH
for sBTC
say, via the deep sETH pool in Uniswap, then they would have to accept that their sBTC
would not arrive immediately, but rather after a short delay - not unlike an order being filled on an exchange.
To put it another way, the crux of this proposal is to migrate from a synchronous atomic swap of one synth to another in a single transaction (based on the current on-chain market price) to an asynchronous model whereby the user indicates an intent to exchange, and their order is filled once prices are updates are received or a reasonable delay expires - whichever comes first.
I propose closing this proposal in favor of Fee Reclamations: https://github.com/Synthetixio/SIPs/issues/57
Problem
Currently when
exchange()
is invoked in Synthetix, the function is called with asource
synth, anamount
in the source synth, and thedestination
synth. The exchange is processed immediately given the current on-chain pricing in the SNX system. The code states:Burn
theamount
ofsource
synthamount
ofsource
synth is indestination
synth (amountReceived
)fee
when required fromamountReceived
, updating it, andIssue
anXDR
synth for thatfee
Issue
theamountReceived
to the user in thedestination
synthStep 2 above calculates the
effectiveValue
ofamount
insource
to an equivalent indestination
, given the current rates of exchange of bothsource
anddestination
toUSD
inExchangeRates
.An exchange itself does NOT impact the size of the debt pool in any way, as it is simply a repricing of debt - a conversion between from one synth to another. Yet when the prices of synths change from oracle updates to
ExchangeRates
- then the debt pool is affected.The current SNX exchange mechanism works like a market order. The user is getting the current market price between
source
todestination
, given the rates the SNX oracle has placed on chain.This entire flow however, exposes front-running risk. As long as an exchange is mined before the SNX oracle updates, any knowledge of what will happen in the upcoming update can be profited from.
We are currently working around this with SIP-12, however this is only really a temporary fix to the problem.
Proposal
Instead of immediately processing an exchange, it is placed into a
queue
, along with the currentblockHeight
. Thisqueue
could be processed by anybody at any time, with the exchanges in the queue only filled when theirsource
anddestination
are updated by an oracle. This then prevents any front-running for good.This functionality would also support limit orders - allowing users to add orders to exchange when the rate between the
source
anddestination
reached a certain threshold.Concerns
1. Processing the queue - who pays the cost
The obvious question is, who processes the queue, and thus who pays the gas? Each exchange costs around
200k
in gas (exchangeGasUsage
). Making the user transact twice - once to put it in the queue and once to process it - is too much friction.Instead, we can create a function
process()
that anyone can call to process the queue and recover their spent gas costs insETH
. This amount should be the equivalent oftx.gasprice * exchangeGasUsage
for each successfully processed exchange.I propose that the payment of this come from the exchange itself, with a user-placed
maxGwei
cap on how much this fee can reach to prevent griefing attacks (where by malicious users invokeprocess()
with enough gas to take significant portions of the exchange away from the exchanger).To ensure the exchange volume is sufficient to pay the processing fee, I propose that for exchanges less than some configurable USD
minThreshold
amount, we allow them to be executed immediately, bypassing thequeue
. This alleviates the issue where small exchanges that can't afford their gas processing costs get stuck in thequeue
. This amount should be small enough that spamming the system with a large amount of these would not alllow profitability of potential front-runners when accounting for gas costs.One downside of this approach is the added friction to the user who now has to add a
maxGwei
amount to their exchange. To mitigate the complexity of this, I suggest we prefill this amount in our dApps using the best estimates of current average & fast gas, helping them customize it if need be.2. Reasonable timing
The next question is when is the queue processed? Or in other words, how long should a user expect to wait for their exchange to be executed (or their order to be filled, in the parlance of traditional order-book exchanges).
This is particularly important when using decentralized Chainlink oracles, which are targeting a twice-daily heartbeat. If a price does not move outside the
1%
threshold, then it's possible to not receive a price in a 12 hour period. Obviously this is an inordinate length of time to ask any user to wait. To mitigate this, I propose we have some number ofblockDelay
, after which, an exchange can be processed, even when an on-chain price has not been received from an oracle.While this does mean that users could still trade on price movements off-chain that have not been reflected on-chain, the profits they can make are negligble the longer the
blockDelay
is, given that price deviations at1%
or more would be broadcast.Implementation
The
Synthetix
contract would need an array ofQueueEntry
items, along with a newaddToQueue()
andprocess()
functions.Example
Let's say the
minThreshold
to use the queue is5
sUSD
total exchange size. And theexchangeGasUsage
is200k
.Now let's imagine we're at block
500
and thequeue
is empty.sETH
is at the rate of200
, being last updated at block450
andsBTC
is at8,000
, last updated at block495
.At block
500
Alice
invokesexchange
for5 sETH into sBTC
,maxGwei = 5
.At block
501
Bob
readsavailableToProcess(gasPrice: 5)
and it returns0
as both the latestsETH
block update is less than500
.At block
501
another userChristina
submits an exchange for 1sUSD
intosETH
. As it's belowminThreshold
, it is processed immediately.At block
504
,sETH
is updated to205
by an oracle. TheETHBTC
rate is now205/8000
. AsAlice
is looking to move out of (or selling)ETH
, this movement is favorable to her.At block
505
Bob
again readsavailableToProcess(gasPrice: 5)
and it still returns0
(assBTC
is still stale).At block
510
,sBTC
is updated to7900
by an oracle. TheETHBTC
rate is now205/7900
. AsAlice
is looking to move into (or buying)BTC
, this movement is favorable to her.At block
510
,Bob
readsavailableToProcess(gasPrice: 5)
which returns1
. TryingavailableToProcess(gasPrice: 5)
returns0
.At block
511
,Bob
invokesprocess()
with a gasPrice of5
gwei. The first and only entry in the queue is found as thesource
anddestination
have a higherblockHeight
than the queue entry, so it is processed.200,000 * 0.000000005 = 0.001 sETH
(which is the equivalent of0.205
sUSD
, or0.02%
).4.999 sETH
. Subtracting the0.3%
exchange fee yields4.985003
sETH
(or~1022
sUSD
worth).Alice
has5 sETH
burned and is issued0.1293317234
sBTC
.Bob
is issued0.001
sETH
. The fee pool is issued0.014997
sETH
at the equivalentXDR
rate.