|
| 1 | +import Test |
| 2 | +import "EVM" |
| 3 | + |
| 4 | +/* --- ERC4626 Vault State Manipulation --- */ |
| 5 | + |
| 6 | +/// Set vault share price by setting totalAssets to a specific base value, then multiplying by the price multiplier |
| 7 | +/// Manipulates both asset.balanceOf(vault) and vault._totalAssets to bypass maxRate capping |
| 8 | +/// Caller should provide baseAssets large enough to prevent slippage during price changes |
| 9 | +access(all) fun setVaultSharePrice( |
| 10 | + vaultAddress: String, |
| 11 | + assetAddress: String, |
| 12 | + assetBalanceSlot: UInt256, |
| 13 | + vaultTotalAssetsSlot: String, |
| 14 | + baseAssets: UFix64, |
| 15 | + priceMultiplier: UFix64, |
| 16 | + signer: Test.TestAccount |
| 17 | +) { |
| 18 | + // Convert UFix64 baseAssets to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8) |
| 19 | + let baseAssetsBytes = baseAssets.toBigEndianBytes() |
| 20 | + var baseAssetsUInt64: UInt64 = 0 |
| 21 | + for byte in baseAssetsBytes { |
| 22 | + baseAssetsUInt64 = (baseAssetsUInt64 << 8) + UInt64(byte) |
| 23 | + } |
| 24 | + let baseAssetsUInt256 = UInt256(baseAssetsUInt64) |
| 25 | + |
| 26 | + // Calculate target: baseAssets * multiplier |
| 27 | + let multiplierBytes = priceMultiplier.toBigEndianBytes() |
| 28 | + var multiplierUInt64: UInt64 = 0 |
| 29 | + for byte in multiplierBytes { |
| 30 | + multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte) |
| 31 | + } |
| 32 | + let targetAssets = (baseAssetsUInt256 * UInt256(multiplierUInt64)) / UInt256(100000000) |
| 33 | + |
| 34 | + let result = Test.executeTransaction( |
| 35 | + Test.Transaction( |
| 36 | + code: Test.readFile("transactions/set_erc4626_vault_price.cdc"), |
| 37 | + authorizers: [signer.address], |
| 38 | + signers: [signer], |
| 39 | + arguments: [vaultAddress, assetAddress, assetBalanceSlot, vaultTotalAssetsSlot, priceMultiplier, targetAssets] |
| 40 | + ) |
| 41 | + ) |
| 42 | + Test.expect(result, Test.beSucceeded()) |
| 43 | +} |
| 44 | + |
| 45 | +/* --- Uniswap V3 Pool State Manipulation --- */ |
| 46 | + |
| 47 | +/// Set Uniswap V3 pool to a specific price via EVM.store |
| 48 | +/// Creates pool if it doesn't exist, then manipulates state |
| 49 | +access(all) fun setPoolToPrice( |
| 50 | + factoryAddress: String, |
| 51 | + tokenAAddress: String, |
| 52 | + tokenBAddress: String, |
| 53 | + fee: UInt64, |
| 54 | + priceTokenBPerTokenA: UFix64, |
| 55 | + tokenABalanceSlot: UInt256, |
| 56 | + tokenBBalanceSlot: UInt256, |
| 57 | + signer: Test.TestAccount |
| 58 | +) { |
| 59 | + // Sort tokens (Uniswap V3 requires token0 < token1) |
| 60 | + let token0 = tokenAAddress < tokenBAddress ? tokenAAddress : tokenBAddress |
| 61 | + let token1 = tokenAAddress < tokenBAddress ? tokenBAddress : tokenAAddress |
| 62 | + let token0BalanceSlot = tokenAAddress < tokenBAddress ? tokenABalanceSlot : tokenBBalanceSlot |
| 63 | + let token1BalanceSlot = tokenAAddress < tokenBAddress ? tokenBBalanceSlot : tokenABalanceSlot |
| 64 | + |
| 65 | + let poolPrice = tokenAAddress < tokenBAddress ? priceTokenBPerTokenA : 1.0 / priceTokenBPerTokenA |
| 66 | + |
| 67 | + let targetSqrtPriceX96 = calculateSqrtPriceX96(price: poolPrice) |
| 68 | + let targetTick = calculateTick(price: poolPrice) |
| 69 | + |
| 70 | + let createResult = Test.executeTransaction( |
| 71 | + Test.Transaction( |
| 72 | + code: Test.readFile("transactions/ensure_uniswap_pool_exists.cdc"), |
| 73 | + authorizers: [signer.address], |
| 74 | + signers: [signer], |
| 75 | + arguments: [factoryAddress, token0, token1, fee, targetSqrtPriceX96] |
| 76 | + ) |
| 77 | + ) |
| 78 | + Test.expect(createResult, Test.beSucceeded()) |
| 79 | + |
| 80 | + let seedResult = Test.executeTransaction( |
| 81 | + Test.Transaction( |
| 82 | + code: Test.readFile("transactions/set_uniswap_v3_pool_price.cdc"), |
| 83 | + authorizers: [signer.address], |
| 84 | + signers: [signer], |
| 85 | + arguments: [factoryAddress, token0, token1, fee, targetSqrtPriceX96, targetTick, token0BalanceSlot, token1BalanceSlot] |
| 86 | + ) |
| 87 | + ) |
| 88 | + Test.expect(seedResult, Test.beSucceeded()) |
| 89 | +} |
| 90 | + |
| 91 | +/* --- Internal Math Utilities --- */ |
| 92 | + |
| 93 | +/// Calculate sqrtPriceX96 from a price ratio |
| 94 | +/// Returns sqrt(price) * 2^96 as a string for Uniswap V3 pool initialization |
| 95 | +access(self) fun calculateSqrtPriceX96(price: UFix64): String { |
| 96 | + // Convert UFix64 to UInt256 (UFix64 has 8 decimal places) |
| 97 | + // price is stored as integer * 10^8 internally |
| 98 | + let priceBytes = price.toBigEndianBytes() |
| 99 | + var priceUInt64: UInt64 = 0 |
| 100 | + for byte in priceBytes { |
| 101 | + priceUInt64 = (priceUInt64 << 8) + UInt64(byte) |
| 102 | + } |
| 103 | + let priceScaled = UInt256(priceUInt64) // This is price * 10^8 |
| 104 | + |
| 105 | + // We want: sqrt(price) * 2^96 |
| 106 | + // = sqrt(priceScaled / 10^8) * 2^96 |
| 107 | + // = sqrt(priceScaled) * 2^96 / sqrt(10^8) |
| 108 | + // = sqrt(priceScaled) * 2^96 / 10^4 |
| 109 | + |
| 110 | + // Calculate sqrt(priceScaled) with scale factor 2^48 for precision |
| 111 | + // sqrt(priceScaled) * 2^48 |
| 112 | + let sqrtPriceScaled = sqrt(n: priceScaled, scaleFactor: UInt256(1) << 48) |
| 113 | + |
| 114 | + // Now we have: sqrt(priceScaled) * 2^48 |
| 115 | + // We want: sqrt(priceScaled) * 2^96 / 10^4 |
| 116 | + // = (sqrt(priceScaled) * 2^48) * 2^48 / 10^4 |
| 117 | + |
| 118 | + let sqrtPriceX96 = (sqrtPriceScaled * (UInt256(1) << 48)) / UInt256(10000) |
| 119 | + |
| 120 | + return sqrtPriceX96.toString() |
| 121 | +} |
| 122 | + |
| 123 | +/// Calculate tick from price ratio |
| 124 | +/// Returns tick = floor(log_1.0001(price)) for Uniswap V3 tick spacing |
| 125 | +access(self) fun calculateTick(price: UFix64): Int256 { |
| 126 | + // Convert UFix64 to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8) |
| 127 | + let priceBytes = price.toBigEndianBytes() |
| 128 | + var priceUInt64: UInt64 = 0 |
| 129 | + for byte in priceBytes { |
| 130 | + priceUInt64 = (priceUInt64 << 8) + UInt64(byte) |
| 131 | + } |
| 132 | + |
| 133 | + // priceUInt64 is price * 10^8 |
| 134 | + // Scale to 10^18 for precision: price * 10^18 = priceUInt64 * 10^10 |
| 135 | + let priceScaled = UInt256(priceUInt64) * UInt256(10000000000) // 10^10 |
| 136 | + let scaleFactor = UInt256(1000000000000000000) // 10^18 |
| 137 | + |
| 138 | + // Calculate ln(price) * 10^18 |
| 139 | + let lnPrice = ln(x: priceScaled, scaleFactor: scaleFactor) |
| 140 | + |
| 141 | + // ln(1.0001) * 10^18 ≈ 99995000333083 |
| 142 | + let ln1_0001 = Int256(99995000333083) |
| 143 | + |
| 144 | + // tick = ln(price) / ln(1.0001) |
| 145 | + // lnPrice is already scaled by 10^18 |
| 146 | + // ln1_0001 is already scaled by 10^18 |
| 147 | + // So: tick = (lnPrice * 10^18) / (ln1_0001 * 10^18) = lnPrice / ln1_0001 |
| 148 | + |
| 149 | + let tick = lnPrice / ln1_0001 |
| 150 | + |
| 151 | + return tick |
| 152 | +} |
| 153 | + |
| 154 | +/// Calculate square root using Newton's method for UInt256 |
| 155 | +/// Returns sqrt(n) * scaleFactor to maintain precision |
| 156 | +access(self) fun sqrt(n: UInt256, scaleFactor: UInt256): UInt256 { |
| 157 | + if n == UInt256(0) { |
| 158 | + return UInt256(0) |
| 159 | + } |
| 160 | + |
| 161 | + // Initial guess: n/2 (scaled) |
| 162 | + var x = (n * scaleFactor) / UInt256(2) |
| 163 | + var prevX = UInt256(0) |
| 164 | + |
| 165 | + // Newton's method: x_new = (x + n*scale^2/x) / 2 |
| 166 | + // Iterate until convergence (max 50 iterations for safety) |
| 167 | + var iterations = 0 |
| 168 | + while x != prevX && iterations < 50 { |
| 169 | + prevX = x |
| 170 | + // x_new = (x + (n * scaleFactor^2) / x) / 2 |
| 171 | + let nScaled = n * scaleFactor * scaleFactor |
| 172 | + x = (x + nScaled / x) / UInt256(2) |
| 173 | + iterations = iterations + 1 |
| 174 | + } |
| 175 | + |
| 176 | + return x |
| 177 | +} |
| 178 | + |
| 179 | +/// Calculate natural logarithm using Taylor series |
| 180 | +/// ln(x) for x > 0, returns ln(x) * scaleFactor for precision |
| 181 | +access(self) fun ln(x: UInt256, scaleFactor: UInt256): Int256 { |
| 182 | + if x == UInt256(0) { |
| 183 | + panic("ln(0) is undefined") |
| 184 | + } |
| 185 | + |
| 186 | + // For better convergence, reduce x to range [0.5, 1.5] using: |
| 187 | + // ln(x) = ln(2^n * y) = n*ln(2) + ln(y) where y is in [0.5, 1.5] |
| 188 | + |
| 189 | + var value = x |
| 190 | + var n = 0 |
| 191 | + |
| 192 | + // Scale down if x > 1.5 * scaleFactor |
| 193 | + let threshold = (scaleFactor * UInt256(3)) / UInt256(2) |
| 194 | + while value > threshold { |
| 195 | + value = value / UInt256(2) |
| 196 | + n = n + 1 |
| 197 | + } |
| 198 | + |
| 199 | + // Scale up if x < 0.5 * scaleFactor |
| 200 | + let lowerThreshold = scaleFactor / UInt256(2) |
| 201 | + while value < lowerThreshold { |
| 202 | + value = value * UInt256(2) |
| 203 | + n = n - 1 |
| 204 | + } |
| 205 | + |
| 206 | + // Now value is in [0.5*scale, 1.5*scale], compute ln(value/scale) |
| 207 | + // Use Taylor series: ln(1+z) = z - z^2/2 + z^3/3 - z^4/4 + ... |
| 208 | + // where z = value/scale - 1 |
| 209 | + |
| 210 | + let z = value > scaleFactor |
| 211 | + ? Int256(value - scaleFactor) |
| 212 | + : -Int256(scaleFactor - value) |
| 213 | + |
| 214 | + // Calculate Taylor series terms until convergence |
| 215 | + var result = z // First term: z |
| 216 | + var term = z |
| 217 | + var i = 2 |
| 218 | + var prevResult = Int256(0) |
| 219 | + |
| 220 | + // Calculate terms until convergence (term becomes negligible or result stops changing) |
| 221 | + // Max 50 iterations for safety |
| 222 | + while i <= 50 && result != prevResult { |
| 223 | + prevResult = result |
| 224 | + |
| 225 | + // term = term * z / scaleFactor |
| 226 | + term = (term * z) / Int256(scaleFactor) |
| 227 | + |
| 228 | + // Add or subtract term/i based on sign |
| 229 | + if i % 2 == 0 { |
| 230 | + result = result - term / Int256(i) |
| 231 | + } else { |
| 232 | + result = result + term / Int256(i) |
| 233 | + } |
| 234 | + i = i + 1 |
| 235 | + } |
| 236 | + |
| 237 | + // Add n * ln(2) * scaleFactor |
| 238 | + // ln(2) ≈ 0.693147180559945309417232121458 |
| 239 | + // ln(2) * 10^18 ≈ 693147180559945309 |
| 240 | + let ln2Scaled = Int256(693147180559945309) |
| 241 | + let nScaled = Int256(n) * ln2Scaled |
| 242 | + |
| 243 | + // Scale to our scaleFactor (assuming scaleFactor is 10^18) |
| 244 | + result = result + nScaled |
| 245 | + |
| 246 | + return result |
| 247 | +} |
0 commit comments