Skip to content

Commit

Permalink
Merge pull request #3 from superfluid-finance/dynamic_gas_price_limit
Browse files Browse the repository at this point in the history
Dynamic (per-flow) gas price limit
  • Loading branch information
ngmachado authored Dec 27, 2024
2 parents 87bc5d4 + d00ad8c commit 0260195
Show file tree
Hide file tree
Showing 7 changed files with 461 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# private key of the account to be used for liquidation transactions
PRIVATE_KEY=...

# coingecko API needed in order to update token prices
COINGECKO_API_KEY=...
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ Make sure `grt.ts` is executable.

See `./grt.ts --help` for more config options.

## Gas price strategy

Graphinator can be configured to make sound decisions despite gas price swings, with the following priorities:
- don't let high-value flows slip through
- don't let gas price spikes drain the sender account
- don't let dust flows open forever

This is achieved by using 3 configuration parameters:
- _reference gas price limit_: this is the limit which should be applied to a reference flow with a flowrate worth 1$ per day. For all flows with known value (meaning, the token price is known to the application) the limit is set proportionally to this reference value.
E.g. if this limit were set to 1 gwei and a flow worth 1000 $/day were critical, graphinator would bid up to 1000 gwei in order to liquidate it.
This proportionality makes sure that an attacker couldn't take advantage of gas price spikes with a times insolvency.
- _fallback gas price limit_: if the token price and thus the value of the flow isn't known, this limit is applied. It will usually be set higher than the reference limit, but still low enough to not risk the account being drained for likely not very urgent liquidations (assuming it's mostly less important tokens we don't know the price for).
- _minimum gas price limit_: since most tokens don't have a minimum deposit set, there can be a lot of "dust streams" out there which are never really worth liquidating.
In case we won't those liquidated anyway, setting this value to a value which is at least occasionally above the chain's gas price makes sure those flows don't linger on insolvent forever.

Limitation: the current implementation does not take into account the min deposit setting of a token.

Token prices are taken from `data/token_prices.json` which can be updated running `bun utils/update-token-prices.js` (you need to provide an env var `COINGECKO_API_KEY`). This prices don't need to be very accurate thus don't need frequent updating.

## License

[MIT](https://choosealicense.com/licenses/mit/)
135 changes: 135 additions & 0 deletions data/token_prices.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
{
"xdai-mainnet": {
"0x59988e47A3503AaFaA0368b9deF095c818Fdca01": 1,
"0x2bf2ba13735160624a0feae98f6ac8f70885ea61": 17195.16,
"0x63e62989d9eb2d37dfdb1f93a22f063635b07d51": 0.00212553,
"0x1234756ccf0660e866305289267211823ae86eec": 0.996915,
"0x7aeca73f38f8f33ab7ff067fed1268384d12324d": 0.01554628,
"0x9757d68a4273635c69d93b84ee3cdac2304dd467": 3946.9,
"0xac945dc430cef2bb8fb78b3cb5fbc936dd2c8ab6": 0.775939,
"0xc0712524b39323eb2437e69226b261d928629dc8": 3.27,
"0xd30aa2f6fc32c4dc8bf3bd5994d89c8f9e81fcd1": 1.14,
"0xe83fd17028c2dd3ca4a9b75f2836d4558fe00686": 0.647644
},
"polygon-mainnet": {
"0x3aD736904E9e65189c3000c7DD2c8AC8bB7cD4e3": 0.578071,
"0x263026e7e53dbfdce5ae55ade22493f828922965": 0.00196513,
"0x00fa3405a6bbd94549f3b4855a9736766c4f237e": 1,
"0x02ef6868d69707b6093a2962bb5fe5661fcc0deb": 1.071,
"0x07b24bbd834c1c546ece89ff95f71d9f13a2ebd1": 1.001,
"0x12c294107772b10815307c05989dabd71c21670e": 0.646793,
"0x1305f6b6df9dc47159d12eb7ac2804d4a33173c2": 1,
"0x1adca32b906883e474aebcba5708b41f3645f941": 0.114399,
"0x1bd951a9c9a5ec665abef684425f025dc8738cca": 0.217891,
"0x229c5d13452dc302499b5c113768a0db0c9d5c05": 0.04447279,
"0x27e1e4e6bc79d93032abef01025811b7e4727e85": 3948.18,
"0x2c530af1f088b836fa0dca23c7ea50e669508c4c": 1805.49,
"0x3038b359240dff5ccd42dffd21f12b428034be38": 1.05,
"0x32cefdf2b3df73bdebaa7cd3b0135b3a79d28dcc": 0.128545,
"0x3d9cc088bd9357e5941b68d26d6d09254a69949d": 0.02961529,
"0x4086ebf75233e8492f1bcda41c7f2a8288c2fb92": 106304,
"0x48a7908771c752aacf5cd0088dad0a0daaea3716": 0.01160509,
"0x49765f8fcf0a1cd4f98da906f0974a9085d43e51": 0.74182,
"0x4bde23854e7c81218463f6c8f331b46144e98eac": 0.983393,
"0x5d6fdc854b46e8b237bd2ccc2714cfa3d18cf58e": 282.64,
"0x5e31d5bdd6c87edff8659d9ead9ce0013fb47184": 1.055,
"0x61a7b6f0a7737d9bd38fdeaf1d4160e16bf23043": 0.00248786,
"0x6328c1c2e258a314bdac5227e9c7d212312297ad": 1.3,
"0x673d41ebc2b499d3545e5a0309124cd94eda3fb0": 0.00150573,
"0x72a9bae5ce6de9816aadcbc24daa09f5d169a980": 1.056,
"0x7f078f02a77e91e67ee592faed23d1cfcb390a60": 0.0475671,
"0x96eac3913bab431c28895f02cf5c56ad2dab8439": 1.14,
"0x992446b88a7e62c7235bd88108f44543c1887c1f": 0.996278,
"0xa1bd23b582c12c22e5e264a0a69847ca0ed9f2b0": 28.11,
"0xb0512060ee623a656da1f25686743474228ba0e6": 1.53,
"0xb63e38d21b31719e6df314d3d2c351df0d4a9162": 0.448186,
"0xb683fb34a77c06931ba62d804252d1f60596a36a": 0.01025227,
"0xc3fa1e27a2a58ee476f04d4f4867f06b6e906cc5": 0.00228216,
"0xcaa7349cea390f89641fe306d93591f87595dc1f": 1,
"0xcae73e9eee8a01b8b7f94b59133e3821f21470ab": 0.146418,
"0xcb5676568febb4e4f0dca9407318836e7a973183": 9.59,
"0xd89c35b586eadfbde1a3b2d36fb5746c6d3601bc": 1.42,
"0xe04ad5d86c40d53a12357e1ba2a9484f60db0da5": 0.577741,
"0xe0d1978033ee7cf12a8246da7f173f9441b769b0": 0.00529792,
"0xe2d04ab74eed9627c828b3fc10e5fc96fae70348": 0.315083,
"0xfac83774854237b6e31c4b051b91015e403956d3": 0.218918,
"0xfbb291570de4b87353b1e0f586df97a1ed856470": 0.00679081,
"0xfd0577c4707367ff9b637f219388919d3be37592": 2.23
},
"optimism-mainnet": {
"0x4ac8bD1bDaE47beeF2D1c6Aa62229509b962Aa0d": 3948.48,
"0x1828bff08bd244f7990eddcd9b19cc654b33cdb4": 2.39,
"0x35adeb0638eb192755b6e52544650603fe65a006": 1.001,
"0x4cab5b9930210e2edc6a905b9c75d615872a1a7e": 0.00822256,
"0x7d342726b69c28d942ad8bfe6ac81b972349d524": 1,
"0x8430f084b939208e2eded1584889c9a66b90562f": 1,
"0x9638ec1d29dfa9835fdb7fa74b5b77b14d6ac77e": 106330,
"0x9f41d0aa24e599fd8d0c180ee3c0f609dc41c622": 1.002
},
"arbitrum-one": {
"0xe6C8d111337D0052b9D88BF5d7D55B7f8385ACd3": 3948.48,
"0x1dbc1809486460dcd189b8a15990bca3272ee04e": 1,
"0x22389dd0df30487a8feaa4eebf98cc64d3273294": 15.66,
"0x521677a61d101a80ce0fb903b13cb485232774ee": 0.999562,
"0x95f1ee4cb6dc16136a79d367a010add361e5192c": 1.055,
"0xb3edb2f90fec1bf1f872a9ef143cfd614773ad04": 0.977141,
"0xefa54be8d63fd0d95edd7965d0bd7477c33995a8": 14.53,
"0xfc55f2854e74b4f42d01a6d3daac4c52d9dfdcff": 1.001
},
"avalanche-c": {
"0xBE916845D8678b5d2F7aD79525A62D7c08ABba7e": 49.01,
"0xc0fbc4967259786c743361a5885ef49380473dcf": 0.163305,
"0x288398f314d472b82c44855f3f6ff20b633c2a97": 1.001,
"0x6af916e2001bc4935e6d2f256363ed54eb8e20e0": 1.055,
"0x7cd00c2b9a78f270b897457ab070274e4a17de83": 1.001,
"0xa60c5bebccdb9738f63891bbdd7fec3e762f9098": 0.510835,
"0xdf37ee57b2efd215a6a8329d6b8b72064f09bbd0": 1
},
"bsc-mainnet": {
"0x529A4116F160c833c61311569D6B33dFF41fD657": 728.78,
"0x0419e1fa3671754f77ec7d5416219a5f9a08b530": 0.999725,
"0x744786ab00ed5a0b77ca754eb6f3ec0607c7fa79": 0.00902639
},
"eth-mainnet": {
"0xC22BeA0Be9872d8B7B3933CEc70Ece4D53A900da": 3948.48,
"0x1ba8603da702602a8657980e825a6daa03dee93a": 1.001,
"0x3fa59ea0eee311782c5a5062da1fabd1a70e1d6d": 0.00529792,
"0x479347dfd0be56f2a5f7bb1506bfd7ab24d4ba26": 0.061749,
"0x4f228bf911ed67730e4b51b1f82ac291b49053ee": 1,
"0x7367ab6df2af082f04298196167f37f1e1629d1e": 1.001,
"0x8f6f22a962899ade3c46627df45d4a05622cebf2": 0.00506274,
"0xd70408b34ed121722631d647d37c4e6641ec363d": 1
},
"celo-mainnet": {
"0x671425Ae1f272Bc6F79beC3ed5C4b00e9c628240": 0.809481,
"0x62b8b11039fcfe5ab0c56e502b1c372a3d2a9c7a": 0.00005154,
"0x3acb9a08697b6db4cd977e8ab42b6f24722e6d6e": 1.003,
"0x51cacc88227a038cb6083a7870daf7fa3ebd906c": 3956.35,
"0x7a5f9c3e43aadc62647ab5d41802db33dc7d8c4b": 0.163491,
"0xd9f9a02e49225c7ab5b40fce8d44d256b0e984fb": 1.051
},
"base-mainnet": {
"0x46fd5cfB4c12D87acD3a13e92BAa53240C661D93": 3948.48,
"0xc0fbc4967259786c743361a5885ef49380473dcf": 0.163305,
"0x04ffb9ce95af003a3aa3611be6c6ca1431151fb5": 0.00065766,
"0x09b1ad979d093377e201d804fa9ac0a9a07cfb0b": 1.9,
"0x1eff3dd78f4a14abfa9fa66579bd3ce9e1b30529": 0.01387201,
"0x304989da2adc80a6568170567d477af5e48dbaae": 4165,
"0x307d8225c6428a1e505f824b1c97cf9127351f0c": 0.0000124,
"0x4db26c973fae52f43bd96a8776c2bf1b0dc29556": 1,
"0x4e395ec7b71dd87a23dd836edb3efe15a6c2002b": 0.01611749,
"0x58122a048878f25c8c5d4b562419500ed74c6f75": 4682.13,
"0x5f2fab273f1f64b6bc6ab8f35314cd21501f35c5": 0.02309759,
"0x65e9b4ae7885bd3e4e1cc4f280863b859d231fc2": 0.0000353,
"0x708169c8c87563ce904e0a7f3bfc1f3b0b767f41": 0.999927,
"0x7d2e87324c9b2cc983804fe53df67f1add3f913c": 1.012,
"0x7ef392131c3ab326016cf7b560f96c91f4f9d4fa": 0.00166606,
"0x8414ab8c70c7b16a46012d49b8111959baf2fc42": 0.00000167,
"0x9097e4a4d75a611b65ab21d98a7d5b1177c050f7": 0.02490377,
"0xcc6bce523c20f582daaf0ccafaae981df46ceb41": 0.00122015,
"0xd04383398dd2426297da660f9cca3d439af9ce1b": 1.001,
"0xdfd428908909cb5e24f5e79e6ad6bde10bdf2327": 106770,
"0xe58267cd7299c29a1b77f4e66cd12dd24a2cd2fd": 0.02690319,
"0xefbe11336b0008dce3797c515e6457cc4841645c": 0.00066834
}
}
24 changes: 19 additions & 5 deletions grt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,23 @@ const argv = await yargs(hideBin(process.argv))
description: 'Address of the Super Token to process. If not set, all "listed" (curated) Super Tokens will be processed',
default: process.env.TOKEN
})
.option('maxGasPriceMwei', {
.option('referenceGasPriceLimitMwei', {
alias: 'r',
type: 'number',
description: 'Set the reference gas price limit in mwei (milli wei) - limit which should be applied to a flow with a daily flowrate worth 1$. Default: 1000 (1 gwei)',
default: process.env.REFERENCE_GAS_PRICE_LIMIT_MWEI ? parseInt(process.env.REFERENCE_GAS_PRICE_LIMIT_MWEI) : 1000
})
.option('fallbackGasPriceLimitMwei', {
alias: 'f',
type: 'number',
description: 'Set the fallback gas price limit in mwei (milli wei) - used for flows with unknown token price. Default: 10 x referenceGasPriceLimit',
default: process.env.FALLBACK_GAS_PRICE_LIMIT_MWEI ? parseInt(process.env.FALLBACK_GAS_PRICE_LIMIT_MWEI) : undefined
})
.option('minGasPriceLimitMwei', {
alias: 'm',
type: 'number',
description: 'Set the max gas price in mwei (milli wei). Default: 10000 (10 gwei)',
default: process.env.MAX_GAS_PRICE_MWEI ? parseInt(process.env.MAX_GAS_PRICE_MWEI) : 10000
description: 'Set the minimum gas price limit in mwei (milli wei) - used to prevent dust streams to persist forever. Default: 0.1 x referenceGasPriceLimit',
default: process.env.MIN_GAS_PRICE_LIMIT_MWEI ? parseInt(process.env.MIN_GAS_PRICE_LIMIT_MWEI) : undefined
})
.option('gasMultiplier', {
alias: 'g',
Expand Down Expand Up @@ -67,9 +79,11 @@ const batchSize = argv.batchSize;
const gasMultiplier = argv.gasMultiplier;
const token = argv.token;
const loop = argv.loop;
const maxGasPrice = argv.maxGasPriceMwei * 1000000;
const referenceGasPriceLimit = argv.referenceGasPriceLimitMwei * 1000000;
const fallbackGasPriceLimit = argv.fallbackGasPriceLimitMwei ? argv.fallbackGasPriceLimitMwei * 1000000 : referenceGasPriceLimit * 10;
const minGasPriceLimit = argv.minGasPriceLimitMwei ? argv.minGasPriceLimitMwei * 1000000 : referenceGasPriceLimit * 0.1;

const ghr = new Graphinator(network, batchSize, gasMultiplier, maxGasPrice);
const ghr = new Graphinator(network, batchSize, gasMultiplier, referenceGasPriceLimit, fallbackGasPriceLimit, minGasPriceLimit);
if(loop) {
const executeLiquidations = async () => {
try {
Expand Down
19 changes: 13 additions & 6 deletions src/datafetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,17 @@ class DataFetcher {
async getFlowsToLiquidate(token: AddressLike, gdaForwarder: Contract, depositConsumedPctThreshold: number): Promise<Flow[]> {

const returnData: Flow[] = [];
const criticalAccounts = await this.getCriticalAccountsByTokenNow(token);
const criticalAccounts = await this.getCriticalAccountsByTokenNow(token, process.env.LIQUIDATE_ALL ? false : true);
const targetToken = new Contract(token.toString(), ISuperTokenAbi, this.provider);

if (criticalAccounts.length > 0) {
console.log(`Found ${criticalAccounts.length} maybe-critical accounts`);
for (const account of criticalAccounts) {
// ONLY_ACCOUNT is just for testing
if (process.env.ONLY_ACCOUNT && account.account.id !== process.env.ONLY_ACCOUNT) {
continue;
}
// TODO: totalNetFlowrate seems to only be CFA.
log(`? Probing ${account.account.id} token ${account.token.id} net fr ${account.totalNetFlowRate} cfa net fr ${account.totalCFANetFlowRate} gda net fr ${await gdaForwarder.getNetFlow(account.token.id, account.account.id)}`);
const rtb = await targetToken.realtimeBalanceOfNow(account.account.id);
const { availableBalance, deposit } = rtb;
Expand All @@ -54,7 +60,7 @@ class DataFetcher {
const cfaNetFlowRate = BigInt(account.totalCFANetFlowRate);
const gdaNetFlowRate = await gdaForwarder.getNetFlow(account.token.id, account.account.id);
let netFlowRate = cfaNetFlowRate + gdaNetFlowRate;
if (netFlowRate >= ZERO) {
if (!process.env.LIQUIDATE_ALL && netFlowRate >= ZERO) {
continue;
}
log(`! Critical ${account.account.id} token ${account.token.id} net fr ${netFlowRate} (cfa ${cfaNetFlowRate} gda ${gdaNetFlowRate})`);
Expand All @@ -72,7 +78,7 @@ class DataFetcher {
});
netFlowRate += BigInt(flow.currentFlowRate);
processedCFAFlows++;
if (netFlowRate >= ZERO) {
if (!process.env.LIQUIDATE_ALL && netFlowRate >= ZERO) {
break;
}
}
Expand All @@ -91,7 +97,7 @@ class DataFetcher {
});
netFlowRate += BigInt(flow.pool.flowRate);
processedGDAFlows++;
if (netFlowRate >= BigInt(0)) {
if (!process.env.LIQUIDATE_ALL && netFlowRate >= BigInt(0)) {
break;
}
}
Expand Down Expand Up @@ -167,15 +173,16 @@ class DataFetcher {
* @param token - The address of the token.
* @returns A promise that resolves to an array of critical accounts.
*/
async getCriticalAccountsByTokenNow(token: AddressLike): Promise<CriticalAccount[]> {
async getCriticalAccountsByTokenNow(token: AddressLike, onlyNegativeNetFlowrate: boolean = true): Promise<CriticalAccount[]> {
console.log(`Getting critical accounts for token ${token} with onlyNegativeNetFlowrate ${onlyNegativeNetFlowrate}`);
const _tokenLowerCase = token.toString().toLowerCase();
const timestamp = Math.floor(Date.now() / 1000);
return this._queryAllPages(
(lastId: string) => `{
accountTokenSnapshots (first: ${MAX_ITEMS},
where: {
id_gt: "${lastId}",
totalNetFlowRate_lt: 0,
${onlyNegativeNetFlowrate ? 'totalNetFlowRate_lt: 0,' : ''}
maybeCriticalAtTimestamp_lt: ${timestamp}
token: "${_tokenLowerCase}"
}
Expand Down
Loading

0 comments on commit 0260195

Please sign in to comment.