diff --git a/netlify.toml b/netlify.toml index f247ca68e3..3c370fa6da 100644 --- a/netlify.toml +++ b/netlify.toml @@ -89,3 +89,6 @@ schedule = "@hourly" [functions."asset-price-and-rates-*"] schedule = "@hourly" + +[functions."asset-master-data-*"] +schedule = "@hourly" diff --git a/ops/prod/config.tf b/ops/prod/config.tf index 78349e63e3..799b8a7eea 100644 --- a/ops/prod/config.tf +++ b/ops/prod/config.tf @@ -98,5 +98,14 @@ locals { PYTH_UPDATER_ETHEREUM_ADMIN_PRIVATE_KEY = var.pyth_updater_ethereum_admin_private_key, # Use PYTH_UPDATER specific variable } ) + pyth_updater_optimism_lambda_variables = merge( + local.shared_env_vars_lambda, + { + DISCORD_WEBHOOK_URL = var.pyth_updater_discord_webhook_url, + UPTIME_PYTH_UPDATER_API = var.uptime_pyth_updater_api, + PYTH_UPDATER_ETHEREUM_ADMIN_ACCOUNT = var.pyth_updater_ethereum_admin_account, # Use PYTH_UPDATER specific variable + PYTH_UPDATER_ETHEREUM_ADMIN_PRIVATE_KEY = var.pyth_updater_ethereum_admin_private_key, # Use PYTH_UPDATER specific variable + } + ) } diff --git a/ops/prod/optimism.tf b/ops/prod/optimism.tf index 2b8498b363..0d14141c4e 100644 --- a/ops/prod/optimism.tf +++ b/ops/prod/optimism.tf @@ -43,4 +43,19 @@ module "optimism_mainnet_liquidator_ecs" { security_group_ids = ["sg-0a3996557af867ad0"] region = var.region liquidator_container_name = "${var.liquidator_container_name}-optimism" +} +module "optimism_mainnet_pyth_rpc_0" { + source = "../modules/lambda" + ecr_repository_name = local.pyth_updater_ecr_repository_name + docker_image_tag = var.bots_image_tag + container_family = "pyth-updater-rpc-0" + environment = "mainnet" + target_chain_id = local.optimism_mainnet_chain_id + container_env_vars = merge( + local.pyth_updater_optimism_lambda_variables, + { WEB3_HTTP_PROVIDER_URLS = local.optimism_mainnet_rpc_0 } + ) + schedule_expression = "rate(5 minutes)" + timeout = 700 + memory_size = 512 } \ No newline at end of file diff --git a/packages/bots/pyth-updater/src/config/index.ts b/packages/bots/pyth-updater/src/config/index.ts index 3faf4915ed..71ae65e687 100644 --- a/packages/bots/pyth-updater/src/config/index.ts +++ b/packages/bots/pyth-updater/src/config/index.ts @@ -1,9 +1,10 @@ -import { base, mode } from '@ionicprotocol/chains'; +import { base, mode, optimism } from '@ionicprotocol/chains'; import { pythConfig as basePythConfig } from './base'; import { pythConfig as modePythConfig } from './mode'; - +import { pythConfig as optimismPythConfig } from './optimism'; export const chainIdToConfig = { [mode.chainId]: modePythConfig, [base.chainId]: basePythConfig, + [optimism.chainId]: optimismPythConfig, }; diff --git a/packages/bots/pyth-updater/src/config/optimism.ts b/packages/bots/pyth-updater/src/config/optimism.ts new file mode 100644 index 0000000000..57b0d2ef58 --- /dev/null +++ b/packages/bots/pyth-updater/src/config/optimism.ts @@ -0,0 +1,21 @@ +import { PythAssetConfig } from '../types'; + +import { pythConfig as commonPythConfig } from './common'; + +export const pythConfig: PythAssetConfig[] = [ + ...commonPythConfig, + { + // price feed for WBTC + priceId: '0xc9d8b075a5c69303365ae23633d4e085199bf5c520a3b90fed1322a0342ffc33', + configRefreshRateInSeconds: 3600, + validTimePeriodSeconds: 86400, // 24 hrs + deviationThresholdBps: 100, // 1% + }, + { + // price feed for SNX + priceId: '0x39d020f60982ed892abbcd4a06a276a9f9b7bfbce003204c110b6e488f502da3', + configRefreshRateInSeconds: 3600, + validTimePeriodSeconds: 86400, // 24 hrs + deviationThresholdBps: 100, // 1% + }, +]; diff --git a/packages/bots/pyth-updater/src/run.ts b/packages/bots/pyth-updater/src/run.ts index 5145bbdf14..5c14650232 100644 --- a/packages/bots/pyth-updater/src/run.ts +++ b/packages/bots/pyth-updater/src/run.ts @@ -1,6 +1,6 @@ import { createPublicClient, createWalletClient, fallback, Hex, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { base, mode } from 'viem/chains'; +import { base, mode, optimism } from 'viem/chains'; import { chainIdToConfig } from './config'; import config from './config/service'; @@ -15,6 +15,8 @@ export const run = async (): Promise => { chain = mode; } else if (config.chainId === base.id) { chain = base; + } else if (config.chainId === optimism.id) { + chain = optimism; } else { throw new Error(`Unsupported chain ID: ${config.chainId}`); } diff --git a/packages/functions/src/abi/FlywheelCore.json b/packages/functions/src/abi/FlywheelCore.json index b2dc7814d4..268a2d4317 100644 --- a/packages/functions/src/abi/FlywheelCore.json +++ b/packages/functions/src/abi/FlywheelCore.json @@ -1,35 +1,4 @@ [ - { - "inputs": [ - { - "internalType": "contract ERC20", - "name": "_rewardToken", - "type": "address" - }, - { - "internalType": "contract IFlywheelRewards", - "name": "_flywheelRewards", - "type": "address" - }, - { - "internalType": "contract IFlywheelBooster", - "name": "_flywheelBooster", - "type": "address" - }, - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "contract Authority", - "name": "_authority", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, { "anonymous": false, "inputs": [ @@ -74,25 +43,6 @@ "name": "AddStrategy", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": true, - "internalType": "contract Authority", - "name": "newAuthority", - "type": "address" - } - ], - "name": "AuthorityUpdated", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -142,101 +92,119 @@ "anonymous": false, "inputs": [ { - "indexed": true, + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, "internalType": "address", - "name": "user", + "name": "oldOwner", "type": "address" }, { - "indexed": true, + "indexed": false, "internalType": "address", "name": "newOwner", "type": "address" } ], - "name": "OwnerUpdated", + "name": "NewOwner", "type": "event" }, { - "inputs": [], - "name": "ONE", - "outputs": [ - { - "internalType": "uint224", - "name": "", - "type": "uint224" - } - ], - "stateMutability": "view", - "type": "function" - }, - { + "anonymous": false, "inputs": [ { - "internalType": "contract ERC20", - "name": "strategy", + "indexed": false, + "internalType": "address", + "name": "oldPendingOwner", "type": "address" }, { + "indexed": false, "internalType": "address", - "name": "user", + "name": "newPendingOwner", "type": "address" } ], - "name": "accrue", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" + "name": "NewPendingOwner", + "type": "event" }, { + "anonymous": false, "inputs": [ { - "internalType": "contract ERC20", - "name": "strategy", - "type": "address" - }, - { + "indexed": true, "internalType": "address", - "name": "user", + "name": "previousOwner", "type": "address" }, { + "indexed": true, "internalType": "address", - "name": "secondUser", + "name": "newOwner", "type": "address" } ], - "name": "accrue", - "outputs": [ + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { + "indexed": false, "internalType": "uint256", - "name": "", + "name": "oldPerformanceFee", "type": "uint256" }, { + "indexed": false, "internalType": "uint256", - "name": "", + "name": "newPerformanceFee", "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "oldFeeRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newFeeRecipient", + "type": "address" } ], + "name": "UpdatedFeeSettings", + "type": "event" + }, + { + "inputs": [], + "name": "_acceptOwner", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { - "internalType": "contract ERC20", - "name": "strategy", + "internalType": "address", + "name": "newPendingOwner", "type": "address" } ], - "name": "addMarketForRewards", + "name": "_setPendingOwner", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -247,52 +215,70 @@ "internalType": "contract ERC20", "name": "strategy", "type": "address" - } + }, + { "internalType": "address", "name": "user", "type": "address" } ], - "name": "addStrategyForRewards", - "outputs": [], + "name": "accrue", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + "internalType": "contract ERC20", + "name": "strategy", + "type": "address" + }, + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "address", "name": "secondUser", "type": "address" } ], - "name": "allStrategies", + "name": "accrue", "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "contract ERC20", - "name": "", + "name": "strategy", "type": "address" } ], - "stateMutability": "view", + "name": "addMarketForRewards", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { - "inputs": [], - "name": "authority", - "outputs": [ + "inputs": [ { - "internalType": "contract Authority", - "name": "", + "internalType": "contract ERC20", + "name": "strategy", "type": "address" } ], + "name": "addStrategyForRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "allStrategies", + "outputs": [ + { "internalType": "contract ERC20", "name": "", "type": "address" } + ], "stateMutability": "view", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "user", - "type": "address" - } + { "internalType": "address", "name": "user", "type": "address" } ], "name": "claimRewards", "outputs": [], @@ -301,20 +287,17 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "user", - "type": "address" - } + { "internalType": "address", "name": "user", "type": "address" } ], "name": "compAccrued", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeRecipient", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, @@ -333,16 +316,8 @@ }, { "inputs": [ - { - "internalType": "contract ERC20", - "name": "market", - "type": "address" - }, - { - "internalType": "address", - "name": "borrower", - "type": "address" - } + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "address", "name": "borrower", "type": "address" } ], "name": "flywheelPreBorrowerAction", "outputs": [], @@ -351,16 +326,8 @@ }, { "inputs": [ - { - "internalType": "contract ERC20", - "name": "market", - "type": "address" - }, - { - "internalType": "address", - "name": "supplier", - "type": "address" - } + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "address", "name": "supplier", "type": "address" } ], "name": "flywheelPreSupplierAction", "outputs": [], @@ -369,21 +336,9 @@ }, { "inputs": [ - { - "internalType": "contract ERC20", - "name": "market", - "type": "address" - }, - { - "internalType": "address", - "name": "src", - "type": "address" - }, - { - "internalType": "address", - "name": "dst", - "type": "address" - } + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "address", "name": "src", "type": "address" }, + { "internalType": "address", "name": "dst", "type": "address" } ], "name": "flywheelPreTransferAction", "outputs": [], @@ -407,38 +362,46 @@ "inputs": [], "name": "getAllStrategies", "outputs": [ - { - "internalType": "contract ERC20[]", - "name": "", - "type": "address[]" - } + { "internalType": "contract ERC20[]", "name": "", "type": "address[]" } ], "stateMutability": "view", "type": "function" }, { - "inputs": [], - "name": "isFlywheel", - "outputs": [ + "inputs": [ { - "internalType": "bool", - "name": "", - "type": "bool" - } + "internalType": "contract ERC20", + "name": "_rewardToken", + "type": "address" + }, + { + "internalType": "contract IFlywheelRewards", + "name": "_flywheelRewards", + "type": "address" + }, + { + "internalType": "contract IFlywheelBooster", + "name": "_flywheelBooster", + "type": "address" + }, + { "internalType": "address", "name": "_owner", "type": "address" } ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "isFlywheel", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "isRewardsDistributor", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "view", "type": "function" }, @@ -452,23 +415,8 @@ ], "name": "marketState", "outputs": [ - { - "components": [ - { - "internalType": "uint224", - "name": "index", - "type": "uint224" - }, - { - "internalType": "uint32", - "name": "lastUpdatedTimestamp", - "type": "uint32" - } - ], - "internalType": "struct FlywheelCore.RewardsState", - "name": "", - "type": "tuple" - } + { "internalType": "uint224", "name": "", "type": "uint224" }, + { "internalType": "uint32", "name": "", "type": "uint32" } ], "stateMutability": "view", "type": "function" @@ -476,58 +424,46 @@ { "inputs": [], "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "rewardToken", - "outputs": [ - { - "internalType": "contract ERC20", - "name": "", - "type": "address" - } - ], + "name": "pendingOwner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "rewardsAccrued", + "inputs": [], + "name": "performanceFee", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rewardToken", "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + { "internalType": "contract ERC20", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ - { - "internalType": "contract Authority", - "name": "newAuthority", - "type": "address" - } + { "internalType": "address", "name": "user", "type": "address" } ], - "name": "setAuthority", - "outputs": [], + "name": "rewardsAccrued", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, @@ -557,65 +493,61 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "setOwner", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { "internalType": "contract ERC20", - "name": "", + "name": "strategy", "type": "address" } ], "name": "strategyState", "outputs": [ - { - "internalType": "uint224", - "name": "index", - "type": "uint224" - }, + { "internalType": "uint224", "name": "index", "type": "uint224" }, { "internalType": "uint32", "name": "lastUpdatedTimestamp", "type": "uint32" } ], - "stateMutability": "view", + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { - "internalType": "contract ERC20", - "name": "", - "type": "address" + "internalType": "uint256", + "name": "_performanceFee", + "type": "uint256" }, + { "internalType": "address", "name": "_feeRecipient", "type": "address" } + ], + "name": "updateFeeSettings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { - "internalType": "address", - "name": "", + "internalType": "contract ERC20", + "name": "strategy", "type": "address" - } + }, + { "internalType": "address", "name": "user", "type": "address" } ], "name": "userIndex", - "outputs": [ - { - "internalType": "uint224", - "name": "", - "type": "uint224" - } - ], - "stateMutability": "view", + "outputs": [{ "internalType": "uint224", "name": "", "type": "uint224" }], + "stateMutability": "nonpayable", "type": "function" } ] diff --git a/packages/functions/src/abi/flywheel.json b/packages/functions/src/abi/flywheel.json new file mode 100644 index 0000000000..31ac1266bb --- /dev/null +++ b/packages/functions/src/abi/flywheel.json @@ -0,0 +1,622 @@ +[ + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "_rewardToken", + "type": "address" + }, + { + "internalType": "contract IFlywheelRewards", + "name": "_flywheelRewards", + "type": "address" + }, + { + "internalType": "contract IFlywheelBooster", + "name": "_flywheelBooster", + "type": "address" + }, + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "contract Authority", + "name": "_authority", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ERC20", + "name": "strategy", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardsDelta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardsIndex", + "type": "uint256" + } + ], + "name": "AccrueRewards", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newStrategy", + "type": "address" + } + ], + "name": "AddStrategy", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract Authority", + "name": "newAuthority", + "type": "address" + } + ], + "name": "AuthorityUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ClaimRewards", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newBooster", + "type": "address" + } + ], + "name": "FlywheelBoosterUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newFlywheelRewards", + "type": "address" + } + ], + "name": "FlywheelRewardsUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "ONE", + "outputs": [ + { + "internalType": "uint224", + "name": "", + "type": "uint224" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "strategy", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "accrue", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "strategy", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "address", + "name": "secondUser", + "type": "address" + } + ], + "name": "accrue", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "strategy", + "type": "address" + } + ], + "name": "addMarketForRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "strategy", + "type": "address" + } + ], + "name": "addStrategyForRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allStrategies", + "outputs": [ + { + "internalType": "contract ERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "authority", + "outputs": [ + { + "internalType": "contract Authority", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "claimRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "compAccrued", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "flywheelBooster", + "outputs": [ + { + "internalType": "contract IFlywheelBooster", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "market", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + } + ], + "name": "flywheelPreBorrowerAction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "market", + "type": "address" + }, + { + "internalType": "address", + "name": "supplier", + "type": "address" + } + ], + "name": "flywheelPreSupplierAction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "market", + "type": "address" + }, + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + } + ], + "name": "flywheelPreTransferAction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "flywheelRewards", + "outputs": [ + { + "internalType": "contract IFlywheelRewards", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllStrategies", + "outputs": [ + { + "internalType": "contract ERC20[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isFlywheel", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isRewardsDistributor", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "strategy", + "type": "address" + } + ], + "name": "marketState", + "outputs": [ + { + "components": [ + { + "internalType": "uint224", + "name": "index", + "type": "uint224" + }, + { + "internalType": "uint32", + "name": "lastUpdatedTimestamp", + "type": "uint32" + } + ], + "internalType": "struct FlywheelCore.RewardsState", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardToken", + "outputs": [ + { + "internalType": "contract ERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "rewardsAccrued", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract Authority", + "name": "newAuthority", + "type": "address" + } + ], + "name": "setAuthority", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IFlywheelBooster", + "name": "newBooster", + "type": "address" + } + ], + "name": "setBooster", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IFlywheelRewards", + "name": "newFlywheelRewards", + "type": "address" + } + ], + "name": "setFlywheelRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "", + "type": "address" + } + ], + "name": "strategyState", + "outputs": [ + { + "internalType": "uint224", + "name": "index", + "type": "uint224" + }, + { + "internalType": "uint32", + "name": "lastUpdatedTimestamp", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "userIndex", + "outputs": [ + { + "internalType": "uint224", + "name": "", + "type": "uint224" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + \ No newline at end of file diff --git a/packages/functions/src/config/environment.ts b/packages/functions/src/config/environment.ts index 17e89bd121..2694c51348 100644 --- a/packages/functions/src/config/environment.ts +++ b/packages/functions/src/config/environment.ts @@ -15,6 +15,7 @@ const environment = { supabaseAssettotalTvlTableName:process.env.SUPABASE_ASSET_TOTAL_TVL_TABLE_NAME ?? '', supabaseAssetTotalTvlPoolName:process.env.SUPABASE_ASSET_TOTAL_TVL_POOL_TABLE_NAME ?? '', supabaseAssetPriceAndRatesTableName: process.env.SUPABASE_ASSET_PRICE_AND_RATES_TABLE_NAME ?? '', + supabaseAssetMasterDataTableName: process.env.SUPABASE_ASSET_MASTER_DATA_TABLE_NAME ?? '', uptimeAssetPriceApi: process.env.UPTIME_ASSET_PRICE_API ?? '', uptimeTotalApyApi: process.env.UPTIME_TOTAL_APY_API ?? '', uptimeTotalHistoryApyApi: process.env.UPTIME_TOTAL_HISTORY_APY_API ?? '', diff --git a/packages/functions/src/controllers/asset-master-data.ts b/packages/functions/src/controllers/asset-master-data.ts new file mode 100644 index 0000000000..43a3576df5 --- /dev/null +++ b/packages/functions/src/controllers/asset-master-data.ts @@ -0,0 +1,302 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { Handler } from '@netlify/functions'; +import { Chain, createPublicClient, http, formatEther, getContract, formatUnits } from 'viem'; +import { chainIdtoChain, chainIdToConfig } from '@ionicprotocol/chains'; +import { IonicSdk } from '@ionicprotocol/sdk'; +import axios from 'axios'; +import CTokenABI from '../abi/CToken.json'; +import FlywheelABI from '../abi/FlywheelCore.json'; +import FlywheelRewardsABI from '../abi/FlywheelCore.json'; +import { environment, supabase } from '../config'; + +const FLYWHEEL_TYPE_MAP: Record = { + [SupportedChains.mode]: { + supply: [], + borrow: [] + } +}; + +interface AssetMasterData { + // Key identifiers + chain_id: SupportedChains; + ctoken_address: string; + underlying_address: string; + pool_address: string; + + // Asset basic info + underlying_name: string; + underlying_symbol: string; + decimals: number; + // Price data + underlying_price: number; + usd_price: number; + exchange_rate: number; + // TVL and supply data + total_supply: string; + total_supply_usd: number; + total_borrow: string; + total_borrow_usd: number; + utilization_rate: number; + // APY/APR data + supply_apy: number; + borrow_apy: number; + reward_apy: number; + total_apy: number; + // Market status + is_listed: boolean; + collateral_factor: number; + reserve_factor: number; + borrow_cap: string; + supply_cap: string; + is_borrow_paused: boolean; + is_mint_paused: boolean; + // Metadata + updated_at: string; + block_number: number; + timestamp: string; + reward_apy_supply: number; + reward_apy_borrow: number; + reward_tokens: string[]; +} + +const fetchTokenPrice = async (cgId: string): Promise => { + try { + const response = await axios.get(`https://api.coingecko.com/api/v3/simple/price?ids=${cgId}&vs_currencies=usd`); + return response.data[cgId]?.usd || 0; + } catch (e) { + console.error('Error fetching token price:', e); + return 0; + } +}; +const validateAssetData = (data: any) => { + const requiredFields = [ + 'underlying_price', + 'usd_price', + 'exchange_rate', + 'supply_apy', + 'borrow_apy' + ]; + requiredFields.forEach(field => { + if (typeof data[field] !== 'number' || isNaN(data[field])) { + throw new Error(`Invalid ${field}: ${data[field]}`); + } + }); + return data; +}; +const formatBigIntValue = (value: string | number | bigint, decimals: number): number => { + const valueStr = value.toString(); + const valueBigInt = BigInt(valueStr); + const divisor = BigInt(10) ** BigInt(decimals); + return Number(valueBigInt) / Number(divisor); +}; +const calculateRewardApy = (rewardRate: bigint, index: bigint): number => { + const annualRewardRate = (rewardRate * BigInt(365 * 24 * 60 * 60)) / index; + return Number(annualRewardRate) / 1e18 * 100; +}; +export const updateAssetMasterData = async (chainId: SupportedChains) => { + try { + const config = chainIdToConfig[chainId]; + const publicClient = createPublicClient({ + chain: chainIdtoChain[chainId] as Chain, + transport: http(config.specificParams.metadata.rpcUrls.default.http[0]), + }); + const sdk = new IonicSdk(publicClient as any, undefined, config); + + // Get pools and their assets + const [poolIndexes, pools] = await sdk.contracts.PoolDirectory.read.getActivePools(); + + if (!pools.length) { + throw new Error('No pools found'); + } + // Get native token price for USD conversion + const cgId = config.specificParams.cgId; + const nativeTokenPrice = await fetchTokenPrice(cgId).catch(() => 0); + + const masterEntries = []; + const mpo = sdk.createMasterPriceOracle(); + // Process assets in parallel batches to avoid timeout + const processAssetBatch = async (assets: any[], pool: any) => { + return Promise.all(assets.map(async (asset) => { + try { + // Get underlying price + const underlyingPrice = await mpo.read.price([asset.underlyingToken]); + const underlyingPriceNum = Number(formatEther(underlyingPrice as bigint)); + const usdPrice = underlyingPriceNum * nativeTokenPrice; + // Get exchange rate with proper decimal handling + let exchangeRateNum = 1; + try { + const cTokenContract = getContract({ + address: asset.cToken as `0x${string}`, + abi: CTokenABI, + client: publicClient + }); + + try { + const exchangeRate = await cTokenContract.read.exchangeRateCurrent(); + exchangeRateNum = Number(formatEther(exchangeRate as bigint)); + await new Promise(resolve => setTimeout(resolve, 1000)); + } catch (err) { + console.log(`Falling back to exchangeRateStored for ${asset.cToken}`); + const exchangeRate = await cTokenContract.read.exchangeRateStored(); + exchangeRateNum = Number(formatEther(exchangeRate as bigint)); + } + } catch (e) { + console.log(`Using default exchange rate for ${asset.cToken}`); + } + // Calculate APYs + const supplyApy = sdk.ratePerBlockToAPY( + asset.supplyRatePerBlock, + Number(config.specificParams.blocksPerYear / BigInt(24 * 365 * 60)) + ); + + const borrowApy = sdk.ratePerBlockToAPY( + asset.borrowRatePerBlock, + Number(config.specificParams.blocksPerYear / BigInt(24 * 365 * 60)) + ); + // Calculate total supply USD and borrow USD using parseFloat like in total-tvl.ts + const totalSupplyUSD = formatBigIntValue(asset.totalSupply, asset.underlyingDecimals) * + usdPrice; + const totalBorrowUSD = formatBigIntValue(asset.totalBorrow, asset.underlyingDecimals) * + usdPrice; + const utilizationRate = !asset.totalBorrow || !asset.totalSupply ? 0 : + formatBigIntValue(asset.totalBorrow, asset.underlyingDecimals) / + (formatBigIntValue(asset.totalSupply, asset.underlyingDecimals) * exchangeRateNum); + // Calculate rewards + const rewardTokens = new Set(); + let rewardApySupply = 0; + let rewardApyBorrow = 0; + try { + const flywheelRewards = await sdk.getFlywheelMarketRewardsByPoolWithAPR(pool.comptroller); + + if (flywheelRewards) { + const marketRewards = flywheelRewards.find(r => r.market === asset.cToken); + if (marketRewards?.rewardsInfo) { + for (const reward of marketRewards.rewardsInfo) { + if (reward.formattedAPR) { + const apyForMarket = Number(formatUnits(reward.formattedAPR, 18)); + + // Check if flywheel is in borrow list + if (FLYWHEEL_TYPE_MAP[chainId]?.borrow?.includes(reward.flywheel)) { + rewardApySupply += Math.min(apyForMarket * 100, 1000); + } else { + rewardApyBorrow += Math.min(apyForMarket * 100, 1000); + } + rewardTokens.add(reward.rewardToken.toLowerCase()); + } + } + } + } + } catch (e) { + console.error(`Error calculating rewards for ${asset.cToken}:`, e); + } + const totalSupplyApy = supplyApy + rewardApySupply; + const totalBorrowApy = (-1 * borrowApy) + rewardApyBorrow; + const comptroller = sdk.createComptroller(pool.comptroller); + const [borrowCap, supplyCap] = await Promise.all([ + comptroller.read.borrowCaps([asset.cToken]), + comptroller.read.supplyCaps([asset.cToken]) + ]); + return { + chain_id: chainId, + ctoken_address: asset.cToken.toLowerCase(), + underlying_address: asset.underlyingToken.toLowerCase(), + pool_address: pool.comptroller.toLowerCase(), + underlying_name: asset.underlyingName || '', + underlying_symbol: asset.underlyingSymbol || '', + decimals: asset.underlyingDecimals, + underlying_price: underlyingPriceNum, + usd_price: usdPrice, + exchange_rate: exchangeRateNum, + total_supply: asset.totalSupply.toString(), + total_supply_usd: totalSupplyUSD, + total_borrow: asset.totalBorrow.toString(), + total_borrow_usd: totalBorrowUSD, + utilization_rate: utilizationRate, + supply_apy: supplyApy, + borrow_apy: borrowApy, + reward_apy: rewardApySupply + rewardApyBorrow, + reward_apy_supply: rewardApySupply, + reward_apy_borrow: rewardApyBorrow, + total_supply_apy: totalSupplyApy, + total_borrow_apy: totalBorrowApy, + is_listed: true, + collateral_factor: Number(formatEther(asset.collateralFactor)), + reserve_factor: Number(formatEther(asset.reserveFactor)), + borrow_cap: borrowCap.toString(), + supply_cap: supplyCap.toString(), + is_borrow_paused: asset.borrowGuardianPaused, + is_mint_paused: asset.mintGuardianPaused, + updated_at: new Date(), + block_number: Number(await publicClient.getBlockNumber()), + timestamp: new Date().toISOString(), + reward_tokens: Array.from(rewardTokens), + }; + } catch (e) { + console.error(`Error processing asset ${asset.cToken}:`, e); + return null; + } + })); + }; + // Process pools + for (const pool of pools) { + try { + const assets = await sdk.contracts.PoolLens.simulate + .getPoolAssetsWithData([pool.comptroller]) + .then(r => r.result) + .catch(() => []); + const processedAssets = await processAssetBatch([...assets] as any[], pool); + masterEntries.push(...processedAssets.filter(Boolean)); + } catch (e) { + console.error(`Error processing pool ${pool.comptroller}:`, e); + } + } + // Insert into database + const { error } = await supabase + .from(environment.supabaseAssetMasterDataTableName) + .insert(masterEntries); + if (error) { + throw error; + } + return masterEntries; + } catch (err) { + console.error('Error in updateAssetMasterData:', err); + throw err; + } +}; +export const createAssetMasterDataHandler = + (chain: SupportedChains): Handler => + async () => { + try { + const result = await updateAssetMasterData(chain); + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Master data collection completed', + count: result.length + }), + }; + } catch (err) { + console.error('Handler error:', err); + return { + statusCode: 500, + body: JSON.stringify({ + message: err instanceof Error ? err.message : 'Unknown error', + stack: err instanceof Error ? err.stack : undefined + }), + }; + } + }; +export const handler = createAssetMasterDataHandler +export const withRetry = async ( + fn: () => Promise, + retries = 3, + delay = 1000 +): Promise => { + try { + return await fn(); + } catch (e) { + if (retries === 0) throw e; + await new Promise(r => setTimeout(r, delay)); + return withRetry(fn, retries - 1, delay * 2); + } +}; diff --git a/packages/functions/src/functions/asset-master-data-base.ts b/packages/functions/src/functions/asset-master-data-base.ts new file mode 100644 index 0000000000..82070d2ffb --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-base.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.base); \ No newline at end of file diff --git a/packages/functions/src/functions/asset-master-data-bob.ts b/packages/functions/src/functions/asset-master-data-bob.ts new file mode 100644 index 0000000000..fd62d1c941 --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-bob.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.bob); \ No newline at end of file diff --git a/packages/functions/src/functions/asset-master-data-fraxtal.ts b/packages/functions/src/functions/asset-master-data-fraxtal.ts new file mode 100644 index 0000000000..745d861813 --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-fraxtal.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.fraxtal); \ No newline at end of file diff --git a/packages/functions/src/functions/asset-master-data-ink.ts b/packages/functions/src/functions/asset-master-data-ink.ts new file mode 100644 index 0000000000..72706f35a2 --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-ink.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.ink); \ No newline at end of file diff --git a/packages/functions/src/functions/asset-master-data-lisk.ts b/packages/functions/src/functions/asset-master-data-lisk.ts new file mode 100644 index 0000000000..3c5b37316a --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-lisk.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.lisk); \ No newline at end of file diff --git a/packages/functions/src/functions/asset-master-data-mode.ts b/packages/functions/src/functions/asset-master-data-mode.ts new file mode 100644 index 0000000000..99bbf1940e --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-mode.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.mode); \ No newline at end of file diff --git a/packages/functions/src/functions/asset-master-data-optimism.ts b/packages/functions/src/functions/asset-master-data-optimism.ts new file mode 100644 index 0000000000..3e863dc195 --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-optimism.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.optimism); \ No newline at end of file diff --git a/packages/functions/src/functions/asset-master-data-superseed.ts b/packages/functions/src/functions/asset-master-data-superseed.ts new file mode 100644 index 0000000000..fd15206146 --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-superseed.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.superseed); \ No newline at end of file diff --git a/packages/functions/src/functions/asset-master-data-worldchain.ts b/packages/functions/src/functions/asset-master-data-worldchain.ts new file mode 100644 index 0000000000..ce16ecd225 --- /dev/null +++ b/packages/functions/src/functions/asset-master-data-worldchain.ts @@ -0,0 +1,4 @@ +import { SupportedChains } from '@ionicprotocol/types'; +import { createAssetMasterDataHandler } from '../controllers/asset-master-data'; + +export const handler = createAssetMasterDataHandler(SupportedChains.worldchain); \ No newline at end of file