diff --git a/cadence/tests/evm_state_helpers.cdc b/cadence/tests/evm_state_helpers.cdc index 97f2c473..5a32e108 100644 --- a/cadence/tests/evm_state_helpers.cdc +++ b/cadence/tests/evm_state_helpers.cdc @@ -27,9 +27,7 @@ access(all) fun setVaultSharePrice( /* --- Uniswap V3 Pool State Manipulation --- */ -/// Set Uniswap V3 pool to a specific price via EVM.store -/// Creates pool if it doesn't exist, then manipulates state -/// Price is specified as UFix128 for high precision (24 decimal places) +/// Set Uniswap V3 pool to a specific price with infinite liquidity (zero slippage). access(all) fun setPoolToPrice( factoryAddress: String, tokenAAddress: String, @@ -45,16 +43,17 @@ access(all) fun setPoolToPrice( code: Test.readFile("transactions/set_uniswap_v3_pool_price.cdc"), authorizers: [signer.address], signers: [signer], - arguments: [factoryAddress, tokenAAddress, tokenBAddress, fee, priceTokenBPerTokenA, tokenABalanceSlot, tokenBBalanceSlot, 0.0, 0.0, 1.0] + arguments: [factoryAddress, tokenAAddress, tokenBAddress, fee, priceTokenBPerTokenA, tokenABalanceSlot, tokenBBalanceSlot, 0.0, 0 as Int256, 0.0, 1.0] ) ) Test.expect(seedResult, Test.beSucceeded()) } /// Set Uniswap V3 pool to a specific price with finite TVL and concentrated liquidity. -/// tvl: total pool TVL in USD (e.g. 10_000_000.0 for $10M) -/// concentration: fraction 0.0-1.0 (e.g. 0.80 for 80% of liquidity in narrow range) -/// tokenBPriceUSD: USD price of tokenB (e.g. 1.0 for stablecoins) +/// tvl: total pool TVL in USD (e.g. 500_000.0) +/// tickRange: ±ticks from current price (e.g. 100 = ±1% for tick_spacing=10) +/// tvlFraction: fraction of TVL placed as liquidity (e.g. 0.95 = 95%) +/// tokenBPriceUSD: USD price of tokenB (1.0 for stablecoins) access(all) fun setPoolToPriceWithTVL( factoryAddress: String, tokenAAddress: String, @@ -63,17 +62,18 @@ access(all) fun setPoolToPriceWithTVL( priceTokenBPerTokenA: UFix128, tokenABalanceSlot: UInt256, tokenBBalanceSlot: UInt256, + signer: Test.TestAccount, tvl: UFix64, - concentration: UFix64, - tokenBPriceUSD: UFix64, - signer: Test.TestAccount + tickRange: Int, + tvlFraction: UFix64, + tokenBPriceUSD: UFix64 ) { let seedResult = Test.executeTransaction( Test.Transaction( code: Test.readFile("transactions/set_uniswap_v3_pool_price.cdc"), authorizers: [signer.address], signers: [signer], - arguments: [factoryAddress, tokenAAddress, tokenBAddress, fee, priceTokenBPerTokenA, tokenABalanceSlot, tokenBBalanceSlot, tvl, concentration, tokenBPriceUSD] + arguments: [factoryAddress, tokenAAddress, tokenBAddress, fee, priceTokenBPerTokenA, tokenABalanceSlot, tokenBBalanceSlot, tvl, Int256(tickRange), tvlFraction, tokenBPriceUSD] ) ) Test.expect(seedResult, Test.beSucceeded()) diff --git a/cadence/tests/evm_state_helpers_test.cdc b/cadence/tests/evm_state_helpers_test.cdc index 51707183..b11e88ce 100644 --- a/cadence/tests/evm_state_helpers_test.cdc +++ b/cadence/tests/evm_state_helpers_test.cdc @@ -195,10 +195,11 @@ fun test_ConcentratedLiquiditySlippage() { priceTokenBPerTokenA: 1.0, tokenABalanceSlot: wflowBalanceSlot, tokenBBalanceSlot: pyusd0BalanceSlot, + signer: testAccount, tvl: 500_000.0, - concentration: 0.80, - tokenBPriceUSD: 1.0, - signer: testAccount + tickRange: 100, + tvlFraction: 0.80, + tokenBPriceUSD: 1.0 ) let concBefore = getBalance(address: testAccount.address, vaultPublicPath: pyusd0PublicPath)! @@ -229,10 +230,11 @@ fun test_ConcentratedLiquiditySlippage() { priceTokenBPerTokenA: 1.0, tokenABalanceSlot: wflowBalanceSlot, tokenBBalanceSlot: pyusd0BalanceSlot, + signer: testAccount, tvl: 5_000_000.0, - concentration: 0.80, - tokenBPriceUSD: 1.0, - signer: testAccount + tickRange: 100, + tvlFraction: 0.80, + tokenBPriceUSD: 1.0 ) let bigBefore = getBalance(address: testAccount.address, vaultPublicPath: pyusd0PublicPath)! @@ -263,10 +265,11 @@ fun test_ConcentratedLiquiditySlippage() { priceTokenBPerTokenA: 1.0, tokenABalanceSlot: wflowBalanceSlot, tokenBBalanceSlot: pyusd0BalanceSlot, + signer: testAccount, tvl: 500_000.0, - concentration: 0.80, - tokenBPriceUSD: 1.0, - signer: testAccount + tickRange: 100, + tvlFraction: 0.80, + tokenBPriceUSD: 1.0 ) let smallBefore = getBalance(address: testAccount.address, vaultPublicPath: pyusd0PublicPath)! diff --git a/cadence/tests/simulation_base_case_stress.cdc b/cadence/tests/simulation_base_case_stress.cdc new file mode 100644 index 00000000..600dbcae --- /dev/null +++ b/cadence/tests/simulation_base_case_stress.cdc @@ -0,0 +1,697 @@ +#test_fork(network: "mainnet-fork", height: 143292255) + +import Test +import BlockchainHelpers + +import "test_helpers.cdc" +import "evm_state_helpers.cdc" +import "simulation_base_case_stress_helpers.cdc" + +import "FlowYieldVaults" +import "FlowToken" +import "MOET" +import "FlowYieldVaultsStrategiesV2" +import "FlowALPv0" +import "DeFiActions" + +// ============================================================================ +// CADENCE ACCOUNTS +// ============================================================================ + +access(all) let flowYieldVaultsAccount = Test.getAccount(0xb1d63873c3cc9f79) +access(all) let flowALPAccount = Test.getAccount(0x6b00ff876c299c61) +access(all) let bandOracleAccount = Test.getAccount(0x6801a6222ebf784a) +access(all) let whaleFlowAccount = Test.getAccount(0x92674150c9213fc9) +access(all) let coaOwnerAccount = Test.getAccount(0xe467b9dd11fa00df) + +// WBTC on Flow EVM +access(all) let WBTC_TOKEN_ID = "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" +access(all) let WBTC_TYPE = CompositeType(WBTC_TOKEN_ID)! + +access(all) var strategyIdentifier = Type<@FlowYieldVaultsStrategiesV2.FUSDEVStrategy>().identifier +access(all) var wbtcTokenIdentifier = WBTC_TOKEN_ID + +// ============================================================================ +// PROTOCOL ADDRESSES +// ============================================================================ + +access(all) let factoryAddress = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" + +// ============================================================================ +// VAULT & TOKEN ADDRESSES +// ============================================================================ + +access(all) let morphoVaultAddress = "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D" +access(all) let pyusd0Address = "0x99aF3EeA856556646C98c8B9b2548Fe815240750" +access(all) let moetAddress = "0x213979bB8A9A86966999b3AA797C1fcf3B967ae2" +access(all) let wbtcAddress = "0x717dae2baf7656be9a9b01dee31d571a9d4c9579" + +// ============================================================================ +// STORAGE SLOT CONSTANTS +// ============================================================================ + +access(all) let moetBalanceSlot = 0 as UInt256 +access(all) let pyusd0BalanceSlot = 1 as UInt256 +access(all) let fusdevBalanceSlot = 12 as UInt256 +access(all) let wbtcBalanceSlot = 5 as UInt256 + +access(all) let morphoVaultTotalSupplySlot = 11 as UInt256 +access(all) let morphoVaultTotalAssetsSlot = 15 as UInt256 + +// ============================================================================ +// SIMULATION TYPES +// ============================================================================ + +access(all) struct SimConfig { + access(all) let prices: [UFix64] + access(all) let tickIntervalSeconds: UFix64 + access(all) let numAgents: Int + access(all) let fundingPerAgent: UFix64 + access(all) let yieldAPR: UFix64 + access(all) let expectedLiquidationCount: Int + /// How often (in ticks) to attempt rebalancing. + /// 1 = rebalance every tick (default) + access(all) let rebalanceInterval: Int + /// Position health thresholds + access(all) let minHealth: UFix64 + access(all) let targetHealth: UFix64 + access(all) let maxHealth: UFix64 + /// Initial HF range — agents are linearly spread across [low, high] + /// (Python sim uses random.uniform; linear spread is the deterministic equivalent) + access(all) let initialHFLow: UFix64 + access(all) let initialHFHigh: UFix64 + /// MOET:YT pool TVL in USD (from fixture's moet_yt.size) + access(all) let moetYtPoolTVL: UFix64 + /// MOET:YT pool TVL fraction — fraction of TVL placed as liquidity (0.95 = 95%) + access(all) let moetYtPoolTVLFraction: UFix64 + /// MOET:YT pool tick range — ±ticks from peg (100 = Python sim default) + access(all) let moetYtPoolTickRange: Int + + init( + prices: [UFix64], + tickIntervalSeconds: UFix64, + numAgents: Int, + fundingPerAgent: UFix64, + yieldAPR: UFix64, + expectedLiquidationCount: Int, + rebalanceInterval: Int, + minHealth: UFix64, + targetHealth: UFix64, + maxHealth: UFix64, + initialHFLow: UFix64, + initialHFHigh: UFix64, + moetYtPoolTVL: UFix64, + moetYtPoolTVLFraction: UFix64, + moetYtPoolTickRange: Int + ) { + self.prices = prices + self.tickIntervalSeconds = tickIntervalSeconds + self.numAgents = numAgents + self.fundingPerAgent = fundingPerAgent + self.yieldAPR = yieldAPR + self.expectedLiquidationCount = expectedLiquidationCount + self.rebalanceInterval = rebalanceInterval + self.minHealth = minHealth + self.targetHealth = targetHealth + self.maxHealth = maxHealth + self.initialHFLow = initialHFLow + self.initialHFHigh = initialHFHigh + self.moetYtPoolTVL = moetYtPoolTVL + self.moetYtPoolTVLFraction = moetYtPoolTVLFraction + self.moetYtPoolTickRange = moetYtPoolTickRange + } +} + +access(all) struct SimResult { + access(all) let rebalanceCount: Int + access(all) let liquidationCount: Int + access(all) let lowestHF: UFix64 + access(all) let finalHF: UFix64 + access(all) let lowestPrice: UFix64 + access(all) let finalPrice: UFix64 + + init( + rebalanceCount: Int, + liquidationCount: Int, + lowestHF: UFix64, + finalHF: UFix64, + lowestPrice: UFix64, + finalPrice: UFix64 + ) { + self.rebalanceCount = rebalanceCount + self.liquidationCount = liquidationCount + self.lowestHF = lowestHF + self.finalHF = finalHF + self.lowestPrice = lowestPrice + self.finalPrice = finalPrice + } +} + +// ============================================================================ +// SETUP +// ============================================================================ + +access(all) +fun setup() { + deployContractsForFork() + + // PYUSD0:morphoVault (routing pool) + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: pyusd0Address, + tokenBAddress: morphoVaultAddress, + fee: 100, + priceTokenBPerTokenA: 1.0, + tokenABalanceSlot: pyusd0BalanceSlot, + tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount + ) + + // MOET:morphoVault (yield token pool) — finite liquidity matching Python sim + // ±100 ticks with 95% of $500K TVL, same as Python _initialize_symmetric_yield_token_positions + let moetYtPool = simulation_ht_vs_aave_pools["moet_yt"]! + setPoolToPriceWithTVL( + factoryAddress: factoryAddress, + tokenAAddress: moetAddress, + tokenBAddress: morphoVaultAddress, + fee: 100, + priceTokenBPerTokenA: 1.0, + tokenABalanceSlot: moetBalanceSlot, + tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount, + tvl: moetYtPool.size, + tickRange: 100, + tvlFraction: moetYtPool.concentration, + tokenBPriceUSD: 1.0 + ) + + // MOET:PYUSD0 (routing pool) + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: moetAddress, + tokenBAddress: pyusd0Address, + fee: 100, + priceTokenBPerTokenA: 1.0, + tokenABalanceSlot: moetBalanceSlot, + tokenBBalanceSlot: pyusd0BalanceSlot, + signer: coaOwnerAccount + ) + + // PYUSD0:WBTC (collateral/liquidation pool) — infinite liquidity for now + let initialBtcPrice = simulation_ht_vs_aave_prices[0] + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: wbtcAddress, + tokenBAddress: pyusd0Address, + fee: 3000, + priceTokenBPerTokenA: UFix128(initialBtcPrice), + tokenABalanceSlot: wbtcBalanceSlot, + tokenBBalanceSlot: pyusd0BalanceSlot, + signer: coaOwnerAccount + ) + + setBandOraclePrices(signer: bandOracleAccount, symbolPrices: { + "BTC": initialBtcPrice, + "USD": 1.0 + }) + + let reserveAmount = 100_000_00.0 + transferFlow(signer: whaleFlowAccount, recipient: flowALPAccount.address, amount: reserveAmount) + mintMoet(signer: flowALPAccount, to: flowALPAccount.address, amount: reserveAmount, beFailed: false) + + transferFlow(signer: whaleFlowAccount, recipient: flowYieldVaultsAccount.address, amount: reserveAmount) + transferFlow(signer: whaleFlowAccount, recipient: coaOwnerAccount.address, amount: reserveAmount) +} + +// ============================================================================ +// HELPERS +// ============================================================================ + +access(all) fun getBTCCollateralFromPosition(pid: UInt64): UFix64 { + let positionDetails = getPositionDetails(pid: pid, beFailed: false) + for balance in positionDetails.balances { + if balance.vaultType == WBTC_TYPE { + if balance.direction == FlowALPv0.BalanceDirection.Credit { + return balance.balance + } + } + } + return 0.0 +} + +/// Compute deterministic YT (ERC4626 vault share) price at a given tick. +/// price = 1.0 + yieldAPR * (seconds / secondsPerYear) +access(all) fun ytPriceAtTick(_ tick: Int, tickIntervalSeconds: UFix64, yieldAPR: UFix64): UFix64 { + let secondsPerYear: UFix64 = 31536000.0 + let elapsedSeconds = UFix64(tick) * tickIntervalSeconds + return 1.0 + yieldAPR * (elapsedSeconds / secondsPerYear) +} + +/// Update oracle, collateral pool, and vault share price each tick. +/// Does NOT touch the MOET/FUSDEV pool — that's managed by the arb bot reset. +access(all) fun applyPriceTick(btcPrice: UFix64, ytPrice: UFix64, signer: Test.TestAccount) { + setBandOraclePrices(signer: bandOracleAccount, symbolPrices: { + "BTC": btcPrice, + "USD": 1.0 + }) + + // PYUSD0:WBTC pool — update BTC price (infinite liquidity) + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: wbtcAddress, + tokenBAddress: pyusd0Address, + fee: 3000, + priceTokenBPerTokenA: UFix128(btcPrice), + tokenABalanceSlot: wbtcBalanceSlot, + tokenBBalanceSlot: pyusd0BalanceSlot, + signer: coaOwnerAccount + ) + + setVaultSharePrice( + vaultAddress: morphoVaultAddress, + assetAddress: pyusd0Address, + assetBalanceSlot: pyusd0BalanceSlot, + totalSupplySlot: morphoVaultTotalSupplySlot, + vaultTotalAssetsSlot: morphoVaultTotalAssetsSlot, + priceMultiplier: ytPrice, + signer: signer + ) +} + +/// Arb bot simulation: reset MOET/FUSDEV pool to peg with finite TVL. +/// Called after all agents trade each tick. Matches Python sim arb bot +/// which pushes the pool back toward peg every tick. +access(all) fun resetYieldPoolToFiniteTVL(ytPrice: UFix64, tvl: UFix64, tvlFraction: UFix64, tickRange: Int) { + setPoolToPriceWithTVL( + factoryAddress: factoryAddress, + tokenAAddress: moetAddress, + tokenBAddress: morphoVaultAddress, + fee: 100, + priceTokenBPerTokenA: UFix128(ytPrice), + tokenABalanceSlot: moetBalanceSlot, + tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount, + tvl: tvl, + tickRange: tickRange, + tvlFraction: tvlFraction, + tokenBPriceUSD: 1.0 + ) +} + +// ============================================================================ +// SIMULATION RUNNER +// ============================================================================ + +access(all) fun runSimulation(config: SimConfig, label: String): SimResult { + let prices = config.prices + let initialPrice = prices[0] + + // Clear scheduled transactions inherited from forked mainnet state + resetTransactionScheduler() + + // Apply initial pricing + applyPriceTick(btcPrice: initialPrice, ytPrice: ytPriceAtTick(0, tickIntervalSeconds: config.tickIntervalSeconds, yieldAPR: config.yieldAPR), signer: coaOwnerAccount) + + // Create agents + let users: [Test.TestAccount] = [] + let pids: [UInt64] = [] + let vaultIds: [UInt64] = [] + + var i = 0 + while i < config.numAgents { + let user = Test.createAccount() + transferFlow(signer: whaleFlowAccount, recipient: user.address, amount: 10.0) + mintBTC(signer: user, amount: config.fundingPerAgent) + grantBeta(flowYieldVaultsAccount, user) + + setVaultSharePrice( + vaultAddress: morphoVaultAddress, + assetAddress: pyusd0Address, + assetBalanceSlot: pyusd0BalanceSlot, + totalSupplySlot: morphoVaultTotalSupplySlot, + vaultTotalAssetsSlot: morphoVaultTotalAssetsSlot, + priceMultiplier: 1.0, + signer: user + ) + + createYieldVault( + signer: user, + strategyIdentifier: strategyIdentifier, + vaultIdentifier: wbtcTokenIdentifier, + amount: config.fundingPerAgent, + beFailed: false + ) + + let pid = (getLastPositionOpenedEvent(Test.eventsOfType(Type())) as! FlowALPv0.Opened).pid + let yieldVaultIDs = getYieldVaultIDs(address: user.address)! + let vaultId = yieldVaultIDs[0] + + // Linearly spread initial HF across [low, high] (Python uses random.uniform) + let agentInitialHF = config.numAgents > 1 + ? config.initialHFLow + (config.initialHFHigh - config.initialHFLow) * UFix64(i) / UFix64(config.numAgents - 1) + : config.initialHFLow + + // Step 1: Coerce position to the desired initial HF. + // Set temporary health params with targetHealth=initialHF, then force-rebalance. + // This makes the on-chain rebalancer push the position to exactly initialHF. + setPositionHealth( + signer: flowALPAccount, + pid: pid, + minHealth: agentInitialHF - 0.01, + targetHealth: agentInitialHF, + maxHealth: agentInitialHF + 0.01 + ) + rebalancePosition(signer: flowALPAccount, pid: pid, force: true, beFailed: false) + + // Step 2: Set the real health thresholds for the simulation. + setPositionHealth( + signer: flowALPAccount, + pid: pid, + minHealth: config.minHealth, + targetHealth: config.targetHealth, + maxHealth: config.maxHealth + ) + + users.append(user) + pids.append(pid) + vaultIds.append(vaultId) + + log(" Agent \(i): pid=\(pid) vaultId=\(vaultId) initialHF=\(agentInitialHF)") + i = i + 1 + } + + log("\n=== SIMULATION: \(label) ===") + log("Agents: \(config.numAgents)") + log("Funding per agent: \(config.fundingPerAgent) BTC (~\(config.fundingPerAgent * initialPrice) MOET)") + log("Tick interval: \(config.tickIntervalSeconds)s") + log("Price points: \(prices.length)") + log("Initial BTC price: $\(prices[0])") + log("Initial HF range: \(config.initialHFLow) - \(config.initialHFHigh)") + log("") + log("Rebalance Triggers:") + log(" HF (Position): triggers when HF < \(config.minHealth), rebalances to HF = \(config.targetHealth)") + log(" Liquidation: HF < 1.0 (on-chain effectiveCollateral/effectiveDebt)") + log("Notes: BTC $100K -> $76,342.50 (-23.66%) over 60 minutes") + + var liquidationCount = 0 + var previousBTCPrice = initialPrice + var lowestPrice = initialPrice + var highestPrice = initialPrice + var lowestHF = 100.0 + var prevVaultRebalanceCount = 0 + var prevPositionRebalanceCount = 0 + + let startTimestamp = getCurrentBlockTimestamp() + + var step = 0 + while step < prices.length { + let absolutePrice = prices[step] + let ytPrice = ytPriceAtTick(step, tickIntervalSeconds: config.tickIntervalSeconds, yieldAPR: config.yieldAPR) + + if absolutePrice < lowestPrice { + lowestPrice = absolutePrice + } + if absolutePrice > highestPrice { + highestPrice = absolutePrice + } + + if absolutePrice == previousBTCPrice { + step = step + 1 + continue + } + + let expectedTimestamp = startTimestamp + UFix64(step) * config.tickIntervalSeconds + let currentTimestamp = getCurrentBlockTimestamp() + if expectedTimestamp > currentTimestamp { + Test.moveTime(by: Fix64(expectedTimestamp - currentTimestamp)) + } + + applyPriceTick(btcPrice: absolutePrice, ytPrice: ytPrice, signer: users[0]) + + // Calculate HF BEFORE rebalancing + var preRebalanceHF: UFix64 = UFix64(getPositionHealth(pid: pids[0], beFailed: false)) + + // Rebalance agents sequentially — each swap moves pool price for next agent + if config.rebalanceInterval <= 1 || step % config.rebalanceInterval == 0 { + var a = 0 + while a < config.numAgents { + rebalanceYieldVault(signer: flowYieldVaultsAccount, id: vaultIds[a], force: false, beFailed: false) + rebalancePosition(signer: flowALPAccount, pid: pids[a], force: false, beFailed: false) + a = a + 1 + } + } + + // Arb bot: reset MOET:FUSDEV pool to peg with finite TVL + resetYieldPoolToFiniteTVL(ytPrice: ytPrice, tvl: config.moetYtPoolTVL, tvlFraction: config.moetYtPoolTVLFraction, tickRange: config.moetYtPoolTickRange) + + // Count actual rebalances that occurred this tick + let currentVaultRebalanceCount = Test.eventsOfType(Type()).length + let currentPositionRebalanceCount = Test.eventsOfType(Type()).length + let tickVaultRebalances = currentVaultRebalanceCount - prevVaultRebalanceCount + let tickPositionRebalances = currentPositionRebalanceCount - prevPositionRebalanceCount + prevVaultRebalanceCount = currentVaultRebalanceCount + prevPositionRebalanceCount = currentPositionRebalanceCount + + // Calculate HF AFTER rebalancing + var postRebalanceHF: UFix64 = UFix64(getPositionHealth(pid: pids[0], beFailed: false)) + + // Track lowest HF (use pre-rebalance to capture the actual low point) + if preRebalanceHF < lowestHF && preRebalanceHF > 0.0 { + lowestHF = preRebalanceHF + } + + // Log every tick with pre→post HF + log(" [t=\(step)] price=$\(absolutePrice) yt=\(ytPrice) HF=\(preRebalanceHF)->\(postRebalanceHF) vaultRebalances=\(tickVaultRebalances) positionRebalances=\(tickPositionRebalances)") + + // Liquidation check (pre-rebalance HF is the danger point) + if preRebalanceHF < 1.0 && preRebalanceHF > 0.0 { + liquidationCount = liquidationCount + 1 + log(" *** LIQUIDATION agent=0 at t=\(step)! HF=\(preRebalanceHF) ***") + } + + previousBTCPrice = absolutePrice + + step = step + 1 + } + + // Count actual rebalance events (not just attempts) + let vaultRebalanceCount = Test.eventsOfType(Type()).length + let positionRebalanceCount = Test.eventsOfType(Type()).length + + // Final state + let finalHF = UFix64(getPositionHealth(pid: pids[0], beFailed: false)) + let finalBTCCollateral = getBTCCollateralFromPosition(pid: pids[0]) + let finalDebt = getMOETDebtFromPosition(pid: pids[0]) + let finalYieldTokens = getAutoBalancerBalance(id: vaultIds[0])! + let finalYtPrice = ytPriceAtTick(prices.length - 1, tickIntervalSeconds: config.tickIntervalSeconds, yieldAPR: config.yieldAPR) + let finalPrice = prices[prices.length - 1] + let collateralValueMOET = finalBTCCollateral * previousBTCPrice + let ytValueMOET = finalYieldTokens * finalYtPrice + + log("\n=== SIMULATION RESULTS ===") + log("Agents: \(config.numAgents)") + log("Rebalance attempts: \(prices.length * config.numAgents)") + log("Vault rebalances: \(vaultRebalanceCount)") + log("Position rebalances: \(positionRebalanceCount)") + log("Liquidation count: \(liquidationCount)") + log("") + log("--- Price ---") + log("Initial BTC price: $\(initialPrice)") + log("Lowest BTC price: $\(lowestPrice)") + log("Highest BTC price: $\(highestPrice)") + log("Final BTC price: $\(finalPrice)") + log("") + log("--- Position ---") + log("Initial HF range: \(config.initialHFLow) - \(config.initialHFHigh)") + log("Lowest HF observed: \(lowestHF)") + log("Final HF (agent 0): \(finalHF)") + log("Final collateral: \(finalBTCCollateral) BTC (value: \(collateralValueMOET) MOET)") + log("Final debt: \(finalDebt) MOET") + log("Final yield tokens: \(finalYieldTokens) (value: \(ytValueMOET) MOET @ yt=\(finalYtPrice))") + log("===========================\n") + + return SimResult( + rebalanceCount: positionRebalanceCount, + liquidationCount: liquidationCount, + lowestHF: lowestHF, + finalHF: finalHF, + lowestPrice: lowestPrice, + finalPrice: finalPrice + ) +} + +// ============================================================================ +// TEST: Aggressive_1.01 — Initial HF 1.1–1.2, Target HF 1.01 +// ============================================================================ + +access(all) +fun test_Aggressive_1_01_ZeroLiquidations() { + // Python: rebalancingHF=targetHF=1.01, initialHF=1.1-1.2 + let result = runSimulation( + config: SimConfig( + prices: simulation_ht_vs_aave_prices, + tickIntervalSeconds: 60.0, + numAgents: 5, + fundingPerAgent: 1.0, + yieldAPR: simulation_ht_vs_aave_constants.yieldAPR, + expectedLiquidationCount: 0, + rebalanceInterval: 1, + minHealth: 1.01, + targetHealth: 1.01000001, + maxHealth: UFix64.max, // Python sim has no upper health bound + initialHFLow: 1.1, // Python initial_hf_range + initialHFHigh: 1.2, + moetYtPoolTVL: simulation_ht_vs_aave_pools["moet_yt"]!.size, + moetYtPoolTVLFraction: simulation_ht_vs_aave_pools["moet_yt"]!.concentration, + moetYtPoolTickRange: 100 // Python sim: ±100 ticks (~1% range) + ), + label: "Aggressive_1.01" + ) + + Test.assertEqual(0, result.liquidationCount) + Test.assert(result.finalHF > 1.0, message: "Expected final HF > 1.0 but got \(result.finalHF)") + Test.assert(result.lowestHF > 1.0, message: "Expected lowest HF > 1.0 but got \(result.lowestHF)") + + log("=== TEST PASSED: Aggressive_1.01 — Zero liquidations under 23.66% BTC crash ===") +} + +// ============================================================================ +// TEST: Balanced_1.1 — Initial HF 1.25–1.45, Target HF 1.1 +// ============================================================================ + +access(all) +fun test_Balanced_1_1_ZeroLiquidations() { + // Python: rebalancingHF=targetHF=1.10, initialHF=1.25-1.45 + let result = runSimulation( + config: SimConfig( + prices: simulation_ht_vs_aave_prices, + tickIntervalSeconds: 60.0, + numAgents: 5, + fundingPerAgent: 1.0, + yieldAPR: simulation_ht_vs_aave_constants.yieldAPR, + expectedLiquidationCount: 0, + rebalanceInterval: 1, + minHealth: 1.1, + targetHealth: 1.10000001, + maxHealth: UFix64.max, // Python sim has no upper health bound + initialHFLow: 1.25, // Python initial_hf_range + initialHFHigh: 1.45, + moetYtPoolTVL: simulation_ht_vs_aave_pools["moet_yt"]!.size, + moetYtPoolTVLFraction: simulation_ht_vs_aave_pools["moet_yt"]!.concentration, + moetYtPoolTickRange: 100 // Python sim: ±100 ticks (~1% range) + ), + label: "Balanced_1.1" + ) + + Test.assertEqual(0, result.liquidationCount) + Test.assert(result.finalHF > 1.0, message: "Expected final HF > 1.0 but got \(result.finalHF)") + Test.assert(result.lowestHF > 1.0, message: "Expected lowest HF > 1.0 but got \(result.lowestHF)") + + log("=== TEST PASSED: Balanced_1.1 — Zero liquidations under 23.66% BTC crash ===") +} + +// ============================================================================ +// TEST: Conservative_1.05 — Initial HF 1.3–1.5, Target HF 1.05 +// ============================================================================ + +access(all) +fun test_Conservative_1_05_ZeroLiquidations() { + // Python: rebalancingHF=targetHF=1.05, initialHF=1.3-1.5 + let result = runSimulation( + config: SimConfig( + prices: simulation_ht_vs_aave_prices, + tickIntervalSeconds: 60.0, + numAgents: 5, + fundingPerAgent: 1.0, + yieldAPR: simulation_ht_vs_aave_constants.yieldAPR, + expectedLiquidationCount: 0, + rebalanceInterval: 1, + minHealth: 1.05, + targetHealth: 1.05000001, + maxHealth: UFix64.max, // Python sim has no upper health bound + initialHFLow: 1.3, // Python initial_hf_range + initialHFHigh: 1.5, + moetYtPoolTVL: simulation_ht_vs_aave_pools["moet_yt"]!.size, + moetYtPoolTVLFraction: simulation_ht_vs_aave_pools["moet_yt"]!.concentration, + moetYtPoolTickRange: 100 // Python sim: ±100 ticks (~1% range) + ), + label: "Conservative_1.05" + ) + + Test.assertEqual(0, result.liquidationCount) + Test.assert(result.finalHF > 1.0, message: "Expected final HF > 1.0 but got \(result.finalHF)") + Test.assert(result.lowestHF > 1.0, message: "Expected lowest HF > 1.0 but got \(result.lowestHF)") + + log("=== TEST PASSED: Conservative_1.05 — Zero liquidations under 23.66% BTC crash ===") +} + +// ============================================================================ +// TEST: Mixed_1.075 — Initial HF 1.1–1.5, Target HF 1.075 +// ============================================================================ + +access(all) +fun test_Mixed_1_075_ZeroLiquidations() { + // Python: rebalancingHF=targetHF=1.075, initialHF=1.1-1.5 + let result = runSimulation( + config: SimConfig( + prices: simulation_ht_vs_aave_prices, + tickIntervalSeconds: 60.0, + numAgents: 5, + fundingPerAgent: 1.0, + yieldAPR: simulation_ht_vs_aave_constants.yieldAPR, + expectedLiquidationCount: 0, + rebalanceInterval: 1, + minHealth: 1.075, + targetHealth: 1.07500001, + maxHealth: UFix64.max, // Python sim has no upper health bound + initialHFLow: 1.1, // Python initial_hf_range + initialHFHigh: 1.5, + moetYtPoolTVL: simulation_ht_vs_aave_pools["moet_yt"]!.size, + moetYtPoolTVLFraction: simulation_ht_vs_aave_pools["moet_yt"]!.concentration, + moetYtPoolTickRange: 100 // Python sim: ±100 ticks (~1% range) + ), + label: "Mixed_1.075" + ) + + Test.assertEqual(0, result.liquidationCount) + Test.assert(result.finalHF > 1.0, message: "Expected final HF > 1.0 but got \(result.finalHF)") + Test.assert(result.lowestHF > 1.0, message: "Expected lowest HF > 1.0 but got \(result.lowestHF)") + + log("=== TEST PASSED: Mixed_1.075 — Zero liquidations under 23.66% BTC crash ===") +} + +// ============================================================================ +// TEST: Moderate_1.025 — Initial HF 1.2–1.4, Target HF 1.025 +// ============================================================================ + +access(all) +fun test_Moderate_1_025_ZeroLiquidations() { + // Python: rebalancingHF=targetHF=1.025, initialHF=1.2-1.4 + let result = runSimulation( + config: SimConfig( + prices: simulation_ht_vs_aave_prices, + tickIntervalSeconds: 60.0, + numAgents: 5, + fundingPerAgent: 1.0, + yieldAPR: simulation_ht_vs_aave_constants.yieldAPR, + expectedLiquidationCount: 0, + rebalanceInterval: 1, + minHealth: 1.025, + targetHealth: 1.02500001, + maxHealth: UFix64.max, // Python sim has no upper health bound + initialHFLow: 1.2, // Python initial_hf_range + initialHFHigh: 1.4, + moetYtPoolTVL: simulation_ht_vs_aave_pools["moet_yt"]!.size, + moetYtPoolTVLFraction: simulation_ht_vs_aave_pools["moet_yt"]!.concentration, + moetYtPoolTickRange: 100 // Python sim: ±100 ticks (~1% range) + ), + label: "Moderate_1.025" + ) + + Test.assertEqual(0, result.liquidationCount) + Test.assert(result.finalHF > 1.0, message: "Expected final HF > 1.0 but got \(result.finalHF)") + Test.assert(result.lowestHF > 1.0, message: "Expected lowest HF > 1.0 but got \(result.lowestHF)") + + log("=== TEST PASSED: Moderate_1.025 — Zero liquidations under 23.66% BTC crash ===") +} + diff --git a/cadence/tests/simulation_base_case_stress_helpers.cdc b/cadence/tests/simulation_base_case_stress_helpers.cdc new file mode 100644 index 00000000..02953aff --- /dev/null +++ b/cadence/tests/simulation_base_case_stress_helpers.cdc @@ -0,0 +1,288 @@ +import Test + +// AUTO-GENERATED — do not edit manually +// UnitZero sim: comprehensive_ht_vs_aave_analysis.py +// UnitZero report: High_Tide_vs_AAVE_Executive_Summary_Clean.md +// +// BTC: $100K → $76,342.50 (-23.66%) over 60 minutes +// Exponential decline: price[t] = 100000 * (76342.5/100000)^(t/60) + +access(all) struct SimAgent { + access(all) let count: Int + access(all) let initialHF: UFix64 + access(all) let rebalancingHF: UFix64 + access(all) let targetHF: UFix64 + access(all) let debtPerAgent: UFix64 + access(all) let totalSystemDebt: UFix64 + + init( + count: Int, + initialHF: UFix64, + rebalancingHF: UFix64, + targetHF: UFix64, + debtPerAgent: UFix64, + totalSystemDebt: UFix64 + ) { + self.count = count + self.initialHF = initialHF + self.rebalancingHF = rebalancingHF + self.targetHF = targetHF + self.debtPerAgent = debtPerAgent + self.totalSystemDebt = totalSystemDebt + } +} + +access(all) struct SimPool { + access(all) let size: UFix64 + access(all) let concentration: UFix64 + access(all) let feeTier: UFix64 + + init(size: UFix64, concentration: UFix64, feeTier: UFix64) { + self.size = size + self.concentration = concentration + self.feeTier = feeTier + } +} + +access(all) struct SimConstants { + access(all) let btcCollateralFactor: UFix64 + access(all) let btcLiquidationThreshold: UFix64 + access(all) let yieldAPR: UFix64 + access(all) let directMintYT: Bool + + init( + btcCollateralFactor: UFix64, + btcLiquidationThreshold: UFix64, + yieldAPR: UFix64, + directMintYT: Bool + ) { + self.btcCollateralFactor = btcCollateralFactor + self.btcLiquidationThreshold = btcLiquidationThreshold + self.yieldAPR = yieldAPR + self.directMintYT = directMintYT + } +} + +// ============================================================================ +// SHARED PRICE CURVE — 61 ticks (minute 0..60) +// ============================================================================ + +access(all) let simulation_ht_vs_aave_prices: [UFix64] = [ + 100000.00000000, + 99551.10988532, + 99104.23479398, + 98659.36568076, + 98216.49354101, + 97775.60941052, + 97336.70436530, + 96899.76952145, + 96464.79603492, + 96031.77510137, + 95600.69795598, + 95171.55587329, + 94744.34016698, + 94319.04218975, + 93895.65333310, + 93474.16502717, + 93054.56874058, + 92636.85598024, + 92221.01829119, + 91807.04725642, + 91394.93449671, + 90984.67167043, + 90576.25047343, + 90169.66263880, + 89764.89993677, + 89361.95417450, + 88960.81719592, + 88561.48088159, + 88163.93714849, + 87768.17794992, + 87374.19527526, + 86981.98114989, + 86591.52763495, + 86202.82682725, + 85815.87085904, + 85430.65189793, + 85047.16214665, + 84665.39384295, + 84285.33925943, + 83906.99070337, + 83530.34051657, + 83155.38107523, + 82782.10478976, + 82410.50410463, + 82040.57149825, + 81672.29948276, + 81305.68060395, + 80940.70744104, + 80577.37260658, + 80215.66874628, + 79855.58853885, + 79497.12469588, + 79140.26996166, + 78785.01711307, + 78431.35895940, + 78079.28834222, + 77728.79813524, + 77379.88124414, + 77032.53060649, + 76686.73919150, + 76342.50000000 +] + +// ============================================================================ +// SHARED PROTOCOL CONSTANTS +// ============================================================================ + +access(all) let simulation_ht_vs_aave_constants: SimConstants = SimConstants( + btcCollateralFactor: 0.75000000, + btcLiquidationThreshold: 0.80000000, + yieldAPR: 0.10000000, + directMintYT: true +) + +access(all) let simulation_ht_vs_aave_pools: {String: SimPool} = { + "moet_yt": SimPool( + size: 500000.00000000, + concentration: 0.95000000, + feeTier: 0.00050000 + ), + "moet_btc": SimPool( + size: 500000.00000000, + concentration: 0.80000000, + feeTier: 0.00300000 + ) +} + +access(all) let simulation_ht_vs_aave_durationMinutes: Int = 60 + +// ============================================================================ +// SCENARIO 1: Aggressive (Target HF 1.01) +// ============================================================================ + +access(all) let simulation_aggressive_1_01_agents: [SimAgent] = [ + SimAgent( + count: 5, + initialHF: 1.10000000, + rebalancingHF: 1.05000000, + targetHF: 1.01000000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ), + SimAgent( + count: 5, + initialHF: 1.20000000, + rebalancingHF: 1.05000000, + targetHF: 1.01000000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ) +] + +access(all) let simulation_aggressive_1_01_expectedLiquidationCount: Int = 0 +access(all) let simulation_aggressive_1_01_expectedAllAgentsSurvive: Bool = true + +// ============================================================================ +// SCENARIO 2: Moderate (Target HF 1.025) +// ============================================================================ + +access(all) let simulation_moderate_1_025_agents: [SimAgent] = [ + SimAgent( + count: 5, + initialHF: 1.20000000, + rebalancingHF: 1.05000000, + targetHF: 1.02500000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ), + SimAgent( + count: 5, + initialHF: 1.40000000, + rebalancingHF: 1.05000000, + targetHF: 1.02500000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ) +] + +access(all) let simulation_moderate_1_025_expectedLiquidationCount: Int = 0 +access(all) let simulation_moderate_1_025_expectedAllAgentsSurvive: Bool = true + +// ============================================================================ +// SCENARIO 3: Conservative (Target HF 1.05) +// ============================================================================ + +access(all) let simulation_conservative_1_05_agents: [SimAgent] = [ + SimAgent( + count: 5, + initialHF: 1.30000000, + rebalancingHF: 1.10000000, + targetHF: 1.05000000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ), + SimAgent( + count: 5, + initialHF: 1.50000000, + rebalancingHF: 1.10000000, + targetHF: 1.05000000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ) +] + +access(all) let simulation_conservative_1_05_expectedLiquidationCount: Int = 0 +access(all) let simulation_conservative_1_05_expectedAllAgentsSurvive: Bool = true + +// ============================================================================ +// SCENARIO 4: Mixed (Target HF 1.075) +// ============================================================================ + +access(all) let simulation_mixed_1_075_agents: [SimAgent] = [ + SimAgent( + count: 5, + initialHF: 1.10000000, + rebalancingHF: 1.05000000, + targetHF: 1.07500000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ), + SimAgent( + count: 5, + initialHF: 1.50000000, + rebalancingHF: 1.05000000, + targetHF: 1.07500000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ) +] + +access(all) let simulation_mixed_1_075_expectedLiquidationCount: Int = 0 +access(all) let simulation_mixed_1_075_expectedAllAgentsSurvive: Bool = true + +// ============================================================================ +// SCENARIO 5: Balanced (Target HF 1.1) +// ============================================================================ + +access(all) let simulation_balanced_1_1_agents: [SimAgent] = [ + SimAgent( + count: 5, + initialHF: 1.25000000, + rebalancingHF: 1.10000000, + targetHF: 1.10000000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ), + SimAgent( + count: 5, + initialHF: 1.45000000, + rebalancingHF: 1.10000000, + targetHF: 1.10000000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 666665.00000000 + ) +] + +access(all) let simulation_balanced_1_1_expectedLiquidationCount: Int = 0 +access(all) let simulation_balanced_1_1_expectedAllAgentsSurvive: Bool = true diff --git a/cadence/tests/simulation_btc_daily_2025.cdc b/cadence/tests/simulation_btc_daily_2025.cdc index 1615a630..3b38423a 100644 --- a/cadence/tests/simulation_btc_daily_2025.cdc +++ b/cadence/tests/simulation_btc_daily_2025.cdc @@ -198,10 +198,11 @@ access(all) fun applyPriceTick(btcPrice: UFix64, ytPrice: UFix64, user: Test.Tes priceTokenBPerTokenA: UFix128(btcPrice), tokenABalanceSlot: wbtcBalanceSlot, tokenBBalanceSlot: pyusd0BalanceSlot, + signer: coaOwnerAccount, tvl: btcPool.size, - concentration: btcPool.concentration, - tokenBPriceUSD: 1.0, - signer: coaOwnerAccount + tickRange: 100, + tvlFraction: btcPool.concentration, + tokenBPriceUSD: 1.0 ) setPoolToPriceWithTVL( @@ -212,10 +213,11 @@ access(all) fun applyPriceTick(btcPrice: UFix64, ytPrice: UFix64, user: Test.Tes priceTokenBPerTokenA: UFix128(ytPrice), tokenABalanceSlot: moetBalanceSlot, tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount, tvl: ytPool.size, - concentration: ytPool.concentration, - tokenBPriceUSD: ytPrice, - signer: coaOwnerAccount + tickRange: 100, + tvlFraction: ytPool.concentration, + tokenBPriceUSD: ytPrice ) setVaultSharePrice( diff --git a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc index 856628ce..391f5ca6 100644 --- a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc +++ b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc @@ -50,9 +50,12 @@ access(all) fun slotToNum(_ slot: String): UInt256 { return num } -// Properly seed Uniswap V3 pool with STRUCTURALLY VALID state -// This creates: slot0, observations, liquidity, ticks (with initialized flag), bitmap, and token balances -// Pass 0.0 for tvl and concentration to create a full-range infinite liquidity pool (useful for no slippage) +// Properly seed Uniswap V3 pool with STRUCTURALLY VALID state. +// This creates: slot0, observations, liquidity, ticks (with initialized flag), bitmap, and token balances. +// +// Two modes: +// Finite: tvl > 0 && tickRange > 0 — ±tickRange ticks from current price, tvlFraction of TVL placed +// Infinite: otherwise — full-range liquidity with 2^128-1 L (zero slippage) transaction( factoryAddress: String, tokenAAddress: String, @@ -62,7 +65,8 @@ transaction( tokenABalanceSlot: UInt256, tokenBBalanceSlot: UInt256, tvl: UFix64, - concentration: UFix64, + tickRange: Int256, + tvlFraction: UFix64, tokenBPriceUSD: UFix64 ) { let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount @@ -187,69 +191,47 @@ transaction( var token0Balance: UInt256 = 0 var token1Balance: UInt256 = 0 - if tvl > 0.0 && concentration > 0.0 && concentration < 1.0 { - // --- Concentrated liquidity mode --- - let halfWidth = 1.0 - concentration + if tvl > 0.0 && tickRange > 0 { + // --- Finite liquidity mode --- + // ±tickRange ticks from current price, tvlFraction of TVL placed + let currentTick = getTickAtSqrtRatio(sqrtPriceX96: targetSqrtPriceX96) + let rawLower = currentTick - tickRange + let rawUpper = currentTick + tickRange - // sqrt(1 +/- halfWidth) via integer sqrt at 1e16 scale for 8-digit precision - let PRECISION: UInt256 = 10_000_000_000_000_000 - let SQRT_PRECISION: UInt256 = 100_000_000 - let halfWidthScaled = UInt256(UInt64(halfWidth * 100_000_000.0)) * 100_000_000 - - let upperMultNum = isqrt(PRECISION + halfWidthScaled) - let lowerMultNum = isqrt(PRECISION - halfWidthScaled) - - var sqrtPriceUpper = targetSqrtPriceX96 * upperMultNum / SQRT_PRECISION - var sqrtPriceLower = targetSqrtPriceX96 * lowerMultNum / SQRT_PRECISION - - let MAX_SQRT: UInt256 = 1461446703485210103287273052203988822378723970341 - let MIN_SQRT: UInt256 = 4295128739 - if sqrtPriceUpper > MAX_SQRT { sqrtPriceUpper = MAX_SQRT } - if sqrtPriceLower < MIN_SQRT + 1 { sqrtPriceLower = MIN_SQRT + 1 } - - let rawTickUpper = getTickAtSqrtRatio(sqrtPriceX96: sqrtPriceUpper) - let rawTickLower = getTickAtSqrtRatio(sqrtPriceX96: sqrtPriceLower) - - // Align tickLower down, tickUpper up to tickSpacing - tickLower = rawTickLower / tickSpacing * tickSpacing - if rawTickLower < 0 && rawTickLower % tickSpacing != 0 { + // Align lower down, upper up to tick spacing + tickLower = rawLower / tickSpacing * tickSpacing + if rawLower < 0 && rawLower % tickSpacing != 0 { tickLower = tickLower - tickSpacing } - tickUpper = rawTickUpper / tickSpacing * tickSpacing - if rawTickUpper > 0 && rawTickUpper % tickSpacing != 0 { + tickUpper = rawUpper / tickSpacing * tickSpacing + if rawUpper > 0 && rawUpper % tickSpacing != 0 { tickUpper = tickUpper + tickSpacing } + assert(tickLower < tickUpper, message: "Tick range is empty after alignment") - assert(tickLower < tickUpper, message: "Concentrated tick range is empty after alignment") - + // Step 2: Compute liquidity and token balances from TVL let sqrtPa = getSqrtRatioAtTick(tick: tickLower) let sqrtPb = getSqrtRatioAtTick(tick: tickUpper) - // Convert TVL/2 from USD to token1 smallest units using token prices let effectiveBPrice = tokenBPriceUSD > 0.0 ? tokenBPriceUSD : 1.0 var token1PriceUSD = effectiveBPrice if tokenAAddress >= tokenBAddress { - // token1 = tokenA; tokenA is worth priceTokenBPerTokenA * tokenBPrice in USD token1PriceUSD = UFix64(priceTokenBPerTokenA) * effectiveBPrice } - let tvlHalfToken1 = tvl / 2.0 / token1PriceUSD - let tvlHalfWhole = UInt256(UInt64(tvlHalfToken1)) - var tvlHalfSmallest = tvlHalfWhole + let fraction = tvlFraction > 0.0 ? tvlFraction : 1.0 + let tvlHalfToken1 = tvl / 2.0 * fraction / token1PriceUSD + var tvlHalfSmallest = UInt256(UInt64(tvlHalfToken1)) var td: UInt8 = 0 while td < token1Decimals { tvlHalfSmallest = tvlHalfSmallest * 10 td = td + 1 } - // L = tvlHalfSmallest * Q96 / (sqrtP - sqrtPa) let sqrtPDiffA = targetSqrtPriceX96 - sqrtPa assert(sqrtPDiffA > 0, message: "sqrtP must be > sqrtPa for liquidity calculation") liquidityAmount = tvlHalfSmallest * Q96 / sqrtPDiffA - // token1 = L * (sqrtP - sqrtPa) / Q96 token1Balance = liquidityAmount * sqrtPDiffA / Q96 - - // token0 = L * (sqrtPb - sqrtP) / sqrtPb * Q96 / sqrtP let sqrtPDiffB = sqrtPb - targetSqrtPriceX96 token0Balance = liquidityAmount * sqrtPDiffB / sqrtPb * Q96 / targetSqrtPriceX96 } else { @@ -750,18 +732,6 @@ access(all) fun bytesToUInt256(_ bytes: [UInt8]): UInt256 { return result } -/// Integer square root via Newton's method. Returns floor(sqrt(x)). -access(all) fun isqrt(_ x: UInt256): UInt256 { - if x == 0 { return 0 } - var z = x - var y = (z + 1) / 2 - while y < z { - z = y - y = (z + x / z) / 2 - } - return z -} - access(all) fun getTokenDecimals(evmContractAddress: EVM.EVMAddress): UInt8 { let zeroAddress = EVM.addressFromString("0x0000000000000000000000000000000000000000") let callResult = EVM.dryCall(