From 6326f2926cd664d246450033cd8efbbf3408cb7a Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:21:12 +0100 Subject: [PATCH 01/18] update sepolia script --- deployments/mainnet_addresses.json | 4 +- deployments/sepolia_addresses.json | 27 +++ scripts/deploy_xlbtc_branch.ts | 334 +++++++++++++++++++++++++++ scripts/types.ts | 13 +- src/price_feeds/XLBTCPriceFeed.cairo | 312 +++++++++++++++++++++++++ 5 files changed, 681 insertions(+), 9 deletions(-) create mode 100644 deployments/sepolia_addresses.json create mode 100644 scripts/deploy_xlbtc_branch.ts create mode 100644 src/price_feeds/XLBTCPriceFeed.cairo diff --git a/deployments/mainnet_addresses.json b/deployments/mainnet_addresses.json index 5f52df6..3656f65 100644 --- a/deployments/mainnet_addresses.json +++ b/deployments/mainnet_addresses.json @@ -2,6 +2,7 @@ "USDU": "0x2f94539f80158f9a48a7acf3747718dfbec9b6f639e2742c1fb44ae7ab5aa04", "gasToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", "collateralRegistry": "0x76512e6722f1891ce15ca7e7ff6514b6898872fb8e6ce881d16108e26457e90", + "hintHelpers": "0x44b632d0e09b042b6521cd4ba250add5387fb68736cb10f2cc2411c1a1c2a84", "WWBTC": { "collateral": "0x75d9e518f46a9ca0404fb0a7d386ce056dadf57fd9a0e8659772cb517be4a18", "addressesRegistry": "0x42a37aa9263b01191286f0f800cc85c676441fb9d27d74bbf3ebcbf4e373d81", @@ -19,9 +20,8 @@ "redemptionManager": "0x1f5ba149ecc34a37228bdbecff3ec84f957318d65bfb99650fbb020df4bb0d0", "batchManager": "0x33fce86e1de643d13ef366059193fc0ca42a08888c0a589866d940e865ceb38", "priceFeed": "0x6716836514e75e9f4eaa0fe93aa0448480a321b669041d6b5f27aa75374ac66", - "hintHelpers": "0x44b632d0e09b042b6521cd4ba250add5387fb68736cb10f2cc2411c1a1c2a84", "multiTroveGetter": "0x780627de12ac84a7887b7f83496a8ece5ea3c5eb7170f9f00587dabfdbe18d1", "troveManagerEventsEmitter": "0x38a9949900e7905f648cf0b50335efdac16a8acb8dfc870835da21c3f68e934", "underlyingAddress": "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac" } -} \ No newline at end of file +} diff --git a/deployments/sepolia_addresses.json b/deployments/sepolia_addresses.json new file mode 100644 index 0000000..9275bd8 --- /dev/null +++ b/deployments/sepolia_addresses.json @@ -0,0 +1,27 @@ +{ + "USDU": "0x4061120aee5424096759c209a6366c6a2f89c50470532c38322f8f78e58f133", + "gasToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "collateralRegistry": "0x4dcbbbff734cc8a8fe2a0af0b73919cd742c164796e16b49939aedebdeccf2a", + "hintHelpers": "0x1efdea1bd822408058aaef0f676fd63cf181956c876623874eb0490cc81fde8", + "WWBTC": { + "collateral": "0xe69925e62651175af9127e35192f682e63a237d8f1f28563fa114658d0e333", + "addressesRegistry": "0x2b419ce0e00c67dac08c05f05b7491a43be8efa6c18a4aab3641a74425f631f", + "borrowerOperations": "0x3479415282ce0a92ab2dd4382d658759b288e2c1e101bd48d0eb363193499c3", + "troveManager": "0x40f67798eda26efc838fecf98c2ce52765960da68c558180806785ba2ff4b19", + "troveNft": "0x707e31eb7eb83fada99d57f12feaa055c05a37a4d88ac1f37967a3578f247e9", + "stabilityPool": "0x2389a210b2053d662716a079f37bb25eee9b7e48bb2a793ea430d5a693aef87", + "sortedTroves": "0x7edc3dd39e36a7e70a6a9b019aca0041fd1dc5738b58f576e42df98d281fcb8", + "activePool": "0x359929753a326ecae73ae1dda6d5d81023538ceb35f3321400e6d53fe2db7d3", + "defaultPool": "0x3d1729f379adc88f5d9a98414e7fcf87cb8656cf117c3d2928a32534633a4b9", + "collSurplusPool": "0x654436795f3f659d891462870821c3575d15d39b791dd767e9e6bc7e4826479", + "gasPool": "0x65dc6451e4a9bfe2630dfe2a9b56731b3f3cedeb55ba3a81918cf611fbc37ba", + "interestRouter": "0x0477a98816f0298D678a8C74Bd06E898Ee4E3bB62FdB9bb05c397f3135aE8398", + "liquidationManager": "0x1fba8f328f3a22201328fbf0b608ba1fd56e628a98fbcb973029b23d0b0ba6", + "redemptionManager": "0x38707456411255e8f60aa9ef30e7561d03e978fdcdc70e1fbbb52cb9f84e93", + "batchManager": "0x305aca6cadcd7d6f4ebd60fd7ea32e020b8eff3fc3709733fc37e0102628f10", + "priceFeed": "0x7602e11dbd40c3943ac546cd872b9b66d63407673226e49ef9405830129ae2e", + "multiTroveGetter": "0x359929753a326ecae73ae1dda6d5d81023538ceb35f3321400e6d53fe2db7d3", + "troveManagerEventsEmitter": "0x3fce3dbd7cd7b59dc13e4f67bcc54f6a8e84d2ed917266543c2669af970e5d1", + "underlyingAddress": "0x5c91074ab62af523b9f260ae380fb193a039994a24eafca59385323a6002f2e" + } +} diff --git a/scripts/deploy_xlbtc_branch.ts b/scripts/deploy_xlbtc_branch.ts new file mode 100644 index 0000000..9ede43d --- /dev/null +++ b/scripts/deploy_xlbtc_branch.ts @@ -0,0 +1,334 @@ +import { Account, CallData, Contract, RpcProvider, logger } from 'starknet'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; +import { BranchAddresses, CONSTANTS } from './types'; +import { log } from './utils'; + +dotenv.config(); +logger.setLogLevel('ERROR'); + +type CachedContract = { + classHash: string; + abi: any; +}; + +const CONTRACTS_TO_DECLARE = [ + 'AddressesRegistry', + 'BorrowerOperations', + 'TroveManager', + 'TroveNFT', + 'StabilityPool', + 'SortedTroves', + 'ActivePool', + 'DefaultPool', + 'CollSurplusPool', + 'GasPool', + 'LiquidationManager', + 'RedemptionManager', + 'BatchManager', + 'TroveManagerEventsEmitter', + 'CollateralWrapper', +] as const; + +const BRANCH_KEY = 'XLBTC'; +const MAINNET_FILE = path.join(__dirname, '..', 'deployments', 'mainnet_addresses.json'); + +async function main() { + const { + PRIVATE_KEY, + DEPLOYER_ADDRESS, + RPC_URL, + OWNER_ADDRESS, + SPONSOR, + } = process.env; + let XLBTC_UNDERLYING_ADDRESS = process.env['XLBTC_UNDERLYING_ADDRESS']; + + const missing: string[] = []; + if (!PRIVATE_KEY) missing.push('PRIVATE_KEY'); + if (!DEPLOYER_ADDRESS) missing.push('DEPLOYER_ADDRESS'); + if (!RPC_URL) missing.push('RPC_URL'); + if (!OWNER_ADDRESS) missing.push('OWNER_ADDRESS'); + if (!SPONSOR) missing.push('SPONSOR'); + + if (missing.length) { + throw new Error(`Missing required env vars: ${missing.join(', ')}`); + } + + const addressesData = JSON.parse(fs.readFileSync(MAINNET_FILE, 'utf8')); + const usduAddress: string | undefined = addressesData['USDU']; + const collateralRegistryAddress: string | undefined = addressesData['collateralRegistry']; + const strkContractAddress: string = addressesData['gasToken'] ?? CONSTANTS.STRK; + const hintHelpersAddress: string | undefined = + addressesData?.WWBTC?.hintHelpers ?? addressesData?.hintHelpers; + + const interestRouterAddress: string = addressesData['interestRouter']; + + if (!usduAddress || !collateralRegistryAddress || !hintHelpersAddress) { + throw new Error('Missing USDU, CollateralRegistry, or HintHelpers address in mainnet_addresses.json'); + } + + const provider = new RpcProvider({ nodeUrl: RPC_URL! }); + const account = new Account(provider, DEPLOYER_ADDRESS!, PRIVATE_KEY!); + + const contractCache: Record = {}; + + async function declareContract(contractName: string): Promise { + if (contractCache[contractName]) return contractCache[contractName]; + + const basePath = path.join(__dirname, '..', 'target', 'dev'); + const sierraPath = path.join(basePath, `usdu_${contractName}.contract_class.json`); + const casmPath = path.join(basePath, `usdu_${contractName}.compiled_contract_class.json`); + + const compiledSierra = JSON.parse(fs.readFileSync(sierraPath, 'utf8')); + const compiledCasm = JSON.parse(fs.readFileSync(casmPath, 'utf8')); + + log(`Declaring ${contractName}...`); + const declareResponse = await account.declareIfNot({ + contract: compiledSierra, + casm: compiledCasm, + }); + + if (declareResponse.transaction_hash) { + await provider.waitForTransaction(declareResponse.transaction_hash, { retryInterval: 5000 }); + } + + const cacheEntry = { classHash: declareResponse.class_hash, abi: compiledSierra.abi }; + contractCache[contractName] = cacheEntry; + return cacheEntry; + } + + async function declareAll() { + log('Declaring contract classes...'); + for (const name of CONTRACTS_TO_DECLARE) { + await declareContract(name); + } + log('All classes declared\n'); + } + + async function deployContract(contractName: string, constructorArgs?: any): Promise { + const cached = await declareContract(contractName); + let constructorCalldata: string[] = []; + + if (constructorArgs) { + const contractCallData = new CallData(cached.abi); + constructorCalldata = contractCallData.compile('constructor', constructorArgs); + } + + const deployResponse = await account.deployContract({ + classHash: cached.classHash, + constructorCalldata, + }); + + await provider.waitForTransaction(deployResponse.transaction_hash, { retryInterval: 5000 }); + log(`Deployed ${contractName}: ${deployResponse.contract_address}`); + return deployResponse.contract_address; + } + + function getCachedAbi(contractName: string) { + const cached = contractCache[contractName]; + if (!cached) { + throw new Error(`Contract ${contractName} not declared`); + } + return cached.abi; + } + + await declareAll(); + + log('Deploying XLBTC branch contracts...'); + + const addressesRegistryAddress = await deployContract('AddressesRegistry', { + ccr: CONSTANTS.CCR_WBTC, + mcr: CONSTANTS.MCR_WBTC, + bcr: CONSTANTS.BCR_ALL, + scr: CONSTANTS.SCR_WBTC, + cap: CONSTANTS.CAP_WBTC, + liquidation_penalty_sp: CONSTANTS.LIQUIDATION_PENALTY_SP_WBTC, + liquidation_penalty_redistribution: CONSTANTS.LIQUIDATION_PENALTY_REDISTRIBUTION_WBTC, + sponsor: SPONSOR, + deployer: DEPLOYER_ADDRESS, + }); + + const borrowerOperationsAddress = await deployContract('BorrowerOperations', { deployer: DEPLOYER_ADDRESS }); + const troveManagerAddress = await deployContract('TroveManager', { deployer: DEPLOYER_ADDRESS }); + const troveNftAddress = await deployContract('TroveNFT', { deployer: DEPLOYER_ADDRESS }); + const stabilityPoolAddress = await deployContract('StabilityPool', { deployer: DEPLOYER_ADDRESS }); + const sortedTrovesAddress = await deployContract('SortedTroves', { deployer: DEPLOYER_ADDRESS }); + const activePoolAddress = await deployContract('ActivePool', { deployer: DEPLOYER_ADDRESS }); + const defaultPoolAddress = await deployContract('DefaultPool', { deployer: DEPLOYER_ADDRESS }); + const collSurplusPoolAddress = await deployContract('CollSurplusPool', { deployer: DEPLOYER_ADDRESS }); + const gasPoolAddress = await deployContract('GasPool', { deployer: DEPLOYER_ADDRESS }); + const liquidationManagerAddress = await deployContract('LiquidationManager', { deployer: DEPLOYER_ADDRESS }); + const redemptionManagerAddress = await deployContract('RedemptionManager', { deployer: DEPLOYER_ADDRESS }); + const batchManagerAddress = await deployContract('BatchManager', { deployer: DEPLOYER_ADDRESS }); + const troveManagerEventsEmitterAddress = await deployContract('TroveManagerEventsEmitter', { deployer: DEPLOYER_ADDRESS }); + const xlbtcPriceFeedAddress = process.env['NETWORK'] === 'mainnet' ? await deployContract('XLBTCPriceFeed', { deployer: DEPLOYER_ADDRESS }) : await deployContract('PriceFeedMock', {}); + + if (process.env['NETWORK'] === 'sepolia') { + XLBTC_UNDERLYING_ADDRESS = await deployContract('UBTC', [ + 'Liquid Staked Lombard Bitcoin', + 'XLBTC', + '8', + ]); + } + + const wrapperAddress = await deployContract('CollateralWrapper', [ + 'Wrapped XL Bitcoin', + 'W-XLBTC', + OWNER_ADDRESS, + XLBTC_UNDERLYING_ADDRESS, + ]); + + log('Initializing branch contracts...'); + const initializerCalls = []; + + const addressesRegistryContract = new Contract(getCachedAbi('AddressesRegistry'), addressesRegistryAddress, provider); + const addressesRegistryCall = addressesRegistryContract.populate('initializer', [ + activePoolAddress, + defaultPoolAddress, + xlbtcPriceFeedAddress, + hintHelpersAddress, + activePoolAddress, // multi trove getter placeholder + activePoolAddress, // metadata nft placeholder + strkContractAddress, + borrowerOperationsAddress, + troveManagerAddress, + troveNftAddress, + gasPoolAddress, + collSurplusPoolAddress, + sortedTrovesAddress, + collateralRegistryAddress, + usduAddress, + interestRouterAddress, + stabilityPoolAddress, + wrapperAddress, + liquidationManagerAddress, + redemptionManagerAddress, + batchManagerAddress, + troveManagerEventsEmitterAddress, + ]); + initializerCalls.push({ + contractAddress: addressesRegistryAddress, + entrypoint: 'initializer', + calldata: addressesRegistryCall.calldata || [], + }); + + const initTargets: Array<[string, string, any[]]> = [ + ['ActivePool', activePoolAddress, [addressesRegistryAddress]], + ['DefaultPool', defaultPoolAddress, [addressesRegistryAddress]], + ['TroveManagerEventsEmitter', troveManagerEventsEmitterAddress, [addressesRegistryAddress]], + ['BorrowerOperations', borrowerOperationsAddress, [addressesRegistryAddress]], + ['TroveManager', troveManagerAddress, [addressesRegistryAddress]], + [ + 'TroveNFT', + troveNftAddress, + [ + addressesRegistryAddress, + 'Uncap Position', + 'UPS', + 'ipfs://bafkreigmavce5idel7goe2x7f6p2fssdh2qvzeo67cgzfjw76tlx2itsfy', + ], + ], + ['GasPool', gasPoolAddress, [addressesRegistryAddress]], + ['CollSurplusPool', collSurplusPoolAddress, [addressesRegistryAddress]], + ['SortedTroves', sortedTrovesAddress, [addressesRegistryAddress]], + ['StabilityPool', stabilityPoolAddress, [addressesRegistryAddress]], + ['LiquidationManager', liquidationManagerAddress, [addressesRegistryAddress]], + ['RedemptionManager', redemptionManagerAddress, [addressesRegistryAddress]], + ['BatchManager', batchManagerAddress, [addressesRegistryAddress]], + ]; + + for (const [name, contractAddress, args] of initTargets) { + const abi = getCachedAbi(name); + const contract = new Contract(abi, contractAddress, provider); + const call = contract.populate('initializer', args); + initializerCalls.push({ + contractAddress, + entrypoint: 'initializer', + calldata: call.calldata || [], + }); + } + + const initTx = await account.execute(initializerCalls); + await provider.waitForTransaction(initTx.transaction_hash, { retryInterval: 5000 }); + log('Branch contracts initialized\n'); + + if (!XLBTC_UNDERLYING_ADDRESS) { + throw new Error('XLBTC_UNDERLYING_ADDRESS is not defined'); + } + + const branchAddresses: BranchAddresses = { + collateral: wrapperAddress, + addressesRegistry: addressesRegistryAddress, + borrowerOperations: borrowerOperationsAddress, + troveManager: troveManagerAddress, + troveNft: troveNftAddress, + stabilityPool: stabilityPoolAddress, + sortedTroves: sortedTrovesAddress, + activePool: activePoolAddress, + defaultPool: defaultPoolAddress, + collSurplusPool: collSurplusPoolAddress, + gasPool: gasPoolAddress, + interestRouter: interestRouterAddress, + liquidationManager: liquidationManagerAddress, + redemptionManager: redemptionManagerAddress, + batchManager: batchManagerAddress, + priceFeed: xlbtcPriceFeedAddress, + hintHelpers: hintHelpersAddress, + multiTroveGetter: activePoolAddress, + troveManagerEventsEmitter: troveManagerEventsEmitterAddress, + underlyingAddress: XLBTC_UNDERLYING_ADDRESS, + }; + + log('Linking branch with CollateralRegistry & USDU...'); + + const loadAbiFromArtifact = (contractName: string) => { + const sierraPath = path.join(__dirname, '..', 'target', 'dev', `usdu_${contractName}.contract_class.json`); + return JSON.parse(fs.readFileSync(sierraPath, 'utf8')).abi; + }; + + const collateralRegistryAbi = loadAbiFromArtifact('CollateralRegistry'); + const usduAbi = loadAbiFromArtifact('USDU'); + + const collateralRegistryContract = new Contract(collateralRegistryAbi, collateralRegistryAddress, provider); + const usduContract = new Contract(usduAbi, usduAddress, provider); + + const addCollateralCall = collateralRegistryContract.populate('add_collateral', [addressesRegistryAddress]); + const addBranchCall = usduContract.populate('add_branch_addresses', [ + branchAddresses.troveManager, + branchAddresses.stabilityPool, + branchAddresses.borrowerOperations, + branchAddresses.activePool, + branchAddresses.redemptionManager, + ]); + + const wiringTx = await account.execute([ + { + contractAddress: collateralRegistryAddress, + entrypoint: 'add_collateral', + calldata: addCollateralCall.calldata || [], + }, + { + contractAddress: usduAddress, + entrypoint: 'add_branch_addresses', + calldata: addBranchCall.calldata || [], + }, + ]); + await provider.waitForTransaction(wiringTx.transaction_hash, { retryInterval: 5000 }); + log('Branch wired into existing core contracts\n'); + + log(`Saving ${BRANCH_KEY} addresses to ${MAINNET_FILE}...`); + const updatedAddresses = { + ...addressesData, + [BRANCH_KEY]: branchAddresses, + }; + fs.writeFileSync(MAINNET_FILE, JSON.stringify(updatedAddresses, null, 2)); + log('Update complete'); + + log('✅ XLBTC branch deployment finished'); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/types.ts b/scripts/types.ts index 464f1d7..7b604fe 100644 --- a/scripts/types.ts +++ b/scripts/types.ts @@ -65,10 +65,12 @@ export const CONSTANTS = { BCR_ALL: '100000000000000000', // 10% // Constants from for UBTC - CCR_UBTC: '1500000000000000000', // 150% - MCR_UBTC: '1100000000000000000', // 110% - SCR_UBTC: '1100000000000000000', // 110% - CAP_UBTC: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + CCR_XLBTC: '1500000000000000000', // 150% + MCR_XLBTC: '1250000000000000000', // 110% + SCR_XLBTC: '1100000000000000000', // 110% + CAP_XLBTC: '500000000000000000000000', // 500,000 UBTC + LIQUIDATION_PENALTY_SP_XLBTC: '50000000000000000', // 5% + LIQUIDATION_PENALTY_REDISTRIBUTION_XLBTC: '200000000000000000', // 20% // Constants from for WBTC CCR_WBTC: '1500000000000000000', // 150% @@ -76,9 +78,6 @@ export const CONSTANTS = { SCR_WBTC: '1100000000000000000', // 110% CAP_WBTC: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', - LIQUIDATION_PENALTY_SP_UBTC: '50000000000000000', // 5% - LIQUIDATION_PENALTY_REDISTRIBUTION_UBTC: '100000000000000000', // 10% - LIQUIDATION_PENALTY_SP_WBTC: '50000000000000000', // 5% LIQUIDATION_PENALTY_REDISTRIBUTION_WBTC: '100000000000000000', // 10% } as const; diff --git a/src/price_feeds/XLBTCPriceFeed.cairo b/src/price_feeds/XLBTCPriceFeed.cairo new file mode 100644 index 0000000..e76b3e7 --- /dev/null +++ b/src/price_feeds/XLBTCPriceFeed.cairo @@ -0,0 +1,312 @@ +// Created by Uncap Labs +// SPDX-License-Identifier: BUSL-1.1 + +// Chainlink's Aggregator Interface +#[starknet::interface] +pub trait IAggregator { + fn latest_round_data(self: @TContractState) -> Round; + fn round_data(self: @TContractState, round_id: u128) -> Round; + fn description(self: @TContractState) -> felt252; + fn decimals(self: @TContractState) -> u8; + fn latest_answer(self: @TContractState) -> u128; +} + +#[derive(Copy, Drop, Serde, PartialEq, starknet::Store)] +pub struct Round { + // used as u128 internally, but necessary for phase-prefixed round ids as returned by proxy + pub round_id: felt252, + pub answer: u128, + pub block_num: u64, + pub started_at: u64, + pub updated_at: u64, +} + +// Composite Price Feed using Pragma, and Chainlink as a fallback. +// Uses the maximum value of BTC/USD and BTC/USD, as long as the difference is less than 1%. +// Otherwise, uses WBTC/USD price. Done to avoid excessive redemption arbitrage. +#[starknet::contract] +pub mod XLBTCPriceFeed { + use core::cmp::max; + use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; + use pragma_lib::types::{DataType, PragmaPricesResponse}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ContractAddress, get_block_timestamp}; + use crate::branch::interfaces::IAddressesRegistry::{ + IAddressesRegistryDispatcher, IAddressesRegistryDispatcherTrait, + }; + use crate::branch::interfaces::IBorrowerOperations::{ + IBorrowerOperationsDispatcher, IBorrowerOperationsDispatcherTrait, + }; + use crate::interfaces::IPriceFeed::IPriceFeed; + use crate::utils::constants::Constants::{DECIMAL_PRECISION, _1PCT}; + use super::{IAggregatorDispatcher, IAggregatorDispatcherTrait}; + + ////////////////////////////////////////////////////////////// + // CONSTANTS // + ////////////////////////////////////////////////////////////// + + const PRICE_FEED_BTC_USD: felt252 = 'BTC/USD'; + const PRICE_FEED_WBTC_USD: felt252 = 'WBTC/USD'; + const STANDARD_ORACLE_DECIMALS: u32 = 8; // Both BTC and WBTC oracles have 8 decimals + const PRICE_SCALING_FACTOR: u256 = 10_000_000_000; // 10^10: (10^18 - 10^8) + const DEVIATION_THRESHOLD: u256 = _1PCT; // 1% + const STALENESS_THRESHOLD: u64 = 60 * 60 * 24; // 1 day in seconds + const MIN_NUM_SOURCES: u32 = + 5; // Minimum number of sources for an oracle to be considered valid + + ////////////////////////////////////////////////////////////// + // STORAGE // + ////////////////////////////////////////////////////////////// + + #[storage] + pub struct Storage { + pragma_oracle_address: ContractAddress, + chainlink_btc_usd_address: ContractAddress, + chainlink_wbtc_usd_address: ContractAddress, + has_been_shutdown: bool, + last_good_price: u256, + addresses_registry: ContractAddress, + } + + ////////////////////////////////////////////////////////////// + // EVENTS // + ////////////////////////////////////////////////////////////// + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ShutDownFromOracleFailure: ShutDownFromOracleFailure, + } + + #[derive(Drop, starknet::Event)] + pub struct ShutDownFromOracleFailure { + pub oracle: ContractAddress, + } + + ////////////////////////////////////////////////////////////// + // ENUMS // + ////////////////////////////////////////////////////////////// + + #[derive(Copy, Clone, Drop)] + pub enum PriceFeedId { + BTCUSD, + WBTCUSD, + } + + + ////////////////////////////////////////////////////////////// + // CONSTRUCTOR // + ////////////////////////////////////////////////////////////// + + #[constructor] + fn constructor( + ref self: ContractState, + pragma_oracle_address: ContractAddress, + chainlink_btc_usd_address: ContractAddress, + chainlink_wbtc_usd_address: ContractAddress, + addresses_registry: ContractAddress, + ) { + self.pragma_oracle_address.write(pragma_oracle_address); + self.chainlink_btc_usd_address.write(chainlink_btc_usd_address); + self.chainlink_wbtc_usd_address.write(chainlink_wbtc_usd_address); + + self.addresses_registry.write(addresses_registry); + + // Fetch the initial price to set the last good price + // Will shutdown if the oracle fails + self.fetch_price_primary(false); + + assert(!self.has_been_shutdown.read(), 'WBTCPF: System already shutdown'); + } + + //////////////////////////////////////////////////////////////// + // EXTERNAL FUNCTIONS // + //////////////////////////////////////////////////////////////// + + #[abi(embed_v0)] + impl IPriceFeedImpl of IPriceFeed { + // Returns: + // - The price, using the current price calculation + // - A bool that is true if: + // --- a) The system was not shutdown prior to this call and + // --- b) an oracle contract failed during this call + fn fetch_price(ref self: ContractState) -> (u256, bool) { + self.fetch_price_primary(false) + } + + // Same as fetch_price, but uses a composite price for redemption (see + // `get_redemption_price`) + fn fetch_redemption_price(ref self: ContractState) -> (u256, bool) { + self.fetch_price_primary(true) + } + + // Returns oracles' price of WBTC/USD + fn get_price(self: @ContractState) -> u256 { + if self.has_been_shutdown.read() { + return self.last_good_price.read(); + } + let (price, _) = self.get_oracles_answer(PriceFeedId::WBTCUSD); + price + } + } + + //////////////////////////////////////////////////////////////// + // INTERNAL FUNCTIONS // + //////////////////////////////////////////////////////////////// + + #[generate_trait] + impl InternalImpl of InternalTrait { + ////////////////////////////////////////////////////////////// + // WRITE FUNCTIONS // + ////////////////////////////////////////////////////////////// + + fn fetch_price_primary(ref self: ContractState, is_redemption: bool) -> (u256, bool) { + // If branch has been shutdown, return the last good price + if self.has_been_shutdown.read() { + return (self.last_good_price.read(), false); + } + + // Always fetch WBTC price first + let (wbtc_price, wbtc_oracle_failure) = self.get_oracles_answer(PriceFeedId::WBTCUSD); + let mut final_price = wbtc_price; + let mut oracle_failure = wbtc_oracle_failure; + + // If it's a redemption, also fetch BTC price and use composite price + if (is_redemption) { + let (btc_price, btc_oracle_failure) = self.get_oracles_answer(PriceFeedId::BTCUSD); + oracle_failure = btc_oracle_failure || wbtc_oracle_failure; + if (!oracle_failure) { + final_price = get_redemption_price(wbtc_price, btc_price); + } + } + + if (oracle_failure) { + self.has_been_shutdown.write(true); + + let ar = IAddressesRegistryDispatcher { + contract_address: self.addresses_registry.read(), + }; + let bo = IBorrowerOperationsDispatcher { + contract_address: ar.get_borrower_operations(), + }; + bo.shutdown_from_oracle_failure(); + + self + .emit( + event: ShutDownFromOracleFailure { + oracle: self.pragma_oracle_address.read(), + }, + ); + + return (self.last_good_price.read(), true); + } else { + // Always store the normal WBTC price as last_good_price, not the redemption price + // This ensures that during shutdown, urgent redemptions use the actual market price + self.last_good_price.write(wbtc_price); + (final_price, false) + } + } + + ////////////////////////////////////////////////////////////// + // READ FUNCTIONS // + ////////////////////////////////////////////////////////////// + + fn get_chainlink_answer(self: @ContractState, price_feed_id: PriceFeedId) -> (u256, bool) { + let cl_address = match price_feed_id { + PriceFeedId::BTCUSD => self.chainlink_btc_usd_address.read(), + PriceFeedId::WBTCUSD => self.chainlink_wbtc_usd_address.read(), + }; + let cl_aggregator = IAggregatorDispatcher { contract_address: cl_address }; + let latest_round = cl_aggregator.latest_round_data(); + let price = latest_round.answer.into() * PRICE_SCALING_FACTOR; + let latest_update = latest_round.updated_at; + let is_stale_price = latest_update + STALENESS_THRESHOLD < get_block_timestamp(); + let price_is_zero = price == 0; + let oracle_failure = is_stale_price || price_is_zero; + + (price, oracle_failure) + } + + fn get_oracles_answer(self: @ContractState, price_feed_id: PriceFeedId) -> (u256, bool) { + // Get the answer from the pragma oracle + let (pragma_price, pragma_failure, pragma_not_enough_sources) = self + .get_pragma_answer(price_feed_id); + + // If pragma failed, try the chainlink oracle + if (pragma_failure) { + let (cl_price, chainlink_failure) = self.get_chainlink_answer(price_feed_id); + if chainlink_failure { + // CL failed, but pragma failed only because of not enough sources, return the + // pragma price, and do not shutdown + if pragma_not_enough_sources { + return (pragma_price, false); + } else { + // CL failed and pragma failed for other reasons, shutdown + return (cl_price, true); + } + } else { + // CL did not fail, return the CL price + return (cl_price, false); + } + } else { + // Pragma did not fail, return the Pragma price + return (pragma_price, false); + } + } + + fn get_pragma_answer( + self: @ContractState, price_feed_id: PriceFeedId, + ) -> (u256, bool, bool) { + let oracle_dispatcher = IPragmaABIDispatcher { + contract_address: self.pragma_oracle_address.read(), + }; + let price_feed = match price_feed_id { + PriceFeedId::BTCUSD => DataType::SpotEntry(PRICE_FEED_BTC_USD), + PriceFeedId::WBTCUSD => DataType::SpotEntry(PRICE_FEED_WBTC_USD), + }; + let response: PragmaPricesResponse = oracle_dispatcher.get_data_median(price_feed); + let price = response.price.into() * PRICE_SCALING_FACTOR; + + let price_is_zero = price == 0; // Price should never be zero + let incorrect_decimals = response + .decimals != STANDARD_ORACLE_DECIMALS; // Decimals should not change + let is_stale_price = response.last_updated_timestamp + + STALENESS_THRESHOLD < get_block_timestamp(); // Price should not be stale + + let not_enough_sources = response.num_sources_aggregated < MIN_NUM_SOURCES; + + let oracle_failure_detected = price_is_zero + || incorrect_decimals + || is_stale_price + || not_enough_sources; // If any of these are true, the oracle failed + + // Checks if the oracle failed ONLY because of not enough sources + let oracle_failure_because_not_enough_sources = oracle_failure_detected + && !(price_is_zero || incorrect_decimals || is_stale_price); + + return (price, oracle_failure_detected, oracle_failure_because_not_enough_sources); + } + } + + //////////////////////////////////////////////////////////////// + // UTILITY FUNCTIONS // + //////////////////////////////////////////////////////////////// + + // Returns the best price (for the protocol) between BTC and WBTC, as long as the difference + // is less than 1%. + fn get_redemption_price(wbtc_price: u256, btc_price: u256) -> u256 { + if within_deviation_threshold(wbtc_price, btc_price) { + return max(wbtc_price, btc_price); + } else { + wbtc_price + } + } + + fn within_deviation_threshold(wbtc_price: u256, btc_price: u256) -> bool { + // Calculate the price deviation between BTC and WBTC + let max = wbtc_price * (DECIMAL_PRECISION + DEVIATION_THRESHOLD) / DECIMAL_PRECISION; + let min = wbtc_price * (DECIMAL_PRECISION - DEVIATION_THRESHOLD) / DECIMAL_PRECISION; + + btc_price >= min && btc_price <= max + } +} From a0c4b76b2b543918a65eadd1ddedd02f493625f3 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:00:13 +0100 Subject: [PATCH 02/18] add new branch --- deployments/sepolia_addresses.json | 68 ++++-- scripts/deploy.ts | 16 +- scripts/deploy_xlbtc_branch.ts | 343 ++++++++++++++++++++++------- 3 files changed, 322 insertions(+), 105 deletions(-) diff --git a/deployments/sepolia_addresses.json b/deployments/sepolia_addresses.json index 9275bd8..1ebf5c4 100644 --- a/deployments/sepolia_addresses.json +++ b/deployments/sepolia_addresses.json @@ -1,27 +1,49 @@ { - "USDU": "0x4061120aee5424096759c209a6366c6a2f89c50470532c38322f8f78e58f133", + "USDU": "0x2f05a7b79478c064d338af2706e19ddbbbd77c102ba46cb293f4e2f9dccf751", "gasToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", - "collateralRegistry": "0x4dcbbbff734cc8a8fe2a0af0b73919cd742c164796e16b49939aedebdeccf2a", - "hintHelpers": "0x1efdea1bd822408058aaef0f676fd63cf181956c876623874eb0490cc81fde8", + "collateralRegistry": "0x8e053cf9353189895637303422000fa246c5789b7fd400455fa2d9e392df7d", "WWBTC": { - "collateral": "0xe69925e62651175af9127e35192f682e63a237d8f1f28563fa114658d0e333", - "addressesRegistry": "0x2b419ce0e00c67dac08c05f05b7491a43be8efa6c18a4aab3641a74425f631f", - "borrowerOperations": "0x3479415282ce0a92ab2dd4382d658759b288e2c1e101bd48d0eb363193499c3", - "troveManager": "0x40f67798eda26efc838fecf98c2ce52765960da68c558180806785ba2ff4b19", - "troveNft": "0x707e31eb7eb83fada99d57f12feaa055c05a37a4d88ac1f37967a3578f247e9", - "stabilityPool": "0x2389a210b2053d662716a079f37bb25eee9b7e48bb2a793ea430d5a693aef87", - "sortedTroves": "0x7edc3dd39e36a7e70a6a9b019aca0041fd1dc5738b58f576e42df98d281fcb8", - "activePool": "0x359929753a326ecae73ae1dda6d5d81023538ceb35f3321400e6d53fe2db7d3", - "defaultPool": "0x3d1729f379adc88f5d9a98414e7fcf87cb8656cf117c3d2928a32534633a4b9", - "collSurplusPool": "0x654436795f3f659d891462870821c3575d15d39b791dd767e9e6bc7e4826479", - "gasPool": "0x65dc6451e4a9bfe2630dfe2a9b56731b3f3cedeb55ba3a81918cf611fbc37ba", - "interestRouter": "0x0477a98816f0298D678a8C74Bd06E898Ee4E3bB62FdB9bb05c397f3135aE8398", - "liquidationManager": "0x1fba8f328f3a22201328fbf0b608ba1fd56e628a98fbcb973029b23d0b0ba6", - "redemptionManager": "0x38707456411255e8f60aa9ef30e7561d03e978fdcdc70e1fbbb52cb9f84e93", - "batchManager": "0x305aca6cadcd7d6f4ebd60fd7ea32e020b8eff3fc3709733fc37e0102628f10", - "priceFeed": "0x7602e11dbd40c3943ac546cd872b9b66d63407673226e49ef9405830129ae2e", - "multiTroveGetter": "0x359929753a326ecae73ae1dda6d5d81023538ceb35f3321400e6d53fe2db7d3", - "troveManagerEventsEmitter": "0x3fce3dbd7cd7b59dc13e4f67bcc54f6a8e84d2ed917266543c2669af970e5d1", - "underlyingAddress": "0x5c91074ab62af523b9f260ae380fb193a039994a24eafca59385323a6002f2e" + "collateral": "0x661c69bdb25beedf6fda005270c0ebd908f0649961b50f0a602adfa6abc4012", + "addressesRegistry": "0x4c224615501cce6282507139b4db690db6edd7c24606a229efa6345fb7c4ce4", + "borrowerOperations": "0x34c7bac3356a015e6b01cc3a7a3453c58b9b3dbfefb7386eb81ee1236036f9c", + "troveManager": "0x2aaf15e3669c491bc9f48799b9d39d081cd1a7dd8c7e45935f916ce47386d56", + "troveNft": "0x541db872ea4fdec09de8d1f3d9e774fb741fe78011352e9816fc6a60f66e9e", + "stabilityPool": "0x47a0c4a4468ba7174d845af4321bb7514fe5f5404a9a1f1b5ece4b7e49be235", + "sortedTroves": "0x5e1d330d6862b1170b5c9f82ddd4ac3be9a08d309c8c8d0c8d2da93076a25db", + "activePool": "0x1e6578aec5f10c1fc40af683cb038ff628a43d6ac8ffb38e00c940873e9e4ef", + "defaultPool": "0x6eabacd1332437995bfca84370bf2c993923957f33cc53eca617c2bf7157173", + "collSurplusPool": "0x11ea4a3dba57cb9e3fdf39abd9d8cc92e41d1ee5284b6293112004da409d31f", + "gasPool": "0x35ea1b94646f154dd69d00e990918dd2d7878bae62769e2d2ed93bd7f321e26", + "interestRouter": "0x03Bb8724887FE0f9DcFC3806053314Ce7F091fB876fc128c5080B27fa77203f2", + "liquidationManager": "0x5623e9f87ec2422495bb036e4f81978980b4db9d53d7aeaab625be36b281d42", + "redemptionManager": "0x61a6ff14e52f54580f6edf453f85cb49eb22868a094521c10211a581ffa7f18", + "batchManager": "0x540e673a3866c20f9faa0b6fe430354174b07e66e47999a4e339df524022e10", + "priceFeed": "0x5d4dbc2a89c965c9f215f4c1e1a5fc1966c1ccf5e37f152c5e09e70231938c8", + "hintHelpers": "0x2645aa0fe3f040600600870c0a0214f727c65edbe073dc9e584ab8fd8c45544", + "multiTroveGetter": "0x1e6578aec5f10c1fc40af683cb038ff628a43d6ac8ffb38e00c940873e9e4ef", + "troveManagerEventsEmitter": "0x49c04436630bd42fa651e58c2f64a7f9c994e327ea8424d63f3a4a0e410229", + "underlyingAddress": "0x661c69bdb25beedf6fda005270c0ebd908f0649961b50f0a602adfa6abc4012" + }, + "XLBTC": { + "collateral": "0x494c0fe65bb6ffae3d5fead7140235e192f546bed01c61b4107f8eff5d14da6", + "addressesRegistry": "0xd0c41d4725084c79fef8db2580c9cfeafbee80ae0d7f5e41ffc5ac0b10b812", + "borrowerOperations": "0xc7335f6fc7d9e773d466a254a33e9567a2a6ec5f59bff2cbdcd901c99453f6", + "troveManager": "0x5d9bc54a46c549704003acd7dd1d1147523a7a4b1f56f8b760e96879c4ab94c", + "troveNft": "0x22b27d5bc8b9d0cca019fdecd6b618e9f331a1dafe1330c3b216b2668cea2d7", + "stabilityPool": "0x1205cfb0931799fa2a49b9e1bdba4e62575a132e2c79796ff8aad1b4e020114", + "sortedTroves": "0x327b1c4ec2b5c25110e08de235bdca0b225e8c6a030edcbe0210f580782b8f5", + "activePool": "0xbb983b4dc87572e8a17e83b9619d95a570971380915298399f17ffce6e0dba", + "defaultPool": "0x60b161ed09825a669df01f582d50038c277ded6d6a6c4e92712945f9eb0ce7b", + "collSurplusPool": "0x7a7496c31c232fd1d5873ae2cbdd9cc0903a1cf3d70db5cca0033da87a5afd6", + "gasPool": "0x5061c60b3ceefb78d1f64f4a63f9db725e2a330ac2691d04bbcd9b443c77dea", + "interestRouter": "0x03Bb8724887FE0f9DcFC3806053314Ce7F091fB876fc128c5080B27fa77203f2", + "liquidationManager": "0x49e9e55dac5ce759bbcbc6cc4685b6b180add055c00f416d6fb2c1a7dd8df89", + "redemptionManager": "0x582c970661fe66019eab098e3709e64d8f89205106638cc36eb39fa860282f9", + "batchManager": "0x3e1cd610ece9421035e20b3b81d371cee2072fd04836b84a59b752de287e312", + "priceFeed": "0x6d5dd946b583d6cacdddfd3e6a53f89dabeb17c317b894a4e449cb2845dcf39", + "hintHelpers": "0x2645aa0fe3f040600600870c0a0214f727c65edbe073dc9e584ab8fd8c45544", + "multiTroveGetter": "0xbb983b4dc87572e8a17e83b9619d95a570971380915298399f17ffce6e0dba", + "troveManagerEventsEmitter": "0x1b83eb6104548ea8474a2dbb32020bc25cefbdd27f10b35a703a5994246652e", + "underlyingAddress": "0x31bdabbece650b229acbd4dba41457e4e12590eba0ff3f36ab3d514a23d415b" } -} +} \ No newline at end of file diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 64f2b41..a56d659 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -232,7 +232,7 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str 'PriceFeedMock', 'TroveManagerEventsEmitter', 'WBTCPriceFeed', - // 'UBTC', + 'UBTC', 'USDU', 'CollateralRegistry', 'CollateralWrapper', @@ -678,13 +678,13 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // Deploy USDU // let usduContractAddress = getContractAddress('USDU'); // if (!usduContractAddress) { - log('USDU not found in loaded addresses, deploying...'); - const usduContractAddress = '0x2f94539f80158f9a48a7acf3747718dfbec9b6f639e2742c1fb44ae7ab5aa04'; - // await deployContract('USDU', { - // name: 'Uncap USD', - // symbol: 'USDU', - // deployer: context.config.deployerAddress, - // }); + // log('USDU not found in loaded addresses, deploying...'); + // const usduContractAddress = '0x2f94539f80158f9a48a7acf3747718dfbec9b6f639e2742c1fb44ae7ab5aa04'; + const usduContractAddress = await deployContract('USDU', { + name: 'Uncap USD', + symbol: 'USDU', + deployer: context.config.deployerAddress, + }); // } else { // log(`Using existing USDU at: ${usduContractAddress}`); // } diff --git a/scripts/deploy_xlbtc_branch.ts b/scripts/deploy_xlbtc_branch.ts index 9ede43d..69dab73 100644 --- a/scripts/deploy_xlbtc_branch.ts +++ b/scripts/deploy_xlbtc_branch.ts @@ -32,7 +32,8 @@ const CONTRACTS_TO_DECLARE = [ ] as const; const BRANCH_KEY = 'XLBTC'; -const MAINNET_FILE = path.join(__dirname, '..', 'deployments', 'mainnet_addresses.json'); + +const ADDRESSES_FILE = path.join(__dirname, '..', 'deployments', `${process.env['NETWORK']}_addresses.json`); async function main() { const { @@ -55,14 +56,17 @@ async function main() { throw new Error(`Missing required env vars: ${missing.join(', ')}`); } - const addressesData = JSON.parse(fs.readFileSync(MAINNET_FILE, 'utf8')); + const addressesData = JSON.parse(fs.readFileSync(ADDRESSES_FILE, 'utf8')); const usduAddress: string | undefined = addressesData['USDU']; const collateralRegistryAddress: string | undefined = addressesData['collateralRegistry']; const strkContractAddress: string = addressesData['gasToken'] ?? CONSTANTS.STRK; const hintHelpersAddress: string | undefined = addressesData?.WWBTC?.hintHelpers ?? addressesData?.hintHelpers; - const interestRouterAddress: string = addressesData['interestRouter']; + const interestRouterAddress: string = addressesData?.WWBTC?.interestRouter; + if (!interestRouterAddress) { + throw new Error('Missing interestRouter address in addresses.json'); + } if (!usduAddress || !collateralRegistryAddress || !hintHelpersAddress) { throw new Error('Missing USDU, CollateralRegistry, or HintHelpers address in mainnet_addresses.json'); @@ -107,6 +111,7 @@ async function main() { } async function deployContract(contractName: string, constructorArgs?: any): Promise { + log(`Deploying ${contractName}...`); const cached = await declareContract(contractName); let constructorCalldata: string[] = []; @@ -180,82 +185,272 @@ async function main() { ]); log('Initializing branch contracts...'); - const initializerCalls = []; - - const addressesRegistryContract = new Contract(getCachedAbi('AddressesRegistry'), addressesRegistryAddress, provider); - const addressesRegistryCall = addressesRegistryContract.populate('initializer', [ - activePoolAddress, - defaultPoolAddress, - xlbtcPriceFeedAddress, - hintHelpersAddress, - activePoolAddress, // multi trove getter placeholder - activePoolAddress, // metadata nft placeholder - strkContractAddress, - borrowerOperationsAddress, - troveManagerAddress, - troveNftAddress, - gasPoolAddress, - collSurplusPoolAddress, - sortedTrovesAddress, - collateralRegistryAddress, - usduAddress, - interestRouterAddress, - stabilityPoolAddress, - wrapperAddress, - liquidationManagerAddress, - redemptionManagerAddress, - batchManagerAddress, - troveManagerEventsEmitterAddress, - ]); - initializerCalls.push({ - contractAddress: addressesRegistryAddress, - entrypoint: 'initializer', - calldata: addressesRegistryCall.calldata || [], - }); - - const initTargets: Array<[string, string, any[]]> = [ - ['ActivePool', activePoolAddress, [addressesRegistryAddress]], - ['DefaultPool', defaultPoolAddress, [addressesRegistryAddress]], - ['TroveManagerEventsEmitter', troveManagerEventsEmitterAddress, [addressesRegistryAddress]], - ['BorrowerOperations', borrowerOperationsAddress, [addressesRegistryAddress]], - ['TroveManager', troveManagerAddress, [addressesRegistryAddress]], - [ - 'TroveNFT', + // Initialize all contracts using multicall + log('Initializing branch contracts with multicall...'); + + // Debug logging to identify undefined variables + log('Debug - Checking all addresses before initializer:'); + log(` activePoolAddress: ${activePoolAddress}`); + log(` defaultPoolAddress: ${defaultPoolAddress}`); + log(` xlbtcPriceFeedAddress: ${xlbtcPriceFeedAddress}`); + log(` hintHelpersAddress: ${hintHelpersAddress}`); + log(` strkContractAddress: ${strkContractAddress}`); + log(` borrowerOperationsAddress: ${borrowerOperationsAddress}`); + log(` troveManagerAddress: ${troveManagerAddress}`); + log(` troveNftAddress: ${troveNftAddress}`); + log(` gasPoolAddress: ${gasPoolAddress}`); + log(` collSurplusPoolAddress: ${collSurplusPoolAddress}`); + log(` sortedTrovesAddress: ${sortedTrovesAddress}`); + log(` collateralRegistryAddress: ${collateralRegistryAddress}`); + log(` usduAddress: ${usduAddress}`); + log(` interestRouterAddress: ${interestRouterAddress}`); + log(` stabilityPoolAddress: ${stabilityPoolAddress}`); + log(` wrapperAddress: ${wrapperAddress}`); + log(` liquidationManagerAddress: ${liquidationManagerAddress}`); + log(` redemptionManagerAddress: ${redemptionManagerAddress}`); + log(` batchManagerAddress: ${batchManagerAddress}`); + log(` troveManagerEventsEmitterAddress: ${troveManagerEventsEmitterAddress}`); + + // Prepare all initializer calls + const initializerCalls = []; + + // AddressesRegistry initializer + const addressesRegistryAbi = getCachedAbi('AddressesRegistry'); + const addressesRegistryContract = new Contract( + addressesRegistryAbi, + addressesRegistryAddress, + provider + ); + + const addressesRegistryCall = addressesRegistryContract.populate('initializer', [ + activePoolAddress, + defaultPoolAddress, + xlbtcPriceFeedAddress, + hintHelpersAddress, + activePoolAddress, // multi_trove_getter - using active_pool as placeholder + activePoolAddress, // metadata_nft - using active_pool as placeholder + strkContractAddress, // strk + borrowerOperationsAddress, + troveManagerAddress, troveNftAddress, - [ - addressesRegistryAddress, - 'Uncap Position', - 'UPS', - 'ipfs://bafkreigmavce5idel7goe2x7f6p2fssdh2qvzeo67cgzfjw76tlx2itsfy', - ], - ], - ['GasPool', gasPoolAddress, [addressesRegistryAddress]], - ['CollSurplusPool', collSurplusPoolAddress, [addressesRegistryAddress]], - ['SortedTroves', sortedTrovesAddress, [addressesRegistryAddress]], - ['StabilityPool', stabilityPoolAddress, [addressesRegistryAddress]], - ['LiquidationManager', liquidationManagerAddress, [addressesRegistryAddress]], - ['RedemptionManager', redemptionManagerAddress, [addressesRegistryAddress]], - ['BatchManager', batchManagerAddress, [addressesRegistryAddress]], - ]; - - for (const [name, contractAddress, args] of initTargets) { - const abi = getCachedAbi(name); - const contract = new Contract(abi, contractAddress, provider); - const call = contract.populate('initializer', args); + gasPoolAddress, + collSurplusPoolAddress, + sortedTrovesAddress, + collateralRegistryAddress, + usduAddress, + interestRouterAddress, + stabilityPoolAddress, + wrapperAddress, // coll_token + liquidationManagerAddress, + redemptionManagerAddress, + batchManagerAddress, + troveManagerEventsEmitterAddress, + ]); initializerCalls.push({ - contractAddress, + contractAddress: addressesRegistryAddress, entrypoint: 'initializer', - calldata: call.calldata || [], + calldata: addressesRegistryCall.calldata || [], + }); + + // ActivePool initializer + const activePoolAbi = getCachedAbi('ActivePool'); + const activePoolContract = new Contract(activePoolAbi, activePoolAddress, provider); + const activePoolCall = activePoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: activePoolAddress, + entrypoint: 'initializer', + calldata: activePoolCall.calldata || [], + }); + + // DefaultPool initializer + const defaultPoolAbi = getCachedAbi('DefaultPool'); + const defaultPoolContract = new Contract(defaultPoolAbi, defaultPoolAddress, provider); + const defaultPoolCall = defaultPoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: defaultPoolAddress, + entrypoint: 'initializer', + calldata: defaultPoolCall.calldata || [], + }); + + // TroveManagerEventsEmitter initializer + const troveManagerEventsEmitterAbi = getCachedAbi('TroveManagerEventsEmitter'); + const troveManagerEventsEmitterContract = new Contract( + troveManagerEventsEmitterAbi, + troveManagerEventsEmitterAddress, + provider + ); + const troveManagerEventsEmitterCall = troveManagerEventsEmitterContract.populate( + 'initializer', + [addressesRegistryAddress] + ); + initializerCalls.push({ + contractAddress: troveManagerEventsEmitterAddress, + entrypoint: 'initializer', + calldata: troveManagerEventsEmitterCall.calldata || [], }); - } - const initTx = await account.execute(initializerCalls); - await provider.waitForTransaction(initTx.transaction_hash, { retryInterval: 5000 }); - log('Branch contracts initialized\n'); + // BorrowerOperations initializer + const borrowerOperationsAbi = getCachedAbi('BorrowerOperations'); + const borrowerOperationsContract = new Contract( + borrowerOperationsAbi, + borrowerOperationsAddress, + provider + ); + const borrowerOperationsCall = borrowerOperationsContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: borrowerOperationsAddress, + entrypoint: 'initializer', + calldata: borrowerOperationsCall.calldata || [], + }); + + // TroveManager initializer + const troveManagerAbi = getCachedAbi('TroveManager'); + const troveManagerContract = new Contract( + troveManagerAbi, + troveManagerAddress, + provider + ); + const troveManagerCall = troveManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: troveManagerAddress, + entrypoint: 'initializer', + calldata: troveManagerCall.calldata || [], + }); + + // TroveNFT initializer + const troveNftAbi = getCachedAbi('TroveNFT'); + const troveNftContract = new Contract(troveNftAbi, troveNftAddress, provider); + const troveNftCall = troveNftContract.populate('initializer', [ + addressesRegistryAddress, + 'Uncap Position', + 'UPS', + 'ipfs://bafkreigmavce5idel7goe2x7f6p2fssdh2qvzeo67cgzfjw76tlx2itsfy', + ]); + initializerCalls.push({ + contractAddress: troveNftAddress, + entrypoint: 'initializer', + calldata: troveNftCall.calldata || [], + }); + + // GasPool initializer + const gasPoolAbi = getCachedAbi('GasPool'); + const gasPoolContract = new Contract(gasPoolAbi, gasPoolAddress, provider); + const gasPoolCall = gasPoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: gasPoolAddress, + entrypoint: 'initializer', + calldata: gasPoolCall.calldata || [], + }); + + // CollSurplusPool initializer + const collSurplusPoolAbi = getCachedAbi('CollSurplusPool'); + const collSurplusPoolContract = new Contract( + collSurplusPoolAbi, + collSurplusPoolAddress, + provider + ); + const collSurplusPoolCall = collSurplusPoolContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: collSurplusPoolAddress, + entrypoint: 'initializer', + calldata: collSurplusPoolCall.calldata || [], + }); + + // SortedTroves initializer + const sortedTrovesAbi = getCachedAbi('SortedTroves'); + const sortedTrovesContract = new Contract( + sortedTrovesAbi, + sortedTrovesAddress, + provider + ); + const sortedTrovesCall = sortedTrovesContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: sortedTrovesAddress, + entrypoint: 'initializer', + calldata: sortedTrovesCall.calldata || [], + }); + + // StabilityPool initializer + const stabilityPoolAbi = getCachedAbi('StabilityPool'); + const stabilityPoolContract = new Contract( + stabilityPoolAbi, + stabilityPoolAddress, + provider + ); + const stabilityPoolCall = stabilityPoolContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: stabilityPoolAddress, + entrypoint: 'initializer', + calldata: stabilityPoolCall.calldata || [], + }); + + // LiquidationManager initializer + const liquidationManagerAbi = getCachedAbi('LiquidationManager'); + const liquidationManagerContract = new Contract( + liquidationManagerAbi, + liquidationManagerAddress, + provider + ); + const liquidationManagerCall = liquidationManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: liquidationManagerAddress, + entrypoint: 'initializer', + calldata: liquidationManagerCall.calldata || [], + }); + + // RedemptionManager initializer + const redemptionManagerAbi = getCachedAbi('RedemptionManager'); + const redemptionManagerContract = new Contract( + redemptionManagerAbi, + redemptionManagerAddress, + provider + ); + const redemptionManagerCall = redemptionManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: redemptionManagerAddress, + entrypoint: 'initializer', + calldata: redemptionManagerCall.calldata || [], + }); + + // BatchManager initializer + const batchManagerAbi = getCachedAbi('BatchManager'); + const batchManagerContract = new Contract( + batchManagerAbi, + batchManagerAddress, + provider + ); + const batchManagerCall = batchManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: batchManagerAddress, + entrypoint: 'initializer', + calldata: batchManagerCall.calldata || [], + }); + + // Execute all initializers in a single multicall + log(`Executing multicall with ${initializerCalls.length} initializers...`); + const multiCall = await account.execute(initializerCalls); + await provider.waitForTransaction(multiCall.transaction_hash, { retryInterval: 5000 }); + log('All branch contracts initialized successfully'); if (!XLBTC_UNDERLYING_ADDRESS) { - throw new Error('XLBTC_UNDERLYING_ADDRESS is not defined'); - } + throw new Error('XLBTC_UNDERLYING_ADDRESS is not defined'); + } const branchAddresses: BranchAddresses = { collateral: wrapperAddress, @@ -317,12 +512,12 @@ async function main() { await provider.waitForTransaction(wiringTx.transaction_hash, { retryInterval: 5000 }); log('Branch wired into existing core contracts\n'); - log(`Saving ${BRANCH_KEY} addresses to ${MAINNET_FILE}...`); + log(`Saving ${BRANCH_KEY} addresses to ${ADDRESSES_FILE}...`); const updatedAddresses = { ...addressesData, [BRANCH_KEY]: branchAddresses, }; - fs.writeFileSync(MAINNET_FILE, JSON.stringify(updatedAddresses, null, 2)); + fs.writeFileSync(ADDRESSES_FILE, JSON.stringify(updatedAddresses, null, 2)); log('Update complete'); log('✅ XLBTC branch deployment finished'); From c74fd69032ba731517c55b9ac268ad7cf784a18e Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:36:20 +0100 Subject: [PATCH 03/18] rename wrapped xlbtc --- scripts/deploy_xlbtc_branch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/deploy_xlbtc_branch.ts b/scripts/deploy_xlbtc_branch.ts index 69dab73..3bff868 100644 --- a/scripts/deploy_xlbtc_branch.ts +++ b/scripts/deploy_xlbtc_branch.ts @@ -178,8 +178,8 @@ async function main() { } const wrapperAddress = await deployContract('CollateralWrapper', [ - 'Wrapped XL Bitcoin', - 'W-XLBTC', + 'Wrapped Liquid Staked Bitcoin', + 'WXLBTC', OWNER_ADDRESS, XLBTC_UNDERLYING_ADDRESS, ]); From 268ee41b08b64abcd62367c99b075b73c16c624f Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:36:38 +0100 Subject: [PATCH 04/18] WXLBTC --- deployments/sepolia_addresses.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployments/sepolia_addresses.json b/deployments/sepolia_addresses.json index 1ebf5c4..c2345c6 100644 --- a/deployments/sepolia_addresses.json +++ b/deployments/sepolia_addresses.json @@ -24,7 +24,7 @@ "troveManagerEventsEmitter": "0x49c04436630bd42fa651e58c2f64a7f9c994e327ea8424d63f3a4a0e410229", "underlyingAddress": "0x661c69bdb25beedf6fda005270c0ebd908f0649961b50f0a602adfa6abc4012" }, - "XLBTC": { + "WXLBTC": { "collateral": "0x494c0fe65bb6ffae3d5fead7140235e192f546bed01c61b4107f8eff5d14da6", "addressesRegistry": "0xd0c41d4725084c79fef8db2580c9cfeafbee80ae0d7f5e41ffc5ac0b10b812", "borrowerOperations": "0xc7335f6fc7d9e773d466a254a33e9567a2a6ec5f59bff2cbdcd901c99453f6", @@ -46,4 +46,4 @@ "troveManagerEventsEmitter": "0x1b83eb6104548ea8474a2dbb32020bc25cefbdd27f10b35a703a5994246652e", "underlyingAddress": "0x31bdabbece650b229acbd4dba41457e4e12590eba0ff3f36ab3d514a23d415b" } -} \ No newline at end of file +} From 7dfcdf540ed4777474e185443e417fb5beef29e9 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:33:09 +0100 Subject: [PATCH 05/18] redeploy sepolia --- deployments/sepolia_addresses.json | 84 +++++++++++++++--------------- scripts/deploy.ts | 2 +- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/deployments/sepolia_addresses.json b/deployments/sepolia_addresses.json index c2345c6..5bbd410 100644 --- a/deployments/sepolia_addresses.json +++ b/deployments/sepolia_addresses.json @@ -1,49 +1,49 @@ { - "USDU": "0x2f05a7b79478c064d338af2706e19ddbbbd77c102ba46cb293f4e2f9dccf751", + "USDU": "0xc2f7f9fbd5ae626562267eaf3fd119dcf4caafebaaf003e67e5253ad48676b", "gasToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", - "collateralRegistry": "0x8e053cf9353189895637303422000fa246c5789b7fd400455fa2d9e392df7d", + "collateralRegistry": "0x52ecdfd338cb3d58f91ad6c9853e277b3f1b6607d74955791fe652c438b50e1", "WWBTC": { - "collateral": "0x661c69bdb25beedf6fda005270c0ebd908f0649961b50f0a602adfa6abc4012", - "addressesRegistry": "0x4c224615501cce6282507139b4db690db6edd7c24606a229efa6345fb7c4ce4", - "borrowerOperations": "0x34c7bac3356a015e6b01cc3a7a3453c58b9b3dbfefb7386eb81ee1236036f9c", - "troveManager": "0x2aaf15e3669c491bc9f48799b9d39d081cd1a7dd8c7e45935f916ce47386d56", - "troveNft": "0x541db872ea4fdec09de8d1f3d9e774fb741fe78011352e9816fc6a60f66e9e", - "stabilityPool": "0x47a0c4a4468ba7174d845af4321bb7514fe5f5404a9a1f1b5ece4b7e49be235", - "sortedTroves": "0x5e1d330d6862b1170b5c9f82ddd4ac3be9a08d309c8c8d0c8d2da93076a25db", - "activePool": "0x1e6578aec5f10c1fc40af683cb038ff628a43d6ac8ffb38e00c940873e9e4ef", - "defaultPool": "0x6eabacd1332437995bfca84370bf2c993923957f33cc53eca617c2bf7157173", - "collSurplusPool": "0x11ea4a3dba57cb9e3fdf39abd9d8cc92e41d1ee5284b6293112004da409d31f", - "gasPool": "0x35ea1b94646f154dd69d00e990918dd2d7878bae62769e2d2ed93bd7f321e26", + "collateral": "0x6c5f4da4070bbecd4851f12cf3086fef4fb6f305580dcc0f1342aab017f8c5", + "addressesRegistry": "0x4c33c1812855b99544f164c0251a1e46bbc7c76f73277b1fbac182fd6b801ac", + "borrowerOperations": "0x5d196a13338df33f38e09efb680a8cb0c9c9fcb97fc2ecd59a39cdec63b4102", + "troveManager": "0x6007132ce3477365bc7c657165d2c92fd040397355b4bfd7842c38156fc78d1", + "troveNft": "0x701b18553aa4d5a9270c8f35d0da08302448d3d481e9f862ee143a076f69c9a", + "stabilityPool": "0x3a9ffc631519eb1be4107b813904543c73a9c2a33ae221446c0d2b29be2f92e", + "sortedTroves": "0x252dffc63ff99189f50e52a1689f9a401ccdbcfb532d2a2b8c967d042b7b8af", + "activePool": "0x726016b80038fd571c62dcbfc922a2b0cc3cdbaf217bd8c6eef3157c9a76057", + "defaultPool": "0x5f36a8b8dd650e0582f3cb7af077e209483addbde85d72c8b2be8a34333cfeb", + "collSurplusPool": "0x6383a77b6cef4d97c4a8c0355e94c9332e800a036cb535a762911c89d589267", + "gasPool": "0x102de271b9f65aa631699158066bf2891d0d6cdbc150355441d7067326c0e8f", "interestRouter": "0x03Bb8724887FE0f9DcFC3806053314Ce7F091fB876fc128c5080B27fa77203f2", - "liquidationManager": "0x5623e9f87ec2422495bb036e4f81978980b4db9d53d7aeaab625be36b281d42", - "redemptionManager": "0x61a6ff14e52f54580f6edf453f85cb49eb22868a094521c10211a581ffa7f18", - "batchManager": "0x540e673a3866c20f9faa0b6fe430354174b07e66e47999a4e339df524022e10", - "priceFeed": "0x5d4dbc2a89c965c9f215f4c1e1a5fc1966c1ccf5e37f152c5e09e70231938c8", - "hintHelpers": "0x2645aa0fe3f040600600870c0a0214f727c65edbe073dc9e584ab8fd8c45544", - "multiTroveGetter": "0x1e6578aec5f10c1fc40af683cb038ff628a43d6ac8ffb38e00c940873e9e4ef", - "troveManagerEventsEmitter": "0x49c04436630bd42fa651e58c2f64a7f9c994e327ea8424d63f3a4a0e410229", - "underlyingAddress": "0x661c69bdb25beedf6fda005270c0ebd908f0649961b50f0a602adfa6abc4012" + "liquidationManager": "0x4f0520ccf88e5affce2efa0bd26c0527119229cd78b2ba4ae1559b42b3b7ca4", + "redemptionManager": "0x6ec8edf9eb1fb7b1cc8aff1e29f140b6cb82faf0aeababc50196c75abc011b0", + "batchManager": "0x61bdd21652a34780f32394986af1dfc126e6e8380b37a15bc12a8c20a36f621", + "priceFeed": "0x23d9ff8d05d52f5048de8c8588dae15f139065f0326ec5e3f1348e922749292", + "hintHelpers": "0x2a6315c1f4323be9fa5fc3fe5152854628945d73f3df851364cdd9bbb9c450f", + "multiTroveGetter": "0x726016b80038fd571c62dcbfc922a2b0cc3cdbaf217bd8c6eef3157c9a76057", + "troveManagerEventsEmitter": "0x4d86c9b70f1d2fcb741127b2f920b572baf0ff35489ec11afb605cccacfd551", + "underlyingAddress": "0x138a381fb3b06c59c626c5038ad718fd587e1366857113a584b317de04db46c" }, - "WXLBTC": { - "collateral": "0x494c0fe65bb6ffae3d5fead7140235e192f546bed01c61b4107f8eff5d14da6", - "addressesRegistry": "0xd0c41d4725084c79fef8db2580c9cfeafbee80ae0d7f5e41ffc5ac0b10b812", - "borrowerOperations": "0xc7335f6fc7d9e773d466a254a33e9567a2a6ec5f59bff2cbdcd901c99453f6", - "troveManager": "0x5d9bc54a46c549704003acd7dd1d1147523a7a4b1f56f8b760e96879c4ab94c", - "troveNft": "0x22b27d5bc8b9d0cca019fdecd6b618e9f331a1dafe1330c3b216b2668cea2d7", - "stabilityPool": "0x1205cfb0931799fa2a49b9e1bdba4e62575a132e2c79796ff8aad1b4e020114", - "sortedTroves": "0x327b1c4ec2b5c25110e08de235bdca0b225e8c6a030edcbe0210f580782b8f5", - "activePool": "0xbb983b4dc87572e8a17e83b9619d95a570971380915298399f17ffce6e0dba", - "defaultPool": "0x60b161ed09825a669df01f582d50038c277ded6d6a6c4e92712945f9eb0ce7b", - "collSurplusPool": "0x7a7496c31c232fd1d5873ae2cbdd9cc0903a1cf3d70db5cca0033da87a5afd6", - "gasPool": "0x5061c60b3ceefb78d1f64f4a63f9db725e2a330ac2691d04bbcd9b443c77dea", + "XLBTC": { + "collateral": "0x2d4cef5f28442f6fd2f22b22c796f5bee60095f03eb5f070c13f64a1427873e", + "addressesRegistry": "0x67f2cebda5cb9e29f442caf2644fc8184fa89c4fe36b6828caebcd7e4b254ae", + "borrowerOperations": "0x52b7a02804ce35c1421221a6a91b339d4b594bcaae3fc14bbbd3c76c4e5c070", + "troveManager": "0x42db76c6c00ffa494ac07218c6165b36fa4ab03ed995b2cb6feb447453abc01", + "troveNft": "0x388d09d92ad5ded31e3af36de7e5f200a7cc671a57a0b14b6294ea5ebd99b06", + "stabilityPool": "0x78ccbc4075c00712a39e740ad6bcb24e1e96b68c7768da7bd57817255e8b980", + "sortedTroves": "0x58d44b855ce725ef13e1ff6d69ee7aa995b9d1c18f730e0082037aa2a8a9ca6", + "activePool": "0x7a165bfaeb48ef3262b87ee80986a93d1d63e1a3f9f0655904cf091b779f108", + "defaultPool": "0x1c5bfcf415d6cf18ac69ea3c433fe925ec737fb2196a26902abe0d99fee8777", + "collSurplusPool": "0x78577b214811d4c4fb1325c1a023f4f8507c90328e6bab5db155cd80bf76efb", + "gasPool": "0x7104c05051f055706dee9002fd5ec5f9e9334a62947b7cf920badb49eb8b07a", "interestRouter": "0x03Bb8724887FE0f9DcFC3806053314Ce7F091fB876fc128c5080B27fa77203f2", - "liquidationManager": "0x49e9e55dac5ce759bbcbc6cc4685b6b180add055c00f416d6fb2c1a7dd8df89", - "redemptionManager": "0x582c970661fe66019eab098e3709e64d8f89205106638cc36eb39fa860282f9", - "batchManager": "0x3e1cd610ece9421035e20b3b81d371cee2072fd04836b84a59b752de287e312", - "priceFeed": "0x6d5dd946b583d6cacdddfd3e6a53f89dabeb17c317b894a4e449cb2845dcf39", - "hintHelpers": "0x2645aa0fe3f040600600870c0a0214f727c65edbe073dc9e584ab8fd8c45544", - "multiTroveGetter": "0xbb983b4dc87572e8a17e83b9619d95a570971380915298399f17ffce6e0dba", - "troveManagerEventsEmitter": "0x1b83eb6104548ea8474a2dbb32020bc25cefbdd27f10b35a703a5994246652e", - "underlyingAddress": "0x31bdabbece650b229acbd4dba41457e4e12590eba0ff3f36ab3d514a23d415b" + "liquidationManager": "0x58d3cce963f09fea312bb374592cbc54c4d87aa892aaf3250a5df768edbf71e", + "redemptionManager": "0x288837971e260e1013639c44fcff039a0d3c932107377237cd381ab4a2bce46", + "batchManager": "0x52e606730c5e2b94f5fcab4c61189295e0fe2a5da74d1b4ef21bbdf2452b63", + "priceFeed": "0x55538ded9fd8db7d26ac3ecac0a035aad301db9deef44ee5b969c10722098a", + "hintHelpers": "0x2a6315c1f4323be9fa5fc3fe5152854628945d73f3df851364cdd9bbb9c450f", + "multiTroveGetter": "0x7a165bfaeb48ef3262b87ee80986a93d1d63e1a3f9f0655904cf091b779f108", + "troveManagerEventsEmitter": "0x22ce6fc0261745477a05adb72cb8b0d49e021560009012c51b9889c5af47af4", + "underlyingAddress": "0x34c38b1b64c0bc824f01d2bc31a94dc8ec770f84efaad6f2ab89aa55eec856c" } -} +} \ No newline at end of file diff --git a/scripts/deploy.ts b/scripts/deploy.ts index a56d659..7d28b61 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -372,7 +372,7 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str let wrapperAddress: string | null = null; const underlyingAddress = collateral; - if (collateralName == 'WWBTC') { + if (collateralName == 'WWBTC' || collateralName == 'WMWBTC') { wrapperAddress = await deployContract('CollateralWrapper', ["WrappedWBTC", "WWBTC", context.config.ownerAddress, collateral]); collateral = wrapperAddress; } From ed038a6df4a984bb98644aef100a96b36419c83e Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:22:25 +0100 Subject: [PATCH 06/18] update addresses registry constants for xlbtc --- scripts/deploy_xlbtc_branch.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/deploy_xlbtc_branch.ts b/scripts/deploy_xlbtc_branch.ts index 3bff868..a78fb67 100644 --- a/scripts/deploy_xlbtc_branch.ts +++ b/scripts/deploy_xlbtc_branch.ts @@ -143,13 +143,13 @@ async function main() { log('Deploying XLBTC branch contracts...'); const addressesRegistryAddress = await deployContract('AddressesRegistry', { - ccr: CONSTANTS.CCR_WBTC, - mcr: CONSTANTS.MCR_WBTC, + ccr: CONSTANTS.CCR_XLBTC, + mcr: CONSTANTS.MCR_XLBTC, bcr: CONSTANTS.BCR_ALL, - scr: CONSTANTS.SCR_WBTC, - cap: CONSTANTS.CAP_WBTC, - liquidation_penalty_sp: CONSTANTS.LIQUIDATION_PENALTY_SP_WBTC, - liquidation_penalty_redistribution: CONSTANTS.LIQUIDATION_PENALTY_REDISTRIBUTION_WBTC, + scr: CONSTANTS.SCR_XLBTC, + cap: CONSTANTS.CAP_XLBTC, + liquidation_penalty_sp: CONSTANTS.LIQUIDATION_PENALTY_SP_XLBTC, + liquidation_penalty_redistribution: CONSTANTS.LIQUIDATION_PENALTY_REDISTRIBUTION_XLBTC, sponsor: SPONSOR, deployer: DEPLOYER_ADDRESS, }); From 0f9dbcce5ec72b0ceb37138179761b8662b1a3f3 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:49:35 +0100 Subject: [PATCH 07/18] tmp commit xlbtc --- src/lib.cairo | 1 + src/price_feeds/XLBTCPriceFeed.cairo | 161 ++++++--------------------- 2 files changed, 35 insertions(+), 127 deletions(-) diff --git a/src/lib.cairo b/src/lib.cairo index 206ac5b..d0b95e6 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -78,6 +78,7 @@ pub mod utils { pub mod price_feeds { pub mod WBTCPriceFeed; + pub mod XLBTCPriceFeed; } pub mod wrappers { diff --git a/src/price_feeds/XLBTCPriceFeed.cairo b/src/price_feeds/XLBTCPriceFeed.cairo index e76b3e7..8a8b28e 100644 --- a/src/price_feeds/XLBTCPriceFeed.cairo +++ b/src/price_feeds/XLBTCPriceFeed.cairo @@ -1,34 +1,13 @@ // Created by Uncap Labs // SPDX-License-Identifier: BUSL-1.1 -// Chainlink's Aggregator Interface -#[starknet::interface] -pub trait IAggregator { - fn latest_round_data(self: @TContractState) -> Round; - fn round_data(self: @TContractState, round_id: u128) -> Round; - fn description(self: @TContractState) -> felt252; - fn decimals(self: @TContractState) -> u8; - fn latest_answer(self: @TContractState) -> u128; -} - -#[derive(Copy, Drop, Serde, PartialEq, starknet::Store)] -pub struct Round { - // used as u128 internally, but necessary for phase-prefixed round ids as returned by proxy - pub round_id: felt252, - pub answer: u128, - pub block_num: u64, - pub started_at: u64, - pub updated_at: u64, -} - -// Composite Price Feed using Pragma, and Chainlink as a fallback. -// Uses the maximum value of BTC/USD and BTC/USD, as long as the difference is less than 1%. -// Otherwise, uses WBTC/USD price. Done to avoid excessive redemption arbitrage. +// Composite Price Feed using Pragma. +// Uses the cnoversion rate of XLBTC. #[starknet::contract] pub mod XLBTCPriceFeed { use core::cmp::max; use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; - use pragma_lib::types::{DataType, PragmaPricesResponse}; + use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; use starknet::{ContractAddress, get_block_timestamp}; use crate::branch::interfaces::IAddressesRegistry::{ @@ -39,20 +18,16 @@ pub mod XLBTCPriceFeed { }; use crate::interfaces::IPriceFeed::IPriceFeed; use crate::utils::constants::Constants::{DECIMAL_PRECISION, _1PCT}; - use super::{IAggregatorDispatcher, IAggregatorDispatcherTrait}; ////////////////////////////////////////////////////////////// // CONSTANTS // ////////////////////////////////////////////////////////////// - const PRICE_FEED_BTC_USD: felt252 = 'BTC/USD'; - const PRICE_FEED_WBTC_USD: felt252 = 'WBTC/USD'; - const STANDARD_ORACLE_DECIMALS: u32 = 8; // Both BTC and WBTC oracles have 8 decimals - const PRICE_SCALING_FACTOR: u256 = 10_000_000_000; // 10^10: (10^18 - 10^8) + const CONVERSION_XLBTC_USD: felt252 = 'CONVERSION_XSTRK/USD'; + const STANDARD_ORACLE_DECIMALS: u32 = 8; // TODO + const PRICE_SCALING_FACTOR: u256 = 10_000_000_000; // 10^10: (10^18 - 10^8) // TODO const DEVIATION_THRESHOLD: u256 = _1PCT; // 1% const STALENESS_THRESHOLD: u64 = 60 * 60 * 24; // 1 day in seconds - const MIN_NUM_SOURCES: u32 = - 5; // Minimum number of sources for an oracle to be considered valid ////////////////////////////////////////////////////////////// // STORAGE // @@ -61,8 +36,6 @@ pub mod XLBTCPriceFeed { #[storage] pub struct Storage { pragma_oracle_address: ContractAddress, - chainlink_btc_usd_address: ContractAddress, - chainlink_wbtc_usd_address: ContractAddress, has_been_shutdown: bool, last_good_price: u256, addresses_registry: ContractAddress, @@ -89,11 +62,9 @@ pub mod XLBTCPriceFeed { #[derive(Copy, Clone, Drop)] pub enum PriceFeedId { - BTCUSD, - WBTCUSD, + XLBTCUSD, } - ////////////////////////////////////////////////////////////// // CONSTRUCTOR // ////////////////////////////////////////////////////////////// @@ -102,13 +73,9 @@ pub mod XLBTCPriceFeed { fn constructor( ref self: ContractState, pragma_oracle_address: ContractAddress, - chainlink_btc_usd_address: ContractAddress, - chainlink_wbtc_usd_address: ContractAddress, addresses_registry: ContractAddress, ) { self.pragma_oracle_address.write(pragma_oracle_address); - self.chainlink_btc_usd_address.write(chainlink_btc_usd_address); - self.chainlink_wbtc_usd_address.write(chainlink_wbtc_usd_address); self.addresses_registry.write(addresses_registry); @@ -116,7 +83,7 @@ pub mod XLBTCPriceFeed { // Will shutdown if the oracle fails self.fetch_price_primary(false); - assert(!self.has_been_shutdown.read(), 'WBTCPF: System already shutdown'); + assert(!self.has_been_shutdown.read(), 'XLPF: System already shutdown'); } //////////////////////////////////////////////////////////////// @@ -145,7 +112,7 @@ pub mod XLBTCPriceFeed { if self.has_been_shutdown.read() { return self.last_good_price.read(); } - let (price, _) = self.get_oracles_answer(PriceFeedId::WBTCUSD); + let (price, _) = self.get_pragma_answer(); price } } @@ -166,21 +133,10 @@ pub mod XLBTCPriceFeed { return (self.last_good_price.read(), false); } - // Always fetch WBTC price first - let (wbtc_price, wbtc_oracle_failure) = self.get_oracles_answer(PriceFeedId::WBTCUSD); - let mut final_price = wbtc_price; - let mut oracle_failure = wbtc_oracle_failure; - - // If it's a redemption, also fetch BTC price and use composite price - if (is_redemption) { - let (btc_price, btc_oracle_failure) = self.get_oracles_answer(PriceFeedId::BTCUSD); - oracle_failure = btc_oracle_failure || wbtc_oracle_failure; - if (!oracle_failure) { - final_price = get_redemption_price(wbtc_price, btc_price); - } - } + // Fetch the XLBTC/USD price from Pragma + let (xlbtc_price, xlbtc_oracle_failure) = self.get_pragma_answer(); - if (oracle_failure) { + if (xlbtc_oracle_failure) { self.has_been_shutdown.write(true); let ar = IAddressesRegistryDispatcher { @@ -200,10 +156,9 @@ pub mod XLBTCPriceFeed { return (self.last_good_price.read(), true); } else { - // Always store the normal WBTC price as last_good_price, not the redemption price - // This ensures that during shutdown, urgent redemptions use the actual market price - self.last_good_price.write(wbtc_price); - (final_price, false) + // Ensures that during shutdown, urgent redemptions use the actual market price + self.last_good_price.write(xlbtc_price); + (xlbtc_price, false) } } @@ -211,61 +166,15 @@ pub mod XLBTCPriceFeed { // READ FUNCTIONS // ////////////////////////////////////////////////////////////// - fn get_chainlink_answer(self: @ContractState, price_feed_id: PriceFeedId) -> (u256, bool) { - let cl_address = match price_feed_id { - PriceFeedId::BTCUSD => self.chainlink_btc_usd_address.read(), - PriceFeedId::WBTCUSD => self.chainlink_wbtc_usd_address.read(), - }; - let cl_aggregator = IAggregatorDispatcher { contract_address: cl_address }; - let latest_round = cl_aggregator.latest_round_data(); - let price = latest_round.answer.into() * PRICE_SCALING_FACTOR; - let latest_update = latest_round.updated_at; - let is_stale_price = latest_update + STALENESS_THRESHOLD < get_block_timestamp(); - let price_is_zero = price == 0; - let oracle_failure = is_stale_price || price_is_zero; - - (price, oracle_failure) - } - - fn get_oracles_answer(self: @ContractState, price_feed_id: PriceFeedId) -> (u256, bool) { - // Get the answer from the pragma oracle - let (pragma_price, pragma_failure, pragma_not_enough_sources) = self - .get_pragma_answer(price_feed_id); - - // If pragma failed, try the chainlink oracle - if (pragma_failure) { - let (cl_price, chainlink_failure) = self.get_chainlink_answer(price_feed_id); - if chainlink_failure { - // CL failed, but pragma failed only because of not enough sources, return the - // pragma price, and do not shutdown - if pragma_not_enough_sources { - return (pragma_price, false); - } else { - // CL failed and pragma failed for other reasons, shutdown - return (cl_price, true); - } - } else { - // CL did not fail, return the CL price - return (cl_price, false); - } - } else { - // Pragma did not fail, return the Pragma price - return (pragma_price, false); - } - } - - fn get_pragma_answer( - self: @ContractState, price_feed_id: PriceFeedId, - ) -> (u256, bool, bool) { - let oracle_dispatcher = IPragmaABIDispatcher { + fn get_pragma_answer(self: @ContractState) -> (u256, bool) { + let pragma_dispatcher = IPragmaABIDispatcher { contract_address: self.pragma_oracle_address.read(), }; - let price_feed = match price_feed_id { - PriceFeedId::BTCUSD => DataType::SpotEntry(PRICE_FEED_BTC_USD), - PriceFeedId::WBTCUSD => DataType::SpotEntry(PRICE_FEED_WBTC_USD), - }; - let response: PragmaPricesResponse = oracle_dispatcher.get_data_median(price_feed); - let price = response.price.into() * PRICE_SCALING_FACTOR; + + let data_type: DataType = DataType::SpotEntry(CONVERSION_XLBTC_USD); + let response: PragmaPricesResponse = pragma_dispatcher + .get_data(data_type, AggregationMode::ConversionRate); + let price = response.price.into() * PRICE_SCALING_FACTOR; // TODO double check let price_is_zero = price == 0; // Price should never be zero let incorrect_decimals = response @@ -273,18 +182,11 @@ pub mod XLBTCPriceFeed { let is_stale_price = response.last_updated_timestamp + STALENESS_THRESHOLD < get_block_timestamp(); // Price should not be stale - let not_enough_sources = response.num_sources_aggregated < MIN_NUM_SOURCES; - let oracle_failure_detected = price_is_zero || incorrect_decimals - || is_stale_price - || not_enough_sources; // If any of these are true, the oracle failed + || is_stale_price; // If any of these are true, the oracle failed - // Checks if the oracle failed ONLY because of not enough sources - let oracle_failure_because_not_enough_sources = oracle_failure_detected - && !(price_is_zero || incorrect_decimals || is_stale_price); - - return (price, oracle_failure_detected, oracle_failure_because_not_enough_sources); + return (price, oracle_failure_detected); } } @@ -293,8 +195,9 @@ pub mod XLBTCPriceFeed { //////////////////////////////////////////////////////////////// // Returns the best price (for the protocol) between BTC and WBTC, as long as the difference - // is less than 1%. + // is less than 1%. todo renaming fn get_redemption_price(wbtc_price: u256, btc_price: u256) -> u256 { + // todo renamign if within_deviation_threshold(wbtc_price, btc_price) { return max(wbtc_price, btc_price); } else { @@ -304,9 +207,13 @@ pub mod XLBTCPriceFeed { fn within_deviation_threshold(wbtc_price: u256, btc_price: u256) -> bool { // Calculate the price deviation between BTC and WBTC - let max = wbtc_price * (DECIMAL_PRECISION + DEVIATION_THRESHOLD) / DECIMAL_PRECISION; - let min = wbtc_price * (DECIMAL_PRECISION - DEVIATION_THRESHOLD) / DECIMAL_PRECISION; - - btc_price >= min && btc_price <= max + let max = wbtc_price + * (DECIMAL_PRECISION + DEVIATION_THRESHOLD) + / DECIMAL_PRECISION; // todo renaming + let min = wbtc_price + * (DECIMAL_PRECISION - DEVIATION_THRESHOLD) + / DECIMAL_PRECISION; // todo renaming + + btc_price >= min && btc_price <= max // todo renaming } } From 272239ef323154d9a9397102eeba19df945d92fa Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Sat, 29 Nov 2025 13:34:52 +0100 Subject: [PATCH 08/18] update XLBTC constants --- scripts/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/types.ts b/scripts/types.ts index 7b604fe..6a7d9d3 100644 --- a/scripts/types.ts +++ b/scripts/types.ts @@ -64,11 +64,11 @@ export const CONSTANTS = { STRK: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d', BCR_ALL: '100000000000000000', // 10% - // Constants from for UBTC + // Constants for XLBTC CCR_XLBTC: '1500000000000000000', // 150% - MCR_XLBTC: '1250000000000000000', // 110% + MCR_XLBTC: '1250000000000000000', // 125% SCR_XLBTC: '1100000000000000000', // 110% - CAP_XLBTC: '500000000000000000000000', // 500,000 UBTC + CAP_XLBTC: '10000000000000000000', // 10 XLBTC LIQUIDATION_PENALTY_SP_XLBTC: '50000000000000000', // 5% LIQUIDATION_PENALTY_REDISTRIBUTION_XLBTC: '200000000000000000', // 20% From 22fc20340751ef89850c412939185e4e6b8c0fa7 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Sat, 29 Nov 2025 13:35:10 +0100 Subject: [PATCH 09/18] remove dead code from xlbtc price feed --- src/price_feeds/XLBTCPriceFeed.cairo | 55 +++++++--------------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/src/price_feeds/XLBTCPriceFeed.cairo b/src/price_feeds/XLBTCPriceFeed.cairo index 8a8b28e..cc5f49f 100644 --- a/src/price_feeds/XLBTCPriceFeed.cairo +++ b/src/price_feeds/XLBTCPriceFeed.cairo @@ -2,10 +2,9 @@ // SPDX-License-Identifier: BUSL-1.1 // Composite Price Feed using Pragma. -// Uses the cnoversion rate of XLBTC. +// Uses the conversion rate of XLBTC. #[starknet::contract] pub mod XLBTCPriceFeed { - use core::cmp::max; use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; @@ -17,15 +16,17 @@ pub mod XLBTCPriceFeed { IBorrowerOperationsDispatcher, IBorrowerOperationsDispatcherTrait, }; use crate::interfaces::IPriceFeed::IPriceFeed; - use crate::utils::constants::Constants::{DECIMAL_PRECISION, _1PCT}; + use crate::utils::constants::Constants::_1PCT; ////////////////////////////////////////////////////////////// // CONSTANTS // ////////////////////////////////////////////////////////////// - const CONVERSION_XLBTC_USD: felt252 = 'CONVERSION_XSTRK/USD'; - const STANDARD_ORACLE_DECIMALS: u32 = 8; // TODO - const PRICE_SCALING_FACTOR: u256 = 10_000_000_000; // 10^10: (10^18 - 10^8) // TODO + const CONVERSION_XLBTC_USD: felt252 = + 'CONVERSION_XSTRK/USD'; // Key of the XLBTC/USD price in Pragma + const STANDARD_ORACLE_DECIMALS: u32 = 8; // TODO: Confirm with Pragma + const PRICE_SCALING_FACTOR: u256 = + 10_000_000_000; // 10^10: (10^18 - 10^8) // TODO depends on standard decimals const DEVIATION_THRESHOLD: u256 = _1PCT; // 1% const STALENESS_THRESHOLD: u64 = 60 * 60 * 24; // 1 day in seconds @@ -81,7 +82,7 @@ pub mod XLBTCPriceFeed { // Fetch the initial price to set the last good price // Will shutdown if the oracle fails - self.fetch_price_primary(false); + self.fetch_price_primary(); assert(!self.has_been_shutdown.read(), 'XLPF: System already shutdown'); } @@ -98,16 +99,15 @@ pub mod XLBTCPriceFeed { // --- a) The system was not shutdown prior to this call and // --- b) an oracle contract failed during this call fn fetch_price(ref self: ContractState) -> (u256, bool) { - self.fetch_price_primary(false) + self.fetch_price_primary() } - // Same as fetch_price, but uses a composite price for redemption (see - // `get_redemption_price`) + // Same as fetch_price fn fetch_redemption_price(ref self: ContractState) -> (u256, bool) { - self.fetch_price_primary(true) + self.fetch_price_primary() } - // Returns oracles' price of WBTC/USD + // Returns oracles' price of XLBTC/USD fn get_price(self: @ContractState) -> u256 { if self.has_been_shutdown.read() { return self.last_good_price.read(); @@ -127,7 +127,7 @@ pub mod XLBTCPriceFeed { // WRITE FUNCTIONS // ////////////////////////////////////////////////////////////// - fn fetch_price_primary(ref self: ContractState, is_redemption: bool) -> (u256, bool) { + fn fetch_price_primary(ref self: ContractState) -> (u256, bool) { // If branch has been shutdown, return the last good price if self.has_been_shutdown.read() { return (self.last_good_price.read(), false); @@ -174,7 +174,7 @@ pub mod XLBTCPriceFeed { let data_type: DataType = DataType::SpotEntry(CONVERSION_XLBTC_USD); let response: PragmaPricesResponse = pragma_dispatcher .get_data(data_type, AggregationMode::ConversionRate); - let price = response.price.into() * PRICE_SCALING_FACTOR; // TODO double check + let price = response.price.into() * PRICE_SCALING_FACTOR; let price_is_zero = price == 0; // Price should never be zero let incorrect_decimals = response @@ -189,31 +189,4 @@ pub mod XLBTCPriceFeed { return (price, oracle_failure_detected); } } - - //////////////////////////////////////////////////////////////// - // UTILITY FUNCTIONS // - //////////////////////////////////////////////////////////////// - - // Returns the best price (for the protocol) between BTC and WBTC, as long as the difference - // is less than 1%. todo renaming - fn get_redemption_price(wbtc_price: u256, btc_price: u256) -> u256 { - // todo renamign - if within_deviation_threshold(wbtc_price, btc_price) { - return max(wbtc_price, btc_price); - } else { - wbtc_price - } - } - - fn within_deviation_threshold(wbtc_price: u256, btc_price: u256) -> bool { - // Calculate the price deviation between BTC and WBTC - let max = wbtc_price - * (DECIMAL_PRECISION + DEVIATION_THRESHOLD) - / DECIMAL_PRECISION; // todo renaming - let min = wbtc_price - * (DECIMAL_PRECISION - DEVIATION_THRESHOLD) - / DECIMAL_PRECISION; // todo renaming - - btc_price >= min && btc_price <= max // todo renaming - } } From fe1d88e8d1651b6f11ac7877b41ac5bd0d60de3f Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Sat, 29 Nov 2025 13:41:50 +0100 Subject: [PATCH 10/18] remove linking branch to CR and USDU --- scripts/deploy_xlbtc_branch.ts | 72 +++++++++++++++++----------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/scripts/deploy_xlbtc_branch.ts b/scripts/deploy_xlbtc_branch.ts index a78fb67..db8f372 100644 --- a/scripts/deploy_xlbtc_branch.ts +++ b/scripts/deploy_xlbtc_branch.ts @@ -475,42 +475,42 @@ async function main() { underlyingAddress: XLBTC_UNDERLYING_ADDRESS, }; - log('Linking branch with CollateralRegistry & USDU...'); - - const loadAbiFromArtifact = (contractName: string) => { - const sierraPath = path.join(__dirname, '..', 'target', 'dev', `usdu_${contractName}.contract_class.json`); - return JSON.parse(fs.readFileSync(sierraPath, 'utf8')).abi; - }; - - const collateralRegistryAbi = loadAbiFromArtifact('CollateralRegistry'); - const usduAbi = loadAbiFromArtifact('USDU'); - - const collateralRegistryContract = new Contract(collateralRegistryAbi, collateralRegistryAddress, provider); - const usduContract = new Contract(usduAbi, usduAddress, provider); - - const addCollateralCall = collateralRegistryContract.populate('add_collateral', [addressesRegistryAddress]); - const addBranchCall = usduContract.populate('add_branch_addresses', [ - branchAddresses.troveManager, - branchAddresses.stabilityPool, - branchAddresses.borrowerOperations, - branchAddresses.activePool, - branchAddresses.redemptionManager, - ]); - - const wiringTx = await account.execute([ - { - contractAddress: collateralRegistryAddress, - entrypoint: 'add_collateral', - calldata: addCollateralCall.calldata || [], - }, - { - contractAddress: usduAddress, - entrypoint: 'add_branch_addresses', - calldata: addBranchCall.calldata || [], - }, - ]); - await provider.waitForTransaction(wiringTx.transaction_hash, { retryInterval: 5000 }); - log('Branch wired into existing core contracts\n'); + // log('Linking branch with CollateralRegistry & USDU...'); + + // const loadAbiFromArtifact = (contractName: string) => { + // const sierraPath = path.join(__dirname, '..', 'target', 'dev', `usdu_${contractName}.contract_class.json`); + // return JSON.parse(fs.readFileSync(sierraPath, 'utf8')).abi; + // }; + + // const collateralRegistryAbi = loadAbiFromArtifact('CollateralRegistry'); + // const usduAbi = loadAbiFromArtifact('USDU'); + + // const collateralRegistryContract = new Contract(collateralRegistryAbi, collateralRegistryAddress, provider); + // const usduContract = new Contract(usduAbi, usduAddress, provider); + + // const addCollateralCall = collateralRegistryContract.populate('add_collateral', [addressesRegistryAddress]); + // const addBranchCall = usduContract.populate('add_branch_addresses', [ + // branchAddresses.troveManager, + // branchAddresses.stabilityPool, + // branchAddresses.borrowerOperations, + // branchAddresses.activePool, + // branchAddresses.redemptionManager, + // ]); + + // const wiringTx = await account.execute([ + // { + // contractAddress: collateralRegistryAddress, + // entrypoint: 'add_collateral', + // calldata: addCollateralCall.calldata || [], + // }, + // { + // contractAddress: usduAddress, + // entrypoint: 'add_branch_addresses', + // calldata: addBranchCall.calldata || [], + // }, + // ]); + // await provider.waitForTransaction(wiringTx.transaction_hash, { retryInterval: 5000 }); + // log('Branch wired into existing core contracts\n'); log(`Saving ${BRANCH_KEY} addresses to ${ADDRESSES_FILE}...`); const updatedAddresses = { From 4f07e4c108e7d362238bac2978deeb10fb90024d Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:00:41 +0100 Subject: [PATCH 11/18] update snforge to 0.50.0 --- Scarb.lock | 8 ++++---- Scarb.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 39f67eb..7b67667 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -130,15 +130,15 @@ dependencies = [ [[package]] name = "snforge_scarb_plugin" -version = "0.48.1" +version = "0.50.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:2dd27e8215eea8785b3930e9f452e11b429ca262b1c1fbb071bfc173b9ebc125" +checksum = "sha256:8c29e5519362d22f2c802e4e348da846de3898cbaeac19b58aded6a009bf188e" [[package]] name = "snforge_std" -version = "0.48.1" +version = "0.50.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:89f759fa685d48ed0ba7152d2ac2eb168da08dfa8b84b2bee96e203dc5b2413e" +checksum = "sha256:db3a9de47952c699f8f3ce649b5b01f09c1f9c170f38b3c7a8df8e50a0188e9b" dependencies = [ "snforge_scarb_plugin", ] diff --git a/Scarb.toml b/Scarb.toml index bb3e57e..ca8da62 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -14,7 +14,7 @@ openzeppelin = "2.0.0" pragma_lib = "2.11.4" [dev-dependencies] -snforge_std = "^0.48.0" +snforge_std = "^0.50.0" assert_macros = "2.10.1" [[target.starknet-contract]] From 398443fde4d88b77a713e9b99a727108863167e5 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:00:53 +0100 Subject: [PATCH 12/18] clean up XLBTCPriceFeed --- src/price_feeds/XLBTCPriceFeed.cairo | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/price_feeds/XLBTCPriceFeed.cairo b/src/price_feeds/XLBTCPriceFeed.cairo index cc5f49f..a55eb3a 100644 --- a/src/price_feeds/XLBTCPriceFeed.cairo +++ b/src/price_feeds/XLBTCPriceFeed.cairo @@ -1,7 +1,7 @@ // Created by Uncap Labs // SPDX-License-Identifier: BUSL-1.1 -// Composite Price Feed using Pragma. +// Price Feed using Pragma. // Uses the conversion rate of XLBTC. #[starknet::contract] pub mod XLBTCPriceFeed { @@ -16,18 +16,15 @@ pub mod XLBTCPriceFeed { IBorrowerOperationsDispatcher, IBorrowerOperationsDispatcherTrait, }; use crate::interfaces::IPriceFeed::IPriceFeed; - use crate::utils::constants::Constants::_1PCT; ////////////////////////////////////////////////////////////// // CONSTANTS // ////////////////////////////////////////////////////////////// const CONVERSION_XLBTC_USD: felt252 = - 'CONVERSION_XSTRK/USD'; // Key of the XLBTC/USD price in Pragma - const STANDARD_ORACLE_DECIMALS: u32 = 8; // TODO: Confirm with Pragma - const PRICE_SCALING_FACTOR: u256 = - 10_000_000_000; // 10^10: (10^18 - 10^8) // TODO depends on standard decimals - const DEVIATION_THRESHOLD: u256 = _1PCT; // 1% + 'CONVERSION_XLBTC/USD'; // Key of the XLBTC/USD price in Pragma + const STANDARD_ORACLE_DECIMALS: u32 = 8; // + const PRICE_SCALING_FACTOR: u256 = 10_000_000_000; // 10^10: (10^18 - 10^8) // const STALENESS_THRESHOLD: u64 = 60 * 60 * 24; // 1 day in seconds ////////////////////////////////////////////////////////////// @@ -57,15 +54,6 @@ pub mod XLBTCPriceFeed { pub oracle: ContractAddress, } - ////////////////////////////////////////////////////////////// - // ENUMS // - ////////////////////////////////////////////////////////////// - - #[derive(Copy, Clone, Drop)] - pub enum PriceFeedId { - XLBTCUSD, - } - ////////////////////////////////////////////////////////////// // CONSTRUCTOR // ////////////////////////////////////////////////////////////// From 8f8e5f42cc8d47de1486061e52dec5664569fbe6 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:01:02 +0100 Subject: [PATCH 13/18] add generic BTCPriceFeed --- src/lib.cairo | 1 + src/price_feeds/BTCPriceFeed.cairo | 179 +++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 src/price_feeds/BTCPriceFeed.cairo diff --git a/src/lib.cairo b/src/lib.cairo index d0b95e6..aea7381 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -77,6 +77,7 @@ pub mod utils { } pub mod price_feeds { + pub mod BTCPriceFeed; pub mod WBTCPriceFeed; pub mod XLBTCPriceFeed; } diff --git a/src/price_feeds/BTCPriceFeed.cairo b/src/price_feeds/BTCPriceFeed.cairo new file mode 100644 index 0000000..a3dec3f --- /dev/null +++ b/src/price_feeds/BTCPriceFeed.cairo @@ -0,0 +1,179 @@ +// Created by Uncap Labs +// SPDX-License-Identifier: BUSL-1.1 + +// Price Feed using Pragma. +// Designed for all BTC-based wrappers. +#[starknet::contract] +pub mod BTCPriceFeed { + use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; + use pragma_lib::types::{DataType, PragmaPricesResponse}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ContractAddress, get_block_timestamp}; + use crate::branch::interfaces::IAddressesRegistry::{ + IAddressesRegistryDispatcher, IAddressesRegistryDispatcherTrait, + }; + use crate::branch::interfaces::IBorrowerOperations::{ + IBorrowerOperationsDispatcher, IBorrowerOperationsDispatcherTrait, + }; + use crate::interfaces::IPriceFeed::IPriceFeed; + + ////////////////////////////////////////////////////////////// + // CONSTANTS // + ////////////////////////////////////////////////////////////// + + const BTC_USD: felt252 = 'BTC/USD'; // Key of the BTC/USD price in Pragma. + const STANDARD_ORACLE_DECIMALS: u32 = 8; // + const PRICE_SCALING_FACTOR: u256 = + 10_000_000_000; // 10^10: (10^18 - 10^8) // TODO depends on standard decimals + const STALENESS_THRESHOLD: u64 = 60 * 60 * 24; // 1 day in seconds + + ////////////////////////////////////////////////////////////// + // STORAGE // + ////////////////////////////////////////////////////////////// + + #[storage] + pub struct Storage { + pragma_oracle_address: ContractAddress, + has_been_shutdown: bool, + last_good_price: u256, + addresses_registry: ContractAddress, + } + + ////////////////////////////////////////////////////////////// + // EVENTS // + ////////////////////////////////////////////////////////////// + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ShutDownFromOracleFailure: ShutDownFromOracleFailure, + } + + #[derive(Drop, starknet::Event)] + pub struct ShutDownFromOracleFailure { + pub oracle: ContractAddress, + } + + ////////////////////////////////////////////////////////////// + // CONSTRUCTOR // + ////////////////////////////////////////////////////////////// + + #[constructor] + fn constructor( + ref self: ContractState, + pragma_oracle_address: ContractAddress, + addresses_registry: ContractAddress, + ) { + self.pragma_oracle_address.write(pragma_oracle_address); + + self.addresses_registry.write(addresses_registry); + + // Fetch the initial price to set the last good price + // Will shutdown if the oracle fails + self.fetch_price_primary(); + + assert(!self.has_been_shutdown.read(), 'BPF: System already shutdown'); + } + + //////////////////////////////////////////////////////////////// + // EXTERNAL FUNCTIONS // + //////////////////////////////////////////////////////////////// + + #[abi(embed_v0)] + impl IPriceFeedImpl of IPriceFeed { + // Returns: + // - The price, using the current price calculation + // - A bool that is true if: + // --- a) The system was not shutdown prior to this call and + // --- b) an oracle contract failed during this call + fn fetch_price(ref self: ContractState) -> (u256, bool) { + self.fetch_price_primary() + } + + // Same as fetch_price + fn fetch_redemption_price(ref self: ContractState) -> (u256, bool) { + self.fetch_price_primary() + } + + // Returns oracles' price of BTC/USD + fn get_price(self: @ContractState) -> u256 { + if self.has_been_shutdown.read() { + return self.last_good_price.read(); + } + let (price, _) = self.get_pragma_answer(); + price + } + } + + //////////////////////////////////////////////////////////////// + // INTERNAL FUNCTIONS // + //////////////////////////////////////////////////////////////// + + #[generate_trait] + impl InternalImpl of InternalTrait { + ////////////////////////////////////////////////////////////// + // WRITE FUNCTIONS // + ////////////////////////////////////////////////////////////// + + fn fetch_price_primary(ref self: ContractState) -> (u256, bool) { + // If branch has been shutdown, return the last good price + if self.has_been_shutdown.read() { + return (self.last_good_price.read(), false); + } + + // Fetch the BTC/USD price from Pragma + let (btc_price, btc_oracle_failure) = self.get_pragma_answer(); + + if (btc_oracle_failure) { + self.has_been_shutdown.write(true); + + let ar = IAddressesRegistryDispatcher { + contract_address: self.addresses_registry.read(), + }; + let bo = IBorrowerOperationsDispatcher { + contract_address: ar.get_borrower_operations(), + }; + bo.shutdown_from_oracle_failure(); + + self + .emit( + event: ShutDownFromOracleFailure { + oracle: self.pragma_oracle_address.read(), + }, + ); + + return (self.last_good_price.read(), true); + } else { + // Ensures that during shutdown, urgent redemptions use the actual market price + self.last_good_price.write(btc_price); + (btc_price, false) + } + } + + ////////////////////////////////////////////////////////////// + // READ FUNCTIONS // + ////////////////////////////////////////////////////////////// + + fn get_pragma_answer(self: @ContractState) -> (u256, bool) { + let pragma_dispatcher = IPragmaABIDispatcher { + contract_address: self.pragma_oracle_address.read(), + }; + let response: PragmaPricesResponse = pragma_dispatcher + .get_data_median(DataType::SpotEntry(BTC_USD)); + + let price = response.price.into() * PRICE_SCALING_FACTOR; + + let price_is_zero = price == 0; // Price should never be zero + let incorrect_decimals = response + .decimals != STANDARD_ORACLE_DECIMALS; // Decimals should not change + let is_stale_price = response.last_updated_timestamp + + STALENESS_THRESHOLD < get_block_timestamp(); // Price should not be stale + + let oracle_failure_detected = price_is_zero + || incorrect_decimals + || is_stale_price; // If any of these are true, the oracle failed + + return (price, oracle_failure_detected); + } + } +} From 1e8215c83827fd48ca7ae71c1372e9b43dd78204 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:43:39 +0100 Subject: [PATCH 14/18] yarn fmt --- scripts/deploy.ts | 21 +- scripts/deploy_xlbtc_branch.ts | 535 ++++++++++++++++----------------- 2 files changed, 277 insertions(+), 279 deletions(-) diff --git a/scripts/deploy.ts b/scripts/deploy.ts index dc703df..9639a98 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -404,7 +404,12 @@ async function deployContracts( let wrapperAddress: string | null = null; const underlyingAddress = collateral; if (collateralName == 'WWBTC' || collateralName == 'WMWBTC') { - wrapperAddress = await deployContract('CollateralWrapper', ["WrappedWBTC", "WWBTC", context.config.ownerAddress, collateral]); + wrapperAddress = await deployContract('CollateralWrapper', [ + 'WrappedWBTC', + 'WWBTC', + context.config.ownerAddress, + collateral, + ]); collateral = wrapperAddress; } @@ -725,13 +730,13 @@ async function deployContracts( // Deploy USDU // let usduContractAddress = getContractAddress('USDU'); // if (!usduContractAddress) { - // log('USDU not found in loaded addresses, deploying...'); - // const usduContractAddress = '0x2f94539f80158f9a48a7acf3747718dfbec9b6f639e2742c1fb44ae7ab5aa04'; - const usduContractAddress = await deployContract('USDU', { - name: 'Uncap USD', - symbol: 'USDU', - deployer: context.config.deployerAddress, - }); + // log('USDU not found in loaded addresses, deploying...'); + // const usduContractAddress = '0x2f94539f80158f9a48a7acf3747718dfbec9b6f639e2742c1fb44ae7ab5aa04'; + const usduContractAddress = await deployContract('USDU', { + name: 'Uncap USD', + symbol: 'USDU', + deployer: context.config.deployerAddress, + }); // } else { // log(`Using existing USDU at: ${usduContractAddress}`); // } diff --git a/scripts/deploy_xlbtc_branch.ts b/scripts/deploy_xlbtc_branch.ts index db8f372..985f75a 100644 --- a/scripts/deploy_xlbtc_branch.ts +++ b/scripts/deploy_xlbtc_branch.ts @@ -33,16 +33,15 @@ const CONTRACTS_TO_DECLARE = [ const BRANCH_KEY = 'XLBTC'; -const ADDRESSES_FILE = path.join(__dirname, '..', 'deployments', `${process.env['NETWORK']}_addresses.json`); +const ADDRESSES_FILE = path.join( + __dirname, + '..', + 'deployments', + `${process.env['NETWORK']}_addresses.json` +); async function main() { - const { - PRIVATE_KEY, - DEPLOYER_ADDRESS, - RPC_URL, - OWNER_ADDRESS, - SPONSOR, - } = process.env; + const { PRIVATE_KEY, DEPLOYER_ADDRESS, RPC_URL, OWNER_ADDRESS, SPONSOR } = process.env; let XLBTC_UNDERLYING_ADDRESS = process.env['XLBTC_UNDERLYING_ADDRESS']; const missing: string[] = []; @@ -69,7 +68,9 @@ async function main() { } if (!usduAddress || !collateralRegistryAddress || !hintHelpersAddress) { - throw new Error('Missing USDU, CollateralRegistry, or HintHelpers address in mainnet_addresses.json'); + throw new Error( + 'Missing USDU, CollateralRegistry, or HintHelpers address in mainnet_addresses.json' + ); } const provider = new RpcProvider({ nodeUrl: RPC_URL! }); @@ -154,27 +155,42 @@ async function main() { deployer: DEPLOYER_ADDRESS, }); - const borrowerOperationsAddress = await deployContract('BorrowerOperations', { deployer: DEPLOYER_ADDRESS }); + const borrowerOperationsAddress = await deployContract('BorrowerOperations', { + deployer: DEPLOYER_ADDRESS, + }); const troveManagerAddress = await deployContract('TroveManager', { deployer: DEPLOYER_ADDRESS }); const troveNftAddress = await deployContract('TroveNFT', { deployer: DEPLOYER_ADDRESS }); - const stabilityPoolAddress = await deployContract('StabilityPool', { deployer: DEPLOYER_ADDRESS }); + const stabilityPoolAddress = await deployContract('StabilityPool', { + deployer: DEPLOYER_ADDRESS, + }); const sortedTrovesAddress = await deployContract('SortedTroves', { deployer: DEPLOYER_ADDRESS }); const activePoolAddress = await deployContract('ActivePool', { deployer: DEPLOYER_ADDRESS }); const defaultPoolAddress = await deployContract('DefaultPool', { deployer: DEPLOYER_ADDRESS }); - const collSurplusPoolAddress = await deployContract('CollSurplusPool', { deployer: DEPLOYER_ADDRESS }); + const collSurplusPoolAddress = await deployContract('CollSurplusPool', { + deployer: DEPLOYER_ADDRESS, + }); const gasPoolAddress = await deployContract('GasPool', { deployer: DEPLOYER_ADDRESS }); - const liquidationManagerAddress = await deployContract('LiquidationManager', { deployer: DEPLOYER_ADDRESS }); - const redemptionManagerAddress = await deployContract('RedemptionManager', { deployer: DEPLOYER_ADDRESS }); + const liquidationManagerAddress = await deployContract('LiquidationManager', { + deployer: DEPLOYER_ADDRESS, + }); + const redemptionManagerAddress = await deployContract('RedemptionManager', { + deployer: DEPLOYER_ADDRESS, + }); const batchManagerAddress = await deployContract('BatchManager', { deployer: DEPLOYER_ADDRESS }); - const troveManagerEventsEmitterAddress = await deployContract('TroveManagerEventsEmitter', { deployer: DEPLOYER_ADDRESS }); - const xlbtcPriceFeedAddress = process.env['NETWORK'] === 'mainnet' ? await deployContract('XLBTCPriceFeed', { deployer: DEPLOYER_ADDRESS }) : await deployContract('PriceFeedMock', {}); + const troveManagerEventsEmitterAddress = await deployContract('TroveManagerEventsEmitter', { + deployer: DEPLOYER_ADDRESS, + }); + const xlbtcPriceFeedAddress = + process.env['NETWORK'] === 'mainnet' + ? await deployContract('XLBTCPriceFeed', { deployer: DEPLOYER_ADDRESS }) + : await deployContract('PriceFeedMock', {}); if (process.env['NETWORK'] === 'sepolia') { XLBTC_UNDERLYING_ADDRESS = await deployContract('UBTC', [ - 'Liquid Staked Lombard Bitcoin', - 'XLBTC', - '8', - ]); + 'Liquid Staked Lombard Bitcoin', + 'XLBTC', + '8', + ]); } const wrapperAddress = await deployContract('CollateralWrapper', [ @@ -185,272 +201,249 @@ async function main() { ]); log('Initializing branch contracts...'); - // Initialize all contracts using multicall - log('Initializing branch contracts with multicall...'); - - // Debug logging to identify undefined variables - log('Debug - Checking all addresses before initializer:'); - log(` activePoolAddress: ${activePoolAddress}`); - log(` defaultPoolAddress: ${defaultPoolAddress}`); - log(` xlbtcPriceFeedAddress: ${xlbtcPriceFeedAddress}`); - log(` hintHelpersAddress: ${hintHelpersAddress}`); - log(` strkContractAddress: ${strkContractAddress}`); - log(` borrowerOperationsAddress: ${borrowerOperationsAddress}`); - log(` troveManagerAddress: ${troveManagerAddress}`); - log(` troveNftAddress: ${troveNftAddress}`); - log(` gasPoolAddress: ${gasPoolAddress}`); - log(` collSurplusPoolAddress: ${collSurplusPoolAddress}`); - log(` sortedTrovesAddress: ${sortedTrovesAddress}`); - log(` collateralRegistryAddress: ${collateralRegistryAddress}`); - log(` usduAddress: ${usduAddress}`); - log(` interestRouterAddress: ${interestRouterAddress}`); - log(` stabilityPoolAddress: ${stabilityPoolAddress}`); - log(` wrapperAddress: ${wrapperAddress}`); - log(` liquidationManagerAddress: ${liquidationManagerAddress}`); - log(` redemptionManagerAddress: ${redemptionManagerAddress}`); - log(` batchManagerAddress: ${batchManagerAddress}`); - log(` troveManagerEventsEmitterAddress: ${troveManagerEventsEmitterAddress}`); - - // Prepare all initializer calls - const initializerCalls = []; - - // AddressesRegistry initializer - const addressesRegistryAbi = getCachedAbi('AddressesRegistry'); - const addressesRegistryContract = new Contract( - addressesRegistryAbi, - addressesRegistryAddress, - provider - ); - - const addressesRegistryCall = addressesRegistryContract.populate('initializer', [ - activePoolAddress, - defaultPoolAddress, - xlbtcPriceFeedAddress, - hintHelpersAddress, - activePoolAddress, // multi_trove_getter - using active_pool as placeholder - activePoolAddress, // metadata_nft - using active_pool as placeholder - strkContractAddress, // strk - borrowerOperationsAddress, - troveManagerAddress, - troveNftAddress, - gasPoolAddress, - collSurplusPoolAddress, - sortedTrovesAddress, - collateralRegistryAddress, - usduAddress, - interestRouterAddress, - stabilityPoolAddress, - wrapperAddress, // coll_token - liquidationManagerAddress, - redemptionManagerAddress, - batchManagerAddress, - troveManagerEventsEmitterAddress, - ]); - initializerCalls.push({ - contractAddress: addressesRegistryAddress, - entrypoint: 'initializer', - calldata: addressesRegistryCall.calldata || [], - }); + // Initialize all contracts using multicall + log('Initializing branch contracts with multicall...'); + + // Debug logging to identify undefined variables + log('Debug - Checking all addresses before initializer:'); + log(` activePoolAddress: ${activePoolAddress}`); + log(` defaultPoolAddress: ${defaultPoolAddress}`); + log(` xlbtcPriceFeedAddress: ${xlbtcPriceFeedAddress}`); + log(` hintHelpersAddress: ${hintHelpersAddress}`); + log(` strkContractAddress: ${strkContractAddress}`); + log(` borrowerOperationsAddress: ${borrowerOperationsAddress}`); + log(` troveManagerAddress: ${troveManagerAddress}`); + log(` troveNftAddress: ${troveNftAddress}`); + log(` gasPoolAddress: ${gasPoolAddress}`); + log(` collSurplusPoolAddress: ${collSurplusPoolAddress}`); + log(` sortedTrovesAddress: ${sortedTrovesAddress}`); + log(` collateralRegistryAddress: ${collateralRegistryAddress}`); + log(` usduAddress: ${usduAddress}`); + log(` interestRouterAddress: ${interestRouterAddress}`); + log(` stabilityPoolAddress: ${stabilityPoolAddress}`); + log(` wrapperAddress: ${wrapperAddress}`); + log(` liquidationManagerAddress: ${liquidationManagerAddress}`); + log(` redemptionManagerAddress: ${redemptionManagerAddress}`); + log(` batchManagerAddress: ${batchManagerAddress}`); + log(` troveManagerEventsEmitterAddress: ${troveManagerEventsEmitterAddress}`); + + // Prepare all initializer calls + const initializerCalls = []; + + // AddressesRegistry initializer + const addressesRegistryAbi = getCachedAbi('AddressesRegistry'); + const addressesRegistryContract = new Contract( + addressesRegistryAbi, + addressesRegistryAddress, + provider + ); + + const addressesRegistryCall = addressesRegistryContract.populate('initializer', [ + activePoolAddress, + defaultPoolAddress, + xlbtcPriceFeedAddress, + hintHelpersAddress, + activePoolAddress, // multi_trove_getter - using active_pool as placeholder + activePoolAddress, // metadata_nft - using active_pool as placeholder + strkContractAddress, // strk + borrowerOperationsAddress, + troveManagerAddress, + troveNftAddress, + gasPoolAddress, + collSurplusPoolAddress, + sortedTrovesAddress, + collateralRegistryAddress, + usduAddress, + interestRouterAddress, + stabilityPoolAddress, + wrapperAddress, // coll_token + liquidationManagerAddress, + redemptionManagerAddress, + batchManagerAddress, + troveManagerEventsEmitterAddress, + ]); + initializerCalls.push({ + contractAddress: addressesRegistryAddress, + entrypoint: 'initializer', + calldata: addressesRegistryCall.calldata || [], + }); - // ActivePool initializer - const activePoolAbi = getCachedAbi('ActivePool'); - const activePoolContract = new Contract(activePoolAbi, activePoolAddress, provider); - const activePoolCall = activePoolContract.populate('initializer', [addressesRegistryAddress]); - initializerCalls.push({ - contractAddress: activePoolAddress, - entrypoint: 'initializer', - calldata: activePoolCall.calldata || [], - }); + // ActivePool initializer + const activePoolAbi = getCachedAbi('ActivePool'); + const activePoolContract = new Contract(activePoolAbi, activePoolAddress, provider); + const activePoolCall = activePoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: activePoolAddress, + entrypoint: 'initializer', + calldata: activePoolCall.calldata || [], + }); - // DefaultPool initializer - const defaultPoolAbi = getCachedAbi('DefaultPool'); - const defaultPoolContract = new Contract(defaultPoolAbi, defaultPoolAddress, provider); - const defaultPoolCall = defaultPoolContract.populate('initializer', [addressesRegistryAddress]); - initializerCalls.push({ - contractAddress: defaultPoolAddress, - entrypoint: 'initializer', - calldata: defaultPoolCall.calldata || [], - }); + // DefaultPool initializer + const defaultPoolAbi = getCachedAbi('DefaultPool'); + const defaultPoolContract = new Contract(defaultPoolAbi, defaultPoolAddress, provider); + const defaultPoolCall = defaultPoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: defaultPoolAddress, + entrypoint: 'initializer', + calldata: defaultPoolCall.calldata || [], + }); - // TroveManagerEventsEmitter initializer - const troveManagerEventsEmitterAbi = getCachedAbi('TroveManagerEventsEmitter'); - const troveManagerEventsEmitterContract = new Contract( - troveManagerEventsEmitterAbi, - troveManagerEventsEmitterAddress, - provider - ); - const troveManagerEventsEmitterCall = troveManagerEventsEmitterContract.populate( - 'initializer', - [addressesRegistryAddress] - ); - initializerCalls.push({ - contractAddress: troveManagerEventsEmitterAddress, - entrypoint: 'initializer', - calldata: troveManagerEventsEmitterCall.calldata || [], - }); + // TroveManagerEventsEmitter initializer + const troveManagerEventsEmitterAbi = getCachedAbi('TroveManagerEventsEmitter'); + const troveManagerEventsEmitterContract = new Contract( + troveManagerEventsEmitterAbi, + troveManagerEventsEmitterAddress, + provider + ); + const troveManagerEventsEmitterCall = troveManagerEventsEmitterContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: troveManagerEventsEmitterAddress, + entrypoint: 'initializer', + calldata: troveManagerEventsEmitterCall.calldata || [], + }); - // BorrowerOperations initializer - const borrowerOperationsAbi = getCachedAbi('BorrowerOperations'); - const borrowerOperationsContract = new Contract( - borrowerOperationsAbi, - borrowerOperationsAddress, - provider - ); - const borrowerOperationsCall = borrowerOperationsContract.populate('initializer', [ - addressesRegistryAddress, - ]); - initializerCalls.push({ - contractAddress: borrowerOperationsAddress, - entrypoint: 'initializer', - calldata: borrowerOperationsCall.calldata || [], - }); + // BorrowerOperations initializer + const borrowerOperationsAbi = getCachedAbi('BorrowerOperations'); + const borrowerOperationsContract = new Contract( + borrowerOperationsAbi, + borrowerOperationsAddress, + provider + ); + const borrowerOperationsCall = borrowerOperationsContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: borrowerOperationsAddress, + entrypoint: 'initializer', + calldata: borrowerOperationsCall.calldata || [], + }); - // TroveManager initializer - const troveManagerAbi = getCachedAbi('TroveManager'); - const troveManagerContract = new Contract( - troveManagerAbi, - troveManagerAddress, - provider - ); - const troveManagerCall = troveManagerContract.populate('initializer', [ - addressesRegistryAddress, - ]); - initializerCalls.push({ - contractAddress: troveManagerAddress, - entrypoint: 'initializer', - calldata: troveManagerCall.calldata || [], - }); + // TroveManager initializer + const troveManagerAbi = getCachedAbi('TroveManager'); + const troveManagerContract = new Contract(troveManagerAbi, troveManagerAddress, provider); + const troveManagerCall = troveManagerContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: troveManagerAddress, + entrypoint: 'initializer', + calldata: troveManagerCall.calldata || [], + }); - // TroveNFT initializer - const troveNftAbi = getCachedAbi('TroveNFT'); - const troveNftContract = new Contract(troveNftAbi, troveNftAddress, provider); - const troveNftCall = troveNftContract.populate('initializer', [ - addressesRegistryAddress, - 'Uncap Position', - 'UPS', - 'ipfs://bafkreigmavce5idel7goe2x7f6p2fssdh2qvzeo67cgzfjw76tlx2itsfy', - ]); - initializerCalls.push({ - contractAddress: troveNftAddress, - entrypoint: 'initializer', - calldata: troveNftCall.calldata || [], - }); + // TroveNFT initializer + const troveNftAbi = getCachedAbi('TroveNFT'); + const troveNftContract = new Contract(troveNftAbi, troveNftAddress, provider); + const troveNftCall = troveNftContract.populate('initializer', [ + addressesRegistryAddress, + 'Uncap Position', + 'UPS', + 'ipfs://bafkreigmavce5idel7goe2x7f6p2fssdh2qvzeo67cgzfjw76tlx2itsfy', + ]); + initializerCalls.push({ + contractAddress: troveNftAddress, + entrypoint: 'initializer', + calldata: troveNftCall.calldata || [], + }); - // GasPool initializer - const gasPoolAbi = getCachedAbi('GasPool'); - const gasPoolContract = new Contract(gasPoolAbi, gasPoolAddress, provider); - const gasPoolCall = gasPoolContract.populate('initializer', [addressesRegistryAddress]); - initializerCalls.push({ - contractAddress: gasPoolAddress, - entrypoint: 'initializer', - calldata: gasPoolCall.calldata || [], - }); + // GasPool initializer + const gasPoolAbi = getCachedAbi('GasPool'); + const gasPoolContract = new Contract(gasPoolAbi, gasPoolAddress, provider); + const gasPoolCall = gasPoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: gasPoolAddress, + entrypoint: 'initializer', + calldata: gasPoolCall.calldata || [], + }); - // CollSurplusPool initializer - const collSurplusPoolAbi = getCachedAbi('CollSurplusPool'); - const collSurplusPoolContract = new Contract( - collSurplusPoolAbi, - collSurplusPoolAddress, - provider - ); - const collSurplusPoolCall = collSurplusPoolContract.populate('initializer', [ - addressesRegistryAddress, - ]); - initializerCalls.push({ - contractAddress: collSurplusPoolAddress, - entrypoint: 'initializer', - calldata: collSurplusPoolCall.calldata || [], - }); + // CollSurplusPool initializer + const collSurplusPoolAbi = getCachedAbi('CollSurplusPool'); + const collSurplusPoolContract = new Contract( + collSurplusPoolAbi, + collSurplusPoolAddress, + provider + ); + const collSurplusPoolCall = collSurplusPoolContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: collSurplusPoolAddress, + entrypoint: 'initializer', + calldata: collSurplusPoolCall.calldata || [], + }); - // SortedTroves initializer - const sortedTrovesAbi = getCachedAbi('SortedTroves'); - const sortedTrovesContract = new Contract( - sortedTrovesAbi, - sortedTrovesAddress, - provider - ); - const sortedTrovesCall = sortedTrovesContract.populate('initializer', [ - addressesRegistryAddress, - ]); - initializerCalls.push({ - contractAddress: sortedTrovesAddress, - entrypoint: 'initializer', - calldata: sortedTrovesCall.calldata || [], - }); + // SortedTroves initializer + const sortedTrovesAbi = getCachedAbi('SortedTroves'); + const sortedTrovesContract = new Contract(sortedTrovesAbi, sortedTrovesAddress, provider); + const sortedTrovesCall = sortedTrovesContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: sortedTrovesAddress, + entrypoint: 'initializer', + calldata: sortedTrovesCall.calldata || [], + }); - // StabilityPool initializer - const stabilityPoolAbi = getCachedAbi('StabilityPool'); - const stabilityPoolContract = new Contract( - stabilityPoolAbi, - stabilityPoolAddress, - provider - ); - const stabilityPoolCall = stabilityPoolContract.populate('initializer', [ - addressesRegistryAddress, - ]); - initializerCalls.push({ - contractAddress: stabilityPoolAddress, - entrypoint: 'initializer', - calldata: stabilityPoolCall.calldata || [], - }); + // StabilityPool initializer + const stabilityPoolAbi = getCachedAbi('StabilityPool'); + const stabilityPoolContract = new Contract(stabilityPoolAbi, stabilityPoolAddress, provider); + const stabilityPoolCall = stabilityPoolContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: stabilityPoolAddress, + entrypoint: 'initializer', + calldata: stabilityPoolCall.calldata || [], + }); - // LiquidationManager initializer - const liquidationManagerAbi = getCachedAbi('LiquidationManager'); - const liquidationManagerContract = new Contract( - liquidationManagerAbi, - liquidationManagerAddress, - provider - ); - const liquidationManagerCall = liquidationManagerContract.populate('initializer', [ - addressesRegistryAddress, - ]); - initializerCalls.push({ - contractAddress: liquidationManagerAddress, - entrypoint: 'initializer', - calldata: liquidationManagerCall.calldata || [], - }); + // LiquidationManager initializer + const liquidationManagerAbi = getCachedAbi('LiquidationManager'); + const liquidationManagerContract = new Contract( + liquidationManagerAbi, + liquidationManagerAddress, + provider + ); + const liquidationManagerCall = liquidationManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: liquidationManagerAddress, + entrypoint: 'initializer', + calldata: liquidationManagerCall.calldata || [], + }); - // RedemptionManager initializer - const redemptionManagerAbi = getCachedAbi('RedemptionManager'); - const redemptionManagerContract = new Contract( - redemptionManagerAbi, - redemptionManagerAddress, - provider - ); - const redemptionManagerCall = redemptionManagerContract.populate('initializer', [ - addressesRegistryAddress, - ]); - initializerCalls.push({ - contractAddress: redemptionManagerAddress, - entrypoint: 'initializer', - calldata: redemptionManagerCall.calldata || [], - }); + // RedemptionManager initializer + const redemptionManagerAbi = getCachedAbi('RedemptionManager'); + const redemptionManagerContract = new Contract( + redemptionManagerAbi, + redemptionManagerAddress, + provider + ); + const redemptionManagerCall = redemptionManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: redemptionManagerAddress, + entrypoint: 'initializer', + calldata: redemptionManagerCall.calldata || [], + }); - // BatchManager initializer - const batchManagerAbi = getCachedAbi('BatchManager'); - const batchManagerContract = new Contract( - batchManagerAbi, - batchManagerAddress, - provider - ); - const batchManagerCall = batchManagerContract.populate('initializer', [ - addressesRegistryAddress, - ]); - initializerCalls.push({ - contractAddress: batchManagerAddress, - entrypoint: 'initializer', - calldata: batchManagerCall.calldata || [], - }); + // BatchManager initializer + const batchManagerAbi = getCachedAbi('BatchManager'); + const batchManagerContract = new Contract(batchManagerAbi, batchManagerAddress, provider); + const batchManagerCall = batchManagerContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: batchManagerAddress, + entrypoint: 'initializer', + calldata: batchManagerCall.calldata || [], + }); - // Execute all initializers in a single multicall - log(`Executing multicall with ${initializerCalls.length} initializers...`); - const multiCall = await account.execute(initializerCalls); - await provider.waitForTransaction(multiCall.transaction_hash, { retryInterval: 5000 }); - log('All branch contracts initialized successfully'); + // Execute all initializers in a single multicall + log(`Executing multicall with ${initializerCalls.length} initializers...`); + const multiCall = await account.execute(initializerCalls); + await provider.waitForTransaction(multiCall.transaction_hash, { retryInterval: 5000 }); + log('All branch contracts initialized successfully'); if (!XLBTC_UNDERLYING_ADDRESS) { - throw new Error('XLBTC_UNDERLYING_ADDRESS is not defined'); - } + throw new Error('XLBTC_UNDERLYING_ADDRESS is not defined'); + } const branchAddresses: BranchAddresses = { collateral: wrapperAddress, @@ -523,7 +516,7 @@ async function main() { log('✅ XLBTC branch deployment finished'); } -main().catch((error) => { +main().catch(error => { console.error(error); process.exit(1); }); From 3d40602d87d06fb4c0e5c154180295db4dcc1c75 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:20:50 +0100 Subject: [PATCH 15/18] add fallback mechanism to BTCPriceFeed --- src/price_feeds/BTCPriceFeed.cairo | 127 +++++++++++++++++++++++------ 1 file changed, 101 insertions(+), 26 deletions(-) diff --git a/src/price_feeds/BTCPriceFeed.cairo b/src/price_feeds/BTCPriceFeed.cairo index a3dec3f..5d0c7b4 100644 --- a/src/price_feeds/BTCPriceFeed.cairo +++ b/src/price_feeds/BTCPriceFeed.cairo @@ -1,8 +1,28 @@ // Created by Uncap Labs // SPDX-License-Identifier: BUSL-1.1 -// Price Feed using Pragma. -// Designed for all BTC-based wrappers. +// Chainlink's Aggregator Interface +#[starknet::interface] +pub trait IAggregator { + fn latest_round_data(self: @TContractState) -> Round; + fn round_data(self: @TContractState, round_id: u128) -> Round; + fn description(self: @TContractState) -> felt252; + fn decimals(self: @TContractState) -> u8; + fn latest_answer(self: @TContractState) -> u128; +} + +#[derive(Copy, Drop, Serde, PartialEq, starknet::Store)] +pub struct Round { + // used as u128 internally, but necessary for phase-prefixed round ids as returned by proxy + pub round_id: felt252, + pub answer: u128, + pub block_num: u64, + pub started_at: u64, + pub updated_at: u64, +} + +// Price Feed using Pragma, and Chainlink as a fallback. +// Uses the BTC/USD price. #[starknet::contract] pub mod BTCPriceFeed { use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; @@ -16,16 +36,18 @@ pub mod BTCPriceFeed { IBorrowerOperationsDispatcher, IBorrowerOperationsDispatcherTrait, }; use crate::interfaces::IPriceFeed::IPriceFeed; + use super::{IAggregatorDispatcher, IAggregatorDispatcherTrait}; ////////////////////////////////////////////////////////////// // CONSTANTS // ////////////////////////////////////////////////////////////// - const BTC_USD: felt252 = 'BTC/USD'; // Key of the BTC/USD price in Pragma. - const STANDARD_ORACLE_DECIMALS: u32 = 8; // - const PRICE_SCALING_FACTOR: u256 = - 10_000_000_000; // 10^10: (10^18 - 10^8) // TODO depends on standard decimals + const PRICE_FEED_BTC_USD: felt252 = 'BTC/USD'; + const STANDARD_ORACLE_DECIMALS: u32 = 8; // BTC oracles have 8 decimals + const PRICE_SCALING_FACTOR: u256 = 10_000_000_000; // 10^10: (10^18 - 10^8) const STALENESS_THRESHOLD: u64 = 60 * 60 * 24; // 1 day in seconds + const MIN_NUM_SOURCES: u32 = + 5; // Minimum number of sources for an oracle to be considered valid ////////////////////////////////////////////////////////////// // STORAGE // @@ -34,6 +56,7 @@ pub mod BTCPriceFeed { #[storage] pub struct Storage { pragma_oracle_address: ContractAddress, + chainlink_btc_usd_address: ContractAddress, has_been_shutdown: bool, last_good_price: u256, addresses_registry: ContractAddress, @@ -62,17 +85,19 @@ pub mod BTCPriceFeed { fn constructor( ref self: ContractState, pragma_oracle_address: ContractAddress, + chainlink_btc_usd_address: ContractAddress, addresses_registry: ContractAddress, ) { self.pragma_oracle_address.write(pragma_oracle_address); + self.chainlink_btc_usd_address.write(chainlink_btc_usd_address); self.addresses_registry.write(addresses_registry); // Fetch the initial price to set the last good price // Will shutdown if the oracle fails - self.fetch_price_primary(); + self.fetch_price_primary(false); - assert(!self.has_been_shutdown.read(), 'BPF: System already shutdown'); + assert(!self.has_been_shutdown.read(), 'WBTCPF: System already shutdown'); } //////////////////////////////////////////////////////////////// @@ -87,20 +112,21 @@ pub mod BTCPriceFeed { // --- a) The system was not shutdown prior to this call and // --- b) an oracle contract failed during this call fn fetch_price(ref self: ContractState) -> (u256, bool) { - self.fetch_price_primary() + self.fetch_price_primary(false) } - // Same as fetch_price + // Same as fetch_price, but uses a composite price for redemption (see + // `get_redemption_price`) fn fetch_redemption_price(ref self: ContractState) -> (u256, bool) { - self.fetch_price_primary() + self.fetch_price_primary(true) } - // Returns oracles' price of BTC/USD + // Returns oracles' price of WBTC/USD fn get_price(self: @ContractState) -> u256 { if self.has_been_shutdown.read() { return self.last_good_price.read(); } - let (price, _) = self.get_pragma_answer(); + let (price, _) = self.get_oracles_answer(); price } } @@ -115,16 +141,18 @@ pub mod BTCPriceFeed { // WRITE FUNCTIONS // ////////////////////////////////////////////////////////////// - fn fetch_price_primary(ref self: ContractState) -> (u256, bool) { + fn fetch_price_primary(ref self: ContractState, is_redemption: bool) -> (u256, bool) { // If branch has been shutdown, return the last good price if self.has_been_shutdown.read() { return (self.last_good_price.read(), false); } - // Fetch the BTC/USD price from Pragma - let (btc_price, btc_oracle_failure) = self.get_pragma_answer(); + // Fetch BTC price first + let (btc_price, btc_oracle_failure) = self.get_oracles_answer(); + let mut final_price = btc_price; + let mut oracle_failure = btc_oracle_failure; - if (btc_oracle_failure) { + if (oracle_failure) { self.has_been_shutdown.write(true); let ar = IAddressesRegistryDispatcher { @@ -144,9 +172,10 @@ pub mod BTCPriceFeed { return (self.last_good_price.read(), true); } else { - // Ensures that during shutdown, urgent redemptions use the actual market price + // Always store the normal BTC price as last_good_price, not the redemption price + // This ensures that during shutdown, urgent redemptions use the actual market price self.last_good_price.write(btc_price); - (btc_price, false) + (final_price, false) } } @@ -154,13 +183,52 @@ pub mod BTCPriceFeed { // READ FUNCTIONS // ////////////////////////////////////////////////////////////// - fn get_pragma_answer(self: @ContractState) -> (u256, bool) { - let pragma_dispatcher = IPragmaABIDispatcher { + fn get_chainlink_answer(self: @ContractState) -> (u256, bool) { + let cl_address = self.chainlink_btc_usd_address.read(); + let cl_aggregator = IAggregatorDispatcher { contract_address: cl_address }; + let latest_round = cl_aggregator.latest_round_data(); + let price = latest_round.answer.into() * PRICE_SCALING_FACTOR; + let latest_update = latest_round.updated_at; + let is_stale_price = latest_update + STALENESS_THRESHOLD < get_block_timestamp(); + let price_is_zero = price == 0; + let oracle_failure = is_stale_price || price_is_zero; + + (price, oracle_failure) + } + + fn get_oracles_answer(self: @ContractState) -> (u256, bool) { + // Get the answer from the pragma oracle + let (pragma_price, pragma_failure, pragma_not_enough_sources) = self + .get_pragma_answer(); + + // If pragma failed, try the chainlink oracle + if (pragma_failure) { + let (cl_price, chainlink_failure) = self.get_chainlink_answer(); + if chainlink_failure { + // CL failed, but pragma failed only because of not enough sources, return the + // pragma price, and do not shutdown + if pragma_not_enough_sources { + return (pragma_price, false); + } else { + // CL failed and pragma failed for other reasons, shutdown + return (cl_price, true); + } + } else { + // CL did not fail, return the CL price + return (cl_price, false); + } + } else { + // Pragma did not fail, return the Pragma price + return (pragma_price, false); + } + } + + fn get_pragma_answer(self: @ContractState) -> (u256, bool, bool) { + let oracle_dispatcher = IPragmaABIDispatcher { contract_address: self.pragma_oracle_address.read(), }; - let response: PragmaPricesResponse = pragma_dispatcher - .get_data_median(DataType::SpotEntry(BTC_USD)); - + let price_feed = DataType::SpotEntry(PRICE_FEED_BTC_USD); + let response: PragmaPricesResponse = oracle_dispatcher.get_data_median(price_feed); let price = response.price.into() * PRICE_SCALING_FACTOR; let price_is_zero = price == 0; // Price should never be zero @@ -169,11 +237,18 @@ pub mod BTCPriceFeed { let is_stale_price = response.last_updated_timestamp + STALENESS_THRESHOLD < get_block_timestamp(); // Price should not be stale + let not_enough_sources = response.num_sources_aggregated < MIN_NUM_SOURCES; + let oracle_failure_detected = price_is_zero || incorrect_decimals - || is_stale_price; // If any of these are true, the oracle failed + || is_stale_price + || not_enough_sources; // If any of these are true, the oracle failed + + // Checks if the oracle failed ONLY because of not enough sources + let oracle_failure_because_not_enough_sources = oracle_failure_detected + && !(price_is_zero || incorrect_decimals || is_stale_price); - return (price, oracle_failure_detected); + return (price, oracle_failure_detected, oracle_failure_because_not_enough_sources); } } } From c3b39ab3fa50e6e73521814ec62529221d9a4963 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:22:22 +0100 Subject: [PATCH 16/18] fix: naming --- src/price_feeds/BTCPriceFeed.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/price_feeds/BTCPriceFeed.cairo b/src/price_feeds/BTCPriceFeed.cairo index 5d0c7b4..2ee087f 100644 --- a/src/price_feeds/BTCPriceFeed.cairo +++ b/src/price_feeds/BTCPriceFeed.cairo @@ -97,7 +97,7 @@ pub mod BTCPriceFeed { // Will shutdown if the oracle fails self.fetch_price_primary(false); - assert(!self.has_been_shutdown.read(), 'WBTCPF: System already shutdown'); + assert(!self.has_been_shutdown.read(), 'BTCPF: System already shutdown'); } //////////////////////////////////////////////////////////////// @@ -121,7 +121,7 @@ pub mod BTCPriceFeed { self.fetch_price_primary(true) } - // Returns oracles' price of WBTC/USD + // Returns oracles' price of BTC/USD fn get_price(self: @ContractState) -> u256 { if self.has_been_shutdown.read() { return self.last_good_price.read(); From 04e81ba95c462cebdb1d576da0dd0efade743c87 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:25:10 +0100 Subject: [PATCH 17/18] remove is_redemption bool --- src/price_feeds/BTCPriceFeed.cairo | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/price_feeds/BTCPriceFeed.cairo b/src/price_feeds/BTCPriceFeed.cairo index 2ee087f..4a756df 100644 --- a/src/price_feeds/BTCPriceFeed.cairo +++ b/src/price_feeds/BTCPriceFeed.cairo @@ -95,7 +95,7 @@ pub mod BTCPriceFeed { // Fetch the initial price to set the last good price // Will shutdown if the oracle fails - self.fetch_price_primary(false); + self.fetch_price_primary(); assert(!self.has_been_shutdown.read(), 'BTCPF: System already shutdown'); } @@ -112,13 +112,12 @@ pub mod BTCPriceFeed { // --- a) The system was not shutdown prior to this call and // --- b) an oracle contract failed during this call fn fetch_price(ref self: ContractState) -> (u256, bool) { - self.fetch_price_primary(false) + self.fetch_price_primary() } - // Same as fetch_price, but uses a composite price for redemption (see - // `get_redemption_price`) + // Same as fetch_price fn fetch_redemption_price(ref self: ContractState) -> (u256, bool) { - self.fetch_price_primary(true) + self.fetch_price_primary() } // Returns oracles' price of BTC/USD @@ -141,7 +140,7 @@ pub mod BTCPriceFeed { // WRITE FUNCTIONS // ////////////////////////////////////////////////////////////// - fn fetch_price_primary(ref self: ContractState, is_redemption: bool) -> (u256, bool) { + fn fetch_price_primary(ref self: ContractState) -> (u256, bool) { // If branch has been shutdown, return the last good price if self.has_been_shutdown.read() { return (self.last_good_price.read(), false); From 3dc35439c1325fa144c2f5652cb778fdc83a5da6 Mon Sep 17 00:00:00 2001 From: Alex Collette Date: Fri, 19 Dec 2025 14:27:21 +0100 Subject: [PATCH 18/18] add script for solvBTC and tBTC --- scripts/deploy_solvbtc_branch.ts | 486 +++++++++++++++++++++++++++++++ scripts/deploy_tbtc_branch.ts | 486 +++++++++++++++++++++++++++++++ scripts/types.ts | 24 +- 3 files changed, 988 insertions(+), 8 deletions(-) create mode 100644 scripts/deploy_solvbtc_branch.ts create mode 100644 scripts/deploy_tbtc_branch.ts diff --git a/scripts/deploy_solvbtc_branch.ts b/scripts/deploy_solvbtc_branch.ts new file mode 100644 index 0000000..0bab855 --- /dev/null +++ b/scripts/deploy_solvbtc_branch.ts @@ -0,0 +1,486 @@ +import { Account, CallData, Contract, RpcProvider, logger } from 'starknet'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; +import { BranchAddresses, CONSTANTS } from './types'; +import { log } from './utils'; + +dotenv.config(); +logger.setLogLevel('ERROR'); + +type CachedContract = { + classHash: string; + abi: any; +}; + +// No CollateralWrapper needed - SolvBTC has 18 decimals natively +const CONTRACTS_TO_DECLARE = [ + 'AddressesRegistry', + 'BorrowerOperations', + 'TroveManager', + 'TroveNFT', + 'StabilityPool', + 'SortedTroves', + 'ActivePool', + 'DefaultPool', + 'CollSurplusPool', + 'GasPool', + 'LiquidationManager', + 'RedemptionManager', + 'BatchManager', + 'TroveManagerEventsEmitter', + 'BTCPriceFeed', +] as const; + +const BRANCH_KEY = 'SOLVBTC'; + +const ADDRESSES_FILE = path.join( + __dirname, + '..', + 'deployments', + `${process.env['NETWORK']}_addresses.json` +); + +async function main() { + const { + PRIVATE_KEY, + DEPLOYER_ADDRESS, + RPC_URL, + OWNER_ADDRESS, + SPONSOR, + SOLVBTC_ADDRESS, + PRAGMA_ORACLE_ADDRESS, + CHAINLINK_BTC_USD_ADDRESS, + } = process.env; + + const missing: string[] = []; + if (!PRIVATE_KEY) missing.push('PRIVATE_KEY'); + if (!DEPLOYER_ADDRESS) missing.push('DEPLOYER_ADDRESS'); + if (!RPC_URL) missing.push('RPC_URL'); + if (!OWNER_ADDRESS) missing.push('OWNER_ADDRESS'); + if (!SPONSOR) missing.push('SPONSOR'); + if (!SOLVBTC_ADDRESS) missing.push('SOLVBTC_ADDRESS'); + if (!PRAGMA_ORACLE_ADDRESS) missing.push('PRAGMA_ORACLE_ADDRESS'); + if (!CHAINLINK_BTC_USD_ADDRESS) missing.push('CHAINLINK_BTC_USD_ADDRESS'); + + if (missing.length) { + throw new Error(`Missing required env vars: ${missing.join(', ')}`); + } + + const addressesData = JSON.parse(fs.readFileSync(ADDRESSES_FILE, 'utf8')); + const usduAddress: string | undefined = addressesData['USDU']; + const collateralRegistryAddress: string | undefined = addressesData['collateralRegistry']; + const strkContractAddress: string = addressesData['gasToken'] ?? CONSTANTS.STRK; + const hintHelpersAddress: string | undefined = + addressesData?.WWBTC?.hintHelpers ?? addressesData?.hintHelpers; + + const interestRouterAddress: string = addressesData?.WWBTC?.interestRouter; + if (!interestRouterAddress) { + throw new Error('Missing interestRouter address in addresses.json'); + } + + if (!usduAddress || !collateralRegistryAddress || !hintHelpersAddress) { + throw new Error( + 'Missing USDU, CollateralRegistry, or HintHelpers address in mainnet_addresses.json' + ); + } + + const provider = new RpcProvider({ nodeUrl: RPC_URL! }); + const account = new Account(provider, DEPLOYER_ADDRESS!, PRIVATE_KEY!); + + const contractCache: Record = {}; + + async function declareContract(contractName: string): Promise { + if (contractCache[contractName]) return contractCache[contractName]; + + const basePath = path.join(__dirname, '..', 'target', 'dev'); + const sierraPath = path.join(basePath, `usdu_${contractName}.contract_class.json`); + const casmPath = path.join(basePath, `usdu_${contractName}.compiled_contract_class.json`); + + const compiledSierra = JSON.parse(fs.readFileSync(sierraPath, 'utf8')); + const compiledCasm = JSON.parse(fs.readFileSync(casmPath, 'utf8')); + + log(`Declaring ${contractName}...`); + const declareResponse = await account.declareIfNot({ + contract: compiledSierra, + casm: compiledCasm, + }); + + if (declareResponse.transaction_hash) { + await provider.waitForTransaction(declareResponse.transaction_hash, { retryInterval: 5000 }); + } + + const cacheEntry = { classHash: declareResponse.class_hash, abi: compiledSierra.abi }; + contractCache[contractName] = cacheEntry; + return cacheEntry; + } + + async function declareAll() { + log('Declaring contract classes...'); + for (const name of CONTRACTS_TO_DECLARE) { + await declareContract(name); + } + log('All classes declared\n'); + } + + async function deployContract(contractName: string, constructorArgs?: any): Promise { + log(`Deploying ${contractName}...`); + const cached = await declareContract(contractName); + let constructorCalldata: string[] = []; + + if (constructorArgs) { + const contractCallData = new CallData(cached.abi); + constructorCalldata = contractCallData.compile('constructor', constructorArgs); + } + + const deployResponse = await account.deployContract({ + classHash: cached.classHash, + constructorCalldata, + }); + + await provider.waitForTransaction(deployResponse.transaction_hash, { retryInterval: 5000 }); + log(`Deployed ${contractName}: ${deployResponse.contract_address}`); + return deployResponse.contract_address; + } + + function getCachedAbi(contractName: string) { + const cached = contractCache[contractName]; + if (!cached) { + throw new Error(`Contract ${contractName} not declared`); + } + return cached.abi; + } + + await declareAll(); + + log('Deploying SolvBTC branch contracts...'); + + const addressesRegistryAddress = await deployContract('AddressesRegistry', { + ccr: CONSTANTS.CCR_SOLVBTC, + mcr: CONSTANTS.MCR_SOLVBTC, + bcr: CONSTANTS.BCR_ALL, + scr: CONSTANTS.SCR_SOLVBTC, + cap: CONSTANTS.CAP_SOLVBTC, + liquidation_penalty_sp: CONSTANTS.LIQUIDATION_PENALTY_SP_SOLVBTC, + liquidation_penalty_redistribution: CONSTANTS.LIQUIDATION_PENALTY_REDISTRIBUTION_SOLVBTC, + sponsor: SPONSOR, + deployer: DEPLOYER_ADDRESS, + }); + + const borrowerOperationsAddress = await deployContract('BorrowerOperations', { + deployer: DEPLOYER_ADDRESS, + }); + const troveManagerAddress = await deployContract('TroveManager', { deployer: DEPLOYER_ADDRESS }); + const troveNftAddress = await deployContract('TroveNFT', { deployer: DEPLOYER_ADDRESS }); + const stabilityPoolAddress = await deployContract('StabilityPool', { + deployer: DEPLOYER_ADDRESS, + }); + const sortedTrovesAddress = await deployContract('SortedTroves', { deployer: DEPLOYER_ADDRESS }); + const activePoolAddress = await deployContract('ActivePool', { deployer: DEPLOYER_ADDRESS }); + const defaultPoolAddress = await deployContract('DefaultPool', { deployer: DEPLOYER_ADDRESS }); + const collSurplusPoolAddress = await deployContract('CollSurplusPool', { + deployer: DEPLOYER_ADDRESS, + }); + const gasPoolAddress = await deployContract('GasPool', { deployer: DEPLOYER_ADDRESS }); + const liquidationManagerAddress = await deployContract('LiquidationManager', { + deployer: DEPLOYER_ADDRESS, + }); + const redemptionManagerAddress = await deployContract('RedemptionManager', { + deployer: DEPLOYER_ADDRESS, + }); + const batchManagerAddress = await deployContract('BatchManager', { deployer: DEPLOYER_ADDRESS }); + const troveManagerEventsEmitterAddress = await deployContract('TroveManagerEventsEmitter', { + deployer: DEPLOYER_ADDRESS, + }); + + // Deploy BTCPriceFeed - needs addresses_registry to be initialized first + // We'll deploy it after the addresses registry is set up + // For now, deploy with a placeholder and then initialize it properly + + log('Initializing branch contracts...'); + log('Initializing branch contracts with multicall...'); + + // Debug logging to identify undefined variables + log('Debug - Checking all addresses before initializer:'); + log(` activePoolAddress: ${activePoolAddress}`); + log(` defaultPoolAddress: ${defaultPoolAddress}`); + log(` hintHelpersAddress: ${hintHelpersAddress}`); + log(` strkContractAddress: ${strkContractAddress}`); + log(` borrowerOperationsAddress: ${borrowerOperationsAddress}`); + log(` troveManagerAddress: ${troveManagerAddress}`); + log(` troveNftAddress: ${troveNftAddress}`); + log(` gasPoolAddress: ${gasPoolAddress}`); + log(` collSurplusPoolAddress: ${collSurplusPoolAddress}`); + log(` sortedTrovesAddress: ${sortedTrovesAddress}`); + log(` collateralRegistryAddress: ${collateralRegistryAddress}`); + log(` usduAddress: ${usduAddress}`); + log(` interestRouterAddress: ${interestRouterAddress}`); + log(` stabilityPoolAddress: ${stabilityPoolAddress}`); + log(` SOLVBTC_ADDRESS: ${SOLVBTC_ADDRESS}`); + log(` liquidationManagerAddress: ${liquidationManagerAddress}`); + log(` redemptionManagerAddress: ${redemptionManagerAddress}`); + log(` batchManagerAddress: ${batchManagerAddress}`); + log(` troveManagerEventsEmitterAddress: ${troveManagerEventsEmitterAddress}`); + + // Deploy BTCPriceFeed - it needs pragma, chainlink, and addresses_registry + const solvbtcPriceFeedAddress = await deployContract('BTCPriceFeed', [ + PRAGMA_ORACLE_ADDRESS, + CHAINLINK_BTC_USD_ADDRESS, + addressesRegistryAddress, + ]); + + log(` solvbtcPriceFeedAddress: ${solvbtcPriceFeedAddress}`); + + // Prepare all initializer calls + const initializerCalls = []; + + // AddressesRegistry initializer + const addressesRegistryAbi = getCachedAbi('AddressesRegistry'); + const addressesRegistryContract = new Contract( + addressesRegistryAbi, + addressesRegistryAddress, + provider + ); + + // For SolvBTC, use SOLVBTC_ADDRESS directly as coll_token (no wrapper needed) + const addressesRegistryCall = addressesRegistryContract.populate('initializer', [ + activePoolAddress, + defaultPoolAddress, + solvbtcPriceFeedAddress, + hintHelpersAddress, + activePoolAddress, // multi_trove_getter - using active_pool as placeholder + activePoolAddress, // metadata_nft - using active_pool as placeholder + strkContractAddress, // strk + borrowerOperationsAddress, + troveManagerAddress, + troveNftAddress, + gasPoolAddress, + collSurplusPoolAddress, + sortedTrovesAddress, + collateralRegistryAddress, + usduAddress, + interestRouterAddress, + stabilityPoolAddress, + SOLVBTC_ADDRESS!, // coll_token - SolvBTC directly (18 decimals, no wrapper) + liquidationManagerAddress, + redemptionManagerAddress, + batchManagerAddress, + troveManagerEventsEmitterAddress, + ]); + initializerCalls.push({ + contractAddress: addressesRegistryAddress, + entrypoint: 'initializer', + calldata: addressesRegistryCall.calldata || [], + }); + + // ActivePool initializer + const activePoolAbi = getCachedAbi('ActivePool'); + const activePoolContract = new Contract(activePoolAbi, activePoolAddress, provider); + const activePoolCall = activePoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: activePoolAddress, + entrypoint: 'initializer', + calldata: activePoolCall.calldata || [], + }); + + // DefaultPool initializer + const defaultPoolAbi = getCachedAbi('DefaultPool'); + const defaultPoolContract = new Contract(defaultPoolAbi, defaultPoolAddress, provider); + const defaultPoolCall = defaultPoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: defaultPoolAddress, + entrypoint: 'initializer', + calldata: defaultPoolCall.calldata || [], + }); + + // TroveManagerEventsEmitter initializer + const troveManagerEventsEmitterAbi = getCachedAbi('TroveManagerEventsEmitter'); + const troveManagerEventsEmitterContract = new Contract( + troveManagerEventsEmitterAbi, + troveManagerEventsEmitterAddress, + provider + ); + const troveManagerEventsEmitterCall = troveManagerEventsEmitterContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: troveManagerEventsEmitterAddress, + entrypoint: 'initializer', + calldata: troveManagerEventsEmitterCall.calldata || [], + }); + + // BorrowerOperations initializer + const borrowerOperationsAbi = getCachedAbi('BorrowerOperations'); + const borrowerOperationsContract = new Contract( + borrowerOperationsAbi, + borrowerOperationsAddress, + provider + ); + const borrowerOperationsCall = borrowerOperationsContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: borrowerOperationsAddress, + entrypoint: 'initializer', + calldata: borrowerOperationsCall.calldata || [], + }); + + // TroveManager initializer + const troveManagerAbi = getCachedAbi('TroveManager'); + const troveManagerContract = new Contract(troveManagerAbi, troveManagerAddress, provider); + const troveManagerCall = troveManagerContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: troveManagerAddress, + entrypoint: 'initializer', + calldata: troveManagerCall.calldata || [], + }); + + // TroveNFT initializer + const troveNftAbi = getCachedAbi('TroveNFT'); + const troveNftContract = new Contract(troveNftAbi, troveNftAddress, provider); + const troveNftCall = troveNftContract.populate('initializer', [ + addressesRegistryAddress, + 'Uncap Position', + 'UPS', + 'ipfs://bafkreigmavce5idel7goe2x7f6p2fssdh2qvzeo67cgzfjw76tlx2itsfy', + ]); + initializerCalls.push({ + contractAddress: troveNftAddress, + entrypoint: 'initializer', + calldata: troveNftCall.calldata || [], + }); + + // GasPool initializer + const gasPoolAbi = getCachedAbi('GasPool'); + const gasPoolContract = new Contract(gasPoolAbi, gasPoolAddress, provider); + const gasPoolCall = gasPoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: gasPoolAddress, + entrypoint: 'initializer', + calldata: gasPoolCall.calldata || [], + }); + + // CollSurplusPool initializer + const collSurplusPoolAbi = getCachedAbi('CollSurplusPool'); + const collSurplusPoolContract = new Contract( + collSurplusPoolAbi, + collSurplusPoolAddress, + provider + ); + const collSurplusPoolCall = collSurplusPoolContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: collSurplusPoolAddress, + entrypoint: 'initializer', + calldata: collSurplusPoolCall.calldata || [], + }); + + // SortedTroves initializer + const sortedTrovesAbi = getCachedAbi('SortedTroves'); + const sortedTrovesContract = new Contract(sortedTrovesAbi, sortedTrovesAddress, provider); + const sortedTrovesCall = sortedTrovesContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: sortedTrovesAddress, + entrypoint: 'initializer', + calldata: sortedTrovesCall.calldata || [], + }); + + // StabilityPool initializer + const stabilityPoolAbi = getCachedAbi('StabilityPool'); + const stabilityPoolContract = new Contract(stabilityPoolAbi, stabilityPoolAddress, provider); + const stabilityPoolCall = stabilityPoolContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: stabilityPoolAddress, + entrypoint: 'initializer', + calldata: stabilityPoolCall.calldata || [], + }); + + // LiquidationManager initializer + const liquidationManagerAbi = getCachedAbi('LiquidationManager'); + const liquidationManagerContract = new Contract( + liquidationManagerAbi, + liquidationManagerAddress, + provider + ); + const liquidationManagerCall = liquidationManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: liquidationManagerAddress, + entrypoint: 'initializer', + calldata: liquidationManagerCall.calldata || [], + }); + + // RedemptionManager initializer + const redemptionManagerAbi = getCachedAbi('RedemptionManager'); + const redemptionManagerContract = new Contract( + redemptionManagerAbi, + redemptionManagerAddress, + provider + ); + const redemptionManagerCall = redemptionManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: redemptionManagerAddress, + entrypoint: 'initializer', + calldata: redemptionManagerCall.calldata || [], + }); + + // BatchManager initializer + const batchManagerAbi = getCachedAbi('BatchManager'); + const batchManagerContract = new Contract(batchManagerAbi, batchManagerAddress, provider); + const batchManagerCall = batchManagerContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: batchManagerAddress, + entrypoint: 'initializer', + calldata: batchManagerCall.calldata || [], + }); + + // Execute all initializers in a single multicall + log(`Executing multicall with ${initializerCalls.length} initializers...`); + const multiCall = await account.execute(initializerCalls); + await provider.waitForTransaction(multiCall.transaction_hash, { retryInterval: 5000 }); + log('All branch contracts initialized successfully'); + + const branchAddresses: BranchAddresses = { + collateral: SOLVBTC_ADDRESS!, // SolvBTC address directly (no wrapper) + addressesRegistry: addressesRegistryAddress, + borrowerOperations: borrowerOperationsAddress, + troveManager: troveManagerAddress, + troveNft: troveNftAddress, + stabilityPool: stabilityPoolAddress, + sortedTroves: sortedTrovesAddress, + activePool: activePoolAddress, + defaultPool: defaultPoolAddress, + collSurplusPool: collSurplusPoolAddress, + gasPool: gasPoolAddress, + interestRouter: interestRouterAddress, + liquidationManager: liquidationManagerAddress, + redemptionManager: redemptionManagerAddress, + batchManager: batchManagerAddress, + priceFeed: solvbtcPriceFeedAddress, + hintHelpers: hintHelpersAddress, + multiTroveGetter: activePoolAddress, + troveManagerEventsEmitter: troveManagerEventsEmitterAddress, + underlyingAddress: null, // No underlying - SolvBTC is used directly + }; + + log(`Saving ${BRANCH_KEY} addresses to ${ADDRESSES_FILE}...`); + const updatedAddresses = { + ...addressesData, + [BRANCH_KEY]: branchAddresses, + }; + fs.writeFileSync(ADDRESSES_FILE, JSON.stringify(updatedAddresses, null, 2)); + log('Update complete'); + + log('SolvBTC branch deployment finished'); +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/deploy_tbtc_branch.ts b/scripts/deploy_tbtc_branch.ts new file mode 100644 index 0000000..d3edbac --- /dev/null +++ b/scripts/deploy_tbtc_branch.ts @@ -0,0 +1,486 @@ +import { Account, CallData, Contract, RpcProvider, logger } from 'starknet'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; +import { BranchAddresses, CONSTANTS } from './types'; +import { log } from './utils'; + +dotenv.config(); +logger.setLogLevel('ERROR'); + +type CachedContract = { + classHash: string; + abi: any; +}; + +// No CollateralWrapper needed - TBTC has 18 decimals natively +const CONTRACTS_TO_DECLARE = [ + 'AddressesRegistry', + 'BorrowerOperations', + 'TroveManager', + 'TroveNFT', + 'StabilityPool', + 'SortedTroves', + 'ActivePool', + 'DefaultPool', + 'CollSurplusPool', + 'GasPool', + 'LiquidationManager', + 'RedemptionManager', + 'BatchManager', + 'TroveManagerEventsEmitter', + 'BTCPriceFeed', +] as const; + +const BRANCH_KEY = 'TBTC'; + +const ADDRESSES_FILE = path.join( + __dirname, + '..', + 'deployments', + `${process.env['NETWORK']}_addresses.json` +); + +async function main() { + const { + PRIVATE_KEY, + DEPLOYER_ADDRESS, + RPC_URL, + OWNER_ADDRESS, + SPONSOR, + TBTC_ADDRESS, + PRAGMA_ORACLE_ADDRESS, + CHAINLINK_BTC_USD_ADDRESS, + } = process.env; + + const missing: string[] = []; + if (!PRIVATE_KEY) missing.push('PRIVATE_KEY'); + if (!DEPLOYER_ADDRESS) missing.push('DEPLOYER_ADDRESS'); + if (!RPC_URL) missing.push('RPC_URL'); + if (!OWNER_ADDRESS) missing.push('OWNER_ADDRESS'); + if (!SPONSOR) missing.push('SPONSOR'); + if (!TBTC_ADDRESS) missing.push('TBTC_ADDRESS'); + if (!PRAGMA_ORACLE_ADDRESS) missing.push('PRAGMA_ORACLE_ADDRESS'); + if (!CHAINLINK_BTC_USD_ADDRESS) missing.push('CHAINLINK_BTC_USD_ADDRESS'); + + if (missing.length) { + throw new Error(`Missing required env vars: ${missing.join(', ')}`); + } + + const addressesData = JSON.parse(fs.readFileSync(ADDRESSES_FILE, 'utf8')); + const usduAddress: string | undefined = addressesData['USDU']; + const collateralRegistryAddress: string | undefined = addressesData['collateralRegistry']; + const strkContractAddress: string = addressesData['gasToken'] ?? CONSTANTS.STRK; + const hintHelpersAddress: string | undefined = + addressesData?.WWBTC?.hintHelpers ?? addressesData?.hintHelpers; + + const interestRouterAddress: string = addressesData?.WWBTC?.interestRouter; + if (!interestRouterAddress) { + throw new Error('Missing interestRouter address in addresses.json'); + } + + if (!usduAddress || !collateralRegistryAddress || !hintHelpersAddress) { + throw new Error( + 'Missing USDU, CollateralRegistry, or HintHelpers address in mainnet_addresses.json' + ); + } + + const provider = new RpcProvider({ nodeUrl: RPC_URL! }); + const account = new Account(provider, DEPLOYER_ADDRESS!, PRIVATE_KEY!); + + const contractCache: Record = {}; + + async function declareContract(contractName: string): Promise { + if (contractCache[contractName]) return contractCache[contractName]; + + const basePath = path.join(__dirname, '..', 'target', 'dev'); + const sierraPath = path.join(basePath, `usdu_${contractName}.contract_class.json`); + const casmPath = path.join(basePath, `usdu_${contractName}.compiled_contract_class.json`); + + const compiledSierra = JSON.parse(fs.readFileSync(sierraPath, 'utf8')); + const compiledCasm = JSON.parse(fs.readFileSync(casmPath, 'utf8')); + + log(`Declaring ${contractName}...`); + const declareResponse = await account.declareIfNot({ + contract: compiledSierra, + casm: compiledCasm, + }); + + if (declareResponse.transaction_hash) { + await provider.waitForTransaction(declareResponse.transaction_hash, { retryInterval: 5000 }); + } + + const cacheEntry = { classHash: declareResponse.class_hash, abi: compiledSierra.abi }; + contractCache[contractName] = cacheEntry; + return cacheEntry; + } + + async function declareAll() { + log('Declaring contract classes...'); + for (const name of CONTRACTS_TO_DECLARE) { + await declareContract(name); + } + log('All classes declared\n'); + } + + async function deployContract(contractName: string, constructorArgs?: any): Promise { + log(`Deploying ${contractName}...`); + const cached = await declareContract(contractName); + let constructorCalldata: string[] = []; + + if (constructorArgs) { + const contractCallData = new CallData(cached.abi); + constructorCalldata = contractCallData.compile('constructor', constructorArgs); + } + + const deployResponse = await account.deployContract({ + classHash: cached.classHash, + constructorCalldata, + }); + + await provider.waitForTransaction(deployResponse.transaction_hash, { retryInterval: 5000 }); + log(`Deployed ${contractName}: ${deployResponse.contract_address}`); + return deployResponse.contract_address; + } + + function getCachedAbi(contractName: string) { + const cached = contractCache[contractName]; + if (!cached) { + throw new Error(`Contract ${contractName} not declared`); + } + return cached.abi; + } + + await declareAll(); + + log('Deploying TBTC branch contracts...'); + + const addressesRegistryAddress = await deployContract('AddressesRegistry', { + ccr: CONSTANTS.CCR_TBTC, + mcr: CONSTANTS.MCR_TBTC, + bcr: CONSTANTS.BCR_ALL, + scr: CONSTANTS.SCR_TBTC, + cap: CONSTANTS.CAP_TBTC, + liquidation_penalty_sp: CONSTANTS.LIQUIDATION_PENALTY_SP_TBTC, + liquidation_penalty_redistribution: CONSTANTS.LIQUIDATION_PENALTY_REDISTRIBUTION_TBTC, + sponsor: SPONSOR, + deployer: DEPLOYER_ADDRESS, + }); + + const borrowerOperationsAddress = await deployContract('BorrowerOperations', { + deployer: DEPLOYER_ADDRESS, + }); + const troveManagerAddress = await deployContract('TroveManager', { deployer: DEPLOYER_ADDRESS }); + const troveNftAddress = await deployContract('TroveNFT', { deployer: DEPLOYER_ADDRESS }); + const stabilityPoolAddress = await deployContract('StabilityPool', { + deployer: DEPLOYER_ADDRESS, + }); + const sortedTrovesAddress = await deployContract('SortedTroves', { deployer: DEPLOYER_ADDRESS }); + const activePoolAddress = await deployContract('ActivePool', { deployer: DEPLOYER_ADDRESS }); + const defaultPoolAddress = await deployContract('DefaultPool', { deployer: DEPLOYER_ADDRESS }); + const collSurplusPoolAddress = await deployContract('CollSurplusPool', { + deployer: DEPLOYER_ADDRESS, + }); + const gasPoolAddress = await deployContract('GasPool', { deployer: DEPLOYER_ADDRESS }); + const liquidationManagerAddress = await deployContract('LiquidationManager', { + deployer: DEPLOYER_ADDRESS, + }); + const redemptionManagerAddress = await deployContract('RedemptionManager', { + deployer: DEPLOYER_ADDRESS, + }); + const batchManagerAddress = await deployContract('BatchManager', { deployer: DEPLOYER_ADDRESS }); + const troveManagerEventsEmitterAddress = await deployContract('TroveManagerEventsEmitter', { + deployer: DEPLOYER_ADDRESS, + }); + + // Deploy BTCPriceFeed - needs addresses_registry to be initialized first + // We'll deploy it after the addresses registry is set up + // For now, deploy with a placeholder and then initialize it properly + + log('Initializing branch contracts...'); + log('Initializing branch contracts with multicall...'); + + // Debug logging to identify undefined variables + log('Debug - Checking all addresses before initializer:'); + log(` activePoolAddress: ${activePoolAddress}`); + log(` defaultPoolAddress: ${defaultPoolAddress}`); + log(` hintHelpersAddress: ${hintHelpersAddress}`); + log(` strkContractAddress: ${strkContractAddress}`); + log(` borrowerOperationsAddress: ${borrowerOperationsAddress}`); + log(` troveManagerAddress: ${troveManagerAddress}`); + log(` troveNftAddress: ${troveNftAddress}`); + log(` gasPoolAddress: ${gasPoolAddress}`); + log(` collSurplusPoolAddress: ${collSurplusPoolAddress}`); + log(` sortedTrovesAddress: ${sortedTrovesAddress}`); + log(` collateralRegistryAddress: ${collateralRegistryAddress}`); + log(` usduAddress: ${usduAddress}`); + log(` interestRouterAddress: ${interestRouterAddress}`); + log(` stabilityPoolAddress: ${stabilityPoolAddress}`); + log(` TBTC_ADDRESS: ${TBTC_ADDRESS}`); + log(` liquidationManagerAddress: ${liquidationManagerAddress}`); + log(` redemptionManagerAddress: ${redemptionManagerAddress}`); + log(` batchManagerAddress: ${batchManagerAddress}`); + log(` troveManagerEventsEmitterAddress: ${troveManagerEventsEmitterAddress}`); + + // Deploy BTCPriceFeed - it needs pragma, chainlink, and addresses_registry + const tbtcPriceFeedAddress = await deployContract('BTCPriceFeed', [ + PRAGMA_ORACLE_ADDRESS, + CHAINLINK_BTC_USD_ADDRESS, + addressesRegistryAddress, + ]); + + log(` tbtcPriceFeedAddress: ${tbtcPriceFeedAddress}`); + + // Prepare all initializer calls + const initializerCalls = []; + + // AddressesRegistry initializer + const addressesRegistryAbi = getCachedAbi('AddressesRegistry'); + const addressesRegistryContract = new Contract( + addressesRegistryAbi, + addressesRegistryAddress, + provider + ); + + // For TBTC, use TBTC_ADDRESS directly as coll_token (no wrapper needed) + const addressesRegistryCall = addressesRegistryContract.populate('initializer', [ + activePoolAddress, + defaultPoolAddress, + tbtcPriceFeedAddress, + hintHelpersAddress, + activePoolAddress, // multi_trove_getter - using active_pool as placeholder + activePoolAddress, // metadata_nft - using active_pool as placeholder + strkContractAddress, // strk + borrowerOperationsAddress, + troveManagerAddress, + troveNftAddress, + gasPoolAddress, + collSurplusPoolAddress, + sortedTrovesAddress, + collateralRegistryAddress, + usduAddress, + interestRouterAddress, + stabilityPoolAddress, + TBTC_ADDRESS!, // coll_token - TBTC directly (18 decimals, no wrapper) + liquidationManagerAddress, + redemptionManagerAddress, + batchManagerAddress, + troveManagerEventsEmitterAddress, + ]); + initializerCalls.push({ + contractAddress: addressesRegistryAddress, + entrypoint: 'initializer', + calldata: addressesRegistryCall.calldata || [], + }); + + // ActivePool initializer + const activePoolAbi = getCachedAbi('ActivePool'); + const activePoolContract = new Contract(activePoolAbi, activePoolAddress, provider); + const activePoolCall = activePoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: activePoolAddress, + entrypoint: 'initializer', + calldata: activePoolCall.calldata || [], + }); + + // DefaultPool initializer + const defaultPoolAbi = getCachedAbi('DefaultPool'); + const defaultPoolContract = new Contract(defaultPoolAbi, defaultPoolAddress, provider); + const defaultPoolCall = defaultPoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: defaultPoolAddress, + entrypoint: 'initializer', + calldata: defaultPoolCall.calldata || [], + }); + + // TroveManagerEventsEmitter initializer + const troveManagerEventsEmitterAbi = getCachedAbi('TroveManagerEventsEmitter'); + const troveManagerEventsEmitterContract = new Contract( + troveManagerEventsEmitterAbi, + troveManagerEventsEmitterAddress, + provider + ); + const troveManagerEventsEmitterCall = troveManagerEventsEmitterContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: troveManagerEventsEmitterAddress, + entrypoint: 'initializer', + calldata: troveManagerEventsEmitterCall.calldata || [], + }); + + // BorrowerOperations initializer + const borrowerOperationsAbi = getCachedAbi('BorrowerOperations'); + const borrowerOperationsContract = new Contract( + borrowerOperationsAbi, + borrowerOperationsAddress, + provider + ); + const borrowerOperationsCall = borrowerOperationsContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: borrowerOperationsAddress, + entrypoint: 'initializer', + calldata: borrowerOperationsCall.calldata || [], + }); + + // TroveManager initializer + const troveManagerAbi = getCachedAbi('TroveManager'); + const troveManagerContract = new Contract(troveManagerAbi, troveManagerAddress, provider); + const troveManagerCall = troveManagerContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: troveManagerAddress, + entrypoint: 'initializer', + calldata: troveManagerCall.calldata || [], + }); + + // TroveNFT initializer + const troveNftAbi = getCachedAbi('TroveNFT'); + const troveNftContract = new Contract(troveNftAbi, troveNftAddress, provider); + const troveNftCall = troveNftContract.populate('initializer', [ + addressesRegistryAddress, + 'Uncap Position', + 'UPS', + 'ipfs://bafkreigmavce5idel7goe2x7f6p2fssdh2qvzeo67cgzfjw76tlx2itsfy', + ]); + initializerCalls.push({ + contractAddress: troveNftAddress, + entrypoint: 'initializer', + calldata: troveNftCall.calldata || [], + }); + + // GasPool initializer + const gasPoolAbi = getCachedAbi('GasPool'); + const gasPoolContract = new Contract(gasPoolAbi, gasPoolAddress, provider); + const gasPoolCall = gasPoolContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: gasPoolAddress, + entrypoint: 'initializer', + calldata: gasPoolCall.calldata || [], + }); + + // CollSurplusPool initializer + const collSurplusPoolAbi = getCachedAbi('CollSurplusPool'); + const collSurplusPoolContract = new Contract( + collSurplusPoolAbi, + collSurplusPoolAddress, + provider + ); + const collSurplusPoolCall = collSurplusPoolContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: collSurplusPoolAddress, + entrypoint: 'initializer', + calldata: collSurplusPoolCall.calldata || [], + }); + + // SortedTroves initializer + const sortedTrovesAbi = getCachedAbi('SortedTroves'); + const sortedTrovesContract = new Contract(sortedTrovesAbi, sortedTrovesAddress, provider); + const sortedTrovesCall = sortedTrovesContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: sortedTrovesAddress, + entrypoint: 'initializer', + calldata: sortedTrovesCall.calldata || [], + }); + + // StabilityPool initializer + const stabilityPoolAbi = getCachedAbi('StabilityPool'); + const stabilityPoolContract = new Contract(stabilityPoolAbi, stabilityPoolAddress, provider); + const stabilityPoolCall = stabilityPoolContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: stabilityPoolAddress, + entrypoint: 'initializer', + calldata: stabilityPoolCall.calldata || [], + }); + + // LiquidationManager initializer + const liquidationManagerAbi = getCachedAbi('LiquidationManager'); + const liquidationManagerContract = new Contract( + liquidationManagerAbi, + liquidationManagerAddress, + provider + ); + const liquidationManagerCall = liquidationManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: liquidationManagerAddress, + entrypoint: 'initializer', + calldata: liquidationManagerCall.calldata || [], + }); + + // RedemptionManager initializer + const redemptionManagerAbi = getCachedAbi('RedemptionManager'); + const redemptionManagerContract = new Contract( + redemptionManagerAbi, + redemptionManagerAddress, + provider + ); + const redemptionManagerCall = redemptionManagerContract.populate('initializer', [ + addressesRegistryAddress, + ]); + initializerCalls.push({ + contractAddress: redemptionManagerAddress, + entrypoint: 'initializer', + calldata: redemptionManagerCall.calldata || [], + }); + + // BatchManager initializer + const batchManagerAbi = getCachedAbi('BatchManager'); + const batchManagerContract = new Contract(batchManagerAbi, batchManagerAddress, provider); + const batchManagerCall = batchManagerContract.populate('initializer', [addressesRegistryAddress]); + initializerCalls.push({ + contractAddress: batchManagerAddress, + entrypoint: 'initializer', + calldata: batchManagerCall.calldata || [], + }); + + // Execute all initializers in a single multicall + log(`Executing multicall with ${initializerCalls.length} initializers...`); + const multiCall = await account.execute(initializerCalls); + await provider.waitForTransaction(multiCall.transaction_hash, { retryInterval: 5000 }); + log('All branch contracts initialized successfully'); + + const branchAddresses: BranchAddresses = { + collateral: TBTC_ADDRESS!, // TBTC address directly (no wrapper) + addressesRegistry: addressesRegistryAddress, + borrowerOperations: borrowerOperationsAddress, + troveManager: troveManagerAddress, + troveNft: troveNftAddress, + stabilityPool: stabilityPoolAddress, + sortedTroves: sortedTrovesAddress, + activePool: activePoolAddress, + defaultPool: defaultPoolAddress, + collSurplusPool: collSurplusPoolAddress, + gasPool: gasPoolAddress, + interestRouter: interestRouterAddress, + liquidationManager: liquidationManagerAddress, + redemptionManager: redemptionManagerAddress, + batchManager: batchManagerAddress, + priceFeed: tbtcPriceFeedAddress, + hintHelpers: hintHelpersAddress, + multiTroveGetter: activePoolAddress, + troveManagerEventsEmitter: troveManagerEventsEmitterAddress, + underlyingAddress: null, // No underlying - TBTC is used directly + }; + + log(`Saving ${BRANCH_KEY} addresses to ${ADDRESSES_FILE}...`); + const updatedAddresses = { + ...addressesData, + [BRANCH_KEY]: branchAddresses, + }; + fs.writeFileSync(ADDRESSES_FILE, JSON.stringify(updatedAddresses, null, 2)); + log('Update complete'); + + log('TBTC branch deployment finished'); +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/types.ts b/scripts/types.ts index 6a7d9d3..ee4cca2 100644 --- a/scripts/types.ts +++ b/scripts/types.ts @@ -64,14 +64,6 @@ export const CONSTANTS = { STRK: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d', BCR_ALL: '100000000000000000', // 10% - // Constants for XLBTC - CCR_XLBTC: '1500000000000000000', // 150% - MCR_XLBTC: '1250000000000000000', // 125% - SCR_XLBTC: '1100000000000000000', // 110% - CAP_XLBTC: '10000000000000000000', // 10 XLBTC - LIQUIDATION_PENALTY_SP_XLBTC: '50000000000000000', // 5% - LIQUIDATION_PENALTY_REDISTRIBUTION_XLBTC: '200000000000000000', // 20% - // Constants from for WBTC CCR_WBTC: '1500000000000000000', // 150% MCR_WBTC: '1150000000000000000', // 115% @@ -80,4 +72,20 @@ export const CONSTANTS = { LIQUIDATION_PENALTY_SP_WBTC: '50000000000000000', // 5% LIQUIDATION_PENALTY_REDISTRIBUTION_WBTC: '100000000000000000', // 10% + + // Constants for TBTC (18 decimals, no wrapper needed) + CCR_TBTC: '1500000000000000000', // 150% + MCR_TBTC: '1250000000000000000', // 125% + SCR_TBTC: '1100000000000000000', // 110% + CAP_TBTC: '10000000000000000000000000000000000000', // 10 TBTC (18 decimals) + LIQUIDATION_PENALTY_SP_TBTC: '50000000000000000', // 5% + LIQUIDATION_PENALTY_REDISTRIBUTION_TBTC: '100000000000000000', // 10% + + // Constants for SolvBTC (18 decimals, no wrapper needed) + CCR_SOLVBTC: '1500000000000000000', // 150% + MCR_SOLVBTC: '1250000000000000000', // 125% + SCR_SOLVBTC: '1100000000000000000', // 110% + CAP_SOLVBTC: '10000000000000000000000000000000000000', // 10 SolvBTC (18 decimals) + LIQUIDATION_PENALTY_SP_SOLVBTC: '50000000000000000', // 5% + LIQUIDATION_PENALTY_REDISTRIBUTION_SOLVBTC: '100000000000000000', // 10% } as const;