diff --git a/Scarb.toml b/Scarb.toml index bb3e57e..84c2025 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -2,8 +2,7 @@ name = "usdu" version = "0.1.0" edition = "2024_07" - -# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html +license = "BUSL-1.1" [features] fuzz = [] diff --git a/deployments/mainnet_addresses.json b/deployments/mainnet_addresses.json index 5f52df6..ea63024 100644 --- a/deployments/mainnet_addresses.json +++ b/deployments/mainnet_addresses.json @@ -2,6 +2,8 @@ "USDU": "0x2f94539f80158f9a48a7acf3747718dfbec9b6f639e2742c1fb44ae7ab5aa04", "gasToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", "collateralRegistry": "0x76512e6722f1891ce15ca7e7ff6514b6898872fb8e6ce881d16108e26457e90", + "hintHelpers": "0x44b632d0e09b042b6521cd4ba250add5387fb68736cb10f2cc2411c1a1c2a84", + "redemptionPreviewer": "0x00951608b658609e64dbaea59d87d8c5d15791bbea2293da26eb5315f55029d1", "WWBTC": { "collateral": "0x75d9e518f46a9ca0404fb0a7d386ce056dadf57fd9a0e8659772cb517be4a18", "addressesRegistry": "0x42a37aa9263b01191286f0f800cc85c676441fb9d27d74bbf3ebcbf4e373d81", @@ -19,9 +21,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..c07f548 --- /dev/null +++ b/deployments/sepolia_addresses.json @@ -0,0 +1,52 @@ +{ + "USDU": "0xc2f7f9fbd5ae626562267eaf3fd119dcf4caafebaaf003e67e5253ad48676b", + "gasToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "collateralRegistry": "0x52ecdfd338cb3d58f91ad6c9853e277b3f1b6607d74955791fe652c438b50e1", + "redemptionPreviewer": "0x0424960710b8d39161235da5b403a9e9a7fb82c8c02d1e8d02b0e5328c8bafa8", + "usdc": "0x0519eef481ac2a9b364fcb701dc5f3f584388dfae6559619857ecd5393cc4b2a", + "WWBTC": { + "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": "0x4f0520ccf88e5affce2efa0bd26c0527119229cd78b2ba4ae1559b42b3b7ca4", + "redemptionManager": "0x6ec8edf9eb1fb7b1cc8aff1e29f140b6cb82faf0aeababc50196c75abc011b0", + "batchManager": "0x61bdd21652a34780f32394986af1dfc126e6e8380b37a15bc12a8c20a36f621", + "priceFeed": "0x23d9ff8d05d52f5048de8c8588dae15f139065f0326ec5e3f1348e922749292", + "hintHelpers": "0x2a6315c1f4323be9fa5fc3fe5152854628945d73f3df851364cdd9bbb9c450f", + "multiTroveGetter": "0x726016b80038fd571c62dcbfc922a2b0cc3cdbaf217bd8c6eef3157c9a76057", + "troveManagerEventsEmitter": "0x4d86c9b70f1d2fcb741127b2f920b572baf0ff35489ec11afb605cccacfd551", + "underlyingAddress": "0x138a381fb3b06c59c626c5038ad718fd587e1366857113a584b317de04db46c" + }, + "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": "0x58d3cce963f09fea312bb374592cbc54c4d87aa892aaf3250a5df768edbf71e", + "redemptionManager": "0x288837971e260e1013639c44fcff039a0d3c932107377237cd381ab4a2bce46", + "batchManager": "0x52e606730c5e2b94f5fcab4c61189295e0fe2a5da74d1b4ef21bbdf2452b63", + "priceFeed": "0x55538ded9fd8db7d26ac3ecac0a035aad301db9deef44ee5b969c10722098a", + "hintHelpers": "0x2a6315c1f4323be9fa5fc3fe5152854628945d73f3df851364cdd9bbb9c450f", + "multiTroveGetter": "0x7a165bfaeb48ef3262b87ee80986a93d1d63e1a3f9f0655904cf091b779f108", + "troveManagerEventsEmitter": "0x22ce6fc0261745477a05adb72cb8b0d49e021560009012c51b9889c5af47af4", + "underlyingAddress": "0x34c38b1b64c0bc824f01d2bc31a94dc8ec770f84efaad6f2ab89aa55eec856c", + "lbtc": "0x0682a8216a6ccddeef730c8f2eb77dcf4fe70e7d8251f5fb7faf120365292c4e" + } +} diff --git a/package.json b/package.json index 0eec620..d124a6d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "author": "", "license": "MIT", "dependencies": { - "starknet": "^7.0.0", + "starknet": "^8.6.0", "dotenv": "^16.3.1" }, "devDependencies": { diff --git a/scripts/close_positions.ts b/scripts/close_positions.ts index 3ee7ce4..ff04524 100644 --- a/scripts/close_positions.ts +++ b/scripts/close_positions.ts @@ -9,7 +9,7 @@ import { MIN_DEBT, MCR, log, - closeReadline + closeReadline, } from './utils'; // Load environment variables @@ -56,7 +56,7 @@ async function queryTroves(borrowerAddress: string): Promise { body: JSON.stringify({ query }), }); - const result = await response.json() as GraphQLResponse; + const result = (await response.json()) as GraphQLResponse; return result.data.troves; } catch (error) { console.error('Error querying GraphQL:', error); @@ -80,7 +80,9 @@ function calculateDebtWithInterest(trove: Trove): bigint { console.log(` Trove ${trove.id}:`); console.log(` - Base debt: ${(Number(baseDebt) / Number(DECIMAL_PRECISION)).toFixed(5)} USDU`); console.log(` - Interest rate (raw): ${trove.interestRate}`); - console.log(` - Interest rate (%): ${(Number(interestRate) / Number(DECIMAL_PRECISION) * 100).toFixed(2)}%`); + console.log( + ` - Interest rate (%): ${((Number(interestRate) / Number(DECIMAL_PRECISION)) * 100).toFixed(2)}%` + ); console.log(` - Days since last action: ${daysSinceLastAction.toFixed(2)}`); // Calculate interest: baseDebt * interestRate * days / 365 * 1.05 @@ -90,12 +92,18 @@ function calculateDebtWithInterest(trove: Trove): bigint { // Calculate: (baseDebt * interestRate * days * buffer) / (365 * DECIMAL_PRECISION * 100) const daysBI = BigInt(Math.floor(daysSinceLastAction)); - const accruedInterest = (baseDebt * interestRate * daysBI * BUFFER_FACTOR) / (DAYS_IN_YEAR * DECIMAL_PRECISION * BigInt(100)); + const accruedInterest = + (baseDebt * interestRate * daysBI * BUFFER_FACTOR) / + (DAYS_IN_YEAR * DECIMAL_PRECISION * BigInt(100)); const totalDebt = baseDebt + accruedInterest; - console.log(` - Accrued interest (with 5% buffer): ${(Number(accruedInterest) / Number(DECIMAL_PRECISION)).toFixed(5)} USDU`); - console.log(` - Total debt: ${(Number(totalDebt) / Number(DECIMAL_PRECISION)).toFixed(5)} USDU`); + console.log( + ` - Accrued interest (with 5% buffer): ${(Number(accruedInterest) / Number(DECIMAL_PRECISION)).toFixed(5)} USDU` + ); + console.log( + ` - Total debt: ${(Number(totalDebt) / Number(DECIMAL_PRECISION)).toFixed(5)} USDU` + ); return totalDebt; } @@ -111,7 +119,7 @@ function parseTroveId(id: string): { branchId: number; troveId: string } { const [branchIdStr, troveId] = parts; return { branchId: parseInt(branchIdStr || '0'), - troveId: troveId || '' + troveId: troveId || '', }; } @@ -141,7 +149,7 @@ async function main() { const addresses = await loadDeploymentAddressesV2(); // Query troves for the borrower - const borrowerAddress = "0x03bb8724887fe0f9dcfc3806053314ce7f091fb876fc128c5080b27fa77203f2"; + const borrowerAddress = '0x03bb8724887fe0f9dcfc3806053314ce7f091fb876fc128c5080b27fa77203f2'; console.log(`šŸ“Š Querying troves for borrower: ${borrowerAddress}\n`); const troves = await queryTroves(borrowerAddress); @@ -171,22 +179,26 @@ async function main() { branchName: getBranchName(branchId), totalDebt: debtWithInterest, collateral, - cr: BigInt(0) // Will be calculated after we get prices + cr: BigInt(0), // Will be calculated after we get prices }; }); // Add 10% buffer to total debt for interest accrual and rounding const totalDebtNeeded = (totalDebtBase * BigInt(110)) / BigInt(100); - console.log(`\nšŸ“Š Total debt (base): ${Number(totalDebtBase) / Number(DECIMAL_PRECISION)} USDU`); - console.log(`šŸ“Š Total debt needed (with 10% buffer): ${Number(totalDebtNeeded) / Number(DECIMAL_PRECISION)} USDU\n`); + console.log( + `\nšŸ“Š Total debt (base): ${Number(totalDebtBase) / Number(DECIMAL_PRECISION)} USDU` + ); + console.log( + `šŸ“Š Total debt needed (with 10% buffer): ${Number(totalDebtNeeded) / Number(DECIMAL_PRECISION)} USDU\n` + ); // Check current USDU balance console.log('šŸ’µ Checking USDU balance...'); - const usduContract = new Contract( - JSON.parse(fs.readFileSync('./abis/USDU.json', 'utf8')), - addresses['USDU'], - provider - ); + const usduContract = new Contract({ + abi: JSON.parse(fs.readFileSync('./abis/USDU.json', 'utf8')), + address: addresses['USDU'], + providerOrAccount: provider, + }); const currentBalance = await usduContract['balanceOf'](account.address); @@ -197,9 +209,10 @@ async function main() { } else if (currentBalance && typeof currentBalance === 'object') { // Check if it has the balance property directly if ('balance' in currentBalance) { - currentBalanceBN = typeof currentBalance.balance === 'bigint' - ? currentBalance.balance - : BigInt(currentBalance.balance?.toString() || '0'); + currentBalanceBN = + typeof currentBalance.balance === 'bigint' + ? currentBalance.balance + : BigInt(currentBalance.balance?.toString() || '0'); } else { // Try uint256 format with low/high const low = currentBalance.low || currentBalance['0'] || 0n; @@ -211,9 +224,12 @@ async function main() { currentBalanceBN = BigInt(0); } - console.log(` - Current USDU balance: ${Number(currentBalanceBN) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` - Current USDU balance: ${Number(currentBalanceBN) / Number(DECIMAL_PRECISION)} USDU` + ); - const usduDeficit = totalDebtNeeded > currentBalanceBN ? totalDebtNeeded - currentBalanceBN : BigInt(0); + const usduDeficit = + totalDebtNeeded > currentBalanceBN ? totalDebtNeeded - currentBalanceBN : BigInt(0); if (usduDeficit > BigInt(0)) { console.log(` - USDU deficit: ${Number(usduDeficit) / Number(DECIMAL_PRECISION)} USDU`); @@ -242,14 +258,16 @@ async function main() { console.log(` - WMWBTC Price: $${Number(wmwbtcPrice) / Number(DECIMAL_PRECISION)}`); // Calculate collateral needed for 150% CR - const targetCR = BigInt(150) * DECIMAL_PRECISION / BigInt(100); // 150% + const targetCR = (BigInt(150) * DECIMAL_PRECISION) / BigInt(100); // 150% const collateralValueNeeded = (usduDeficit * targetCR) / DECIMAL_PRECISION; const wrapperCollateralAmount = (collateralValueNeeded * DECIMAL_PRECISION) / wmwbtcPrice; console.log(`\nšŸ—ļø Opening new WMWBTC trove to mint USDU:`); console.log(` - Target CR: 150%`); console.log(` - USDU to mint: ${Number(usduDeficit) / Number(DECIMAL_PRECISION)} USDU`); - console.log(` - Wrapper collateral needed: ${Number(wrapperCollateralAmount) / Number(DECIMAL_PRECISION)} WMWBTC`); + console.log( + ` - Wrapper collateral needed: ${Number(wrapperCollateralAmount) / Number(DECIMAL_PRECISION)} WMWBTC` + ); // Get underlying token address const branchAddresses = addresses[wmwbtcBranch]; @@ -263,7 +281,9 @@ async function main() { const DECIMAL_CONVERSION = BigInt(10) ** BigInt(10); // 10^10 const underlyingAmount = wrapperCollateralAmount / DECIMAL_CONVERSION; - console.log(` - Underlying amount needed: ${Number(underlyingAmount) / Number(BigInt(10) ** BigInt(8))} MockWBTC`); + console.log( + ` - Underlying amount needed: ${Number(underlyingAmount) / Number(BigInt(10) ** BigInt(8))} MockWBTC` + ); // Mint and approve underlying tokens, wrap them, then open trove console.log('\nšŸ’° Preparing WMWBTC collateral...'); @@ -274,8 +294,8 @@ async function main() { entrypoint: 'mint', calldata: CallData.compile({ recipient: account.address, - amount: uint256.bnToUint256(underlyingAmount) - }) + amount: uint256.bnToUint256(underlyingAmount), + }), }, // 2. Approve underlying tokens to wrapper contract (8 decimals) { @@ -283,16 +303,16 @@ async function main() { entrypoint: 'approve', calldata: CallData.compile({ spender: wmwbtcContracts.collateralContract.address, - amount: uint256.bnToUint256(underlyingAmount) - }) + amount: uint256.bnToUint256(underlyingAmount), + }), }, // 3. Wrap the underlying tokens to get wrapped tokens (18 decimals) { contractAddress: wmwbtcContracts.collateralContract.address, entrypoint: 'wrap', calldata: CallData.compile({ - amount: uint256.bnToUint256(underlyingAmount) - }) + amount: uint256.bnToUint256(underlyingAmount), + }), }, // 4. Approve wrapped tokens to borrower operations (18 decimals) { @@ -300,9 +320,9 @@ async function main() { entrypoint: 'approve', calldata: CallData.compile({ spender: wmwbtcContracts.branchAddresses.borrowerOperations, - amount: uint256.bnToUint256(wrapperCollateralAmount) - }) - } + amount: uint256.bnToUint256(wrapperCollateralAmount), + }), + }, ]; // Open new trove @@ -317,12 +337,14 @@ async function main() { usdu_amount: uint256.bnToUint256(usduDeficit), upper_hint: uint256.bnToUint256(0n), lower_hint: uint256.bnToUint256(0n), - annual_interest_rate: uint256.bnToUint256(BigInt(Math.floor(0.05 * Number(DECIMAL_PRECISION)))), // 5% interest rate + annual_interest_rate: uint256.bnToUint256( + BigInt(Math.floor(0.05 * Number(DECIMAL_PRECISION))) + ), // 5% interest rate max_upfront_fee: uint256.bnToUint256(BigInt(10000) * DECIMAL_PRECISION), // Very high tolerance to avoid failures add_manager: account.address, remove_manager: account.address, - receiver: account.address - }) + receiver: account.address, + }), }; const allCalls = [...mintCalls, openTroveCall]; @@ -334,7 +356,7 @@ async function main() { await provider.waitForTransaction(openTx.transaction_hash, { retryInterval: 2000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); console.log(' āœ… New trove opened successfully!\n'); @@ -358,7 +380,11 @@ async function main() { // Get the trove manager contract (it has UncapBaseComponent embedded) const troveManagerAddress = branchContracts.branchAddresses.troveManager; const troveManagerAbi = JSON.parse(fs.readFileSync('./abis/TroveManager.json', 'utf8')); - const troveManagerContract = new Contract(troveManagerAbi, troveManagerAddress, provider); + const troveManagerContract = new Contract({ + abi: troveManagerAbi, + address: troveManagerAddress, + providerOrAccount: provider, + }); // Calculate TCR manually try { @@ -407,7 +433,9 @@ async function main() { // Store the price for CR calculations branchPrices[branchName] = price; - console.log(` - Total Collateral: ${Number(entireColl) / Number(DECIMAL_PRECISION)} ${branchName}`); + console.log( + ` - Total Collateral: ${Number(entireColl) / Number(DECIMAL_PRECISION)} ${branchName}` + ); console.log(` - Total Debt: ${Number(entireDebt) / Number(DECIMAL_PRECISION)} USDU`); console.log(` - Current Price: $${Number(price) / Number(DECIMAL_PRECISION)}`); @@ -428,7 +456,9 @@ async function main() { if (tcrPercentage < CCR) { console.log(` āš ļø WARNING: TCR is below CCR! System is in critical state.`); - console.log(` The system needs ${(CCR - tcrPercentage).toFixed(2)}% improvement to reach CCR.`); + console.log( + ` The system needs ${(CCR - tcrPercentage).toFixed(2)}% improvement to reach CCR.` + ); } else { console.log(` āœ… TCR is healthy (above CCR)`); console.log(` Buffer above CCR: ${(tcrPercentage - CCR).toFixed(2)}%`); @@ -437,7 +467,6 @@ async function main() { // Count troves for this branch const branchTroveCount = troveDetails.filter(t => t.branchName === branchName).length; console.log(` - Number of troves to close in this branch: ${branchTroveCount}`); - } catch (error) { console.error(` āŒ Failed to get TCR for ${branchName}:`, error); } @@ -499,7 +528,10 @@ async function main() { // Check current allowance console.log(` Checking allowance for ${branchName} branch...`); - const allowanceResult = await usduContract['allowance'](account.address, borrowerOpsAddress); + const allowanceResult = await usduContract['allowance']( + account.address, + borrowerOpsAddress + ); // Handle both possible response formats let currentAllowance: bigint; @@ -513,11 +545,15 @@ async function main() { currentAllowance = BigInt(0); } - console.log(` - Current allowance: ${Number(currentAllowance) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` - Current allowance: ${Number(currentAllowance) / Number(DECIMAL_PRECISION)} USDU` + ); console.log(` - Required: ${Number(branchTotalDebt) / Number(DECIMAL_PRECISION)} USDU`); if (currentAllowance < branchTotalDebt) { - console.log(` - Need to approve: ${Number(branchTotalDebt - currentAllowance) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` - Need to approve: ${Number(branchTotalDebt - currentAllowance) / Number(DECIMAL_PRECISION)} USDU` + ); // Add approval call to batch approvalCalls.push({ @@ -525,8 +561,8 @@ async function main() { entrypoint: 'approve', calldata: CallData.compile({ spender: borrowerOpsAddress, - amount: uint256.bnToUint256(branchTotalDebt) - }) + amount: uint256.bnToUint256(branchTotalDebt), + }), }); } else { console.log(` āœ… Sufficient allowance already exists`); @@ -544,7 +580,7 @@ async function main() { await provider.waitForTransaction(approveTx.transaction_hash, { retryInterval: 2000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); console.log(` āœ… All approvals completed\n`); @@ -568,7 +604,9 @@ async function main() { // Calculate USDU needed for this batch with 10% buffer for interest accrual const batchUsduBase = batchTroves.reduce((sum, trove) => sum + trove.totalDebt, BigInt(0)); const batchUsduNeeded = (batchUsduBase * BigInt(110)) / BigInt(100); // 10% buffer - console.log(` - USDU needed for batch (with 10% buffer): ${Number(batchUsduNeeded) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` - USDU needed for batch (with 10% buffer): ${Number(batchUsduNeeded) / Number(DECIMAL_PRECISION)} USDU` + ); // Check current USDU balance before processing this batch const currentBatchBalance = await usduContract['balanceOf'](account.address); @@ -577,9 +615,10 @@ async function main() { currentBatchBalanceBN = currentBatchBalance; } else if (currentBatchBalance && typeof currentBatchBalance === 'object') { if ('balance' in currentBatchBalance) { - currentBatchBalanceBN = typeof currentBatchBalance.balance === 'bigint' - ? currentBatchBalance.balance - : BigInt(currentBatchBalance.balance?.toString() || '0'); + currentBatchBalanceBN = + typeof currentBatchBalance.balance === 'bigint' + ? currentBatchBalance.balance + : BigInt(currentBatchBalance.balance?.toString() || '0'); } else { const low = currentBatchBalance.low || currentBatchBalance['0'] || 0n; const high = currentBatchBalance.high || currentBatchBalance['1'] || 0n; @@ -589,13 +628,20 @@ async function main() { currentBatchBalanceBN = BigInt(0); } - console.log(` - Current USDU balance: ${Number(currentBatchBalanceBN) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` - Current USDU balance: ${Number(currentBatchBalanceBN) / Number(DECIMAL_PRECISION)} USDU` + ); // Check if we need to mint more USDU for this batch - const batchUsduDeficit = batchUsduNeeded > currentBatchBalanceBN ? batchUsduNeeded - currentBatchBalanceBN : BigInt(0); + const batchUsduDeficit = + batchUsduNeeded > currentBatchBalanceBN + ? batchUsduNeeded - currentBatchBalanceBN + : BigInt(0); if (batchUsduDeficit > BigInt(0)) { - console.log(` āš ļø USDU deficit for batch: ${Number(batchUsduDeficit) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` āš ļø USDU deficit for batch: ${Number(batchUsduDeficit) / Number(DECIMAL_PRECISION)} USDU` + ); console.log(` - Minting additional USDU...\n`); // Setup WMWBTC contracts for minting @@ -616,7 +662,7 @@ async function main() { } // Calculate collateral needed for 150% CR - const targetCR = BigInt(150) * DECIMAL_PRECISION / BigInt(100); + const targetCR = (BigInt(150) * DECIMAL_PRECISION) / BigInt(100); const collateralValueNeeded = (batchUsduDeficit * targetCR) / DECIMAL_PRECISION; const wrapperCollateralAmount = (collateralValueNeeded * DECIMAL_PRECISION) / wmwbtcPrice; @@ -632,7 +678,9 @@ async function main() { const DECIMAL_CONVERSION = BigInt(10) ** BigInt(10); const underlyingAmount = wrapperCollateralAmount / DECIMAL_CONVERSION; - console.log(` - Minting ${Number(underlyingAmount) / Number(BigInt(10) ** BigInt(8))} MockWBTC, wrapping, and opening trove...`); + console.log( + ` - Minting ${Number(underlyingAmount) / Number(BigInt(10) ** BigInt(8))} MockWBTC, wrapping, and opening trove...` + ); const mintCalls: Call[] = [ { @@ -640,32 +688,32 @@ async function main() { entrypoint: 'mint', calldata: CallData.compile({ recipient: account.address, - amount: uint256.bnToUint256(underlyingAmount) - }) + amount: uint256.bnToUint256(underlyingAmount), + }), }, { contractAddress: underlyingAddress, entrypoint: 'approve', calldata: CallData.compile({ spender: wmwbtcContracts.collateralContract.address, - amount: uint256.bnToUint256(underlyingAmount) - }) + amount: uint256.bnToUint256(underlyingAmount), + }), }, { contractAddress: wmwbtcContracts.collateralContract.address, entrypoint: 'wrap', calldata: CallData.compile({ - amount: uint256.bnToUint256(underlyingAmount) - }) + amount: uint256.bnToUint256(underlyingAmount), + }), }, { contractAddress: wmwbtcContracts.collateralContract.address, entrypoint: 'approve', calldata: CallData.compile({ spender: wmwbtcContracts.branchAddresses.borrowerOperations, - amount: uint256.bnToUint256(wrapperCollateralAmount) - }) - } + amount: uint256.bnToUint256(wrapperCollateralAmount), + }), + }, ]; const ownerIndex = Date.now() + batchIdx; @@ -679,12 +727,14 @@ async function main() { usdu_amount: uint256.bnToUint256(batchUsduDeficit), upper_hint: uint256.bnToUint256(0n), lower_hint: uint256.bnToUint256(0n), - annual_interest_rate: uint256.bnToUint256(BigInt(Math.floor(0.05 * Number(DECIMAL_PRECISION)))), + annual_interest_rate: uint256.bnToUint256( + BigInt(Math.floor(0.05 * Number(DECIMAL_PRECISION))) + ), max_upfront_fee: uint256.bnToUint256(BigInt(10000) * DECIMAL_PRECISION), add_manager: account.address, remove_manager: account.address, - receiver: account.address - }) + receiver: account.address, + }), }; const allMintCalls = [...mintCalls, openTroveCall]; @@ -693,24 +743,26 @@ async function main() { console.log(` - Mint tx hash: ${openTx.transaction_hash}`); await provider.waitForTransaction(openTx.transaction_hash, { retryInterval: 2000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); console.log(` āœ… Additional USDU minted\n`); // Update allowance for the newly minted USDU const wmwbtcBorrowerOps = wmwbtcContracts.branchAddresses.borrowerOperations; - const approveTx = await account.execute([{ - contractAddress: addresses['USDU'], - entrypoint: 'approve', - calldata: CallData.compile({ - spender: wmwbtcBorrowerOps, - amount: uint256.bnToUint256(batchUsduDeficit) - }) - }]); + const approveTx = await account.execute([ + { + contractAddress: addresses['USDU'], + entrypoint: 'approve', + calldata: CallData.compile({ + spender: wmwbtcBorrowerOps, + amount: uint256.bnToUint256(batchUsduDeficit), + }), + }, + ]); console.log(` - Approval tx hash: ${approveTx.transaction_hash}`); await provider.waitForTransaction(approveTx.transaction_hash, { retryInterval: 2000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); console.log(` āœ… Approval completed\n`); } @@ -722,30 +774,39 @@ async function main() { console.log(` - Trove ${trove.id}:`); console.log(` • Branch: ${trove.branchName}`); console.log(` • Trove ID: ${trove.parsedTroveId}`); - console.log(` • Total debt to repay: ${Number(trove.totalDebt) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` • Total debt to repay: ${Number(trove.totalDebt) / Number(DECIMAL_PRECISION)} USDU` + ); // Setup contracts for the specific branch - const branchContracts = await setupContracts(trove.branchName, addresses, provider, account); + const branchContracts = await setupContracts( + trove.branchName, + addresses, + provider, + account + ); // Add close trove call for this trove (no approval needed) batchCalls.push({ contractAddress: branchContracts.branchAddresses.borrowerOperations, entrypoint: 'close_trove', calldata: CallData.compile({ - trove_id: uint256.bnToUint256(BigInt(trove.parsedTroveId)) - }) + trove_id: uint256.bnToUint256(BigInt(trove.parsedTroveId)), + }), }); } try { - console.log(`\nšŸ“¤ Submitting batch ${batchNumber} transaction (${batchCalls.length} calls)...`); + console.log( + `\nšŸ“¤ Submitting batch ${batchNumber} transaction (${batchCalls.length} calls)...` + ); const closeTx = await account.execute(batchCalls); console.log(` - Transaction hash: ${closeTx.transaction_hash}`); console.log(' - Waiting for confirmation...'); await provider.waitForTransaction(closeTx.transaction_hash, { retryInterval: 2000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); console.log(` āœ… Batch ${batchNumber} closed successfully!\n`); @@ -763,7 +824,6 @@ async function main() { } console.log('✨ All troves closed successfully!'); - } catch (error) { console.error('āŒ Script failed:', error); process.exit(1); @@ -776,4 +836,4 @@ async function main() { main().catch(error => { console.error('Unhandled error:', error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 64f2b41..2f73b5f 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -20,13 +20,13 @@ logger.setLogLevel('DEBUG'); // Create readline interface for user input const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }); // Promisify readline question function question(prompt: string): Promise { - return new Promise((resolve) => { - rl.question(prompt, (answer) => { + return new Promise(resolve => { + rl.question(prompt, answer => { resolve(answer); }); }); @@ -45,24 +45,25 @@ const AVAILABLE_BRANCHES: { [key: string]: BranchConfig } = { name: 'UncapBTC', symbol: 'UBTC', decimals: 18, - description: 'Standard BTC collateral with 18 decimals' + description: 'Standard BTC collateral with 18 decimals', }, WMWBTC: { name: 'MockWBTC', symbol: 'MWBTC', decimals: 8, - description: 'Wrapped Mock WBTC (8 decimals wrapped to 18)' + description: 'Wrapped Mock WBTC (8 decimals wrapped to 18)', }, WWBTC: { name: 'Wrapped Bitcoin', symbol: 'WBTC', decimals: 8, description: 'Wrapped Bitcoin (8 decimals)', - } + }, }; function loadConfig(): DeploymentConfig { - const network: 'sepolia' | 'mainnet' = (process.env['NETWORK'] as 'sepolia' | 'mainnet') || 'sepolia'; + const network: 'sepolia' | 'mainnet' = + (process.env['NETWORK'] as 'sepolia' | 'mainnet') || 'sepolia'; const privateKey = process.env['PRIVATE_KEY']; const deployerAddress = process.env['DEPLOYER_ADDRESS']; @@ -92,13 +93,23 @@ function loadConfig(): DeploymentConfig { if (!wbtcAddress) missingVars.push('WBTC_ADDRESS'); if (missingVars.length > 0) { - throw new Error( - `Missing required environment variables: ${missingVars.join(', ')}` - ); + throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`); } // TypeScript now knows these are all defined after the check above - if (!privateKey || !deployerAddress || !rpcUrl || !salt || !sponsor || !ownerAddress || !pilAddress || !pragmaOracleAddress || !chainlinkBtcUsdAddress || !chainlinkWbtcUsdAddress || !wbtcAddress) { + if ( + !privateKey || + !deployerAddress || + !rpcUrl || + !salt || + !sponsor || + !ownerAddress || + !pilAddress || + !pragmaOracleAddress || + !chainlinkBtcUsdAddress || + !chainlinkWbtcUsdAddress || + !wbtcAddress + ) { // This should never happen after the check above, but satisfies TypeScript throw new Error('Unexpected missing environment variables'); } @@ -116,12 +127,16 @@ function loadConfig(): DeploymentConfig { chainlinkWbtcUsdAddress, pragmaOracleAddress, wbtcAddress, -}; + }; } function setupContext(config: DeploymentConfig): DeploymentContext { const provider = new RpcProvider({ nodeUrl: config.rpcUrl }); - const account = new Account(provider, config.deployerAddress, config.privateKey); + const account = new Account({ + provider, + address: config.deployerAddress, + signer: config.privateKey, + }); const forceDeploy = process.env['FORCE_DEPLOY'] === 'true'; const addresses = forceDeploy ? {} : loadDeploymentAddresses(); @@ -153,17 +168,17 @@ async function getUserBranchSelection(): Promise { Object.entries(AVAILABLE_BRANCHES).forEach(([key, config]) => { console.log(` ${key}: ${config.description}`); }); - + console.log('\nšŸ“ Which branches would you like to deploy?'); console.log(' Options:'); console.log(' - Press Enter for all branches'); console.log(' - Enter branch names separated by commas (e.g., WWBTC,UBTC,GBTC)'); console.log(' - Enter a single branch name (e.g., UBTC)'); - + const input = await question('\nYour selection: '); - + let selectedBranches: string[]; - + if (input.trim() === '') { // Default to all branches selectedBranches = Object.keys(AVAILABLE_BRANCHES); @@ -171,20 +186,25 @@ async function getUserBranchSelection(): Promise { } else { // Parse user input selectedBranches = input.split(',').map(s => s.trim().toUpperCase()); - + // Validate selections const invalidBranches = selectedBranches.filter(b => !AVAILABLE_BRANCHES[b]); if (invalidBranches.length > 0) { - throw new Error(`Invalid branch names: ${invalidBranches.join(', ')}. Valid options are: ${Object.keys(AVAILABLE_BRANCHES).join(', ')}`); + throw new Error( + `Invalid branch names: ${invalidBranches.join(', ')}. Valid options are: ${Object.keys(AVAILABLE_BRANCHES).join(', ')}` + ); } - + console.log(`\nāœ… Selected branches: ${selectedBranches.join(', ')}`); } - + return selectedBranches; } -async function deployContracts(context: DeploymentContext, selectedBranches: string[]): Promise { +async function deployContracts( + context: DeploymentContext, + selectedBranches: string[] +): Promise { const strkContractAddress = CONSTANTS.STRK; const contractCache: ClassCache = {}; @@ -271,7 +291,7 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); } - + // Ensure we have the full class hash string const fullClassHash = declareResponse.class_hash; log(`Contract ${contractName} class hash: ${fullClassHash}`); @@ -280,10 +300,7 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str } // Function to deploy a contract using cached class hash - async function deployContract( - contractName: string, - constructorArgs?: any - ): Promise { + async function deployContract(contractName: string, constructorArgs?: any): Promise { const cached = contractCache[contractName]; if (!cached) { throw new Error(`Contract ${contractName} not found in cache. Ensure it was declared first.`); @@ -341,20 +358,26 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str }); // Deploy all the contracts for this branch - const borrowerOperationsAddress = await deployContract('BorrowerOperations', { deployer: deployer}); - const troveManagerAddress = await deployContract('TroveManager', { deployer: deployer}); - const troveNftAddress = await deployContract('TroveNFT', { deployer: deployer}); - const stabilityPoolAddress = await deployContract('StabilityPool', { deployer: deployer}); - const sortedTrovesAddress = await deployContract('SortedTroves', { deployer: deployer}); - const activePoolAddress = await deployContract('ActivePool', { deployer: deployer}); - const defaultPoolAddress = await deployContract('DefaultPool', { deployer: deployer}); - const collSurplusPoolAddress = await deployContract('CollSurplusPool', { deployer: deployer}); - const gasPoolAddress = await deployContract('GasPool', { deployer: deployer}); + const borrowerOperationsAddress = await deployContract('BorrowerOperations', { + deployer: deployer, + }); + const troveManagerAddress = await deployContract('TroveManager', { deployer: deployer }); + const troveNftAddress = await deployContract('TroveNFT', { deployer: deployer }); + const stabilityPoolAddress = await deployContract('StabilityPool', { deployer: deployer }); + const sortedTrovesAddress = await deployContract('SortedTroves', { deployer: deployer }); + const activePoolAddress = await deployContract('ActivePool', { deployer: deployer }); + const defaultPoolAddress = await deployContract('DefaultPool', { deployer: deployer }); + const collSurplusPoolAddress = await deployContract('CollSurplusPool', { deployer: deployer }); + const gasPoolAddress = await deployContract('GasPool', { deployer: deployer }); const interestRouterAddress = context.config.pilAddress; - const liquidationManagerAddress = await deployContract('LiquidationManager', { deployer: deployer}); - const redemptionManagerAddress = await deployContract('RedemptionManager', { deployer: deployer}); - const batchManagerAddress = await deployContract('BatchManager', { deployer: deployer}); - + const liquidationManagerAddress = await deployContract('LiquidationManager', { + deployer: deployer, + }); + const redemptionManagerAddress = await deployContract('RedemptionManager', { + deployer: deployer, + }); + const batchManagerAddress = await deployContract('BatchManager', { deployer: deployer }); + let priceFeedAddress: string; if (collateralName === 'UBTC') { priceFeedAddress = await deployContract('PriceFeedMock', []); @@ -362,18 +385,31 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str priceFeedAddress = await deployContract('PriceFeedMock', []); } else if (collateralName === 'WMWBTC') { priceFeedAddress = await deployContract('PriceFeedMock', []); - } else if (collateralName === 'WWBTC') { // Mainnet deployment - priceFeedAddress = await deployContract('WBTCPriceFeed', [context.config.pragmaOracleAddress, context.config.chainlinkBtcUsdAddress, context.config.chainlinkWbtcUsdAddress, addressesRegistryAddress]); + } else if (collateralName === 'WWBTC') { + // Mainnet deployment + priceFeedAddress = await deployContract('WBTCPriceFeed', [ + context.config.pragmaOracleAddress, + context.config.chainlinkBtcUsdAddress, + context.config.chainlinkWbtcUsdAddress, + addressesRegistryAddress, + ]); } else { throw new Error(`Unsupported collateral: ${collateralName}`); } - const troveManagerEventsEmitterAddress = await deployContract('TroveManagerEventsEmitter', { deployer: deployer}); + const troveManagerEventsEmitterAddress = await deployContract('TroveManagerEventsEmitter', { + deployer: deployer, + }); let wrapperAddress: string | null = null; const underlyingAddress = collateral; if (collateralName == 'WWBTC') { - wrapperAddress = await deployContract('CollateralWrapper', ["WrappedWBTC", "WWBTC", context.config.ownerAddress, collateral]); + wrapperAddress = await deployContract('CollateralWrapper', [ + 'WrappedWBTC', + 'WWBTC', + context.config.ownerAddress, + collateral, + ]); collateral = wrapperAddress; } @@ -385,11 +421,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // AddressesRegistry initializer const addressesRegistryAbi = getCachedAbi('AddressesRegistry'); - const addressesRegistryContract = new Contract( - addressesRegistryAbi, - addressesRegistryAddress, - context.provider - ); + const addressesRegistryContract = new Contract({ + abi: addressesRegistryAbi, + address: addressesRegistryAddress, + providerOrAccount: context.provider, + }); const addressesRegistryCall = addressesRegistryContract.populate('initializer', [ activePoolAddress, defaultPoolAddress, @@ -422,7 +458,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // ActivePool initializer const activePoolAbi = getCachedAbi('ActivePool'); - const activePoolContract = new Contract(activePoolAbi, activePoolAddress, context.provider); + const activePoolContract = new Contract({ + abi: activePoolAbi, + address: activePoolAddress, + providerOrAccount: context.provider, + }); const activePoolCall = activePoolContract.populate('initializer', [addressesRegistryAddress]); initializerCalls.push({ contractAddress: activePoolAddress, @@ -432,7 +472,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // DefaultPool initializer const defaultPoolAbi = getCachedAbi('DefaultPool'); - const defaultPoolContract = new Contract(defaultPoolAbi, defaultPoolAddress, context.provider); + const defaultPoolContract = new Contract({ + abi: defaultPoolAbi, + address: defaultPoolAddress, + providerOrAccount: context.provider, + }); const defaultPoolCall = defaultPoolContract.populate('initializer', [addressesRegistryAddress]); initializerCalls.push({ contractAddress: defaultPoolAddress, @@ -442,11 +486,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // TroveManagerEventsEmitter initializer const troveManagerEventsEmitterAbi = getCachedAbi('TroveManagerEventsEmitter'); - const troveManagerEventsEmitterContract = new Contract( - troveManagerEventsEmitterAbi, - troveManagerEventsEmitterAddress, - context.provider - ); + const troveManagerEventsEmitterContract = new Contract({ + abi: troveManagerEventsEmitterAbi, + address: troveManagerEventsEmitterAddress, + providerOrAccount: context.provider, + }); const troveManagerEventsEmitterCall = troveManagerEventsEmitterContract.populate( 'initializer', [addressesRegistryAddress] @@ -459,11 +503,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // BorrowerOperations initializer const borrowerOperationsAbi = getCachedAbi('BorrowerOperations'); - const borrowerOperationsContract = new Contract( - borrowerOperationsAbi, - borrowerOperationsAddress, - context.provider - ); + const borrowerOperationsContract = new Contract({ + abi: borrowerOperationsAbi, + address: borrowerOperationsAddress, + providerOrAccount: context.provider, + }); const borrowerOperationsCall = borrowerOperationsContract.populate('initializer', [ addressesRegistryAddress, ]); @@ -475,11 +519,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // TroveManager initializer const troveManagerAbi = getCachedAbi('TroveManager'); - const troveManagerContract = new Contract( - troveManagerAbi, - troveManagerAddress, - context.provider - ); + const troveManagerContract = new Contract({ + abi: troveManagerAbi, + address: troveManagerAddress, + providerOrAccount: context.provider, + }); const troveManagerCall = troveManagerContract.populate('initializer', [ addressesRegistryAddress, ]); @@ -491,7 +535,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // TroveNFT initializer const troveNftAbi = getCachedAbi('TroveNFT'); - const troveNftContract = new Contract(troveNftAbi, troveNftAddress, context.provider); + const troveNftContract = new Contract({ + abi: troveNftAbi, + address: troveNftAddress, + providerOrAccount: context.provider, + }); const troveNftCall = troveNftContract.populate('initializer', [ addressesRegistryAddress, 'Uncap Position', @@ -506,7 +554,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // GasPool initializer const gasPoolAbi = getCachedAbi('GasPool'); - const gasPoolContract = new Contract(gasPoolAbi, gasPoolAddress, context.provider); + const gasPoolContract = new Contract({ + abi: gasPoolAbi, + address: gasPoolAddress, + providerOrAccount: context.provider, + }); const gasPoolCall = gasPoolContract.populate('initializer', [addressesRegistryAddress]); initializerCalls.push({ contractAddress: gasPoolAddress, @@ -516,11 +568,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // CollSurplusPool initializer const collSurplusPoolAbi = getCachedAbi('CollSurplusPool'); - const collSurplusPoolContract = new Contract( - collSurplusPoolAbi, - collSurplusPoolAddress, - context.provider - ); + const collSurplusPoolContract = new Contract({ + abi: collSurplusPoolAbi, + address: collSurplusPoolAddress, + providerOrAccount: context.provider, + }); const collSurplusPoolCall = collSurplusPoolContract.populate('initializer', [ addressesRegistryAddress, ]); @@ -532,11 +584,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // SortedTroves initializer const sortedTrovesAbi = getCachedAbi('SortedTroves'); - const sortedTrovesContract = new Contract( - sortedTrovesAbi, - sortedTrovesAddress, - context.provider - ); + const sortedTrovesContract = new Contract({ + abi: sortedTrovesAbi, + address: sortedTrovesAddress, + providerOrAccount: context.provider, + }); const sortedTrovesCall = sortedTrovesContract.populate('initializer', [ addressesRegistryAddress, ]); @@ -548,11 +600,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // StabilityPool initializer const stabilityPoolAbi = getCachedAbi('StabilityPool'); - const stabilityPoolContract = new Contract( - stabilityPoolAbi, - stabilityPoolAddress, - context.provider - ); + const stabilityPoolContract = new Contract({ + abi: stabilityPoolAbi, + address: stabilityPoolAddress, + providerOrAccount: context.provider, + }); const stabilityPoolCall = stabilityPoolContract.populate('initializer', [ addressesRegistryAddress, ]); @@ -564,11 +616,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // LiquidationManager initializer const liquidationManagerAbi = getCachedAbi('LiquidationManager'); - const liquidationManagerContract = new Contract( - liquidationManagerAbi, - liquidationManagerAddress, - context.provider - ); + const liquidationManagerContract = new Contract({ + abi: liquidationManagerAbi, + address: liquidationManagerAddress, + providerOrAccount: context.provider, + }); const liquidationManagerCall = liquidationManagerContract.populate('initializer', [ addressesRegistryAddress, ]); @@ -580,11 +632,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // RedemptionManager initializer const redemptionManagerAbi = getCachedAbi('RedemptionManager'); - const redemptionManagerContract = new Contract( - redemptionManagerAbi, - redemptionManagerAddress, - context.provider - ); + const redemptionManagerContract = new Contract({ + abi: redemptionManagerAbi, + address: redemptionManagerAddress, + providerOrAccount: context.provider, + }); const redemptionManagerCall = redemptionManagerContract.populate('initializer', [ addressesRegistryAddress, ]); @@ -596,11 +648,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // BatchManager initializer const batchManagerAbi = getCachedAbi('BatchManager'); - const batchManagerContract = new Contract( - batchManagerAbi, - batchManagerAddress, - context.provider - ); + const batchManagerContract = new Contract({ + abi: batchManagerAbi, + address: batchManagerAddress, + providerOrAccount: context.provider, + }); const batchManagerCall = batchManagerContract.populate('initializer', [ addressesRegistryAddress, ]); @@ -644,19 +696,19 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // Step 1: Declare all contracts upfront await declareAllContracts(); - + // Step 2: Deploy collaterals and core contracts log('Step 2: Deploying contracts with no initializer'); - + // Deploy collateral tokens for selected branches const collateralAddresses: { [key: string]: string } = {}; - + for (const branchKey of selectedBranches) { const branchConfig = AVAILABLE_BRANCHES[branchKey]; if (!branchConfig) { throw new Error(`Branch configuration not found for ${branchKey}`); } - + let collateralAddress = getContractAddress(branchKey); if (branchKey === 'WWBTC') { @@ -671,22 +723,22 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str } else { log(`Using existing ${branchKey} at: ${collateralAddress}`); } - + collateralAddresses[branchKey] = collateralAddress; } // 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'; + // await deployContract('USDU', { + // name: 'Uncap USD', + // symbol: 'USDU', + // deployer: context.config.deployerAddress, + // }); // } else { - // log(`Using existing USDU at: ${usduContractAddress}`); + // log(`Using existing USDU at: ${usduContractAddress}`); // } // Deploy CollateralRegistry @@ -695,20 +747,22 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str }); // Deploy HintHelpers - const hintHelpersAddress = await deployContract('HintHelpers', {collateral_registry: collateralRegistryAddress}); + const hintHelpersAddress = await deployContract('HintHelpers', { + collateral_registry: collateralRegistryAddress, + }); // Step 3: Deploy branch contracts using deployNewBranch log('Step 3: Deploying branch contracts using deployNewBranch'); - + const deployedBranches: { [key: string]: BranchAddresses } = {}; - + for (const branchKey of selectedBranches) { log(`\nšŸš€ Deploying ${branchKey} branch...`); const collateralAddr = collateralAddresses[branchKey]; if (!collateralAddr) { throw new Error(`Collateral address not found for ${branchKey}`); } - + const branchAddresses = await deployNewBranch( branchKey, collateralAddr, @@ -721,17 +775,17 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // Initialize CollateralRegistry and USDU with multicall log('Initializing CollateralRegistry and USDU with multicall...'); - + const finalInitCalls = []; // CollateralRegistry initializer with only selected branches const collateralRegistryAbi = getCachedAbi('CollateralRegistry'); - const collateralRegistryContract = new Contract( - collateralRegistryAbi, - collateralRegistryAddress, - context.provider - ); - + const collateralRegistryContract = new Contract({ + abi: collateralRegistryAbi, + address: collateralRegistryAddress, + providerOrAccount: context.provider, + }); + // Get address registries for selected branches const addressRegistries = selectedBranches.map(branchKey => { const branch = deployedBranches[branchKey]; @@ -740,7 +794,7 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str } return branch.addressesRegistry; }); - + const collateralRegistryCall = collateralRegistryContract.populate('initializer', [ usduContractAddress, addressRegistries, @@ -753,7 +807,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // USDU initializer const usduAbi = getCachedAbi('USDU'); - const usdu = new Contract(usduAbi, usduContractAddress, context.provider); + const usdu = new Contract({ + abi: usduAbi, + address: usduContractAddress, + providerOrAccount: context.provider, + }); const usduInitCall = usdu.populate('initializer', [collateralRegistryAddress]); finalInitCalls.push({ contractAddress: usduContractAddress, @@ -783,9 +841,10 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str } // Transfer ownership of CollateralRegistry to config.ownerAddress - const collateralRegistryTransferOwnershipCall = collateralRegistryContract.populate('transfer_ownership', [ - context.config.ownerAddress, - ]); + const collateralRegistryTransferOwnershipCall = collateralRegistryContract.populate( + 'transfer_ownership', + [context.config.ownerAddress] + ); finalInitCalls.push({ contractAddress: collateralRegistryAddress, entrypoint: 'transfer_ownership', @@ -796,10 +855,11 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str // Execute all final initializations in a single multicall log(`----\nExecuting final multicall with ${finalInitCalls.length} calls...-----\n`); const finalMultiCall = await context.account.execute(finalInitCalls); - await context.provider.waitForTransaction(finalMultiCall.transaction_hash, { retryInterval: 5000 }); + await context.provider.waitForTransaction(finalMultiCall.transaction_hash, { + retryInterval: 5000, + }); log('CollateralRegistry and USDU initialized successfully'); log(`Ownership of CollateralRegistry transferred to: ${context.config.ownerAddress}`); - } catch (error) { if (error instanceof Error && error.message.includes('AlreadyInitialized')) { log('USDU already initialized, skipping...'); @@ -817,7 +877,7 @@ async function deployContracts(context: DeploymentContext, selectedBranches: str gasToken: strkContractAddress, collateralRegistry: collateralRegistryAddress, }; - + // Add deployed branches to addresses for (const branchKey of selectedBranches) { addressesToSave[branchKey] = deployedBranches[branchKey]; @@ -838,7 +898,7 @@ async function main(): Promise { const config = loadConfig(); const context = setupContext(config); - + // Get user selection for which branches to deploy const selectedBranches = await getUserBranchSelection(); rl.close(); // Close readline interface after selection diff --git a/scripts/liquidation_setup.ts b/scripts/liquidation_setup.ts index 9343d5f..474f5da 100644 --- a/scripts/liquidation_setup.ts +++ b/scripts/liquidation_setup.ts @@ -14,7 +14,7 @@ import { checkAndApproveSponsor, question, closeReadline, - setupContracts + setupContracts, } from './utils'; interface TroveSetupParams { @@ -57,7 +57,9 @@ async function selectBranch(addresses: DeploymentAddresses): Promise { break; } - console.log(`āŒ Invalid branch. Please enter a number (1-${availableBranches.length}) or branch name (${availableBranches.join(', ')})`); + console.log( + `āŒ Invalid branch. Please enter a number (1-${availableBranches.length}) or branch name (${availableBranches.join(', ')})` + ); } return branch; @@ -137,8 +139,12 @@ async function open_troves_batch( // Build multicall for minting and approving collateral console.log(`\nšŸ’° Preparing collateral for batch...`); - console.log(` - Total collateral: ${Number(totalCollateralNeeded) / Number(DECIMAL_PRECISION)} ${branch}`); - console.log(` - Total USDU to mint: ${Number(totalUsduInBatch) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` - Total collateral: ${Number(totalCollateralNeeded) / Number(DECIMAL_PRECISION)} ${branch}` + ); + console.log( + ` - Total USDU to mint: ${Number(totalUsduInBatch) / Number(DECIMAL_PRECISION)} USDU` + ); // Check if this is a wrapped token that needs special handling const isWrappedToken = branch === 'WMWBTC' || branch === 'WWBTC'; @@ -161,8 +167,12 @@ async function open_troves_batch( const DECIMAL_CONVERSION = BigInt(10) ** BigInt(10); // 10^10 const underlyingAmount = totalCollateralNeeded / DECIMAL_CONVERSION; - console.log(` - Wrapper amount (18 decimals): ${Number(totalCollateralNeeded) / Number(DECIMAL_PRECISION)}`); - console.log(` - Underlying amount (8 decimals): ${Number(underlyingAmount) / Number(BigInt(10) ** BigInt(8))}`); + console.log( + ` - Wrapper amount (18 decimals): ${Number(totalCollateralNeeded) / Number(DECIMAL_PRECISION)}` + ); + console.log( + ` - Underlying amount (8 decimals): ${Number(underlyingAmount) / Number(BigInt(10) ** BigInt(8))}` + ); setupCalls = [ // 1. Approve underlying tokens to wrapper contract (8 decimals) @@ -171,16 +181,16 @@ async function open_troves_batch( entrypoint: 'approve', calldata: CallData.compile({ spender: contracts.collateralContract.address, - amount: uint256.bnToUint256(underlyingAmount) - }) + amount: uint256.bnToUint256(underlyingAmount), + }), }, // 2. Wrap the underlying tokens to get wrapped tokens (18 decimals) { contractAddress: contracts.collateralContract.address, entrypoint: 'wrap', calldata: CallData.compile({ - amount: uint256.bnToUint256(underlyingAmount) - }) + amount: uint256.bnToUint256(underlyingAmount), + }), }, // 3. Approve wrapped tokens to borrower operations (18 decimals) { @@ -188,9 +198,9 @@ async function open_troves_batch( entrypoint: 'approve', calldata: CallData.compile({ spender: contracts.branchAddresses.borrowerOperations, - amount: uint256.bnToUint256(totalCollateralNeeded) - }) - } + amount: uint256.bnToUint256(totalCollateralNeeded), + }), + }, ]; } else { // Regular token flow @@ -201,8 +211,8 @@ async function open_troves_batch( entrypoint: 'mint', calldata: CallData.compile({ recipient: account.address, - amount: uint256.bnToUint256(totalCollateralNeeded) - }) + amount: uint256.bnToUint256(totalCollateralNeeded), + }), }, // Approve borrower operations to spend all collateral { @@ -210,9 +220,9 @@ async function open_troves_batch( entrypoint: 'approve', calldata: CallData.compile({ spender: contracts.branchAddresses.borrowerOperations, - amount: uint256.bnToUint256(totalCollateralNeeded) - }) - } + amount: uint256.bnToUint256(totalCollateralNeeded), + }), + }, ]; } @@ -245,8 +255,8 @@ async function open_troves_batch( max_upfront_fee: uint256.bnToUint256(maxUpfrontFee), add_manager: '0x0', remove_manager: '0x0', - receiver: '0x0' - }) + receiver: '0x0', + }), }; }); @@ -275,7 +285,7 @@ async function open_troves_batch( const provider = contracts.borrowerOpsContract.providerOrAccount; const receipt = await provider.waitForTransaction(tx.transaction_hash, { retryInterval: 2000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); if (receipt.isSuccess()) { @@ -284,10 +294,14 @@ async function open_troves_batch( // Display liquidation metrics for each trove troveParamsList.forEach((params, index) => { const currentPrice = params.currentPrice; - const priceDropPercent = ((Number(currentPrice) / Number(DECIMAL_PRECISION) - params.liquidationPrice) / - (Number(currentPrice) / Number(DECIMAL_PRECISION))) * 100; + const priceDropPercent = + ((Number(currentPrice) / Number(DECIMAL_PRECISION) - params.liquidationPrice) / + (Number(currentPrice) / Number(DECIMAL_PRECISION))) * + 100; const globalTroveNumber = (batchNumber - 1) * batchSize + index; - console.log(` Trove #${globalTroveNumber}: Price must drop ${priceDropPercent.toFixed(1)}% to trigger liquidation`); + console.log( + ` Trove #${globalTroveNumber}: Price must drop ${priceDropPercent.toFixed(1)}% to trigger liquidation` + ); }); return tx.transaction_hash; @@ -299,8 +313,8 @@ async function open_troves_batch( lastError = error; // Check if it's a nonce error - const isNonceError = error.message?.includes('nonce') || - error.message?.includes('Invalid transaction nonce'); + const isNonceError = + error.message?.includes('nonce') || error.message?.includes('Invalid transaction nonce'); if (isNonceError && attempt < MAX_RETRIES) { console.log(` āš ļø Nonce error detected, will retry...`); @@ -353,22 +367,22 @@ async function main() { // Define test scenarios with positions interface ScenarioConfig { - totalUsduK: number; // Total USDU to mint (in thousands, e.g., 100 = 100k USDU) - positionCount: number; // Number of positions + totalUsduK: number; // Total USDU to mint (in thousands, e.g., 100 = 100k USDU) + positionCount: number; // Number of positions liquidationPrice: number; // Liquidation price in USD - interestRate: number; // Interest rate for all positions in this scenario + interestRate: number; // Interest rate for all positions in this scenario } const scenarioConfigs: ScenarioConfig[] = [ - { totalUsduK: 100, positionCount: 5, liquidationPrice: 20000, interestRate: 0.05 }, // 100k USDU, 5 positions at $20k - { totalUsduK: 100, positionCount: 5, liquidationPrice: 65000, interestRate: 0.05 }, // 100k USDU, 5 positions at $65k - { totalUsduK: 10, positionCount: 1, liquidationPrice: 70000, interestRate: 0.05 }, // 10k USDU, 1 position at $70k - { totalUsduK: 90, positionCount: 1, liquidationPrice: 75000, interestRate: 0.05 }, // 90k USDU, 1 position at $75k - { totalUsduK: 200, positionCount: 90, liquidationPrice: 80000, interestRate: 0.06 }, // 100k USDU, 80 positions at $80k - { totalUsduK: 100, positionCount: 25, liquidationPrice: 80000, interestRate: 0.06 }, // 100k USDU, 25 positions at $80k - { totalUsduK: 100, positionCount: 35, liquidationPrice: 80000, interestRate: 0.06 }, // 200k USDU, 45 positions at $80k - { totalUsduK: 80, positionCount: 30, liquidationPrice: 85000, interestRate: 0.07 }, // 80k USDU, 30 positions at $85k - { totalUsduK: 20, positionCount: 1, liquidationPrice: 90000, interestRate: 0.08 }, // 20k USDU, 1 position at $90k + { totalUsduK: 100, positionCount: 5, liquidationPrice: 20000, interestRate: 0.05 }, // 100k USDU, 5 positions at $20k + { totalUsduK: 100, positionCount: 5, liquidationPrice: 65000, interestRate: 0.05 }, // 100k USDU, 5 positions at $65k + { totalUsduK: 10, positionCount: 1, liquidationPrice: 70000, interestRate: 0.05 }, // 10k USDU, 1 position at $70k + { totalUsduK: 90, positionCount: 1, liquidationPrice: 75000, interestRate: 0.05 }, // 90k USDU, 1 position at $75k + { totalUsduK: 200, positionCount: 90, liquidationPrice: 80000, interestRate: 0.06 }, // 100k USDU, 80 positions at $80k + { totalUsduK: 100, positionCount: 25, liquidationPrice: 80000, interestRate: 0.06 }, // 100k USDU, 25 positions at $80k + { totalUsduK: 100, positionCount: 35, liquidationPrice: 80000, interestRate: 0.06 }, // 200k USDU, 45 positions at $80k + { totalUsduK: 80, positionCount: 30, liquidationPrice: 85000, interestRate: 0.07 }, // 80k USDU, 30 positions at $85k + { totalUsduK: 20, positionCount: 1, liquidationPrice: 90000, interestRate: 0.08 }, // 20k USDU, 1 position at $90k ]; // Generate individual positions from scenario configs @@ -382,28 +396,42 @@ async function main() { console.log('\nāš ļø Checking scenario viability...'); for (const config of scenarioConfigs) { - const debtPerPosition = BigInt(config.totalUsduK * 1000) * DECIMAL_PRECISION / BigInt(config.positionCount); + const debtPerPosition = + (BigInt(config.totalUsduK * 1000) * DECIMAL_PRECISION) / BigInt(config.positionCount); // Calculate collateral needed based on liquidation price - let collateralPerPosition = calculateCollateralFromDebt(debtPerPosition, config.liquidationPrice); + let collateralPerPosition = calculateCollateralFromDebt( + debtPerPosition, + config.liquidationPrice + ); // Check if ICR would be below MCR at current price - const icrAtCurrentPrice = (collateralPerPosition * currentPrice * BigInt(100)) / debtPerPosition / DECIMAL_PRECISION; + const icrAtCurrentPrice = + (collateralPerPosition * currentPrice * BigInt(100)) / debtPerPosition / DECIMAL_PRECISION; if (icrAtCurrentPrice < BigInt(110)) { // Need more collateral to meet MCR at current price const originalCollateral = collateralPerPosition; // Add a 1% buffer above MCR to account for rounding errors - const MCR_WITH_BUFFER = BigInt(111) * DECIMAL_PRECISION / BigInt(100); // 111% instead of 110% + const MCR_WITH_BUFFER = (BigInt(111) * DECIMAL_PRECISION) / BigInt(100); // 111% instead of 110% // collateral = (debt * MCR_WITH_BUFFER) / current_price collateralPerPosition = (debtPerPosition * MCR_WITH_BUFFER) / currentPrice; - const newIcr = (collateralPerPosition * currentPrice * BigInt(100)) / debtPerPosition / DECIMAL_PRECISION; + const newIcr = + (collateralPerPosition * currentPrice * BigInt(100)) / + debtPerPosition / + DECIMAL_PRECISION; - console.log(` āš ļø Liquidation at $${config.liquidationPrice.toLocaleString()}: Adjusting collateral to meet MCR`); + console.log( + ` āš ļø Liquidation at $${config.liquidationPrice.toLocaleString()}: Adjusting collateral to meet MCR` + ); console.log(` - Current BTC price: $${currentPriceUSD.toFixed(2)}`); - console.log(` - Original collateral: ${Number(originalCollateral) / Number(DECIMAL_PRECISION)}`); - console.log(` - Adjusted collateral: ${Number(collateralPerPosition) / Number(DECIMAL_PRECISION)}`); + console.log( + ` - Original collateral: ${Number(originalCollateral) / Number(DECIMAL_PRECISION)}` + ); + console.log( + ` - Adjusted collateral: ${Number(collateralPerPosition) / Number(DECIMAL_PRECISION)}` + ); console.log(` - Original ICR: ${Number(icrAtCurrentPrice)}%`); console.log(` - New ICR: ${Number(newIcr)}%`); } @@ -413,7 +441,7 @@ async function main() { collateralAmount: collateralPerPosition, liquidationPrice: config.liquidationPrice, debtSize: debtPerPosition, - interestRate: config.interestRate + interestRate: config.interestRate, }); } } @@ -426,18 +454,23 @@ async function main() { console.log('------------------------------'); for (const config of scenarioConfigs) { - const debtPerPosition = config.totalUsduK * 1000 / config.positionCount; + const debtPerPosition = (config.totalUsduK * 1000) / config.positionCount; const totalCollateral = scenarios .filter(s => s.liquidationPrice === config.liquidationPrice) .slice(0, config.positionCount) .reduce((sum, s) => sum + s.collateralAmount, BigInt(0)); - const priceDropPercent = ((currentPriceUSD - config.liquidationPrice) / currentPriceUSD) * 100; + const priceDropPercent = + ((currentPriceUSD - config.liquidationPrice) / currentPriceUSD) * 100; - console.log(`\n $${config.liquidationPrice.toLocaleString()} liquidation (${priceDropPercent > 0 ? priceDropPercent.toFixed(1) + '% drop' : 'above current price'}):`); + console.log( + `\n $${config.liquidationPrice.toLocaleString()} liquidation (${priceDropPercent > 0 ? priceDropPercent.toFixed(1) + '% drop' : 'above current price'}):` + ); console.log(` - Positions: ${config.positionCount}`); console.log(` - Total USDU: ${config.totalUsduK}k ($${debtPerPosition.toFixed(0)} each)`); - console.log(` - Total collateral: ${(Number(totalCollateral) / Number(DECIMAL_PRECISION)).toFixed(4)} ${branch}`); + console.log( + ` - Total collateral: ${(Number(totalCollateral) / Number(DECIMAL_PRECISION)).toFixed(4)} ${branch}` + ); console.log(` - Interest rate: ${(config.interestRate * 100).toFixed(1)}%`); } console.log(); @@ -447,9 +480,13 @@ async function main() { if (invalidScenarios.length > 0) { console.log('āš ļø Warning: Some scenarios result in debt below minimum requirement:'); invalidScenarios.forEach((s, i) => { - console.log(` - Liquidation at $${s.liquidationPrice}: ${Number(s.debtSize) / Number(DECIMAL_PRECISION)} USDU < ${Number(MIN_DEBT) / Number(DECIMAL_PRECISION)} USDU (minimum)`); + console.log( + ` - Liquidation at $${s.liquidationPrice}: ${Number(s.debtSize) / Number(DECIMAL_PRECISION)} USDU < ${Number(MIN_DEBT) / Number(DECIMAL_PRECISION)} USDU (minimum)` + ); }); - console.log('\n These positions may fail to open. Consider adjusting liquidation prices or collateral amounts.\n'); + console.log( + '\n These positions may fail to open. Consider adjusting liquidation prices or collateral amounts.\n' + ); } // Check and approve sponsor @@ -467,14 +504,21 @@ async function main() { console.log('\nšŸ’µ Checking USDU supply...'); const usduAddress = addresses['USDU']; const usduAbi = JSON.parse(fs.readFileSync('./abis/USDU.json', 'utf8')); - const usduContract = new Contract(usduAbi, usduAddress, provider); + const usduContract = new Contract({ + abi: usduAbi, + address: usduAddress, + providerOrAccount: provider, + }); const initialSupplyResult = await usduContract['total_supply'](); // Handle both possible response formats - const initialSupply = typeof initialSupplyResult === 'bigint' - ? initialSupplyResult - : uint256.uint256ToBN(initialSupplyResult); - console.log(` - Initial USDU supply: ${Number(initialSupply) / Number(DECIMAL_PRECISION)} USDU`); + const initialSupply = + typeof initialSupplyResult === 'bigint' + ? initialSupplyResult + : uint256.uint256ToBN(initialSupplyResult); + console.log( + ` - Initial USDU supply: ${Number(initialSupply) / Number(DECIMAL_PRECISION)} USDU` + ); // Generate starting owner index based on current timestamp to ensure uniqueness const startingOwnerIndex = BigInt(Math.floor(Date.now() / 1000)); // Unix timestamp in seconds @@ -491,7 +535,7 @@ async function main() { currentPrice: Number(currentPrice), collateralAmount: scenario.collateralAmount, ownerIndex: startingOwnerIndex + BigInt(i), - interestRate: scenario.interestRate + interestRate: scenario.interestRate, })); // Process in batches of up to 20 troves @@ -521,17 +565,26 @@ async function main() { console.log(` Progress: ${successCount}/${allTroveParams.length} troves created`); } catch (error: any) { console.error(`\nāŒ CRITICAL ERROR in batch ${batchNumber}:`); - console.error(` Batch contains troves #${(batchNumber-1)*BATCH_SIZE} to #${Math.min(batchNumber*BATCH_SIZE-1, allTroveParams.length-1)}`); + console.error( + ` Batch contains troves #${(batchNumber - 1) * BATCH_SIZE} to #${Math.min(batchNumber * BATCH_SIZE - 1, allTroveParams.length - 1)}` + ); // Show details of failed batch console.error('\n Failed batch details:'); batchParams.forEach((params, idx) => { - const globalIdx = (batchNumber-1)*BATCH_SIZE + idx; - const icr = (params.collateralAmount * currentPrice * BigInt(100)) / params.debtSize / DECIMAL_PRECISION; + const globalIdx = (batchNumber - 1) * BATCH_SIZE + idx; + const icr = + (params.collateralAmount * currentPrice * BigInt(100)) / + params.debtSize / + DECIMAL_PRECISION; console.error(` Trove #${globalIdx}:`); console.error(` - Liquidation price: $${params.liquidationPrice}`); - console.error(` - Debt: ${Number(params.debtSize) / Number(DECIMAL_PRECISION)} USDU`); - console.error(` - Collateral: ${Number(params.collateralAmount) / Number(DECIMAL_PRECISION)} ${branch}`); + console.error( + ` - Debt: ${Number(params.debtSize) / Number(DECIMAL_PRECISION)} USDU` + ); + console.error( + ` - Collateral: ${Number(params.collateralAmount) / Number(DECIMAL_PRECISION)} ${branch}` + ); console.error(` - ICR at current price: ${Number(icr)}%`); }); @@ -556,13 +609,16 @@ async function main() { console.log('\nšŸ’µ Checking final USDU supply...'); const finalSupplyResult = await usduContract['total_supply'](); // Handle both possible response formats - const finalSupply = typeof finalSupplyResult === 'bigint' - ? finalSupplyResult - : uint256.uint256ToBN(finalSupplyResult); + const finalSupply = + typeof finalSupplyResult === 'bigint' + ? finalSupplyResult + : uint256.uint256ToBN(finalSupplyResult); console.log(` - Final USDU supply: ${Number(finalSupply) / Number(DECIMAL_PRECISION)} USDU`); const usduMinted = finalSupply - initialSupply; - console.log(` - USDU minted in this session: ${Number(usduMinted) / Number(DECIMAL_PRECISION)} USDU`); + console.log( + ` - USDU minted in this session: ${Number(usduMinted) / Number(DECIMAL_PRECISION)} USDU` + ); // Calculate expected vs actual USDU minted const expectedUsdu = scenarioConfigs.reduce((sum, c) => sum + c.totalUsduK * 1000, 0); @@ -572,7 +628,9 @@ async function main() { console.log(`\nšŸ“ˆ USDU Minting Summary:`); console.log(` - Expected USDU (debt only): ${expectedUsdu.toLocaleString()} USDU`); console.log(` - Actual USDU minted: ${actualUsduMinted.toLocaleString()} USDU`); - console.log(` - Difference (upfront fees): ${difference.toFixed(2)} USDU (${(difference / expectedUsdu * 100).toFixed(2)}%)`) + console.log( + ` - Difference (upfront fees): ${difference.toFixed(2)} USDU (${((difference / expectedUsdu) * 100).toFixed(2)}%)` + ); console.log('\n========================================'); console.log('✨ Liquidation setup completed!'); @@ -581,15 +639,22 @@ async function main() { console.log(` - Branch: ${branch}`); console.log(` - Current ${branch} price: $${currentPriceUSD.toFixed(2)}`); console.log(` - Total positions created: ${successCount}`); - console.log(` - Total USDU minted: ${scenarioConfigs.reduce((sum, c) => sum + c.totalUsduK, 0)}k`); - console.log(` - Owner indices: ${startingOwnerIndex} to ${startingOwnerIndex + BigInt(successCount - 1)}`); + console.log( + ` - Total USDU minted: ${scenarioConfigs.reduce((sum, c) => sum + c.totalUsduK, 0)}k` + ); + console.log( + ` - Owner indices: ${startingOwnerIndex} to ${startingOwnerIndex + BigInt(successCount - 1)}` + ); // Show breakdown by liquidation price console.log('\nšŸ“‰ Liquidation price breakdown:'); const priceGroups = new Map(); for (const config of scenarioConfigs) { if (priceGroups.has(config.liquidationPrice)) { - priceGroups.set(config.liquidationPrice, priceGroups.get(config.liquidationPrice)! + config.positionCount); + priceGroups.set( + config.liquidationPrice, + priceGroups.get(config.liquidationPrice)! + config.positionCount + ); } else { priceGroups.set(config.liquidationPrice, config.positionCount); } @@ -605,7 +670,6 @@ async function main() { console.log(' 1. Monitor collateral price'); console.log(' 2. When price drops to liquidation levels, run liquidation bot'); console.log(' 3. Verify liquidations are processed correctly'); - } catch (error) { console.error('\nāŒ Fatal error:', error); process.exit(1); @@ -618,4 +682,4 @@ async function main() { main().catch(error => { console.error('Fatal error:', error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/scripts/open_positions.ts b/scripts/open_positions.ts index 26a79e1..ccd4eab 100644 --- a/scripts/open_positions.ts +++ b/scripts/open_positions.ts @@ -18,8 +18,8 @@ const MEAN_CR = 2.0; // 200% const CR_STD_DEV = 0.35; // Standard deviation for CR const MIN_INTEREST_RATE = 0.005; // 0.5% -const MAX_INTEREST_RATE = 0.20; // 20% -const MEAN_INTEREST_RATE = 0.10; // 10% mean (centered between 0.5% and 20%) +const MAX_INTEREST_RATE = 0.2; // 20% +const MEAN_INTEREST_RATE = 0.1; // 10% mean (centered between 0.5% and 20%) const INTEREST_RATE_STD_DEV = 0.05; // Standard deviation // Collateral amounts in USD @@ -45,13 +45,13 @@ interface BranchAddresses { // Create readline interface for CLI prompts const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }); // Promisify readline question function question(prompt: string): Promise { - return new Promise((resolve) => { - rl.question(prompt, (answer) => { + return new Promise(resolve => { + rl.question(prompt, answer => { resolve(answer); }); }); @@ -59,7 +59,8 @@ function question(prompt: string): Promise { // Box-Muller transform for generating Gaussian distributed random numbers function generateGaussian(mean: number, stdDev: number): number { - let u = 0, v = 0; + let u = 0, + v = 0; while (u === 0) u = Math.random(); // Converting [0,1) to (0,1) while (v === 0) v = Math.random(); const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); @@ -72,12 +73,12 @@ function generateCollateralAmountUSD(): number { // Use log-normal distribution const logMedian = Math.log(MEDIAN_COLLATERAL_USD); const logStdDev = 0.8; // Adjust for desired spread - + let amount = Math.exp(generateGaussian(logMedian, logStdDev)); - + // Clamp to min/max bounds amount = Math.max(MIN_COLLATERAL_USD, Math.min(MAX_COLLATERAL_USD, amount)); - + return amount; } @@ -98,7 +99,7 @@ function generateInterestRate(): number { } async function loadDeploymentAddresses(): Promise { - const addressesPath = './deployments/deployment_addresses.json'; + const addressesPath = './deployments/sepolia_addresses.json'; if (!fs.existsSync(addressesPath)) { throw new Error('Deployment addresses file not found'); } @@ -111,11 +112,13 @@ async function setupAccount(): Promise<{ account: Account; provider: RpcProvider const rpcUrl = process.env['RPC_URL']; if (!privateKey || !accountAddress || !rpcUrl) { - throw new Error('Missing required environment variables: PRIVATE_KEY, DEPLOYER_ADDRESS, RPC_URL'); + throw new Error( + 'Missing required environment variables: PRIVATE_KEY, DEPLOYER_ADDRESS, RPC_URL' + ); } const provider = new RpcProvider({ nodeUrl: rpcUrl }); - const account = new Account(provider, accountAddress, privateKey); + const account = new Account({ provider, address: accountAddress, signer: privateKey }); return { account, provider }; } @@ -133,22 +136,22 @@ async function getUserInputs(addresses: DeploymentAddresses): Promise<{ isWrapper: boolean; }> { const availableBranches = getAvailableBranches(addresses); - + console.log('\n🌿 Available branches:'); availableBranches.forEach((branch, index) => { console.log(` ${index + 1}. ${branch}`); }); - + // Get branch selection let branch: string; while (true) { const branchInput = await question(`\nSelect branch (default: GBTC): `); - + if (branchInput === '') { branch = 'GBTC'; break; } - + // Check if user entered a number const branchNumber = parseInt(branchInput); if (!isNaN(branchNumber) && branchNumber > 0 && branchNumber <= availableBranches.length) { @@ -158,61 +161,63 @@ async function getUserInputs(addresses: DeploymentAddresses): Promise<{ break; } } - + // Check if user entered a branch name if (availableBranches.includes(branchInput)) { branch = branchInput; break; } - - console.log(`āŒ Invalid branch. Please enter a number (1-${availableBranches.length}) or branch name (${availableBranches.join(', ')})`); + + console.log( + `āŒ Invalid branch. Please enter a number (1-${availableBranches.length}) or branch name (${availableBranches.join(', ')})` + ); } - + // Get number of positions let totalTroves: number; while (true) { const trovesInput = await question(`\nNumber of positions to open (default: 100): `); - + if (trovesInput === '') { totalTroves = 100; break; } - + const num = parseInt(trovesInput); if (!isNaN(num) && num > 0 && num <= 10000) { totalTroves = num; break; } - + console.log('āŒ Please enter a valid number between 1 and 10000'); } - + // Get batch size let batchSize: number; while (true) { const batchInput = await question(`\nBatch size (default: 20, max: 50): `); - + if (batchInput === '') { batchSize = 20; break; } - + const num = parseInt(batchInput); if (!isNaN(num) && num > 0 && num <= 50) { batchSize = num; break; } - + console.log('āŒ Please enter a valid batch size between 1 and 50'); } - + // Ask if it's a wrapper (for wrapped collateral like WMWBTC) let isWrapper = false; const wrapperInput = await question(`\nIs this a wrapped collateral? (y/N): `); if (wrapperInput.toLowerCase() === 'y' || wrapperInput.toLowerCase() === 'yes') { isWrapper = true; } - + return { branch, totalTroves, batchSize, isWrapper }; } @@ -222,19 +227,19 @@ async function main() { // Setup const { account, provider } = await setupAccount(); const addresses = await loadDeploymentAddresses(); - + // Get user inputs const { branch, totalTroves, batchSize, isWrapper } = await getUserInputs(addresses); - + // Close readline interface rl.close(); - + console.log('\nšŸ“Š Configuration:'); console.log(` - Branch: ${branch}`); console.log(` - Total troves: ${totalTroves}`); console.log(` - Batch size: ${batchSize}`); console.log(` - Is Wrapper: ${isWrapper}`); - + // Get branch addresses const branchAddresses: BranchAddresses = addresses[branch]; if (!branchAddresses) { @@ -250,19 +255,23 @@ async function main() { let actualCollateralAddress = branchAddresses.collateral; let underlyingDecimals = 18; // Default decimals let collateralAbi; - + if (isWrapper) { console.log('\nšŸŽ Detecting wrapped collateral...'); - + // Load wrapper ABI to get underlying token const wrapperAbi = JSON.parse(fs.readFileSync('./abis/CollateralWrapper.json', 'utf8')); - const wrapperContract = new Contract(wrapperAbi, branchAddresses.collateral, provider); - + const wrapperContract = new Contract({ + abi: wrapperAbi, + address: branchAddresses.collateral, + providerOrAccount: provider, + }); + // Get underlying token address const underlyingToken = await wrapperContract['get_underlying_token'](); actualCollateralAddress = '0x' + BigInt(underlyingToken).toString(16); console.log(` - Underlying token: ${actualCollateralAddress}`); - + // Get underlying token decimals // Try to load specific ABI for underlying token, fallback to ERC20 let underlyingAbiPath = './abis/MWBTC.json'; @@ -270,12 +279,16 @@ async function main() { underlyingAbiPath = './abis/UBTC.json'; // Use UBTC as ERC20 template } const underlyingAbi = JSON.parse(fs.readFileSync(underlyingAbiPath, 'utf8')); - const underlyingContract = new Contract(underlyingAbi, actualCollateralAddress, provider); - + const underlyingContract = new Contract({ + abi: underlyingAbi, + address: actualCollateralAddress, + providerOrAccount: provider, + }); + const decimalsResult = await underlyingContract['decimals'](); underlyingDecimals = Number(decimalsResult); console.log(` - Underlying decimals: ${underlyingDecimals}`); - + collateralAbi = underlyingAbi; } else { // Try to load branch-specific ABI first, fallback to UBTC @@ -286,9 +299,12 @@ async function main() { } collateralAbi = JSON.parse(fs.readFileSync(collateralAbiPath, 'utf8')); } - + console.log('\nšŸ“ Contract addresses:'); - console.log(` - ${branch} collateral${isWrapper ? ' (wrapper)' : ''}:`, branchAddresses.collateral); + console.log( + ` - ${branch} collateral${isWrapper ? ' (wrapper)' : ''}:`, + branchAddresses.collateral + ); if (isWrapper) { console.log(` - Underlying collateral:`, actualCollateralAddress); } @@ -302,27 +318,47 @@ async function main() { const priceFeedAbi = JSON.parse(fs.readFileSync('./abis/PriceFeed.json', 'utf8')); const addressRegistryAbi = JSON.parse(fs.readFileSync('./abis/AddressesRegistry.json', 'utf8')); const strkAbi = JSON.parse(fs.readFileSync('./abis/MSTRK.json', 'utf8')); - const wrapperAbi = isWrapper ? JSON.parse(fs.readFileSync('./abis/CollateralWrapper.json', 'utf8')) : null; - - // Create contract instances - const collateralContract = new Contract(collateralAbi, actualCollateralAddress, provider); - const borrowerOpsContract = new Contract(borrowerOpsAbi, branchAddresses.borrowerOperations, provider); - const priceFeedContract = new Contract(priceFeedAbi, branchAddresses.priceFeed, provider); - const addressRegistryContract = new Contract(addressRegistryAbi, branchAddresses.addressesRegistry, provider); - const strkContract = new Contract(strkAbi, gasTokenAddress, provider); - const wrapperContract = isWrapper ? new Contract(wrapperAbi!, branchAddresses.collateral, provider) : null; - - // Connect contracts to account - collateralContract.connect(account); - borrowerOpsContract.connect(account); - strkContract.connect(account); - if (wrapperContract) { - wrapperContract.connect(account); - } + const wrapperAbi = isWrapper + ? JSON.parse(fs.readFileSync('./abis/CollateralWrapper.json', 'utf8')) + : null; + + // Create contract instances - use account for read-write, provider for read-only + const collateralContract = new Contract({ + abi: collateralAbi, + address: actualCollateralAddress, + providerOrAccount: account, + }); + const borrowerOpsContract = new Contract({ + abi: borrowerOpsAbi, + address: branchAddresses.borrowerOperations, + providerOrAccount: account, + }); + const priceFeedContract = new Contract({ + abi: priceFeedAbi, + address: branchAddresses.priceFeed, + providerOrAccount: provider, + }); + const addressRegistryContract = new Contract({ + abi: addressRegistryAbi, + address: branchAddresses.addressesRegistry, + providerOrAccount: provider, + }); + const strkContract = new Contract({ + abi: strkAbi, + address: gasTokenAddress, + providerOrAccount: account, + }); + const wrapperContract = isWrapper + ? new Contract({ + abi: wrapperAbi!, + address: branchAddresses.collateral, + providerOrAccount: account, + }) + : null; console.log(`\nšŸ“ˆ Fetching current ${branch} price...`); const priceResult = await priceFeedContract['fetch_price'](); - const collateralPrice = uint256.uint256ToBN({low: priceResult['0'], high: priceResult['1']}); + const collateralPrice = uint256.uint256ToBN({ low: priceResult['0'], high: priceResult['1'] }); console.log(` ${branch} Price: $${Number(collateralPrice) / Number(DECIMAL_PRECISION)}`); console.log('\nšŸ” Checking sponsor...'); @@ -333,52 +369,69 @@ async function main() { const totalStrkNeeded = STRK_GAS_COMPENSATION * BigInt(totalTroves); const sponsorBalance = await strkContract['balanceOf'](sponsor); console.log(` Sponsor STRK balance: ${Number(sponsorBalance) / Number(DECIMAL_PRECISION)} STRK`); - console.log(` Required STRK for gas compensation: ${Number(totalStrkNeeded) / Number(DECIMAL_PRECISION)} STRK`); + console.log( + ` Required STRK for gas compensation: ${Number(totalStrkNeeded) / Number(DECIMAL_PRECISION)} STRK` + ); if (BigInt(sponsorBalance) < totalStrkNeeded) { - throw new Error(`Sponsor doesn't have enough STRK. Has: ${sponsorBalance}, needs: ${totalStrkNeeded}`); + throw new Error( + `Sponsor doesn't have enough STRK. Has: ${sponsorBalance}, needs: ${totalStrkNeeded}` + ); } // Check sponsor's approval to borrower operations - const sponsorAllowance = await strkContract['allowance'](sponsor, branchAddresses.borrowerOperations); - console.log(` Sponsor STRK allowance to BorrowerOps: ${Number(sponsorAllowance) / Number(DECIMAL_PRECISION)} STRK`); + const sponsorAllowance = await strkContract['allowance']( + sponsor, + branchAddresses.borrowerOperations + ); + console.log( + ` Sponsor STRK allowance to BorrowerOps: ${Number(sponsorAllowance) / Number(DECIMAL_PRECISION)} STRK` + ); if (BigInt(sponsorAllowance) < totalStrkNeeded) { - console.log(` āš ļø Sponsor hasn't approved enough STRK. Approved: ${sponsorAllowance}, needs: ${totalStrkNeeded}`); - + console.log( + ` āš ļø Sponsor hasn't approved enough STRK. Approved: ${sponsorAllowance}, needs: ${totalStrkNeeded}` + ); + // Check if current account is the sponsor (normalize addresses for comparison) const sponsorAddress = '0x' + BigInt(sponsor).toString(16).padStart(64, '0'); const accountAddress = '0x' + BigInt(account.address).toString(16).padStart(64, '0'); if (sponsorAddress.toLowerCase() === accountAddress.toLowerCase()) { console.log(` šŸ“ Current account is sponsor, approving STRK...`); - + try { - const approveTx = await account.execute([{ - contractAddress: gasTokenAddress, - entrypoint: 'approve', - calldata: CallData.compile({ - spender: branchAddresses.borrowerOperations, - amount: uint256.bnToUint256(totalStrkNeeded) - }) - }]); - + const approveTx = await account.execute([ + { + contractAddress: gasTokenAddress, + entrypoint: 'approve', + calldata: CallData.compile({ + spender: branchAddresses.borrowerOperations, + amount: uint256.bnToUint256(totalStrkNeeded), + }), + }, + ]); + console.log(` Transaction hash: ${approveTx.transaction_hash}`); console.log(' Waiting for confirmation...'); - + await provider.waitForTransaction(approveTx.transaction_hash, { retryInterval: 5000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); - + console.log(` āœ… STRK approved successfully!`); } catch (error) { console.error(` āŒ Failed to approve STRK:`, error); throw new Error('Failed to approve STRK for sponsor'); } } else { - console.log(` āŒ Current account (${account.address}) is not the sponsor (${sponsorAddress})`); + console.log( + ` āŒ Current account (${account.address}) is not the sponsor (${sponsorAddress})` + ); console.log(` Please switch to the sponsor account or have the sponsor approve STRK tokens`); - throw new Error(`Sponsor hasn't approved enough STRK. Approved: ${sponsorAllowance}, needs: ${totalStrkNeeded}`); + throw new Error( + `Sponsor hasn't approved enough STRK. Approved: ${sponsorAllowance}, needs: ${totalStrkNeeded}` + ); } } @@ -389,7 +442,7 @@ async function main() { console.log(`šŸŽ² Starting owner_index: ${startingOwnerIndex}`); console.log('\nšŸ—ļø Preparing troves...'); - + // Generate all trove parameters first interface TroveParams { ownerIndex: bigint; @@ -413,31 +466,37 @@ async function main() { // Calculate collateral amount from USD value // Adjust for underlying token decimals if using wrapper const decimalAdjustment = isWrapper ? BigInt(10 ** underlyingDecimals) : DECIMAL_PRECISION; - const collateralAmount = BigInt(Math.floor(collateralUSD * Number(DECIMAL_PRECISION))) * decimalAdjustment / collateralPrice; - + const collateralAmount = + (BigInt(Math.floor(collateralUSD * Number(DECIMAL_PRECISION))) * decimalAdjustment) / + collateralPrice; + // Calculate USDU to borrow based on collateral ratio - const maxUsduAmount = collateralAmount * collateralPrice / (BigInt(Math.floor(collateralRatio * 100)) * DECIMAL_PRECISION / BigInt(100)); - + const maxUsduAmount = + (collateralAmount * collateralPrice) / + ((BigInt(Math.floor(collateralRatio * 100)) * DECIMAL_PRECISION) / BigInt(100)); + // Ensure we meet minimum debt requirement const usduAmount = maxUsduAmount > MIN_DEBT ? maxUsduAmount : MIN_DEBT; - + // Recalculate actual CR to ensure it's valid - const actualCR = Number(collateralAmount * collateralPrice * BigInt(100) / usduAmount) / 100; + const actualCR = Number((collateralAmount * collateralPrice * BigInt(100)) / usduAmount) / 100; totalCollateralNeeded += collateralAmount; - + troves.push({ ownerIndex, collateralAmount, usduAmount, interestRate, - collateralUSD + collateralUSD, }); if (i % 20 === 0) { const displayDecimals = isWrapper ? BigInt(10 ** underlyingDecimals) : DECIMAL_PRECISION; console.log(`\n Trove #${i + 1}:`); - console.log(` - ${branch} Amount: ${Number(collateralAmount) / Number(displayDecimals)} (~$${collateralUSD.toFixed(0)})`); + console.log( + ` - ${branch} Amount: ${Number(collateralAmount) / Number(displayDecimals)} (~$${collateralUSD.toFixed(0)})` + ); console.log(` - USDU Amount: ${Number(usduAmount) / Number(DECIMAL_PRECISION)}`); console.log(` - Collateral Ratio: ${(actualCR * 100).toFixed(1)}%`); console.log(` - Interest Rate: ${(interestRate * 100).toFixed(2)}%`); @@ -445,12 +504,16 @@ async function main() { } const displayDecimals = isWrapper ? BigInt(10 ** underlyingDecimals) : DECIMAL_PRECISION; - console.log(`\nšŸ“Š Total ${isWrapper ? 'underlying' : branch} collateral needed: ${Number(totalCollateralNeeded) / Number(displayDecimals)}`); - + console.log( + `\nšŸ“Š Total ${isWrapper ? 'underlying' : branch} collateral needed: ${Number(totalCollateralNeeded) / Number(displayDecimals)}` + ); + // First, mint and approve all collateral needed - console.log(`\nšŸ’° Minting and ${isWrapper ? 'wrapping' : 'approving'} all ${branch} collateral...`); + console.log( + `\nšŸ’° Minting and ${isWrapper ? 'wrapping' : 'approving'} all ${branch} collateral...` + ); const setupCalls: Call[] = []; - + if (isWrapper) { // For wrapped collateral: // 1. Mint underlying token @@ -459,41 +522,41 @@ async function main() { entrypoint: 'mint', calldata: CallData.compile({ recipient: account.address, - amount: uint256.bnToUint256(totalCollateralNeeded) - }) + amount: uint256.bnToUint256(totalCollateralNeeded), + }), }); - + // 2. Approve wrapper to spend underlying token setupCalls.push({ contractAddress: actualCollateralAddress, entrypoint: 'approve', calldata: CallData.compile({ spender: branchAddresses.collateral, // wrapper address - amount: uint256.bnToUint256(totalCollateralNeeded) - }) + amount: uint256.bnToUint256(totalCollateralNeeded), + }), }); - + // 3. Wrap the tokens setupCalls.push({ contractAddress: branchAddresses.collateral, // wrapper address entrypoint: 'wrap', calldata: CallData.compile({ - amount: uint256.bnToUint256(totalCollateralNeeded) - }) + amount: uint256.bnToUint256(totalCollateralNeeded), + }), }); - + // 4. Approve borrower operations to spend wrapped tokens // Note: wrapped amount will be scaled by the wrapper's scaling factor const scalingFactor = BigInt(10 ** (18 - underlyingDecimals)); // wrapper ensures 18 decimals const wrappedAmount = totalCollateralNeeded * scalingFactor; - + setupCalls.push({ contractAddress: branchAddresses.collateral, // wrapper address entrypoint: 'approve', calldata: CallData.compile({ spender: branchAddresses.borrowerOperations, - amount: uint256.bnToUint256(wrappedAmount) - }) + amount: uint256.bnToUint256(wrappedAmount), + }), }); } else { // For regular collateral: @@ -503,8 +566,8 @@ async function main() { entrypoint: 'mint', calldata: CallData.compile({ recipient: account.address, - amount: uint256.bnToUint256(totalCollateralNeeded) - }) + amount: uint256.bnToUint256(totalCollateralNeeded), + }), }); // 2. Approve borrower operations @@ -513,8 +576,8 @@ async function main() { entrypoint: 'approve', calldata: CallData.compile({ spender: branchAddresses.borrowerOperations, - amount: uint256.bnToUint256(totalCollateralNeeded) - }) + amount: uint256.bnToUint256(totalCollateralNeeded), + }), }); } @@ -523,18 +586,23 @@ async function main() { const setupTx = await account.execute(setupCalls); console.log(` Transaction hash: ${setupTx.transaction_hash}`); console.log(' Waiting for confirmation...'); - + const setupReceipt = await provider.waitForTransaction(setupTx.transaction_hash, { retryInterval: 5000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); if (!setupReceipt.isSuccess()) { throw new Error(`Failed to mint and approve ${branch}`); } - console.log(` āœ… ${branch} collateral ${isWrapper ? 'minted, wrapped and' : 'minted and'} approved successfully!`); + console.log( + ` āœ… ${branch} collateral ${isWrapper ? 'minted, wrapped and' : 'minted and'} approved successfully!` + ); } catch (error) { - console.error(`āŒ Failed to ${isWrapper ? 'mint, wrap and approve' : 'mint and approve'} ${branch}:`, error); + console.error( + `āŒ Failed to ${isWrapper ? 'mint, wrap and approve' : 'mint and approve'} ${branch}:`, + error + ); return; } @@ -549,16 +617,16 @@ async function main() { const batchNumber = Math.floor(batchStart / batchSize) + 1; const totalBatches = Math.ceil(totalTroves / batchSize); const batchTroves = troves.slice(batchStart, batchEnd); - + console.log(`\nšŸ“¦ Batch ${batchNumber}/${totalBatches} (Troves ${batchStart + 1}-${batchEnd})`); - + // Build calls for this batch const batchCalls: Call[] = []; - + for (const trove of batchTroves) { const annualInterestRate = BigInt(Math.floor(trove.interestRate * Number(DECIMAL_PRECISION))); const maxUpfrontFee = BigInt(10000) * DECIMAL_PRECISION; - + // If using wrapper, scale the collateral amount for the open_trove call let collAmountForTrove = trove.collateralAmount; if (isWrapper) { @@ -580,26 +648,28 @@ async function main() { max_upfront_fee: uint256.bnToUint256(maxUpfrontFee), add_manager: '0x0', remove_manager: '0x0', - receiver: '0x0' - }) + receiver: '0x0', + }), }); } try { console.log(` šŸ“¤ Submitting batch ${batchNumber} with ${batchCalls.length} troves...`); - + const tx = await account.execute(batchCalls); console.log(` Transaction hash: ${tx.transaction_hash}`); console.log(' Waiting for confirmation...'); - + const receipt = await provider.waitForTransaction(tx.transaction_hash, { retryInterval: 5000, - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); if (receipt.isSuccess()) { successCount += batchCalls.length; - console.log(` āœ… Batch ${batchNumber} successful! Total progress: ${successCount}/${totalTroves}`); + console.log( + ` āœ… Batch ${batchNumber} successful! Total progress: ${successCount}/${totalTroves}` + ); } else { failCount += batchCalls.length; console.log(` āŒ Batch ${batchNumber} failed! Total failures: ${failCount}`); @@ -608,9 +678,10 @@ async function main() { failCount += batchCalls.length; console.error(` āŒ Error in batch ${batchNumber}:`, error); console.log(` Total failures: ${failCount}`); - + // Optionally continue or break based on error rate - if (failCount > totalTroves * 0.2) { // Stop if more than 20% failures + if (failCount > totalTroves * 0.2) { + // Stop if more than 20% failures console.error('Too many failures, stopping script'); break; } @@ -635,4 +706,4 @@ async function main() { main().catch(error => { console.error('Fatal error:', error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/scripts/utils.ts b/scripts/utils.ts index 0c2f640..56584bd 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -12,7 +12,7 @@ dotenv.config(); export const DECIMAL_PRECISION = BigInt('1000000000000000000'); // 1e18 export const MIN_DEBT = BigInt(2000) * DECIMAL_PRECISION; // 2000 USDU export const STRK_GAS_COMPENSATION = BigInt(10) * DECIMAL_PRECISION; // 10 STRK -export const MCR = BigInt(110) * DECIMAL_PRECISION / BigInt(100); // 110% minimum collateral ratio +export const MCR = (BigInt(110) * DECIMAL_PRECISION) / BigInt(100); // 110% minimum collateral ratio // Common interfaces export interface DeploymentAddresses { @@ -160,11 +160,13 @@ export async function setupAccount(): Promise<{ account: Account; provider: RpcP const rpcUrl = process.env['RPC_URL']; if (!privateKey || !accountAddress || !rpcUrl) { - throw new Error('Missing required environment variables: PRIVATE_KEY, DEPLOYER_ADDRESS, RPC_URL'); + throw new Error( + 'Missing required environment variables: PRIVATE_KEY, DEPLOYER_ADDRESS, RPC_URL' + ); } const provider = new RpcProvider({ nodeUrl: rpcUrl }); - const account = new Account(provider, accountAddress, privateKey); + const account = new Account({ provider, address: accountAddress, signer: privateKey }); console.log('šŸ” Account setup:'); console.log(` - Address: ${accountAddress}`); @@ -178,10 +180,13 @@ export function getAvailableBranches(addresses: DeploymentAddresses): string[] { return Object.keys(addresses).filter(key => !excludeKeys.includes(key)); } -export async function getCurrentPrice(priceFeedContract: Contract, branch: string): Promise { +export async function getCurrentPrice( + priceFeedContract: Contract, + branch: string +): Promise { console.log(`\nšŸ“ˆ Fetching current ${branch} price...`); const priceResult = await priceFeedContract['fetch_price'](); - const collateralPrice = uint256.uint256ToBN({low: priceResult['0'], high: priceResult['1']}); + const collateralPrice = uint256.uint256ToBN({ low: priceResult['0'], high: priceResult['1'] }); console.log(` - ${branch} Price: $${Number(collateralPrice) / Number(DECIMAL_PRECISION)}`); return collateralPrice; } @@ -202,19 +207,29 @@ export async function checkAndApproveSponsor( // Check sponsor STRK balance const totalStrkNeeded = STRK_GAS_COMPENSATION * BigInt(totalTroves); const sponsorBalance = await strkContract['balanceOf'](sponsor); - console.log(` - Sponsor STRK balance: ${Number(sponsorBalance) / Number(DECIMAL_PRECISION)} STRK`); - console.log(` - Required STRK for gas compensation: ${Number(totalStrkNeeded) / Number(DECIMAL_PRECISION)} STRK`); + console.log( + ` - Sponsor STRK balance: ${Number(sponsorBalance) / Number(DECIMAL_PRECISION)} STRK` + ); + console.log( + ` - Required STRK for gas compensation: ${Number(totalStrkNeeded) / Number(DECIMAL_PRECISION)} STRK` + ); if (BigInt(sponsorBalance) < totalStrkNeeded) { - throw new Error(`Sponsor doesn't have enough STRK. Has: ${sponsorBalance}, needs: ${totalStrkNeeded}`); + throw new Error( + `Sponsor doesn't have enough STRK. Has: ${sponsorBalance}, needs: ${totalStrkNeeded}` + ); } // Check sponsor's approval to borrower operations const sponsorAllowance = await strkContract['allowance'](sponsor, borrowerOpsAddress); - console.log(` - Sponsor STRK allowance to BorrowerOps: ${Number(sponsorAllowance) / Number(DECIMAL_PRECISION)} STRK`); + console.log( + ` - Sponsor STRK allowance to BorrowerOps: ${Number(sponsorAllowance) / Number(DECIMAL_PRECISION)} STRK` + ); if (BigInt(sponsorAllowance) < totalStrkNeeded) { - console.log(` āš ļø Sponsor hasn't approved enough STRK. Approved: ${sponsorAllowance}, needs: ${totalStrkNeeded}`); + console.log( + ` āš ļø Sponsor hasn't approved enough STRK. Approved: ${sponsorAllowance}, needs: ${totalStrkNeeded}` + ); // Check if current account is the sponsor const sponsorAddress = '0x' + BigInt(sponsor).toString(16).padStart(64, '0'); @@ -224,21 +239,23 @@ export async function checkAndApproveSponsor( console.log(` šŸ“ Current account is sponsor, approving STRK...`); try { - const approveTx = await account.execute([{ - contractAddress: gasTokenAddress, - entrypoint: 'approve', - calldata: CallData.compile({ - spender: borrowerOpsAddress, - amount: uint256.bnToUint256(totalStrkNeeded) - }) - }]); + const approveTx = await account.execute([ + { + contractAddress: gasTokenAddress, + entrypoint: 'approve', + calldata: CallData.compile({ + spender: borrowerOpsAddress, + amount: uint256.bnToUint256(totalStrkNeeded), + }), + }, + ]); console.log(` - Transaction hash: ${approveTx.transaction_hash}`); console.log(' - Waiting for confirmation...'); await provider.waitForTransaction(approveTx.transaction_hash, { retryInterval: 2000, // Reduced from 5000ms to 2000ms for faster polling - successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'] + successStates: ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1'], }); console.log(` āœ… STRK approved successfully!`); @@ -261,7 +278,7 @@ export function getReadline(): readline.Interface { if (!rlInstance) { rlInstance = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }); } return rlInstance; @@ -277,8 +294,8 @@ export function closeReadline(): void { // Promisify readline question export function question(prompt: string): Promise { const rl = getReadline(); - return new Promise((resolve) => { - rl.question(prompt, (answer) => { + return new Promise(resolve => { + rl.question(prompt, answer => { resolve(answer); }); }); @@ -328,17 +345,32 @@ export async function setupContracts( const addressRegistryAbi = JSON.parse(fs.readFileSync('./abis/AddressesRegistry.json', 'utf8')); const strkAbi = JSON.parse(fs.readFileSync('./abis/MSTRK.json', 'utf8')); - // Create contract instances - const collateralContract = new Contract(collateralAbi, branchAddresses.collateral, provider); - const borrowerOpsContract = new Contract(borrowerOpsAbi, branchAddresses.borrowerOperations, provider); - const priceFeedContract = new Contract(priceFeedAbi, branchAddresses.priceFeed, provider); - const addressRegistryContract = new Contract(addressRegistryAbi, branchAddresses.addressesRegistry, provider); - const strkContract = new Contract(strkAbi, gasTokenAddress, provider); - - // Connect contracts to account - collateralContract.connect(account); - borrowerOpsContract.connect(account); - strkContract.connect(account); + // Create contract instances - use account for read-write, provider for read-only + const collateralContract = new Contract({ + abi: collateralAbi, + address: branchAddresses.collateral, + providerOrAccount: account, + }); + const borrowerOpsContract = new Contract({ + abi: borrowerOpsAbi, + address: branchAddresses.borrowerOperations, + providerOrAccount: account, + }); + const priceFeedContract = new Contract({ + abi: priceFeedAbi, + address: branchAddresses.priceFeed, + providerOrAccount: provider, + }); + const addressRegistryContract = new Contract({ + abi: addressRegistryAbi, + address: branchAddresses.addressesRegistry, + providerOrAccount: provider, + }); + const strkContract = new Contract({ + abi: strkAbi, + address: gasTokenAddress, + providerOrAccount: account, + }); return { collateralContract, @@ -346,6 +378,6 @@ export async function setupContracts( priceFeedContract, addressRegistryContract, strkContract, - branchAddresses + branchAddresses, }; } diff --git a/src/RedemptionPreviewer.cairo b/src/RedemptionPreviewer.cairo new file mode 100644 index 0000000..1dbf559 --- /dev/null +++ b/src/RedemptionPreviewer.cairo @@ -0,0 +1,238 @@ +// Created by Uncap Labs +// SPDX-License-Identifier: BUSL-1.1 + +#[starknet::contract] +pub mod RedemptionPreviewer { + use core::array::ArrayTrait; + use core::cmp::min; + use core::num::traits::Zero; + use core::traits::Into; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use crate::branch::trove::interfaces::ITroveManager::{ + ITroveManagerDispatcher, ITroveManagerDispatcherTrait, + }; + use crate::components::interfaces::IUncapBase::{ + IUncapBaseDispatcher, IUncapBaseDispatcherTrait, + }; + use crate::interfaces::ICollateralRegistry::{ + ICollateralRegistryDispatcher, ICollateralRegistryDispatcherTrait, + }; + use crate::interfaces::IRedemptionPreviewer::{ + BranchRedemptionAllocation, IRedemptionPreviewer, RedemptionPreview, + }; + use crate::utils::constants::Constants::{ + DECIMAL_PRECISION, REDEMPTION_BETA, REDEMPTION_FEE_FLOOR, REDEMPTION_MINUTE_DECAY_FACTOR, + }; + use crate::utils::math::math_lib; + + ////////////////////////////////////////////////////////////// + // STORAGE // + ////////////////////////////////////////////////////////////// + + #[storage] + struct Storage { + collateral_registry: ContractAddress, + usdu: ContractAddress, + } + + //////////////////////////////////////////////////////////////// + // CONSTRUCTOR // + //////////////////////////////////////////////////////////////// + + #[constructor] + fn constructor( + ref self: ContractState, collateral_registry: ContractAddress, usdu: ContractAddress, + ) { + assert(collateral_registry.is_non_zero(), 'RP: invalid collateral registry'); + assert(usdu.is_non_zero(), 'RP: invalid usdu address'); + self.collateral_registry.write(collateral_registry); + self.usdu.write(usdu); + } + + //////////////////////////////////////////////////////////////// + // VIEW FUNCTIONS // + //////////////////////////////////////////////////////////////// + + #[abi(embed_v0)] + impl RedemptionPreviewerImpl of IRedemptionPreviewer { + fn get_collateral_registry(self: @ContractState) -> ContractAddress { + self.collateral_registry.read() + } + + fn get_redemption_preview(self: @ContractState, usdu_amount: u256) -> RedemptionPreview { + let requested_usdu_amount = usdu_amount; + let mut usdu_amount = usdu_amount; + + let collateral_registry = ICollateralRegistryDispatcher { + contract_address: self.collateral_registry.read(), + }; + let num_collaterals = collateral_registry.get_num_collaterals(); + + let mut unbacked_portions: Array = ArrayTrait::new(); + let mut prices: Array = ArrayTrait::new(); + let mut redeemable_flags: Array = ArrayTrait::new(); + let mut total_unbacked: u256 = 0; + + // Gather and accumulate unbacked portions (matches CollateralRegistry lines 200-216) + for i in 0..num_collaterals { + let trove_manager_address = collateral_registry.get_trove_manager(i); + let trove_manager = ITroveManagerDispatcher { + contract_address: trove_manager_address, + }; + let (unbacked_portion, price, redeemable) = trove_manager + .get_unbacked_portion_price_and_redeemability(); + + prices.append(price); + redeemable_flags.append(redeemable); + + if redeemable { + total_unbacked += unbacked_portion; + unbacked_portions.append(unbacked_portion); + } else { + unbacked_portions.append(0); + } + } + + // Fallback to branch debt if all unbacked portions are zero (matches CollateralRegistry + // lines 221-252) + if total_unbacked == 0 { + unbacked_portions = ArrayTrait::new(); + for i in 0..num_collaterals { + let trove_manager_address = collateral_registry.get_trove_manager(i); + let trove_manager = ITroveManagerDispatcher { + contract_address: trove_manager_address, + }; + let (_, _, redeemable) = trove_manager + .get_unbacked_portion_price_and_redeemability(); + + if redeemable { + let trove_manager_base = IUncapBaseDispatcher { + contract_address: trove_manager_address, + }; + let unbacked_portion = trove_manager_base.get_entire_branch_debt(); + total_unbacked += unbacked_portion; + unbacked_portions.append(unbacked_portion); + } else { + unbacked_portions.append(0); + } + }; + } else if usdu_amount > total_unbacked { + // Cap redemption amount to total unbacked (matches CollateralRegistry lines + // 246-251) + usdu_amount = total_unbacked; + } + + let effective_usdu_amount = usdu_amount; + let total_weight_snapshot = total_unbacked; + + // Calculate redemption rate (matches CollateralRegistry lines 254-266) + let usdu = IERC20Dispatcher { contract_address: self.usdu.read() }; + let total_usdu_supply = usdu.total_supply(); + + let updated_base_rate = InternalImpl::get_updated_base_rate_from_redemption( + collateral_registry, usdu_amount, total_usdu_supply, + ); + let redemption_rate = InternalImpl::calculate_redemption_rate(updated_base_rate); + + // Compute redemption amount for each collateral (matches CollateralRegistry lines + // 273-302) + let mut allocations: Array = ArrayTrait::new(); + + for i in 0..num_collaterals { + let trove_manager_address = collateral_registry.get_trove_manager(i); + let unbacked_portion = *unbacked_portions.at(i.into()); + let price = *prices.at(i.into()); + let is_redeemable = *redeemable_flags.at(i.into()); + + let mut redeem_amount: u256 = 0; + let mut collateral_amount: u256 = 0; + + if unbacked_portion > 0 { + // Exact same calculation as CollateralRegistry line 277 + redeem_amount = usdu_amount * unbacked_portion / total_unbacked; + + if redeem_amount > 0 && price > 0 { + // Calculate collateral amount after fee deduction + // Fee is: redemption_rate * redeem_amount / DECIMAL_PRECISION + let fee = redemption_rate * redeem_amount / DECIMAL_PRECISION; + let usdu_after_fee = redeem_amount - fee; + // Collateral = usdu_after_fee * DECIMAL_PRECISION / price + collateral_amount = usdu_after_fee * DECIMAL_PRECISION / price; + } + + // Ensure per-branch redeems add up exactly (matches CollateralRegistry lines + // 298-300) + usdu_amount -= redeem_amount; + total_unbacked -= unbacked_portion; + } + + allocations + .append( + BranchRedemptionAllocation { + collateral_index: i, + trove_manager: trove_manager_address, + redemption_manager: collateral_registry.get_redemption_manager(i), + price, + is_redeemable, + weight: unbacked_portion, + usdu_amount: redeem_amount, + collateral_amount, + }, + ); + } + + RedemptionPreview { + requested_usdu_amount, + effective_usdu_amount, + total_weight: total_weight_snapshot, + redemption_rate, + allocations, + } + } + } + + //////////////////////////////////////////////////////////////// + // INTERNAL FUNCTIONS // + //////////////////////////////////////////////////////////////// + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Calculates the decayed base rate based on time passed since last fee operation. + /// Matches CollateralRegistry::calculate_decayed_base_rate (lines 399-406) + fn calculate_decayed_base_rate(collateral_registry: ICollateralRegistryDispatcher) -> u256 { + let minutes_passed = collateral_registry.minutes_passed_since_last_fee_op(); + let decay_factor = math_lib::dec_pow( + REDEMPTION_MINUTE_DECAY_FACTOR, minutes_passed.into(), + ); + let base_rate = collateral_registry.get_base_rate(); + let decayed_base_rate = base_rate * decay_factor / DECIMAL_PRECISION; + decayed_base_rate + } + + /// Calculates the redemption rate from the base rate. + /// Matches CollateralRegistry::calculate_redemption_rate (lines 414-416) + fn calculate_redemption_rate(base_rate: u256) -> u256 { + min(REDEMPTION_FEE_FLOOR + base_rate, DECIMAL_PRECISION) + } + + /// Calculates the updated base rate after a redemption. + /// Matches CollateralRegistry::get_updated_base_rate_from_redemption (lines 433-445) + fn get_updated_base_rate_from_redemption( + collateral_registry: ICollateralRegistryDispatcher, + redeem_amount: u256, + total_usdu_supply: u256, + ) -> u256 { + // Decay the base rate + let decayed_base_rate = Self::calculate_decayed_base_rate(collateral_registry); + + // Get the fraction of total supply that was redeemed + let redeemed_usdu_fraction = redeem_amount * DECIMAL_PRECISION / total_usdu_supply; + + let new_base_rate = decayed_base_rate + redeemed_usdu_fraction / REDEMPTION_BETA; + let new_base_rate_capped = min(new_base_rate, DECIMAL_PRECISION); + new_base_rate_capped + } + } +} diff --git a/src/interfaces/IRedemptionPreviewer.cairo b/src/interfaces/IRedemptionPreviewer.cairo new file mode 100644 index 0000000..d521f10 --- /dev/null +++ b/src/interfaces/IRedemptionPreviewer.cairo @@ -0,0 +1,31 @@ +// Created by Uncap Labs +// SPDX-License-Identifier: BUSL-1.1 + +use starknet::ContractAddress; + +#[derive(Drop, Serde)] +pub struct BranchRedemptionAllocation { + pub collateral_index: u8, + pub trove_manager: ContractAddress, + pub redemption_manager: ContractAddress, + pub price: u256, + pub is_redeemable: bool, + pub weight: u256, + pub usdu_amount: u256, + pub collateral_amount: u256, +} + +#[derive(Drop, Serde)] +pub struct RedemptionPreview { + pub requested_usdu_amount: u256, + pub effective_usdu_amount: u256, + pub total_weight: u256, + pub redemption_rate: u256, + pub allocations: Array, +} + +#[starknet::interface] +pub trait IRedemptionPreviewer { + fn get_collateral_registry(self: @TContractState) -> ContractAddress; + fn get_redemption_preview(self: @TContractState, usdu_amount: u256) -> RedemptionPreview; +} diff --git a/src/lib.cairo b/src/lib.cairo index 206ac5b..abac06f 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -53,6 +53,7 @@ pub mod interfaces { pub mod ICollateralRegistry; pub mod IHintHelpers; pub mod IPriceFeed; + pub mod IRedemptionPreviewer; pub mod IUSDU; } @@ -89,4 +90,5 @@ pub mod wrappers { pub mod CollateralRegistry; pub mod HintHelpers; +pub mod RedemptionPreviewer; pub mod USDU; diff --git a/tests/RedemptionPreviewerTest.cairo b/tests/RedemptionPreviewerTest.cairo new file mode 100644 index 0000000..024c84c --- /dev/null +++ b/tests/RedemptionPreviewerTest.cairo @@ -0,0 +1,325 @@ +// Created by Uncap Labs +// SPDX-License-Identifier: BUSL-1.1 + +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_block_timestamp_global, +}; +use starknet::{ContractAddress, get_block_timestamp}; +use usdu::interfaces::ICollateralRegistry::ICollateralRegistryDispatcherTrait; +use usdu::interfaces::IRedemptionPreviewer::{ + IRedemptionPreviewerDispatcher, IRedemptionPreviewerDispatcherTrait, +}; +use usdu::utils::constants::Constants::{DECIMAL_PRECISION, ONE_DAY, _1PCT}; +use crate::MultiCollateral::{ + NUM_COLLATERALS, make_multi_collateral_sp_deposit_and_claim, + open_multi_collateral_trove_no_hints_100pct_with_index, setup_multi_collateral_test_default, +}; +use crate::test_contracts::BaseTest::BaseTest; + +/// Deploy the RedemptionPreviewer contract +fn deploy_redemption_previewer( + collateral_registry: ContractAddress, usdu: ContractAddress, +) -> IRedemptionPreviewerDispatcher { + let contract = declare("RedemptionPreviewer").unwrap().contract_class(); + let mut calldata: Array = array![]; + calldata.append(collateral_registry.into()); + calldata.append(usdu.into()); + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + IRedemptionPreviewerDispatcher { contract_address } +} + +#[test] +fn test_redemption_previewer_matches_actual_redemption() { + let (deployed_contracts, test_accounts) = setup_multi_collateral_test_default(); + + // Deploy RedemptionPreviewer + let redemption_previewer = deploy_redemption_previewer( + deployed_contracts.collateral_registry.contract_address, + deployed_contracts.usdu.contract_address, + ); + + // Open troves on all 4 collaterals with different SP deposits to create varying unbacked + // portions + // Collateral 0: 10k USDU, 0 in SP (fully unbacked) + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 0, + test_accounts.A, + 0, + 10 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + + // Collateral 1: 10k USDU, 5k in SP (50% unbacked) + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 1, + test_accounts.A, + 0, + 100 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + make_multi_collateral_sp_deposit_and_claim( + deployed_contracts.contracts_array, 1, test_accounts.A, 5_000 * DECIMAL_PRECISION, + ); + + // Collateral 2: 10k USDU, 9k in SP (10% unbacked) + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 2, + test_accounts.A, + 0, + 10 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + make_multi_collateral_sp_deposit_and_claim( + deployed_contracts.contracts_array, 2, test_accounts.A, 9_000 * DECIMAL_PRECISION, + ); + + // Collateral 3: 10k USDU, 10k in SP (0% unbacked - but will still participate proportionally) + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 3, + test_accounts.A, + 0, + 10 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + make_multi_collateral_sp_deposit_and_claim( + deployed_contracts.contracts_array, 3, test_accounts.A, 10_000 * DECIMAL_PRECISION, + ); + + // Let time pass to reduce redemption rate + start_cheat_block_timestamp_global(get_block_timestamp() + ONE_DAY.try_into().unwrap()); + + let redeem_amount = 1_600 * DECIMAL_PRECISION; + + // Get initial collateral balances + let mut initial_coll_balances: Array = ArrayTrait::new(); + let mut i: u32 = 0; + while i < NUM_COLLATERALS { + let coll_token = IERC20Dispatcher { + contract_address: *deployed_contracts.contracts_array.at(i).ubtc.contract_address, + }; + initial_coll_balances.append(coll_token.balance_of(test_accounts.A)); + i += 1; + } + + // Get redemption preview BEFORE actual redemption + let preview = redemption_previewer.get_redemption_preview(redeem_amount); + + // Verify preview basics + assert(preview.requested_usdu_amount == redeem_amount, 'Wrong requested amount'); + assert(preview.allocations.len() == NUM_COLLATERALS, 'Wrong num allocations'); + + // Perform actual redemption + BaseTest::redeem(deployed_contracts.collateral_registry, test_accounts.A, redeem_amount); + + // Get final collateral balances and compare with preview + let mut i: u32 = 0; + while i < NUM_COLLATERALS { + let coll_token = IERC20Dispatcher { + contract_address: *deployed_contracts.contracts_array.at(i).ubtc.contract_address, + }; + let final_balance = coll_token.balance_of(test_accounts.A); + let actual_coll_received = final_balance - *initial_coll_balances.at(i); + let preview_coll_amount = *preview.allocations.at(i).collateral_amount; + + // The preview collateral amount should match the actual collateral received + // Allow small tolerance for rounding + BaseTest::assert_approx_eq_abs( + actual_coll_received, + preview_coll_amount, + 10, // 0.001 token tolerance + 'Preview coll mismatch', + ); + + i += 1; + }; +} + +#[test] +fn test_redemption_previewer_usdu_amounts_match() { + let (deployed_contracts, test_accounts) = setup_multi_collateral_test_default(); + + let redemption_previewer = deploy_redemption_previewer( + deployed_contracts.collateral_registry.contract_address, + deployed_contracts.usdu.contract_address, + ); + + // Open troves on all 4 collaterals + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 0, + test_accounts.A, + 0, + 10 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 1, + test_accounts.A, + 0, + 100 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + make_multi_collateral_sp_deposit_and_claim( + deployed_contracts.contracts_array, 1, test_accounts.A, 5_000 * DECIMAL_PRECISION, + ); + + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 2, + test_accounts.A, + 0, + 10 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + make_multi_collateral_sp_deposit_and_claim( + deployed_contracts.contracts_array, 2, test_accounts.A, 9_000 * DECIMAL_PRECISION, + ); + + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 3, + test_accounts.A, + 0, + 10 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + make_multi_collateral_sp_deposit_and_claim( + deployed_contracts.contracts_array, 3, test_accounts.A, 10_000 * DECIMAL_PRECISION, + ); + + start_cheat_block_timestamp_global(get_block_timestamp() + ONE_DAY.try_into().unwrap()); + + let redeem_amount = 1_600 * DECIMAL_PRECISION; + + // Get preview + let preview = redemption_previewer.get_redemption_preview(redeem_amount); + + // Verify that sum of USDU amounts across allocations equals effective_usdu_amount + let mut total_usdu_in_allocations: u256 = 0; + let mut i: u32 = 0; + while i < NUM_COLLATERALS { + total_usdu_in_allocations += *preview.allocations.at(i).usdu_amount; + i += 1; + } + + BaseTest::assert_approx_eq_abs( + total_usdu_in_allocations, + preview.effective_usdu_amount, + 10, // Allow tiny rounding error + 'USDU allocation sum mismatch', + ); +} + +#[test] +fn test_redemption_previewer_with_amount_exceeding_unbacked() { + let (deployed_contracts, test_accounts) = setup_multi_collateral_test_default(); + + let redemption_previewer = deploy_redemption_previewer( + deployed_contracts.collateral_registry.contract_address, + deployed_contracts.usdu.contract_address, + ); + + // Open small troves to create limited unbacked debt + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 0, + test_accounts.A, + 0, + 10 * DECIMAL_PRECISION, + 1_000 * DECIMAL_PRECISION, // Only 1k USDU + 5 * _1PCT, + ); + + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 1, + test_accounts.A, + 0, + 100 * DECIMAL_PRECISION, + 1_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + + start_cheat_block_timestamp_global(get_block_timestamp() + ONE_DAY.try_into().unwrap()); + + // Try to redeem more than total unbacked + let excessive_redeem_amount = 100_000 * DECIMAL_PRECISION; + + let preview = redemption_previewer.get_redemption_preview(excessive_redeem_amount); + + // effective_usdu_amount should be capped at total_weight (total unbacked) + assert(preview.effective_usdu_amount <= preview.total_weight, 'Should cap at total unbacked'); + assert( + preview.effective_usdu_amount < preview.requested_usdu_amount, + 'Should be less than requested', + ); +} + +#[test] +fn test_redemption_previewer_redemption_rate_matches() { + let (deployed_contracts, test_accounts) = setup_multi_collateral_test_default(); + + let redemption_previewer = deploy_redemption_previewer( + deployed_contracts.collateral_registry.contract_address, + deployed_contracts.usdu.contract_address, + ); + + // Open a trove + open_multi_collateral_trove_no_hints_100pct_with_index( + deployed_contracts.contracts_array, + 0, + test_accounts.A, + 0, + 10 * DECIMAL_PRECISION, + 10_000 * DECIMAL_PRECISION, + 5 * _1PCT, + ); + + start_cheat_block_timestamp_global(get_block_timestamp() + ONE_DAY.try_into().unwrap()); + + let redeem_amount = 1_000 * DECIMAL_PRECISION; + + let preview = redemption_previewer.get_redemption_preview(redeem_amount); + + // Compare with CollateralRegistry's calculation + let registry_rate = deployed_contracts + .collateral_registry + .get_redemption_rate_for_redeemed_amount(redeem_amount); + + BaseTest::assert_approx_eq_abs( + preview.redemption_rate, registry_rate, 10, 'Redemption rate mismatch', + ); +} + +#[test] +fn test_redemption_previewer_collateral_registry_address() { + let (deployed_contracts, _) = setup_multi_collateral_test_default(); + + let redemption_previewer = deploy_redemption_previewer( + deployed_contracts.collateral_registry.contract_address, + deployed_contracts.usdu.contract_address, + ); + + assert( + redemption_previewer + .get_collateral_registry() == deployed_contracts + .collateral_registry + .contract_address, + 'Wrong registry address', + ); +} diff --git a/tests/lib.cairo b/tests/lib.cairo index 6a7dba9..321435e 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -33,6 +33,7 @@ pub mod MCRDecreaseTest; pub mod MultiCollateral; pub mod MultiCollateralTestingDeployment; pub mod PoolsTest; +pub mod RedemptionPreviewerTest; pub mod RedemptionsTest; pub mod Reinitialization; pub mod SetPriceFeedTest; diff --git a/yarn.lock b/yarn.lock index 4cd3852..c6edd78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,7 +78,7 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@noble/curves@1.7.0", "@noble/curves@~1.7.0": +"@noble/curves@~1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== @@ -116,10 +116,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@scure/base@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" - integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== +"@scure/base@~1.2.1": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" + integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== "@scure/starknet@1.1.0": version "1.1.0" @@ -129,16 +129,16 @@ "@noble/curves" "~1.7.0" "@noble/hashes" "~1.6.0" -"@starknet-io/starknet-types-07@npm:@starknet-io/types-js@~0.7.10": - version "0.7.10" - resolved "https://registry.yarnpkg.com/@starknet-io/types-js/-/types-js-0.7.10.tgz#d21dc973d0cd04d7b6293ce461f2f06a5873c760" - integrity sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w== - "@starknet-io/starknet-types-08@npm:@starknet-io/types-js@~0.8.4": version "0.8.4" resolved "https://registry.yarnpkg.com/@starknet-io/types-js/-/types-js-0.8.4.tgz#bbc07422e89cb5bac45da28e8457f0f17535950d" integrity sha512-0RZ3TZHcLsUTQaq1JhDSCM8chnzO4/XNsSCozwDET64JK5bjFDIf2ZUkta+tl5Nlbf4usoU7uZiDI/Q57kt2SQ== +"@starknet-io/starknet-types-09@npm:@starknet-io/types-js@~0.9.1": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@starknet-io/types-js/-/types-js-0.9.2.tgz#08a794a35f2785878812ebd151a261399900b594" + integrity sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg== + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -920,10 +920,10 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lossless-json@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lossless-json/-/lossless-json-4.1.1.tgz#b7cbac00c222a68072a9037563dfc4c71cee52f0" - integrity sha512-HusN80C0ohtT9kOHQH7EuUaqzRQsnekpa+2ot8OzvW0iC08dq/YtM/7uKwwajldQsCrHyC8q9fz3t3L+TmDltA== +lossless-json@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lossless-json/-/lossless-json-4.3.0.tgz#7d26864820ecf08aee800213fc193666e794802a" + integrity sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g== make-error@^1.1.1: version "1.3.6" @@ -1171,19 +1171,19 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -starknet@^7.0.0: - version "7.6.4" - resolved "https://registry.yarnpkg.com/starknet/-/starknet-7.6.4.tgz#8ca2f3decbecde6316e7561b39f6a296a7fa33b5" - integrity sha512-FB20IaLCDbh/XomkB+19f5jmNxG+RzNdRO7QUhm7nfH81UPIt2C/MyWAlHCYkbv2wznSEb73wpxbp9tytokTgQ== +starknet@^8.6.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/starknet/-/starknet-8.9.0.tgz#9dcfb90358e16d4514c3026a968a11905f50ffbc" + integrity sha512-uhXSCKW0SMBGckd4yVlscVz4DMkQqZ+/RTpfxZ5J46e6AuCUHOqGKv61tqgUP9GhdvgHV/XyN+kdmlyZn3rXjA== dependencies: - "@noble/curves" "1.7.0" - "@noble/hashes" "1.6.0" - "@scure/base" "1.2.1" + "@noble/curves" "~1.7.0" + "@noble/hashes" "~1.6.0" + "@scure/base" "~1.2.1" "@scure/starknet" "1.1.0" - "@starknet-io/starknet-types-07" "npm:@starknet-io/types-js@~0.7.10" "@starknet-io/starknet-types-08" "npm:@starknet-io/types-js@~0.8.4" + "@starknet-io/starknet-types-09" "npm:@starknet-io/types-js@~0.9.1" abi-wan-kanabi "2.2.4" - lossless-json "^4.0.1" + lossless-json "^4.2.0" pako "^2.0.4" ts-mixer "^6.0.3"