From f1fee69fea35914161149d6a8eed23f0f4864e8a Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Sat, 12 Aug 2023 21:42:55 +0200 Subject: [PATCH] schema: implement price oracle handlers --- abis/IERC20.json | 224 +++++++++++++++++++++++++++++++++++++++++ schema.graphql | 23 +++++ src/Deployer.ts | 4 +- src/ERC20.ts | 96 ++++++++++++++++++ src/Networks.ts | 49 +++++++++ src/PriceOracle.ts | 31 ++++++ subgraph.template.yaml | 24 +++++ 7 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 abis/IERC20.json create mode 100644 src/ERC20.ts create mode 100644 src/Networks.ts create mode 100644 src/PriceOracle.ts diff --git a/abis/IERC20.json b/abis/IERC20.json new file mode 100644 index 0000000..cfe55d1 --- /dev/null +++ b/abis/IERC20.json @@ -0,0 +1,224 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/schema.graphql b/schema.graphql index 87169a2..831cde0 100644 --- a/schema.graphql +++ b/schema.graphql @@ -47,6 +47,22 @@ type PriceOracle @entity { name: String! implementation: Implementation! environment: Environment! + feeds: [PriceOracleFeed!] @derivedFrom(field: "priceOracle") + signers: [PriceOracleSigner!] @derivedFrom(field: "priceOracle") +} + +type PriceOracleSigner @entity { + id: ID! + priceOracle: PriceOracle! + signer: String! +} + +type PriceOracleFeed @entity { + id: ID! + priceOracle: PriceOracle! + base: ERC20! + quote: ERC20! + feed: String! } type Task @entity { @@ -63,3 +79,10 @@ type Implementation @entity { stateless: Boolean! deprecated: Boolean! } + +type ERC20 @entity { + id: ID! + name: String! + symbol: String! + decimals: Int! +} diff --git a/src/Deployer.ts b/src/Deployer.ts index 72efcf2..16ac441 100644 --- a/src/Deployer.ts +++ b/src/Deployer.ts @@ -1,6 +1,6 @@ import { Address, Bytes, crypto, log } from '@graphprotocol/graph-ts' -import { Authorizer as AuthorizerTemplate } from '../types/templates' +import { Authorizer as AuthorizerTemplate, PriceOracle as PriceOracleTemplate } from '../types/templates' import { Authorizer, Environment, PriceOracle, SmartVault, Task } from '../types/schema' import { AuthorizerDeployed, PriceOracleDeployed, SmartVaultDeployed, TaskDeployed } from '../types/Deployer/Deployer' @@ -34,6 +34,8 @@ export function handlePriceOracleDeployed(event: PriceOracleDeployed): void { priceOracle.implementation = implementation.id priceOracle.environment = environment.id priceOracle.save() + + PriceOracleTemplate.create(event.params.instance) } export function handleSmartVaultDeployed(event: SmartVaultDeployed): void { diff --git a/src/ERC20.ts b/src/ERC20.ts new file mode 100644 index 0000000..f968b48 --- /dev/null +++ b/src/ERC20.ts @@ -0,0 +1,96 @@ +import { Address, log } from '@graphprotocol/graph-ts' + +import { ERC20 as ERC20Entity } from '../types/schema' +import { ERC20 as ERC20Contract } from '../types/templates/PriceOracle/ERC20' + +import { isAvalanche, isBinance, isEthNetwork, isFantom, isGnosis, isMaticNetwork } from './Networks' + +const NATIVE_TOKEN_ADDRESS = Address.fromString('0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee') + +export function loadOrCreateNativeToken(): ERC20Entity { + let id = NATIVE_TOKEN_ADDRESS.toHexString() + let erc20 = ERC20Entity.load(id) + + if (erc20 === null) { + erc20 = new ERC20Entity(id) + erc20.name = getNativeTokenName() + erc20.symbol = getNativeTokenSymbol() + erc20.decimals = 18 + erc20.save() + } + + return erc20 +} + +export function loadOrCreateERC20(address: Address): ERC20Entity { + if (address.equals(NATIVE_TOKEN_ADDRESS)) return loadOrCreateNativeToken() + + let id = address.toHexString() + let erc20 = ERC20Entity.load(id) + + if (erc20 === null) { + erc20 = new ERC20Entity(id) + erc20.name = getERC20Name(address) + erc20.symbol = getERC20Symbol(address) + erc20.decimals = getERC20Decimals(address) + erc20.save() + } + + return erc20 +} + +export function getERC20Decimals(address: Address): i32 { + let erc20Contract = ERC20Contract.bind(address) + let decimalsCall = erc20Contract.try_decimals() + + if (!decimalsCall.reverted) { + return decimalsCall.value + } + + log.warning('decimals() call reverted for {}', [address.toHexString()]) + return 0 +} + +export function getERC20Name(address: Address): string { + let erc20Contract = ERC20Contract.bind(address) + let nameCall = erc20Contract.try_name() + + if (!nameCall.reverted) { + return nameCall.value + } + + log.warning('name() call reverted for {}', [address.toHexString()]) + return 'Unknown' +} + +export function getERC20Symbol(address: Address): string { + let erc20Contract = ERC20Contract.bind(address) + let symbolCall = erc20Contract.try_symbol() + + if (!symbolCall.reverted) { + return symbolCall.value + } + + log.warning('symbol() call reverted for {}', [address.toHexString()]) + return 'Unknown' +} + +export function getNativeTokenSymbol(): string { + if (isEthNetwork()) return 'ETH' + if (isMaticNetwork()) return 'MATIC' + if (isAvalanche()) return 'AVAX' + if (isBinance()) return 'BNB' + if (isFantom()) return 'FTM' + if (isGnosis()) return 'DAI' + return 'Unknown' +} + +export function getNativeTokenName(): string { + if (isEthNetwork()) return 'Ether' + if (isMaticNetwork()) return 'Matic' + if (isAvalanche()) return 'Avax' + if (isBinance()) return 'BNB' + if (isFantom()) return 'Fantom' + if (isGnosis()) return 'Dai' + return 'Unknown' +} diff --git a/src/Networks.ts b/src/Networks.ts new file mode 100644 index 0000000..685d731 --- /dev/null +++ b/src/Networks.ts @@ -0,0 +1,49 @@ +import { dataSource } from '@graphprotocol/graph-ts' + +export function isEthNetwork(): boolean { + return isMainnet() || isGoerli() || isArbitrum() || isOptimism() +} + +export function isMaticNetwork(): boolean { + return isPolygon() || isMumbai() +} + +export function isMainnet(): boolean { + return dataSource.network() == 'mainnet' +} + +export function isGoerli(): boolean { + return dataSource.network() == 'goerli' +} + +export function isArbitrum(): boolean { + return dataSource.network() == 'arbitrum' || dataSource.network() == 'arbitrum-one' +} + +export function isOptimism(): boolean { + return dataSource.network() == 'optimism' +} + +export function isPolygon(): boolean { + return dataSource.network() == 'matic' || dataSource.network() == 'polygon' +} + +export function isMumbai(): boolean { + return dataSource.network() == 'mumbai' +} + +export function isAvalanche(): boolean { + return dataSource.network() == 'avalanche' +} + +export function isBinance(): boolean { + return dataSource.network() == 'bsc' +} + +export function isFantom(): boolean { + return dataSource.network() == 'fantom' +} + +export function isGnosis(): boolean { + return dataSource.network() == 'gnosis' +} diff --git a/src/PriceOracle.ts b/src/PriceOracle.ts new file mode 100644 index 0000000..27da0da --- /dev/null +++ b/src/PriceOracle.ts @@ -0,0 +1,31 @@ +import { Address, store } from '@graphprotocol/graph-ts' + +import { PriceOracleFeed } from '../types/schema' +import { PriceOracleSigner } from '../types/schema' +import { FeedSet, SignerSet } from '../types/templates/PriceOracle/PriceOracle' + +import { loadOrCreateERC20 } from './ERC20' + +export function handleSignerSet(event: SignerSet): void { + let signerId = event.address.toHexString() + '/signer/' + event.params.signer.toHexString() + if (!event.params.allowed) return store.remove('PriceOracleSigner', signerId) + + let signer = PriceOracleSigner.load(signerId) + if (signer == null) signer = new PriceOracleSigner(signerId) + signer.priceOracle = event.address.toHexString() + signer.save() +} + +export function handleFeedSet(event: FeedSet): void { + let baseQuote = event.params.base.toHexString() + '/' + event.params.quote.toHexString() + let feedId = event.address.toHexString() + '/feed/' + baseQuote + if (!event.params.feed.equals(Address.zero())) return store.remove('PriceOracleFeed', feedId) + + let feed = PriceOracleFeed.load(feedId) + if (feed == null) feed = new PriceOracleFeed(feedId) + feed.base = loadOrCreateERC20(event.params.base).id + feed.quote = loadOrCreateERC20(event.params.quote).id + feed.feed = event.params.feed.toHexString() + feed.priceOracle = event.address.toHexString() + feed.save() +} diff --git a/subgraph.template.yaml b/subgraph.template.yaml index 6c4f392..842c49d 100644 --- a/subgraph.template.yaml +++ b/subgraph.template.yaml @@ -73,6 +73,30 @@ templates: - event: Unauthorized(indexed address,indexed address,indexed bytes4) handler: handleUnauthorized file: ./src/Authorizer.ts + - kind: ethereum/contract + name: PriceOracle + network: {{network}} + source: + abi: PriceOracle + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - PriceOracle + abis: + - name: PriceOracle + file: ./node_modules/@mimic-fi/v3-price-oracle/artifacts/contracts/interfaces/IPriceOracle.sol/IPriceOracle.json + - name: ERC20 + file: ./abis/IERC20.json + eventHandlers: + - event: SignerSet(indexed address,bool) + handler: handleSignerSet + receipt: true + - event: FeedSet(indexed address,indexed address,address) + handler: handleFeedSet + receipt: true + file: ./src/PriceOracle.ts - kind: ethereum/contract name: SmartVault network: {{network}}