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 84c2025..1a54afe 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -13,7 +13,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]] diff --git a/deployments/sepolia_addresses.json b/deployments/sepolia_addresses.json index c07f548..bcb4659 100644 --- a/deployments/sepolia_addresses.json +++ b/deployments/sepolia_addresses.json @@ -4,6 +4,7 @@ "collateralRegistry": "0x52ecdfd338cb3d58f91ad6c9853e277b3f1b6607d74955791fe652c438b50e1", "redemptionPreviewer": "0x0424960710b8d39161235da5b403a9e9a7fb82c8c02d1e8d02b0e5328c8bafa8", "usdc": "0x0519eef481ac2a9b364fcb701dc5f3f584388dfae6559619857ecd5393cc4b2a", + "hintHelpers": "0x2a6315c1f4323be9fa5fc3fe5152854628945d73f3df851364cdd9bbb9c450f", "WWBTC": { "collateral": "0x6c5f4da4070bbecd4851f12cf3086fef4fb6f305580dcc0f1342aab017f8c5", "addressesRegistry": "0x4c33c1812855b99544f164c0251a1e46bbc7c76f73277b1fbac182fd6b801ac", @@ -21,7 +22,6 @@ "redemptionManager": "0x6ec8edf9eb1fb7b1cc8aff1e29f140b6cb82faf0aeababc50196c75abc011b0", "batchManager": "0x61bdd21652a34780f32394986af1dfc126e6e8380b37a15bc12a8c20a36f621", "priceFeed": "0x23d9ff8d05d52f5048de8c8588dae15f139065f0326ec5e3f1348e922749292", - "hintHelpers": "0x2a6315c1f4323be9fa5fc3fe5152854628945d73f3df851364cdd9bbb9c450f", "multiTroveGetter": "0x726016b80038fd571c62dcbfc922a2b0cc3cdbaf217bd8c6eef3157c9a76057", "troveManagerEventsEmitter": "0x4d86c9b70f1d2fcb741127b2f920b572baf0ff35489ec11afb605cccacfd551", "underlyingAddress": "0x138a381fb3b06c59c626c5038ad718fd587e1366857113a584b317de04db46c" diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 2f73b5f..9639a98 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -252,7 +252,7 @@ async function deployContracts( 'PriceFeedMock', 'TroveManagerEventsEmitter', 'WBTCPriceFeed', - // 'UBTC', + 'UBTC', 'USDU', 'CollateralRegistry', 'CollateralWrapper', @@ -403,7 +403,7 @@ async function deployContracts( let wrapperAddress: string | null = null; const underlyingAddress = collateral; - if (collateralName == 'WWBTC') { + if (collateralName == 'WWBTC' || collateralName == 'WMWBTC') { wrapperAddress = await deployContract('CollateralWrapper', [ 'WrappedWBTC', 'WWBTC', @@ -730,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'; - // 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_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/deploy_xlbtc_branch.ts b/scripts/deploy_xlbtc_branch.ts new file mode 100644 index 0000000..985f75a --- /dev/null +++ b/scripts/deploy_xlbtc_branch.ts @@ -0,0 +1,522 @@ +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 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; + 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(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 XLBTC branch contracts...'); + + const addressesRegistryAddress = await deployContract('AddressesRegistry', { + ccr: CONSTANTS.CCR_XLBTC, + mcr: CONSTANTS.MCR_XLBTC, + bcr: CONSTANTS.BCR_ALL, + 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, + }); + + 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 Liquid Staked Bitcoin', + 'WXLBTC', + OWNER_ADDRESS, + XLBTC_UNDERLYING_ADDRESS, + ]); + + 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 || [], + }); + + // 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'); + + 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 ${ADDRESSES_FILE}...`); + const updatedAddresses = { + ...addressesData, + [BRANCH_KEY]: branchAddresses, + }; + fs.writeFileSync(ADDRESSES_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..ee4cca2 100644 --- a/scripts/types.ts +++ b/scripts/types.ts @@ -64,21 +64,28 @@ export const CONSTANTS = { STRK: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d', BCR_ALL: '100000000000000000', // 10% - // Constants from for UBTC - CCR_UBTC: '1500000000000000000', // 150% - MCR_UBTC: '1100000000000000000', // 110% - SCR_UBTC: '1100000000000000000', // 110% - CAP_UBTC: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', - // Constants from for WBTC CCR_WBTC: '1500000000000000000', // 150% MCR_WBTC: '1150000000000000000', // 115% 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% + + // 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; diff --git a/src/lib.cairo b/src/lib.cairo index abac06f..1644874 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -78,7 +78,9 @@ pub mod utils { } pub mod price_feeds { + pub mod BTCPriceFeed; pub mod WBTCPriceFeed; + pub mod XLBTCPriceFeed; } pub mod wrappers { diff --git a/src/price_feeds/BTCPriceFeed.cairo b/src/price_feeds/BTCPriceFeed.cairo new file mode 100644 index 0000000..4a756df --- /dev/null +++ b/src/price_feeds/BTCPriceFeed.cairo @@ -0,0 +1,253 @@ +// 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, +} + +// 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}; + 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 super::{IAggregatorDispatcher, IAggregatorDispatcherTrait}; + + ////////////////////////////////////////////////////////////// + // CONSTANTS // + ////////////////////////////////////////////////////////////// + + 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 // + ////////////////////////////////////////////////////////////// + + #[storage] + pub struct Storage { + pragma_oracle_address: ContractAddress, + chainlink_btc_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, + } + + ////////////////////////////////////////////////////////////// + // CONSTRUCTOR // + ////////////////////////////////////////////////////////////// + + #[constructor] + 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(); + + assert(!self.has_been_shutdown.read(), 'BTCPF: 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_oracles_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 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 (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 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); + (final_price, false) + } + } + + ////////////////////////////////////////////////////////////// + // READ FUNCTIONS // + ////////////////////////////////////////////////////////////// + + 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 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 + 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); + } + } +} diff --git a/src/price_feeds/XLBTCPriceFeed.cairo b/src/price_feeds/XLBTCPriceFeed.cairo new file mode 100644 index 0000000..a55eb3a --- /dev/null +++ b/src/price_feeds/XLBTCPriceFeed.cairo @@ -0,0 +1,180 @@ +// Created by Uncap Labs +// SPDX-License-Identifier: BUSL-1.1 + +// Price Feed using Pragma. +// Uses the conversion rate of XLBTC. +#[starknet::contract] +pub mod XLBTCPriceFeed { + use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; + use pragma_lib::types::{AggregationMode, 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 CONVERSION_XLBTC_USD: felt252 = + '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 + + ////////////////////////////////////////////////////////////// + // 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(), 'XLPF: 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 XLBTC/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 XLBTC/USD price from Pragma + let (xlbtc_price, xlbtc_oracle_failure) = self.get_pragma_answer(); + + if (xlbtc_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(xlbtc_price); + (xlbtc_price, false) + } + } + + ////////////////////////////////////////////////////////////// + // READ FUNCTIONS // + ////////////////////////////////////////////////////////////// + + fn get_pragma_answer(self: @ContractState) -> (u256, bool) { + let pragma_dispatcher = IPragmaABIDispatcher { + contract_address: self.pragma_oracle_address.read(), + }; + + 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; + + 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); + } + } +}