[contract_manager] Add logic for tracking fee denominations and dollar values (#1394)

* tokens

* progress

* progress

* progress

* infra for storing tokens and using them in fee calculations

* precommit

* cleanup

* cleanup

* fix
This commit is contained in:
Jayant Krishnamurthy 2024-03-28 06:26:04 -07:00 committed by GitHub
parent 6fb5ab483d
commit 0f7a9cc334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 252 additions and 30 deletions

View File

@ -21,8 +21,8 @@
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
},
"dependencies": {
"@coral-xyz/anchor": "^0.29.0",
"@certusone/wormhole-sdk": "^0.9.8",
"@coral-xyz/anchor": "^0.29.0",
"@injectivelabs/networks": "1.0.68",
"@mysten/sui.js": "^0.49.1",
"@pythnetwork/cosmwasm-deploy-tools": "*",
@ -31,6 +31,7 @@
"@pythnetwork/pyth-sui-js": "*",
"@types/yargs": "^17.0.32",
"aptos": "^1.5.0",
"axios": "^0.24.0",
"bs58": "^5.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"

View File

@ -19,6 +19,18 @@ const parser = yargs(hideBin(process.argv))
async function main() {
const argv = await parser.argv;
const prices: Record<string, number> = {};
for (const token of Object.values(DefaultStore.tokens)) {
const price = await token.getPriceForMinUnit();
// We're going to ignore the value of tokens that aren't configured
// in the store -- these are likely not worth much anyway.
if (price !== undefined) {
prices[token.id] = price;
}
}
let totalFeeUsd = 0;
for (const contract of Object.values(DefaultStore.contracts)) {
if (contract.getChain().isMainnet() === argv.testnet) continue;
if (
@ -27,12 +39,26 @@ async function main() {
contract instanceof CosmWasmPriceFeedContract
) {
try {
console.log(`${contract.getId()} ${await contract.getTotalFee()}`);
const fee = await contract.getTotalFee();
let feeUsd = 0;
if (fee.denom !== undefined && prices[fee.denom] !== undefined) {
feeUsd = Number(fee.amount) * prices[fee.denom];
totalFeeUsd += feeUsd;
console.log(
`${contract.getId()} ${fee.amount} ${fee.denom} ($${feeUsd})`
);
} else {
console.log(
`${contract.getId()} ${fee.amount} ${fee.denom} ($ value unknown)`
);
}
} catch (e) {
console.error(`Error fetching fees for ${contract.getId()}`, e);
}
}
}
console.log(`Total fees in USD: $${totalFeeUsd}`);
}
main();

View File

@ -23,10 +23,12 @@ import { Network } from "@injectivelabs/networks";
import { SuiClient } from "@mysten/sui.js/client";
import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519";
import { TransactionObject } from "web3/eth/types";
import { TokenId } from "./token";
export type ChainConfig = Record<string, string> & {
mainnet: boolean;
id: string;
nativeToken: TokenId;
};
export abstract class Chain extends Storable {
public wormholeChainName: ChainName;
@ -37,12 +39,14 @@ export abstract class Chain extends Storable {
* @param mainnet whether this chain is mainnet or testnet/devnet
* @param wormholeChainName the name of the wormhole chain that this chain is associated with.
* Note that pyth has included additional chain names and ids to the wormhole spec.
* @param nativeToken the id of the token used to pay gas on this chain
* @protected
*/
protected constructor(
protected id: string,
protected mainnet: boolean,
wormholeChainName: string
wormholeChainName: string,
protected nativeToken: TokenId | undefined
) {
super();
this.wormholeChainName = wormholeChainName as ChainName;
@ -65,6 +69,10 @@ export abstract class Chain extends Storable {
return this.mainnet;
}
public getNativeToken(): TokenId | undefined {
return this.nativeToken;
}
/**
* Returns the payload for a governance SetFee instruction for contracts deployed on this chain
* @param fee the new fee to set
@ -125,7 +133,7 @@ export abstract class Chain extends Storable {
export class GlobalChain extends Chain {
static type = "GlobalChain";
constructor() {
super("global", true, "unset");
super("global", true, "unset", undefined);
}
generateGovernanceUpgradePayload(): Buffer {
@ -163,12 +171,13 @@ export class CosmWasmChain extends Chain {
id: string,
mainnet: boolean,
wormholeChainName: string,
nativeToken: TokenId | undefined,
public endpoint: string,
public gasPrice: string,
public prefix: string,
public feeDenom: string
) {
super(id, mainnet, wormholeChainName);
super(id, mainnet, wormholeChainName, nativeToken);
}
static fromJson(parsed: ChainConfig): CosmWasmChain {
@ -180,7 +189,8 @@ export class CosmWasmChain extends Chain {
parsed.endpoint,
parsed.gasPrice,
parsed.prefix,
parsed.feeDenom
parsed.feeDenom,
parsed.nativeToken
);
}
@ -248,9 +258,10 @@ export class SuiChain extends Chain {
id: string,
mainnet: boolean,
wormholeChainName: string,
nativeToken: TokenId | undefined,
public rpcUrl: string
) {
super(id, mainnet, wormholeChainName);
super(id, mainnet, wormholeChainName, nativeToken);
}
static fromJson(parsed: ChainConfig): SuiChain {
@ -259,6 +270,7 @@ export class SuiChain extends Chain {
parsed.id,
parsed.mainnet,
parsed.wormholeChainName,
parsed.nativeToken,
parsed.rpcUrl
);
}
@ -314,11 +326,12 @@ export class EvmChain extends Chain {
constructor(
id: string,
mainnet: boolean,
nativeToken: TokenId | undefined,
private rpcUrl: string,
private networkId: number
) {
// On EVM networks we use the chain id as the wormhole chain name
super(id, mainnet, id);
super(id, mainnet, id, nativeToken);
}
static fromJson(parsed: ChainConfig & { networkId: number }): EvmChain {
@ -326,6 +339,7 @@ export class EvmChain extends Chain {
return new EvmChain(
parsed.id,
parsed.mainnet,
parsed.nativeToken,
parsed.rpcUrl,
parsed.networkId
);
@ -468,9 +482,10 @@ export class AptosChain extends Chain {
id: string,
mainnet: boolean,
wormholeChainName: string,
nativeToken: TokenId | undefined,
public rpcUrl: string
) {
super(id, mainnet, wormholeChainName);
super(id, mainnet, wormholeChainName, nativeToken);
}
getClient(): AptosClient {
@ -508,6 +523,7 @@ export class AptosChain extends Chain {
parsed.id,
parsed.mainnet,
parsed.wormholeChainName,
parsed.nativeToken,
parsed.rpcUrl
);
}

View File

@ -3,6 +3,7 @@ import { ApiError, BCS, CoinClient, TxnBuilderTypes } from "aptos";
import { AptosChain, Chain } from "../chains";
import { DataSource } from "xc_admin_common";
import { WormholeContract } from "./wormhole";
import { TokenQty } from "../token";
type WormholeState = {
chain_id: { number: string };
@ -91,7 +92,11 @@ export class AptosPriceFeedContract extends PriceFeedContract {
static fromJson(
chain: Chain,
parsed: { type: string; stateId: string; wormholeStateId: string }
parsed: {
type: string;
stateId: string;
wormholeStateId: string;
}
): AptosPriceFeedContract {
if (parsed.type !== AptosPriceFeedContract.type)
throw new Error("Invalid type");
@ -260,9 +265,13 @@ export class AptosPriceFeedContract extends PriceFeedContract {
return AptosPriceFeedContract.type;
}
async getTotalFee(): Promise<bigint> {
async getTotalFee(): Promise<TokenQty> {
const client = new CoinClient(this.chain.getClient());
return await client.checkBalance(this.stateId);
const amount = await client.checkBalance(this.stateId);
return {
amount,
denom: this.chain.getNativeToken(),
};
}
async getValidTimePeriod() {

View File

@ -17,6 +17,7 @@ import {
TxResult,
} from "../base";
import { WormholeContract } from "./wormhole";
import { TokenQty } from "../token";
/**
* Variables here need to be snake case to match the on-chain contract configs
@ -332,13 +333,16 @@ export class CosmWasmPriceFeedContract extends PriceFeedContract {
return this.chain;
}
async getTotalFee(): Promise<bigint> {
async getTotalFee(): Promise<TokenQty> {
const client = await CosmWasmClient.connect(this.chain.endpoint);
const coin = await client.getBalance(
this.address,
this.getChain().feeDenom
);
return BigInt(coin.amount);
return {
amount: BigInt(coin.amount),
denom: this.chain.getNativeToken(),
};
}
async getValidTimePeriod() {

View File

@ -5,6 +5,7 @@ import { PriceFeedContract, PrivateKey, Storable } from "../base";
import { Chain, EvmChain } from "../chains";
import { DataSource, EvmExecute } from "xc_admin_common";
import { WormholeContract } from "./wormhole";
import { TokenQty } from "../token";
// Just to make sure tx gas limit is enough
const EXTENDED_ENTROPY_ABI = [
@ -724,9 +725,13 @@ export class EvmPriceFeedContract extends PriceFeedContract {
return Web3.utils.keccak256(strippedCode);
}
async getTotalFee(): Promise<bigint> {
async getTotalFee(): Promise<TokenQty> {
const web3 = new Web3(this.chain.getRpcUrl());
return BigInt(await web3.eth.getBalance(this.address));
const amount = BigInt(await web3.eth.getBalance(this.address));
return {
amount,
denom: this.chain.getNativeToken(),
};
}
async getLastExecutedGovernanceSequence() {

View File

@ -13,6 +13,7 @@ import {
EvmPriceFeedContract,
SuiPriceFeedContract,
} from "./contracts";
import { Token } from "./token";
import { PriceFeedContract, Storable } from "./base";
import { parse, stringify } from "yaml";
import { readdirSync, readFileSync, statSync, writeFileSync } from "fs";
@ -22,11 +23,13 @@ export class Store {
public chains: Record<string, Chain> = { global: new GlobalChain() };
public contracts: Record<string, PriceFeedContract> = {};
public entropy_contracts: Record<string, EvmEntropyContract> = {};
public tokens: Record<string, Token> = {};
public vaults: Record<string, Vault> = {};
constructor(public path: string) {
this.loadAllChains();
this.loadAllContracts();
this.loadAllTokens();
this.loadAllVaults();
}
@ -143,6 +146,20 @@ export class Store {
});
}
loadAllTokens() {
this.getYamlFiles(`${this.path}/tokens/`).forEach((yamlFile) => {
const parsedArray = parse(readFileSync(yamlFile, "utf-8"));
for (const parsed of parsedArray) {
if (parsed.type !== Token.type) return;
const token = Token.fromJson(parsed);
if (this.tokens[token.getId()])
throw new Error(`Multiple tokens with id ${token.getId()} found`);
this.tokens[token.getId()] = token;
}
});
}
loadAllVaults() {
this.getYamlFiles(`${this.path}/vaults/`).forEach((yamlFile) => {
const parsedArray = parse(readFileSync(yamlFile, "utf-8"));

View File

@ -0,0 +1,81 @@
import axios from "axios";
import { KeyValueConfig, Storable } from "./base";
export type TokenId = string;
/**
* A quantity of a token, represented as an integer number of the minimum denomination of the token.
* This can also represent a quantity of an unknown token (represented by an undefined denom).
*/
export type TokenQty = {
amount: bigint;
denom: TokenId | undefined;
};
/**
* A token represents a cryptocurrency like ETH or BTC.
* The main use of this class is to calculate the dollar value of accrued fees.
*/
export class Token extends Storable {
static type = "token";
public constructor(
public id: TokenId,
// The hexadecimal pyth id of the tokens X/USD price feed
// (get this from hermes or the Pyth docs page)
public pythId: string | undefined,
public decimals: number
) {
super();
}
getId(): TokenId {
return this.id;
}
getType(): string {
return Token.type;
}
/**
* Get the dollar value of 1 token. Returns undefined for tokens that do
* not have a configured pricing method.
*/
async getPrice(): Promise<number | undefined> {
if (this.pythId) {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids%5B%5D=${this.pythId}&parsed=true`;
const response = await axios.get(url);
const price = response.data.parsed[0].price;
// Note that this conversion can lose some precision.
// We don't really care about that in this application.
return parseInt(price.price) * Math.pow(10, price.expo);
} else {
// We may support other pricing methodologies in the future but whatever.
return undefined;
}
}
/**
* Get the dollar value of the minimum representable quantity of this token.
* E.g., for ETH, this method will return the dollar value of 1 wei.
*/
async getPriceForMinUnit(): Promise<number | undefined> {
const price = await this.getPrice();
return price ? price / Math.pow(10, this.decimals) : undefined;
}
toJson(): KeyValueConfig {
return {
id: this.id,
...(this.pythId !== undefined ? { pythId: this.pythId } : {}),
};
}
static fromJson(parsed: {
id: string;
pythId?: string;
decimals: number;
}): Token {
return new Token(parsed.id, parsed.pythId, parsed.decimals);
}
}

View File

@ -8,6 +8,7 @@
mainnet: true
rpcUrl: https://fullnode.mainnet.aptoslabs.com/v1
type: AptosChain
nativeToken: APT
- id: movement_move_devnet
wormholeChainName: movement_move_devnet
mainnet: false

View File

@ -13,6 +13,7 @@
rpcUrl: https://evmos-evm.publicnode.com
networkId: 9001
type: EvmChain
nativeToken: EVMOS
- id: canto
mainnet: true
rpcUrl: https://canto.slingshot.finance
@ -68,6 +69,7 @@
rpcUrl: https://rpc.gnosischain.com
networkId: 100
type: EvmChain
nativeToken: DAI
- id: fantom_testnet
mainnet: false
rpcUrl: https://fantom-testnet.blastapi.io/$ENV_BLAST_API_KEY
@ -83,6 +85,7 @@
rpcUrl: https://rpc.ankr.com/fantom
networkId: 250
type: EvmChain
nativeToken: FTM
- id: mumbai
mainnet: false
rpcUrl: https://polygon-testnet.blastapi.io/$ENV_BLAST_API_KEY
@ -108,6 +111,7 @@
rpcUrl: https://rpc.mantle.xyz/
networkId: 5000
type: EvmChain
nativeToken: MNT
- id: kava_testnet
mainnet: false
rpcUrl: https://evm.testnet.kava.io
@ -128,6 +132,7 @@
rpcUrl: https://eth-mainnet.blastapi.io/$ENV_BLAST_API_KEY
networkId: 1
type: EvmChain
nativeToken: ETH
- id: bsc_testnet
mainnet: false
rpcUrl: https://rpc.ankr.com/bsc_testnet_chapel
@ -143,6 +148,7 @@
rpcUrl: https://mainnet.aurora.dev
networkId: 1313161554
type: EvmChain
nativeToken: NEAR
- id: bsc
mainnet: true
rpcUrl: https://rpc.ankr.com/bsc
@ -178,6 +184,7 @@
rpcUrl: https://polygon-rpc.com
networkId: 137
type: EvmChain
nativeToken: MATIC
- id: wemix_testnet
mainnet: false
rpcUrl: https://api.test.wemix.com
@ -188,11 +195,13 @@
rpcUrl: https://rpc-mainnet.kcc.network
networkId: 321
type: EvmChain
nativeToken: KCS
- id: polygon_zkevm
mainnet: true
rpcUrl: https://zkevm-rpc.com
networkId: 1101
type: EvmChain
nativeToken: ETH
- id: celo_alfajores_testnet
mainnet: false
rpcUrl: https://alfajores-forno.celo-testnet.org
@ -208,21 +217,25 @@
rpcUrl: https://zksync2-mainnet.zksync.io
networkId: 324
type: EvmChain
nativeToken: ETH
- id: base
mainnet: true
rpcUrl: https://developer-access-mainnet.base.org/
networkId: 8453
type: EvmChain
nativeToken: ETH
- id: arbitrum
mainnet: true
rpcUrl: https://arb1.arbitrum.io/rpc
networkId: 42161
type: EvmChain
nativeToken: ETH
- id: optimism
mainnet: true
rpcUrl: https://rpc.ankr.com/optimism
networkId: 10
type: EvmChain
nativeToken: ETH
- id: kcc_testnet
mainnet: false
rpcUrl: https://rpc-testnet.kcc.network
@ -243,6 +256,7 @@
rpcUrl: https://linea.rpc.thirdweb.com
networkId: 59144
type: EvmChain
nativeToken: ETH
- id: shimmer_testnet
mainnet: false
rpcUrl: https://json-rpc.evm.testnet.shimmer.network
@ -373,6 +387,7 @@
rpcUrl: https://rpc.coredao.org
networkId: 1116
type: EvmChain
nativeToken: CORE
- id: tomochain
mainnet: true
rpcUrl: https://rpc.tomochain.com
@ -443,6 +458,7 @@
rpcUrl: https://mainnet.hashio.io/api
networkId: 295
type: EvmChain
nativeToken: HBAR
- id: filecoin_calibration
mainnet: false
rpcUrl: https://rpc.ankr.com/filecoin_testnet

View File

@ -0,0 +1,44 @@
- id: ETH
pythId: ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
decimals: 18
type: token
- id: APT
pythId: 03ae4db29ed4ae33d323568895aa00337e658e348b37509f5372ae51f0af00d5
decimals: 8
type: token
- id: EVMOS
pythId: c19405e4c8bdcbf2a66c37ae05a27d385c8309e9d648ed20dc6ee717e7d30e17
decimals: 18
type: token
- id: MATIC
pythId: 5de33a9112c2b700b8d30b8a3402c103578ccfa2765696471cc672bd5cf6ac52
decimals: 18
type: token
- id: NEAR
pythId: c415de8d2eba7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750
decimals: 18
type: token
- id: FTM
pythId: 5c6c0d2386e3352356c3ab84434fafb5ea067ac2678a38a338c4a69ddc4bdb0c
decimals: 18
type: token
- id: DAI
pythId: b0948a5e5313200c632b51bb5ca32f6de0d36e9950a942d19751e833f70dabfd
decimals: 18
type: token
- id: KCS
pythId: c8acad81438490d4ebcac23b3e93f31cdbcb893fcba746ea1c66b89684faae2f
decimals: 18
type: token
- id: MNT
pythId: 4e3037c822d852d79af3ac80e35eb420ee3b870dca49f9344a38ef4773fb0585
decimals: 18
type: token
- id: HBAR
pythId: 3728e591097635310e6341af53db8b7ee42da9b3a8d918f9463ce9cca886dfbd
decimals: 8
type: token
- id: CORE
pythId: 9b4503710cc8c53f75c30e6e4fda1a7064680ef2e0ee97acd2e3a7c37b3c830c
decimals: 18
type: token

30
package-lock.json generated
View File

@ -55,6 +55,7 @@
"@pythnetwork/pyth-sui-js": "*",
"@types/yargs": "^17.0.32",
"aptos": "^1.5.0",
"axios": "^0.24.0",
"bs58": "^5.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"
@ -23808,11 +23809,11 @@
}
},
"node_modules/axios": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@ -31326,9 +31327,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@ -79140,11 +79141,11 @@
"integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg=="
},
"axios": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"requires": {
"follow-redirects": "^1.15.0",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@ -81094,6 +81095,7 @@
"@pythnetwork/pyth-sui-js": "*",
"@types/yargs": "^17.0.32",
"aptos": "^1.5.0",
"axios": "^0.24.0",
"bs58": "^5.0.0",
"prettier": "^2.6.2",
"ts-node": "^10.9.1",
@ -86038,9 +86040,9 @@
"peer": true
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
},
"for-each": {
"version": "0.3.3",