Gathers data from bonds, liquidity and Olympus treasury.
Used in the dashboard https://app.olympusdao.finance/ and the Olympus Playground.
Deployed at https://thegraph.com/hosted-service/subgraph/olympusdao/olympus-protocol-metrics
Run yarn
Note that the Graph Protocol compiles from AssemblyScript to WASM. AssemblyScript is strongly-typed, and is similar to TypeScript. However, there are a number of expected features of TypeScript (e.g. try/catch) that aren't implemented in AssemblyScript. A few suggestions:
- Utilise the linting that has been set up in this repository
- Build frequently (
yarn build), as linting does not pick up all problems - Variables that are nullable need to be typed accordingly
- You may run into a TS2322 compiler error when handling null values. The workaround for this is to use the strict equality operator (
===), instead of loose equality (==) or negation (!someValue). e.g.if (someValue === null). This is due to a limitation in the AssemblyScript compiler. - The Graph Protocol Discord is very helpful to get support. See the
subgraph-developmentchannel.
The matchstick-as package is used to perform testing on the subgraph code. The syntax is close to that of
jest. See this page for examples: https://github.com/LimeChain/demo-subgraph
To run tests: yarn test
If you receive a non-sensical test result (e.g. duplicated test cases, or a test failing that should be passing), try running yarn test:force. The build cache will sometimes get corrupted/broken.
- Add the ABI into the
abis/folder. - Add a reference to the ABI under the respective data source in
subgraph.yaml - Run
yarn codegento generate AssemblyScript files from the new ABI(s)
- If necessary, create an account and subgraph in the Subgraph Studio: https://thegraph.com/studio/
- The subgraph should be called
olympus-protocol-metrics
- The subgraph should be called
- Add the Subgraph Studio deploy key to the
GRAPH_STUDIO_TOKENvariable in.env(using.env.sample) - Authenticate using
yarn auth:dev - Update the
SUBGRAPH_VERSIONvariable in the.subgraph-versionfile. - Run
yarn build - Run
yarn deploy:dev
A URL for the GraphQL Explorer will be provided.
This subgraph is deployed on the Graph Protocol's Hosted Service:
- For historical reasons, as the hosted service was the only option at the time.
- Going forward, the Graph Network does not yet offer multi-chain indexing, so the hosted service will still be required.
- Note that indexing takes a significant amount of time (weeks!) at the moment. Investigation is required to look into how to improve the indexing performance.
To deploy, do the following:
- Add the Subgraph Studio deploy key to the
GRAPH_TOKENvariable in.env(using.env.sample) - Authenticate using
yarn auth - Run
yarn build - Run
yarn deploy
A set of Docker containers is pre-configured to enable local testing of the subgraph.
- Copy the
docker/.env.samplefile todocker/.envand set the Alchemy API key - Run the Docker stack:
yarn run-local - Create the subgraph in the local graph node:
yarn create-local(after every restart of the graph node stack) - Deploy the subgraph:
yarn deploy-local --version-label 0.1.0 - Access the GraphQL query interface: http://localhost:8000/subgraphs/name/olympus/graphql
Tokens are defined and mapped in the src/utils/Constants.ts file.
To add a new token:
- Define a constant value with the address of the ERC20 contract, with
.toLowerCase()appended - Define a constant value with the address of the Uniswap V2 or V3 liquidity pool
- Add the token to either the
ERC20_STABLE_TOKENSorERC20_VOLATILE_TOKENSarray (as appropriate) - Add a mapping under
PAIR_HANDLERbetween the ERC20 contract and the liquidity pool contract - If the token is present in any wallets outside of
WALLET_ADDRESSES, yet should be reported as part of the tresury, add it to {NON_TREASURY_ASSET_WHITELIST}.
Tokens are defined and mapped in the src/utils/Constants.ts file.
To add a new wallet:
- Define a constant value with the address of the wallet
- Add the constant to the
WALLET_ADDRESSESarray
Price lookups are mapped in the src/utils/Constants.ts file.
To add a new price lookup:
- Define a constant value with the address of the liquidity pool (e.g.
PAIR_UNISWAP_V2_ALCX_ETH), with.toLowerCase()appended - Add an entry to the
LIQUIDITY_POOL_TOKEN_LOOKUPconstant, which maps the pair type (Balancer, Curve, UniswapV2, UniswapV3) to the liquidity pool address
Price lookups are performed in the following manner through the getUSDRate function:
- If the token is a stablecoin, return a rate of
1. - If the token is one of the base tokens (OHM or ETH), return the respective rate.
- Otherwise, use
getPairHandlerto find the appropriate liquidity pool that will enable a price lookup into USD.
Protocol-owned liquidity is mapped in the src/utils/Constants.ts file.
To add a new liquidity entry:
- Define a constant value with the address of the liquidity pool (e.g.
PAIR_UNISWAP_V2_ALCX_ETH), with.toLowerCase()appended - Add an entry to the
LIQUIDITY_OWNEDconstant, which maps the pool type (Balancer, Curve, UniswapV2, UniswapV3) to the liquidity pool address - Add an entry to the
LIQUIDITY_PAIR_TOKENSconstant, which maps the liquidity pool address to the tokens that it is composed of. This could be determined on-chain, but is easier/quicker if done statically. - If the entry is present in any wallets outside of {WALLET_ADDRESSES}, yet should be reported as part of the tresury, add it to
NON_TREASURY_ASSET_WHITELIST.
Some liquidity tokens (e.g. Curve OHMETH) can be staked in Convex, which in turn emits a staked token (e.g. cvxOHMETH).
To add a new mapping:
- Define a constant value with the address of the staked token, with
.toLowerCase()appended - Create a mapping between the original token (e.g.
ERC20_CRV_OHMETH) and the staked token (e.g.ERC20_CVX_OHMETH) in theCONVEX_STAKED_TOKENSmap - Add the Convex staking contract to the
CONVEX_STAKING_CONTRACTSarray
Although Ethereum addresses are not case-sensitive, the mix of uppercase and lowercase letters can create problems when using contract addresses as keys in a map.
To work around this, the following have been implemented:
- At the location where a constant is defined, it is forced into lowercase
- All functions that access
Mapobjects convert the given key into lowercase
There are a number of inter-related components in the metrics:
-
The schema is defined in the
schema.graphqlfile- Some metrics return integer or decimal values (e.g.
treasuryMarketValue) - Alongside the decimal values are additional values suffixed by
Components(e.g.treasuryMarketValueComponents). These give a detailed breakdown of the tokens and source contracts/wallets. See the Debugging section below for more detail. - Any changes to the
schema.graphqlfile need to be applied by runningyarn codegen, which will generate/update thegenerated/schema.tsfile.
- Some metrics return integer or decimal values (e.g.
-
Nested objects are supported in the GraphQL schema (e.g.
ProtocolMetricis related toTokenRecords), but it is not immediately obvious how to use them.- The functions generated and written to the
schema.tsfile will not directly reference the related entity in the getters and setters. For example:
get treasuryRiskFreeValueComponents(): string { let value = this.get("treasuryRiskFreeValueComponents"); return value!.toString(); } set treasuryRiskFreeValueComponents(value: string) { this.set("treasuryRiskFreeValueComponents", Value.fromString(value)); }
- Instead, the
idof an entity is used. ATokenRecordsentity is instantiated with a uniqueid(e.g.TreasuryMarketValue), and can be loaded (load(id: string)) and saved (save()) accordingly. Theidshould be passed to the setter to link theTokenRecordsentity to theProtocolMetric. - Explicit loading and saving of
TokenRecordsandTokenRecordobjects should not be required, however, as it is all handled through theTokenRecordHelpermodule.
- The functions generated and written to the
-
A
metricNameparameter is passed from the top-level (ProtocolMetricsmodule) down into the different levels of utility functions that index the blockchain data. ThemetricNameis used, alongside the block number, to create anidthat is unique. Not using themetricNameresults in data from one metric (e.g. liquid backing) clobbering that of another metric (e.g. total backing).
Each metric has a "component" variant that contains the details of the assets that are summed to result in the reported value.
For example, the treasuryTotalBacking metric has treasuryTotalBackingComponents.
The components metric returns data in the JSON format, along with some other fields, like so:
{
"data": {
"protocolMetrics": [
{
"treasuryMarketValueComponents": {
"value": "431707239.3836405267139378357370496",
"records": [
{
"id": "MarketValue-aDAI-Aave Allocator V1-14381377",
"token": "aDAI",
"tokenAddress": "0x028171bca77440897b824ca71d1c56cac55b68a3",
"source": "Aave Allocator V1",
"sourceAddress": "0x0e1177e47151be72e5992e0975000e73ab5fd9d4",
"balance": "10657221.863207801175897664",
"rate": "1",
"multiplier": "1",
"value": "10657221.863207801175897664"
}
]
}
}
]
}
}Follow these steps to convert the JSON data into CSV:
- Copy everything (including the square bracket,
[) after"records":up to and including the next square bracket in the query results. - Open JSON to CSV Converter
- Paste the copied content into the field.
- Download the CSV.
