Skip to content

Instantly share code, notes, and snippets.

@noahlitvin
Created December 10, 2021 16:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save noahlitvin/43ce903b0a3dd3d9773275f5f796df2e to your computer and use it in GitHub Desktop.
Save noahlitvin/43ce903b0a3dd3d9773275f5f796df2e to your computer and use it in GitHub Desktop.
Debt Synthesis Oracle Front-running Simulations
/*
:'######::'##::: ##:'##::::'##:::::'######::'####:'##::::'##:'##::::'##:'##::::::::::'###::::'########::'#######::'########::
'##... ##: ###:: ##:. ##::'##:::::'##... ##:. ##:: ###::'###: ##:::: ##: ##:::::::::'## ##:::... ##..::'##.... ##: ##.... ##:
##:::..:: ####: ##::. ##'##:::::: ##:::..::: ##:: ####'####: ##:::: ##: ##::::::::'##:. ##::::: ##:::: ##:::: ##: ##:::: ##:
. ######:: ## ## ##:::. ###:::::::. ######::: ##:: ## ### ##: ##:::: ##: ##:::::::'##:::. ##:::: ##:::: ##:::: ##: ########::
:..... ##: ##. ####::: ## ##:::::::..... ##:: ##:: ##. #: ##: ##:::: ##: ##::::::: #########:::: ##:::: ##:::: ##: ##.. ##:::
'##::: ##: ##:. ###:: ##:. ##:::::'##::: ##:: ##:: ##:.:: ##: ##:::: ##: ##::::::: ##.... ##:::: ##:::: ##:::: ##: ##::. ##::
. ######:: ##::. ##: ##:::. ##::::. ######::'####: ##:::: ##:. #######:: ########: ##:::: ##:::: ##::::. #######:: ##:::. ##:
:......:::..::::..::..:::::..::::::......:::....::..:::::..:::.......:::........::..:::::..:::::..::::::.......:::..:::::..::
*/
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