Created
December 10, 2021 16:04
-
-
Save noahlitvin/43ce903b0a3dd3d9773275f5f796df2e to your computer and use it in GitHub Desktop.
Debt Synthesis Oracle Front-running Simulations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
:'######::'##::: ##:'##::::'##:::::'######::'####:'##::::'##:'##::::'##:'##::::::::::'###::::'########::'#######::'########:: | |
'##... ##: ###:: ##:. ##::'##:::::'##... ##:. ##:: ###::'###: ##:::: ##: ##:::::::::'## ##:::... ##..::'##.... ##: ##.... ##: | |
##:::..:: ####: ##::. ##'##:::::: ##:::..::: ##:: ####'####: ##:::: ##: ##::::::::'##:. ##::::: ##:::: ##:::: ##: ##:::: ##: | |
. ######:: ## ## ##:::. ###:::::::. ######::: ##:: ## ### ##: ##:::: ##: ##:::::::'##:::. ##:::: ##:::: ##:::: ##: ########:: | |
:..... ##: ##. ####::: ## ##:::::::..... ##:: ##:: ##. #: ##: ##:::: ##: ##::::::: #########:::: ##:::: ##:::: ##: ##.. ##::: | |
'##::: ##: ##:. ###:: ##:. ##:::::'##::: ##:: ##:: ##:.:: ##: ##:::: ##: ##::::::: ##.... ##:::: ##:::: ##:::: ##: ##::. ##:: | |
. ######:: ##::. ##: ##:::. ##::::. ######::'####: ##:::: ##:. #######:: ########: ##:::: ##:::: ##::::. #######:: ##:::. ##: | |
:......:::..::::..::..:::::..::::::......:::....::..:::::..:::.......:::........::..:::::..:::::..::::::.......:::..:::::..:: | |
*/ | |
let marketPrices = { | |
'usd': 1, | |
'eth': 4400, | |
'btc': 44000 | |
} | |
function resetPrices() { | |
marketPrices = { | |
'usd': 1, | |
'eth': 4400, | |
'btc': 44000 | |
} | |
} | |
class SynthesisOracle { | |
chains = [] | |
constructor(...chains) { | |
this.chains = chains | |
chains.forEach(c => { | |
c.oracle = this | |
}) | |
} | |
get totalDebtShares() { | |
return this.chains.reduce((prevVal, currVal) => { return currVal.debtShares + prevVal }, 0) | |
} | |
get totalDebt() { // a.k.a totalIssuedSynths | |
return this.chains.reduce((prevVal, currVal) => { return currVal.debt + prevVal }, 0) | |
} | |
} | |
class Chain { | |
oracle = null | |
debtSharesByAddress = {} | |
synths = { | |
'usd': 0, | |
'btc': 0, | |
'eth': 0 | |
} | |
prices = {} | |
// These values may fall out of sync if activity occurs between oracle updates. | |
// This is what we're concerned may be exploitable. | |
totalDebtShares = 0 | |
totalDebt = 0 // a.k.a totalIssuedSynths | |
get debt() { | |
return Object.entries(this.synths).reduce((prevVal, [currencyKey, amount]) => { | |
return prevVal + marketPrices[currencyKey] * amount | |
}, 0) | |
} | |
get debtShares() { | |
return Object.values(this.debtSharesByAddress).reduce((prevVal, currVal) => currVal + prevVal, 0) | |
} | |
getDebtSharesByAddress(address) { | |
return this.debtSharesByAddress[address] || 0 | |
} | |
receiveSynthesisOracleUpdate() { | |
this.totalDebtShares = this.oracle.totalDebtShares | |
this.totalDebt = this.oracle.totalDebt | |
} | |
receivePriceOracleUpdate() { | |
this.prices = Object.assign({}, marketPrices) | |
} | |
mint(staker, amount) { | |
if (this.totalDebtShares == 0) { | |
this.debtSharesByAddress[staker] = amount | |
} else { | |
this.debtSharesByAddress[staker] = this.debtSharesByAddress[staker] ? this.debtSharesByAddress[staker] : 0 | |
this.debtSharesByAddress[staker] += amount * this.totalDebtShares / this.totalDebt | |
} | |
this.synths['usd'] += amount | |
} | |
exchange(from, to, amount) { | |
this.synths[from] -= amount | |
this.synths[to] += this.prices[from] * amount / this.prices[to] | |
if (this.synths[from] < 0) { | |
throw "Can't exchange this much" | |
} | |
} | |
burn(staker, amount) { | |
this.debtSharesByAddress[staker] -= amount * this.totalDebtShares / this.totalDebt | |
this.synths['usd'] -= amount | |
if (this.debtSharesByAddress[staker] < 0 || this.synths['usd'] < 0) { | |
throw "Can't burn this much" | |
} | |
} | |
} | |
/* | |
'########:'########::'######::'########:::::'######:::::'###:::::'######::'########::'######:: | |
... ##..:: ##.....::'##... ##:... ##..:::::'##... ##:::'## ##:::'##... ##: ##.....::'##... ##: | |
::: ##:::: ##::::::: ##:::..::::: ##::::::: ##:::..:::'##:. ##:: ##:::..:: ##::::::: ##:::..:: | |
::: ##:::: ######:::. ######::::: ##::::::: ##:::::::'##:::. ##:. ######:: ######:::. ######:: | |
::: ##:::: ##...:::::..... ##:::: ##::::::: ##::::::: #########::..... ##: ##...:::::..... ##: | |
::: ##:::: ##:::::::'##::: ##:::: ##::::::: ##::: ##: ##.... ##:'##::: ##: ##:::::::'##::: ##: | |
::: ##:::: ########:. ######::::: ##:::::::. ######:: ##:::: ##:. ######:: ########:. ######:: | |
:::..:::::........:::......::::::..:::::::::......:::..:::::..:::......:::........:::......::: | |
*/ | |
function scenario1() { | |
/* | |
In this scenario, a front-runner (staker1) attempts to reduce their debt responsibility | |
by minting on L1 and then quickly minting on L2 before the oracle makes an update to L2. | |
They are not successful. | |
*/ | |
let l1 = new Chain() | |
let l2 = new Chain() | |
let oracle = new SynthesisOracle(l1, l2) | |
let staker1 = '0x001' | |
let staker2 = '0x002' | |
l1.mint(staker2, 200) | |
l1.receiveSynthesisOracleUpdate() | |
l2.receiveSynthesisOracleUpdate() | |
l1.mint(staker1, 50) | |
// We don't receive an oracle update here. This might be concerning because l2.totalDebt and l2.totalDebtShares are temporarily inaccurate. | |
l2.mint(staker1, 150) | |
// staker1 and staker2 should have the same amount of debt shares, since they're responsible for the same proportion of the total debt. | |
const staker1TotalDebtShares = l1.getDebtSharesByAddress(staker1) + l2.getDebtSharesByAddress(staker1) | |
const staker2TotalDebtShares = l1.getDebtSharesByAddress(staker2) + l2.getDebtSharesByAddress(staker2) | |
console.log( | |
staker1TotalDebtShares == staker2TotalDebtShares ? "Success" : "Failure" | |
) | |
} | |
function scenario2() { | |
/* | |
Here we demonstrate that front-running a synthesis oracle update during a price | |
swing is equivalent to front-running a price oracle update if the front-runner | |
is responsible for the entire network's debt. The opportunity scales down based | |
on the proportion of total network debt minted by the front-runner. | |
*/ | |
function yieldFromPriceFrontrun() { | |
/* | |
In this scenario, a staker anticipates the price of BTC doubling before a price | |
oracle update is received by the chain. They receive a risk-free 100% yield. | |
*/ | |
resetPrices() | |
let l1 = new Chain() | |
l1.receivePriceOracleUpdate() | |
let staker = '0x001' | |
const INITIAL_USD = 100 | |
l1.mint(staker, INITIAL_USD) | |
// The staker anticipates an increase in BTC price before the price oracle update. | |
marketPrices['btc'] = marketPrices['btc'] * 2 | |
l1.exchange('usd', 'btc', 100) | |
l1.receivePriceOracleUpdate() | |
// Exchange all of the BTC back to USD... profit! | |
l1.exchange('btc', 'usd', l1.synths['btc']) | |
return (l1.debt - INITIAL_USD) / INITIAL_USD * 100 | |
} | |
function yieldFromSynthesisFrontrun(frontrunnerDebtOwnership) { | |
/* | |
In this scenario, a staker holding BTC who is responsible for | |
`frontrunnerDebtOwnership`% of the debt across both chains anticipates | |
the price of BTC doubling before a synthesis oracle update is received. | |
They receive a risk-free `frontrunnerDebtOwnership`% yield. | |
*/ | |
resetPrices() | |
let l1 = new Chain() | |
let l2 = new Chain() | |
let oracle = new SynthesisOracle(l1, l2) | |
l1.receivePriceOracleUpdate() | |
l2.receivePriceOracleUpdate() | |
let staker1 = '0x001' | |
let staker2 = '0x002' | |
const INITIAL_USD = frontrunnerDebtOwnership | |
l1.mint(staker1, INITIAL_USD) | |
l1.exchange('usd', 'btc', INITIAL_USD) | |
l2.mint(staker2, 100 - INITIAL_USD) | |
l1.receivePriceOracleUpdate() | |
l2.receivePriceOracleUpdate() | |
l1.receiveSynthesisOracleUpdate() | |
l2.receiveSynthesisOracleUpdate() | |
// The price of BTC doubles. Both networks receive a price update. | |
marketPrices['btc'] = marketPrices['btc'] * 2 | |
l1.receivePriceOracleUpdate() | |
l2.receivePriceOracleUpdate() | |
// The synthesis oracle update is delayed! | |
// l1.receiveSynthesisOracleUpdate() | |
// l2.receiveSynthesisOracleUpdate() | |
// The front-runner should need to burn their share of the total debt to exit completely. | |
const amountToExit = l1.debtSharesByAddress[staker1] / oracle.totalDebtShares * oracle.totalDebt | |
// Instead, the front-runner is able to completely exit the network with their initial investment, even though the total network debt has increased. | |
l1.exchange('btc', 'usd', l1.synths['btc']) | |
l1.burn(staker1, INITIAL_USD) | |
return (amountToExit - INITIAL_USD) / INITIAL_USD * 100 | |
} | |
console.log( | |
yieldFromPriceFrontrun() == yieldFromSynthesisFrontrun(100) ? "Success" : "Failure" | |
) | |
console.log( | |
yieldFromSynthesisFrontrun(100) == yieldFromSynthesisFrontrun(50) * 2 ? "Success" : "Failure" | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment