SIP-37: Fee Reclamations and Rebates
Author | |
---|---|
Discussions-To | https://discord.gg/3uJ5rAy |
Status | Implemented |
Created | 2020-01-20 |
Simple Summary
Deduct profits or rebate losses occurred by exchanges made immediately prior to a market shift.
Abstract
In order to prevent front-running the standard latency of Ethereum block processing, ensure that all synth exchanges take into account any imminent changes to market prices. We can do this by settling the owed funds in successive exchanges of that target synth, as long as the exchange happens after a waiting period of N minutes (configurable by SCCP).
Motivation
In effect, users who exchange
in Synthetix are actually converting debt from one form to another at the current market price on-chain. If a user trades to take advantage of latency between prices detectable in the off-chain market (the SPOT rate) and the on-chain one, then they make profit at the expense of the entire SNX stakers (as each staker holds a debt position - a percentage of the entire debt pool).
SIP-12 was implemented as a mechanism to slow down any front-running, adding latency to their exchanges. However, while it prevents true front-running (anyone watching the Ethereum mempool for oracle updates and front-running the actual market prices being committed on-chain), it doesn't help much with oracle latency when the network congestion is low. Consider that oracles can't constantly push prices, at the very least they need to wait for any previous prices to be committed on-chain, and it would be incredibly expensive (and arguably a waste of resources) to try to get an oracle update in each and every block. If Ethereum network congestion is low (and thus gas prices are low), user exchanges can still get in quite quickly and the max gas limit doesn't help much. On top of that, the max gas limit can inhibit legitimate users who accidentally hit it when it adjusts down during regular adjustments.
By creating a short waiting period after exchanges in which exchanges or transfers out of that target synth are restricted, we can then settle the account automatically.
Specification
When a user exchanges src
synth for dest
synth, the waiting period of N minutes begins. Any transfer
of dest
synth will fail during this window, as will an exchange
from dest
synth to any other synth, or a burn of sUSD
if it was the dest
synth of any pending exchange. If another exchange into the same dest
synth is performed before N minutes expires, the waiting period restarts with N minutes remaining.
Once N minutes has expired, the following exchange
from the dest
synth to any other, or a burn of sUSD
, will invoke settle
- calculating the difference between the exchanged prices and those at the end of the waiting period. If the user made profit, it is taken to be front-run profit, and the profit is burned from the user's holding of the dest
synth. If the user made a loss, this loss is issued to them from the dest
synth. The exchange
then continues as normal.
In the case of a user trying to transfer
some amount
of the dest
synth after the waiting period has expired, this will always fail if the amount
+ totalOwing
is more than the user's balanceOf
. The user has to first invoke settle
before a synth can be transferred. Otherwise, the transfer
will continue as normal.
The calculation of owing
or owed
is as follows:
Amount * (1 - feeRate) * (srcRate/destRate - newSrcRate/newDestRate)
If the result is negative, the amount is owed
as a rebate, otherwise its owing
as a
Examples (with feeRate
at 0.003
):
-
100 sUSD into sETH at a ETHUSD rate of 100:1 (1/100) which raises to 105:1 (1/105), the
owing
would be:100 * 0.997 * (1/100 - 1/105) = 0.04747619048 sETH
. -
100 sETH into sBTC at a BTCUSD rate of 10,000:1 and ETHUSD rate of 100:1 which raises to 105:1, the user would be rebated an
owed
amount of:100 * 0.997 * (100/10000 - 105/10000) = 0.04985 sBTC
Rationale
The goal is to reclaim any fees owing whilst not impacting usability and composability. Preventing exchange
, transfer
and burn
of the dest
synth during the waiting period is necessary to ensure the user has the synths to reclaim if need be.
Once the period is over, we invoke settle
within exchange
or burn
of the dest
synth to limit complexity for the user.
Whilst we can also invoke settle
within transfer
of the dest
synth, there are concerns that this will break ERC20
assumptions. When transfer(amount)
is invoked, there are assumptions in the Ethereum ecosystem that amount
will be received by the recipient. For instance, when Synthetix was Havven, there were issues integrating sUSD
into DEXes as the previous sUSD
transfer fees meant that these DEXes had to consider these in their accounting systems, which was often too complex for them. That being said, this proposal includes the addition of a transferAndSettle
function for users who want that functionality in a single transaction.
Test Cases
Given the following preconditions:
-
Jessica has a wallet which holds 100 sUSD and this wallet has never exchanged before,
-
and the price of ETHUSD is 100:1, and BTCUSD is 10000:1
-
and the waiting period (M) is set to 3 minutes.
When
- she exchanges 100 sUSD into 1 sETH.
Then
- ✅ it succeeds as sETH has no reclamation fees for this wallet.
When
- she exchanges 100 sUSD into 1 sETH
- and she immediately attempts to transfer 0.1 sETH
Then
- ❌ it fails as the waiting period has not expired
When
- she exchanges 100 sUSD into 1 sETH
- and she immediately attempts to exchange 1 sETH for sBTC
Then
- ❌ it fails as the waiting period has not expired
When
- she exchanges 50 sUSD into 0.5 sETH.
- and she immediately attempts to exchange 50 sUSD into 0.005 sBTC
Then
- ✅ it succeeds as sBTC has no reclamation fees for this wallet
When
- she exchanges 50 sUSD into 0.5 sETH.
- and 1 minute she immediately attempts to exchange another 50 sUSD into 0.5 sETH
Then
- ✅ it succeeds, and the waiting period is reset to 3 minutes
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and 2 minutes later the price of ETHUSD goes up to 100.25:1
- ⏳ and another minute later she attempts to transfer this sETH
Then
- ❌ the transfer fails because she owes
100 * 0.997 * (1/100 - 1/100.25) = 0.002486284289 sETH
she must settle first
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and 2 minutes later the price of ETHUSD goes up to 100.25:1
- ⏳ and another minute later she attempts to transfer 0.90 sETH
Then
- ✅ the transfer succeeds because even though she owes
0.002486284289
sETH
, she still has0.997 - 002486284289 = 0.9945137157
sETH that is transferable
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and 1 minute later the price of ETHUSD goes up to 103:1
- ⏳ and 2 more minutes later she attempts to transfer all of her sETH
Then
- ❌ the transfer fails because she made
100 * 0.997 * (1/100 - 1/103) = 0.02903883495
sETH. She must either invokesettle
separately before being able to transfer the sETH or invoketransferAndSettle
to combine the actions.
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and a minute later the price of ETHUSD goes up to 103:1
- ⏳ and 2 more minutes later she invokes
settle
for sETH - and immediately transfers this sETH to another wallet
Then
- ✅ the transfer succeeds as the prior
settle
invocation burned100 * 0.997 * (1/100-1/103) = 0.02903883495 sETH
, and transfer detected no fees remaining.
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and a minute later the price of ETHUSD goes up to 103:1
- ⏳ and 2 more minutes later she attempts to exchange 1 sETH for sBTC
Then
- ✅ the exchange succeeds, burning
0.02903883495 sETH
and converting the rest into sBTC (minus the exchange fee).
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and a minute later the price of ETHUSD goes down to 95:1
- ⏳ and 2 more minutes later she attempts to exchange 1 sETH for sBTC
Then
- ✅ the exchange succeeds, issuing her
100 * 0.997 * (1/100 - 1/95) = 0.05247368421 sETH
, and converting the entire amount (0.967961165 sETH
) into sBTC (minus the exchange fee).
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and no oracle update for ETHUSD occurs after 3 minutes
- ⏳ once 3 minutes from exchange have elapsed she attempts to exchange
Then
- ✅ the exchange succeeds and no rebate or reclamation is required
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and no oracle update for ETHUSD occurs after 3 minutes
- ⏳ once 3 minutes from exchange have elapsed she exchanges 1 sETH for sUSD (paying a further 30bps fee)
- ⏳ and a minute later the price of ETHUSD goes down to 90:1
- ⏳ she burns
50
sUSD
Then
- ❌ the burn fails as the waiting period for sUSD is still ongoing
When
- she exchanges 100 sUSD into 1 sETH (paying a 30bps fee)
- ⏳ and no oracle update for ETHUSD occurs after 3 minutes
- ⏳ once 3 minutes from exchange have elapsed she exchanges all of her 0.997 sETH for sUSD (paying a further 30bps fee)
- ⏳ and a minute later the price of ETHUSD goes down to 90:1
- ⏳ and two minutes later 3 minutes have elapsed since her last sUSD exchange
- ⏳ she burns
50
sUSD
Then
- She has
0.997 * 0.997 * 100/1 = 99.4009
sUSD in her account when theburn
starts 9.94009
sUSD is reclaimed from the user (0.997 * 0.997 * (100/1 - 90/1)
- and
50
sUSD is burned. - ✅ She is left with
99.4009 - 9.94009 - 50 = 39.46081 sUSD
Implementation
-
Synthetix.exchange()
invoked from synthsrc
todest
byuser
foramount
-
Are we currently within a waiting period for any exchange into
src
?- Yes: ❌ Fail the transaction
- No: ✅
- Invoke
settle(src)
- Proceed with the
exchange
as per usual - Persist this exchange in the user queue for
dest
synth
- Invoke
-
-
Synthetix.settle(synth)
invoked withsynth
byuser
-
Are we currently within a waiting period for any exchange into
synth
?- Yes: ❌ Fail the transaction
- No: Sum the
owing
andowed
amounts on all unsettledsynth
exchanges astotalOwing
andtotalOwed
- Is the totalOwing > 0
- Yes: ✅ Reclaim the
totalOwing
ofsynth
from the user by burning it
- Yes: ✅ Reclaim the
- Is the totalOwed < 0
- Yes: ✅ Rebate the absolute value
totalOwed
ofsynth
to the user by issuing it
- Yes: ✅ Rebate the absolute value
- Finally, remove all
synth
exchanges for the user
- Is the totalOwing > 0
-
-
Synth.transfer()
invoked from synthsrc
byuser
foramount
- Are we currently within a waiting period for any exchange into
src
?- Yes: ❌ Fail the transaction
- No: Sum the
owing
amounts on all unsettledsynth
exchanges astotalOwing
- Is the user's balance >= amount + totalOwing
- Yes: ✅ Proceed with transfer as usual
- No: ❌ Fail the transaction
- Is the user's balance >= amount + totalOwing
- Are we currently within a waiting period for any exchange into
-
Synth.burnSynths()
invoked byuser
foramount
- Are we currently within a waiting period for any exchange into
sUSD
?- Yes: ❌ Fail the transaction
- No: ✅
- Invoke
settle(src)
- Proceed with the
burn
as per usual
- Invoke
- Are we currently within a waiting period for any exchange into
Copyright
Copyright and related rights waived via CC0.