From 728649516f551bfb317506f9662d42c4e8abb142 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 12 Mar 2026 14:35:29 -0700 Subject: [PATCH 1/5] Update Flow CLI Install to use Latest Version --- .github/workflows/cadence_tests.yml | 2 +- .github/workflows/e2e_tests.yml | 2 +- .github/workflows/incrementfi_tests.yml | 2 +- .github/workflows/punchswap.yml | 2 +- .github/workflows/scheduled_rebalance_tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml index f3e23728..0a8693a7 100644 --- a/.github/workflows/cadence_tests.yml +++ b/.github/workflows/cadence_tests.yml @@ -37,7 +37,7 @@ jobs: restore-keys: | ${{ runner.os }}-flow-emulator-fork- - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" - name: Flow CLI Version run: flow version - name: Update PATH diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index f349df89..1801af75 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -30,7 +30,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" - name: Flow CLI Version run: flow version - name: Update PATH diff --git a/.github/workflows/incrementfi_tests.yml b/.github/workflows/incrementfi_tests.yml index 98d3191e..b2a269b8 100644 --- a/.github/workflows/incrementfi_tests.yml +++ b/.github/workflows/incrementfi_tests.yml @@ -20,7 +20,7 @@ jobs: token: ${{ secrets.GH_PAT }} submodules: recursive - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" - name: Flow CLI Version run: flow version - name: Update PATH diff --git a/.github/workflows/punchswap.yml b/.github/workflows/punchswap.yml index 47f38a7c..af61fca4 100644 --- a/.github/workflows/punchswap.yml +++ b/.github/workflows/punchswap.yml @@ -26,7 +26,7 @@ jobs: cache-dependency-path: | **/go.sum - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" - name: Flow CLI Version run: flow version - name: Update PATH diff --git a/.github/workflows/scheduled_rebalance_tests.yml b/.github/workflows/scheduled_rebalance_tests.yml index 6c226f16..97ad33a2 100644 --- a/.github/workflows/scheduled_rebalance_tests.yml +++ b/.github/workflows/scheduled_rebalance_tests.yml @@ -30,7 +30,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" - name: Flow CLI Version run: flow version - name: Update PATH From 02efefbe86f7ad69d64fb7a95121fa7bd0c2af41 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 12 Mar 2026 14:49:09 -0700 Subject: [PATCH 2/5] Fix tests --- .../transactions/set_erc4626_vault_price.cdc | 19 +++++++++++--- .../set_uniswap_v3_pool_price.cdc | 26 +++++++++++-------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/cadence/tests/transactions/set_erc4626_vault_price.cdc b/cadence/tests/transactions/set_erc4626_vault_price.cdc index 3d870b2e..5896ed26 100644 --- a/cadence/tests/transactions/set_erc4626_vault_price.cdc +++ b/cadence/tests/transactions/set_erc4626_vault_price.cdc @@ -6,7 +6,20 @@ import "FlowEVMBridgeUtils" access(all) fun computeMappingSlot(_ values: [AnyStruct]): String { let encoded = EVM.encodeABI(values) let hashBytes = HashAlgorithm.KECCAK_256.hash(encoded) - return String.encodeHex(hashBytes) + return "0x\(String.encodeHex(hashBytes))" +} + +// Helper: Convert UInt256 to zero-padded 64-char hex string (32 bytes) with 0x prefix +access(all) fun slotHex(_ value: UInt256): String { + let raw = value.toBigEndianBytes() + var padded: [UInt8] = [] + var padCount = 32 - raw.length + while padCount > 0 { + padded.append(0) + padCount = padCount - 1 + } + padded = padded.concat(raw) + return "0x\(String.encodeHex(padded))" } // Helper: Compute ERC20 balanceOf storage slot @@ -84,7 +97,7 @@ transaction( let finalTargetSupply = targetAssets * supplyMultiplier let supplyValue = String.encodeHex(finalTargetSupply.toBigEndianBytes()) - EVM.store(target: vault, slot: String.encodeHex(totalSupplySlot.toBigEndianBytes()), value: supplyValue) + EVM.store(target: vault, slot: slotHex(totalSupplySlot), value: supplyValue) // Update asset.balanceOf(vault) to finalTargetAssets let vaultBalanceSlot = computeBalanceOfSlot(holderAddress: vaultAddress, balanceSlot: assetBalanceSlot) @@ -120,6 +133,6 @@ transaction( assert(newSlotBytes.length == 32, message: "Vault storage slot must be exactly 32 bytes, got \(newSlotBytes.length) (lastUpdate: \(lastUpdateBytes.length), maxRate: \(maxRateBytes.length), assets: \(paddedAssets.length))") let newSlotValue = String.encodeHex(newSlotBytes) - EVM.store(target: vault, slot: String.encodeHex(vaultTotalAssetsSlot.toBigEndianBytes()), value: newSlotValue) + EVM.store(target: vault, slot: slotHex(vaultTotalAssetsSlot), value: newSlotValue) } } diff --git a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc index 6b4b1b0a..e96a5093 100644 --- a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc +++ b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc @@ -4,7 +4,7 @@ import EVM from "MockEVM" access(all) fun computeMappingSlot(_ values: [AnyStruct]): String { let encoded = EVM.encodeABI(values) let hashBytes = HashAlgorithm.KECCAK_256.hash(encoded) - return String.encodeHex(hashBytes) + return "0x\(String.encodeHex(hashBytes))" } // Helper: Compute ERC20 balanceOf storage slot @@ -33,12 +33,16 @@ access(all) fun toHex32(_ value: UInt256): String { // Helper: Convert a slot number (UInt256) to its padded hex string for EVM.store/load access(all) fun slotHex(_ slotNum: UInt256): String { - return toHex32(slotNum) + return "0x\(toHex32(slotNum))" } // Helper: Parse a hex slot string back to UInt256 -access(all) fun slotToNum(_ slotHex: String): UInt256 { - let bytes = slotHex.decodeHex() +access(all) fun slotToNum(_ slot: String): UInt256 { + var hex = slot + if hex.length > 2 && hex.slice(from: 0, upTo: 2) == "0x" { + hex = hex.slice(from: 2, upTo: hex.length) + } + let bytes = hex.decodeHex() var num = 0 as UInt256 for byte in bytes { num = num * 256 + UInt256(byte) @@ -201,22 +205,22 @@ transaction( assert(slot0Value.length == 64, message: "slot0 must be 64 hex chars") // --- Slot 0: slot0 (packed) --- - EVM.store(target: poolAddr, slot: "0", value: slot0Value) + EVM.store(target: poolAddr, slot: slotHex(0), value: slot0Value) // Verify round-trip - let readBack = EVM.load(target: poolAddr, slot: "0") + let readBack = EVM.load(target: poolAddr, slot: slotHex(0)) let readBackHex = String.encodeHex(readBack) assert(readBackHex == slot0Value, message: "slot0 read-back mismatch - storage corruption!") // --- Slots 1-3: feeGrowthGlobal0X128, feeGrowthGlobal1X128, protocolFees = 0 --- let zero32 = "0000000000000000000000000000000000000000000000000000000000000000" - EVM.store(target: poolAddr, slot: "1", value: zero32) - EVM.store(target: poolAddr, slot: "2", value: zero32) - EVM.store(target: poolAddr, slot: "3", value: zero32) + EVM.store(target: poolAddr, slot: slotHex(1), value: zero32) + EVM.store(target: poolAddr, slot: slotHex(2), value: zero32) + EVM.store(target: poolAddr, slot: slotHex(3), value: zero32) // --- Slot 4: liquidity = uint128 max --- let liquidityAmount: UInt256 = 340282366920938463463374607431768211455 // 2^128 - 1 - EVM.store(target: poolAddr, slot: "4", value: toHex32(liquidityAmount)) + EVM.store(target: poolAddr, slot: slotHex(4), value: toHex32(liquidityAmount)) // --- Initialize boundary ticks --- // Tick storage layout per tick (4 consecutive slots): @@ -293,7 +297,7 @@ transaction( assert(obs0Bytes.length == 32, message: "observations[0] must be exactly 32 bytes") - EVM.store(target: poolAddr, slot: "8", value: String.encodeHex(obs0Bytes)) + EVM.store(target: poolAddr, slot: slotHex(8), value: String.encodeHex(obs0Bytes)) // --- Fund pool with token balances --- // Calculate 1 billion tokens in each token's decimal format From 24841b42f03b23af54b9d8472e27031efe40513e Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Wed, 25 Mar 2026 20:30:23 -0700 Subject: [PATCH 3/5] Add .flow-fork-cache to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0a94edeb..f58d4fed 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ db # logs run_logs/*.log + +# flow +.flow-fork-cache/ \ No newline at end of file From 9c8d88d3043bac4a512034d05c635c2b443f6285 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 26 Mar 2026 09:19:32 -0700 Subject: [PATCH 4/5] Add BTC Flash Crash Moderate Simulation --- .../tests/flash_crash_moderate_helpers.cdc | 2980 +++++++++++++++++ .../forked_flash_crash_moderate_test.cdc | 457 +++ cadence/tests/test_helpers.cdc | 80 +- .../tests/transactions/set_erc20_balance.cdc | 36 + cadence/tests/transactions/transfer_wbtc.cdc | 30 + 5 files changed, 3582 insertions(+), 1 deletion(-) create mode 100644 cadence/tests/flash_crash_moderate_helpers.cdc create mode 100644 cadence/tests/forked_flash_crash_moderate_test.cdc create mode 100644 cadence/tests/transactions/set_erc20_balance.cdc create mode 100644 cadence/tests/transactions/transfer_wbtc.cdc diff --git a/cadence/tests/flash_crash_moderate_helpers.cdc b/cadence/tests/flash_crash_moderate_helpers.cdc new file mode 100644 index 00000000..99f5391c --- /dev/null +++ b/cadence/tests/flash_crash_moderate_helpers.cdc @@ -0,0 +1,2980 @@ +import Test + +// AUTO-GENERATED from flash_crash_moderate.json — do not edit manually +// Run: python3 scripts/generate_fixture.py + +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 + } +} + +access(all) let flash_crash_moderate_prices: [UFix64] = [ + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 100000.00000000, + 96000.00000000, + 92000.00000000, + 88000.00000000, + 84000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80000.00000000, + 80015.34000000, + 80030.68000000, + 80046.02000000, + 80061.35000000, + 80076.68000000, + 80092.00000000, + 80107.32000000, + 80122.64000000, + 80137.95000000, + 80153.26000000, + 80168.56000000, + 80183.86000000, + 80199.16000000, + 80214.45000000, + 80229.74000000, + 80245.02000000, + 80260.30000000, + 80275.58000000, + 80290.85000000, + 80306.12000000, + 80321.38000000, + 80336.64000000, + 80351.90000000, + 80367.15000000, + 80382.40000000, + 80397.65000000, + 80412.89000000, + 80428.13000000, + 80443.36000000, + 80458.59000000, + 80473.81000000, + 80489.03000000, + 80504.25000000, + 80519.46000000, + 80534.67000000, + 80549.88000000, + 80565.08000000, + 80580.28000000, + 80595.47000000, + 80610.66000000, + 80625.85000000, + 80641.03000000, + 80656.20000000, + 80671.38000000, + 80686.55000000, + 80701.71000000, + 80716.88000000, + 80732.03000000, + 80747.19000000, + 80762.34000000, + 80777.48000000, + 80792.62000000, + 80807.76000000, + 80822.90000000, + 80838.03000000, + 80853.15000000, + 80868.27000000, + 80883.39000000, + 80898.51000000, + 80913.62000000, + 80928.72000000, + 80943.82000000, + 80958.92000000, + 80974.02000000, + 80989.11000000, + 81004.19000000, + 81019.27000000, + 81034.35000000, + 81049.43000000, + 81064.50000000, + 81079.56000000, + 81094.62000000, + 81109.68000000, + 81124.74000000, + 81139.79000000, + 81154.83000000, + 81169.87000000, + 81184.91000000, + 81199.95000000, + 81214.98000000, + 81230.00000000, + 81245.02000000, + 81260.04000000, + 81275.06000000, + 81290.07000000, + 81305.07000000, + 81320.07000000, + 81335.07000000, + 81350.07000000, + 81365.06000000, + 81380.04000000, + 81395.02000000, + 81410.00000000, + 81424.97000000, + 81439.94000000, + 81454.91000000, + 81469.87000000, + 81484.83000000, + 81499.78000000, + 81514.73000000, + 81529.68000000, + 81544.62000000, + 81559.56000000, + 81574.49000000, + 81589.42000000, + 81604.35000000, + 81619.27000000, + 81634.19000000, + 81649.10000000, + 81664.01000000, + 81678.91000000, + 81693.81000000, + 81708.71000000, + 81723.60000000, + 81738.49000000, + 81753.38000000, + 81768.26000000, + 81783.14000000, + 81798.01000000, + 81812.88000000, + 81827.74000000, + 81842.60000000, + 81857.46000000, + 81872.31000000, + 81887.16000000, + 81902.01000000, + 81916.85000000, + 81931.68000000, + 81946.52000000, + 81961.34000000, + 81976.17000000, + 81990.99000000, + 82005.81000000, + 82020.62000000, + 82035.43000000, + 82050.23000000, + 82065.03000000, + 82079.83000000, + 82094.62000000, + 82109.40000000, + 82124.19000000, + 82138.97000000, + 82153.74000000, + 82168.51000000, + 82183.28000000, + 82198.04000000, + 82212.80000000, + 82227.56000000, + 82242.31000000, + 82257.06000000, + 82271.80000000, + 82286.54000000, + 82301.27000000, + 82316.00000000, + 82330.73000000, + 82345.45000000, + 82360.17000000, + 82374.88000000, + 82389.59000000, + 82404.30000000, + 82419.00000000, + 82433.70000000, + 82448.39000000, + 82463.08000000, + 82477.77000000, + 82492.45000000, + 82507.13000000, + 82521.80000000, + 82536.47000000, + 82551.13000000, + 82565.80000000, + 82580.45000000, + 82595.11000000, + 82609.75000000, + 82624.40000000, + 82639.04000000, + 82653.67000000, + 82668.31000000, + 82682.93000000, + 82697.56000000, + 82712.18000000, + 82726.79000000, + 82741.41000000, + 82756.01000000, + 82770.62000000, + 82785.22000000, + 82799.81000000, + 82814.40000000, + 82828.99000000, + 82843.57000000, + 82858.15000000, + 82872.72000000, + 82887.29000000, + 82901.86000000, + 82916.42000000, + 82930.98000000, + 82945.53000000, + 82960.08000000, + 82974.63000000, + 82989.17000000, + 83003.71000000, + 83018.24000000, + 83032.77000000, + 83047.29000000, + 83061.81000000, + 83076.33000000, + 83090.84000000, + 83105.35000000, + 83119.85000000, + 83134.35000000, + 83148.85000000, + 83163.34000000, + 83177.83000000, + 83192.31000000, + 83206.79000000, + 83221.26000000, + 83235.74000000, + 83250.20000000, + 83264.66000000, + 83279.12000000, + 83293.58000000, + 83308.03000000, + 83322.47000000, + 83336.91000000, + 83351.35000000, + 83365.78000000, + 83380.21000000, + 83394.64000000, + 83409.06000000, + 83423.47000000, + 83437.89000000, + 83452.30000000, + 83466.70000000, + 83481.10000000, + 83495.49000000, + 83509.89000000, + 83524.27000000, + 83538.66000000, + 83553.03000000, + 83567.41000000, + 83581.78000000, + 83596.15000000, + 83610.51000000, + 83624.87000000, + 83639.22000000, + 83653.57000000, + 83667.91000000, + 83682.26000000, + 83696.59000000, + 83710.92000000, + 83725.25000000, + 83739.58000000, + 83753.90000000, + 83768.21000000, + 83782.53000000, + 83796.83000000, + 83811.14000000, + 83825.43000000, + 83839.73000000, + 83854.02000000, + 83868.31000000, + 83882.59000000, + 83896.87000000, + 83911.14000000, + 83925.41000000, + 83939.68000000, + 83953.94000000, + 83968.19000000, + 83982.45000000, + 83996.69000000, + 84010.94000000, + 84025.18000000, + 84039.41000000, + 84053.65000000, + 84067.87000000, + 84082.10000000, + 84096.31000000, + 84110.53000000, + 84124.74000000, + 84138.95000000, + 84153.15000000, + 84167.34000000, + 84181.54000000, + 84195.73000000, + 84209.91000000, + 84224.09000000, + 84238.27000000, + 84252.44000000, + 84266.61000000, + 84280.77000000, + 84294.93000000, + 84309.09000000, + 84323.24000000, + 84337.38000000, + 84351.53000000, + 84365.66000000, + 84379.80000000, + 84393.93000000, + 84408.05000000, + 84422.17000000, + 84436.29000000, + 84450.40000000, + 84464.51000000, + 84478.62000000, + 84492.71000000, + 84506.81000000, + 84520.90000000, + 84534.99000000, + 84549.07000000, + 84563.15000000, + 84577.22000000, + 84591.29000000, + 84605.36000000, + 84619.42000000, + 84633.48000000, + 84647.53000000, + 84661.58000000, + 84675.62000000, + 84689.66000000, + 84703.70000000, + 84717.73000000, + 84731.75000000, + 84745.78000000, + 84759.80000000, + 84773.81000000, + 84787.82000000, + 84801.82000000, + 84815.83000000, + 84829.82000000, + 84843.81000000, + 84857.80000000, + 84871.79000000, + 84885.77000000, + 84899.74000000, + 84913.71000000, + 84927.68000000, + 84941.64000000, + 84955.60000000, + 84969.55000000, + 84983.50000000, + 84997.45000000, + 85011.39000000, + 85025.32000000, + 85039.26000000, + 85053.18000000, + 85067.11000000, + 85081.03000000, + 85094.94000000, + 85108.85000000, + 85122.76000000, + 85136.66000000, + 85150.56000000, + 85164.45000000, + 85178.34000000, + 85192.22000000, + 85206.10000000, + 85219.98000000, + 85233.85000000, + 85247.72000000, + 85261.58000000, + 85275.44000000, + 85289.29000000, + 85303.14000000, + 85316.99000000, + 85330.83000000, + 85344.67000000, + 85358.50000000, + 85372.33000000, + 85386.15000000, + 85399.97000000, + 85413.78000000, + 85427.60000000, + 85441.40000000, + 85455.20000000, + 85469.00000000, + 85482.79000000, + 85496.58000000, + 85510.37000000, + 85524.15000000, + 85537.92000000, + 85551.69000000, + 85565.46000000, + 85579.22000000, + 85592.98000000, + 85606.73000000, + 85620.48000000, + 85634.23000000, + 85647.97000000, + 85661.71000000, + 85675.44000000, + 85689.16000000, + 85702.89000000, + 85716.61000000, + 85730.32000000, + 85744.03000000, + 85757.74000000, + 85771.44000000, + 85785.13000000, + 85798.83000000, + 85812.51000000, + 85826.20000000, + 85839.88000000, + 85853.55000000, + 85867.22000000, + 85880.89000000, + 85894.55000000, + 85908.21000000, + 85921.86000000, + 85935.51000000, + 85949.15000000, + 85962.79000000, + 85976.42000000, + 85990.06000000, + 86003.68000000, + 86017.30000000, + 86030.92000000, + 86044.53000000, + 86058.14000000, + 86071.75000000, + 86085.35000000, + 86098.94000000, + 86112.53000000, + 86126.12000000, + 86139.70000000, + 86153.28000000, + 86166.85000000, + 86180.42000000, + 86193.98000000, + 86207.54000000, + 86221.10000000, + 86234.65000000, + 86248.19000000, + 86261.74000000, + 86275.27000000, + 86288.81000000, + 86302.34000000, + 86315.86000000, + 86329.38000000, + 86342.89000000, + 86356.41000000, + 86369.91000000, + 86383.41000000, + 86396.91000000, + 86410.40000000, + 86423.89000000, + 86437.38000000, + 86450.86000000, + 86464.33000000, + 86477.80000000, + 86491.27000000, + 86504.73000000, + 86518.19000000, + 86531.64000000, + 86545.09000000, + 86558.53000000, + 86571.97000000, + 86585.40000000, + 86598.84000000, + 86612.26000000, + 86625.68000000, + 86639.10000000, + 86652.51000000, + 86665.92000000, + 86679.32000000, + 86692.72000000, + 86706.12000000, + 86719.51000000, + 86732.89000000, + 86746.27000000, + 86759.65000000, + 86773.02000000, + 86786.39000000, + 86799.75000000, + 86813.11000000, + 86826.46000000, + 86839.81000000, + 86853.16000000, + 86866.50000000, + 86879.83000000, + 86893.16000000, + 86906.49000000, + 86919.81000000, + 86933.13000000, + 86946.44000000, + 86959.75000000, + 86973.06000000, + 86986.36000000, + 86999.65000000, + 87012.94000000, + 87026.23000000, + 87039.51000000, + 87052.79000000, + 87066.06000000, + 87079.33000000, + 87092.59000000, + 87105.85000000, + 87119.10000000, + 87132.35000000, + 87145.60000000, + 87158.84000000, + 87172.07000000, + 87185.30000000, + 87198.53000000, + 87211.75000000, + 87224.97000000, + 87238.19000000, + 87251.39000000, + 87264.60000000, + 87277.80000000, + 87290.99000000, + 87304.18000000, + 87317.37000000, + 87330.55000000, + 87343.73000000, + 87356.90000000, + 87370.07000000, + 87383.23000000, + 87396.39000000, + 87409.54000000, + 87422.69000000, + 87435.84000000, + 87448.98000000, + 87462.11000000, + 87475.24000000, + 87488.37000000, + 87501.49000000, + 87514.61000000, + 87527.72000000, + 87540.83000000, + 87553.93000000, + 87567.03000000, + 87580.13000000, + 87593.22000000, + 87606.30000000, + 87619.38000000, + 87632.46000000, + 87645.53000000, + 87658.59000000, + 87671.66000000, + 87684.71000000, + 87697.77000000, + 87710.81000000, + 87723.86000000, + 87736.90000000, + 87749.93000000, + 87762.96000000, + 87775.99000000, + 87789.01000000, + 87802.02000000, + 87815.03000000, + 87828.04000000, + 87841.04000000, + 87854.04000000, + 87867.03000000, + 87880.02000000, + 87893.00000000, + 87905.98000000, + 87918.96000000, + 87931.93000000, + 87944.89000000, + 87957.85000000, + 87970.81000000, + 87983.76000000, + 87996.70000000, + 88009.64000000, + 88022.58000000, + 88035.51000000, + 88048.44000000, + 88061.36000000, + 88074.28000000, + 88087.20000000, + 88100.11000000, + 88113.01000000, + 88125.91000000, + 88138.80000000, + 88151.69000000, + 88164.58000000, + 88177.46000000, + 88190.34000000, + 88203.21000000, + 88216.08000000, + 88228.94000000, + 88241.80000000, + 88254.65000000, + 88267.50000000, + 88280.34000000, + 88293.18000000, + 88306.01000000, + 88318.84000000, + 88331.67000000, + 88344.49000000, + 88357.30000000, + 88370.11000000, + 88382.92000000, + 88395.72000000, + 88408.52000000, + 88421.31000000, + 88434.09000000, + 88446.88000000, + 88459.65000000, + 88472.43000000, + 88485.20000000, + 88497.96000000, + 88510.72000000, + 88523.47000000, + 88536.22000000, + 88548.97000000, + 88561.71000000, + 88574.44000000, + 88587.17000000, + 88599.90000000, + 88612.62000000, + 88625.33000000, + 88638.05000000, + 88650.75000000, + 88663.45000000, + 88676.15000000, + 88688.84000000, + 88701.53000000, + 88714.22000000, + 88726.89000000, + 88739.57000000, + 88752.24000000, + 88764.90000000, + 88777.56000000, + 88790.21000000, + 88802.86000000, + 88815.51000000, + 88828.15000000, + 88840.78000000, + 88853.42000000, + 88866.04000000, + 88878.66000000, + 88891.28000000, + 88903.89000000, + 88916.50000000, + 88929.10000000, + 88941.70000000, + 88954.29000000, + 88966.88000000, + 88979.46000000, + 88992.04000000, + 89004.61000000, + 89017.18000000, + 89029.74000000, + 89042.30000000, + 89054.86000000, + 89067.41000000, + 89079.95000000, + 89092.49000000, + 89105.02000000, + 89117.56000000, + 89130.08000000, + 89142.60000000, + 89155.12000000, + 89167.63000000, + 89180.13000000, + 89192.64000000, + 89205.13000000, + 89217.62000000, + 89230.11000000, + 89242.59000000, + 89255.07000000, + 89267.54000000, + 89280.01000000, + 89292.47000000, + 89304.93000000, + 89317.38000000, + 89329.83000000, + 89342.27000000, + 89354.71000000, + 89367.15000000, + 89379.58000000, + 89392.00000000, + 89404.42000000, + 89416.83000000, + 89429.24000000, + 89441.65000000, + 89454.05000000, + 89466.44000000, + 89478.83000000, + 89491.22000000, + 89503.60000000, + 89515.97000000, + 89528.34000000, + 89540.71000000, + 89553.07000000, + 89565.42000000, + 89577.78000000, + 89590.12000000, + 89602.46000000, + 89614.80000000, + 89627.13000000, + 89639.46000000, + 89651.78000000, + 89664.10000000, + 89676.41000000, + 89688.72000000, + 89701.02000000, + 89713.31000000, + 89725.61000000, + 89737.89000000, + 89750.18000000, + 89762.46000000, + 89774.73000000, + 89787.00000000, + 89799.26000000, + 89811.52000000, + 89823.77000000, + 89836.02000000, + 89848.26000000, + 89860.50000000, + 89872.73000000, + 89884.96000000, + 89897.19000000, + 89909.40000000, + 89921.62000000, + 89933.83000000, + 89946.03000000, + 89958.23000000, + 89970.42000000, + 89982.61000000, + 89994.80000000, + 90006.98000000, + 90019.15000000, + 90031.32000000, + 90043.48000000, + 90055.64000000, + 90067.80000000, + 90079.95000000, + 90092.09000000, + 90104.23000000, + 90116.37000000, + 90128.50000000, + 90140.62000000, + 90152.74000000, + 90164.85000000, + 90176.96000000, + 90189.07000000, + 90201.17000000, + 90213.26000000, + 90225.35000000, + 90237.44000000, + 90249.52000000, + 90261.59000000, + 90273.66000000, + 90285.73000000, + 90297.79000000, + 90309.84000000, + 90321.89000000, + 90333.94000000, + 90345.98000000, + 90358.01000000, + 90370.04000000, + 90382.07000000, + 90394.09000000, + 90406.10000000, + 90418.11000000, + 90430.12000000, + 90442.12000000, + 90454.11000000, + 90466.10000000, + 90478.09000000, + 90490.07000000, + 90502.04000000, + 90514.01000000, + 90525.98000000, + 90537.93000000, + 90549.89000000, + 90561.84000000, + 90573.78000000, + 90585.72000000, + 90597.66000000, + 90609.59000000, + 90621.51000000, + 90633.43000000, + 90645.34000000, + 90657.25000000, + 90669.16000000, + 90681.06000000, + 90692.95000000, + 90704.84000000, + 90716.72000000, + 90728.60000000, + 90740.48000000, + 90752.34000000, + 90764.21000000, + 90776.07000000, + 90787.92000000, + 90799.77000000, + 90811.61000000, + 90823.45000000, + 90835.28000000, + 90847.11000000, + 90858.93000000, + 90870.75000000, + 90882.56000000, + 90894.37000000, + 90906.17000000, + 90917.97000000, + 90929.76000000, + 90941.55000000, + 90953.33000000, + 90965.11000000, + 90976.88000000, + 90988.65000000, + 91000.41000000, + 91012.17000000, + 91023.92000000, + 91035.66000000, + 91047.41000000, + 91059.14000000, + 91070.87000000, + 91082.60000000, + 91094.32000000, + 91106.03000000, + 91117.74000000, + 91129.45000000, + 91141.15000000, + 91152.84000000, + 91164.53000000, + 91176.22000000, + 91187.90000000, + 91199.57000000, + 91211.24000000, + 91222.91000000, + 91234.56000000, + 91246.22000000, + 91257.87000000, + 91269.51000000, + 91281.15000000, + 91292.78000000, + 91304.41000000, + 91316.03000000, + 91327.65000000, + 91339.26000000, + 91350.87000000, + 91362.47000000, + 91374.07000000, + 91385.66000000, + 91397.24000000, + 91408.82000000, + 91420.40000000, + 91431.97000000, + 91443.54000000, + 91455.10000000, + 91466.65000000, + 91478.20000000, + 91489.75000000, + 91501.28000000, + 91512.82000000, + 91524.35000000, + 91535.87000000, + 91547.39000000, + 91558.90000000, + 91570.41000000, + 91581.91000000, + 91593.41000000, + 91604.90000000, + 91616.39000000, + 91627.87000000, + 91639.35000000, + 91650.82000000, + 91662.29000000, + 91673.75000000, + 91685.20000000, + 91696.65000000, + 91708.10000000, + 91719.54000000, + 91730.97000000, + 91742.40000000, + 91753.83000000, + 91765.24000000, + 91776.66000000, + 91788.07000000, + 91799.47000000, + 91810.87000000, + 91822.26000000, + 91833.64000000, + 91845.03000000, + 91856.40000000, + 91867.77000000, + 91879.14000000, + 91890.50000000, + 91901.86000000, + 91913.21000000, + 91924.55000000, + 91935.89000000, + 91947.22000000, + 91958.55000000, + 91969.88000000, + 91981.19000000, + 91992.51000000, + 92003.81000000, + 92015.12000000, + 92026.41000000, + 92037.70000000, + 92048.99000000, + 92060.27000000, + 92071.55000000, + 92082.82000000, + 92094.08000000, + 92105.34000000, + 92116.59000000, + 92127.84000000, + 92139.09000000, + 92150.32000000, + 92161.56000000, + 92172.78000000, + 92184.00000000, + 92195.22000000, + 92206.43000000, + 92217.64000000, + 92228.84000000, + 92240.03000000, + 92251.22000000, + 92262.41000000, + 92273.59000000, + 92284.76000000, + 92295.93000000, + 92307.09000000, + 92318.25000000, + 92329.40000000, + 92340.55000000, + 92351.69000000, + 92362.82000000, + 92373.95000000, + 92385.08000000, + 92396.20000000, + 92407.31000000, + 92418.42000000, + 92429.52000000, + 92440.62000000, + 92451.71000000, + 92462.80000000, + 92473.88000000, + 92484.96000000, + 92496.03000000, + 92507.09000000, + 92518.15000000, + 92529.21000000, + 92540.25000000, + 92551.30000000, + 92562.34000000, + 92573.37000000, + 92584.39000000, + 92595.42000000, + 92606.43000000, + 92617.44000000, + 92628.45000000, + 92639.45000000, + 92650.44000000, + 92661.43000000, + 92672.41000000, + 92683.39000000, + 92694.36000000, + 92705.33000000, + 92716.29000000, + 92727.25000000, + 92738.20000000, + 92749.14000000, + 92760.08000000, + 92771.01000000, + 92781.94000000, + 92792.87000000, + 92803.78000000, + 92814.69000000, + 92825.60000000, + 92836.50000000, + 92847.40000000, + 92858.29000000, + 92869.17000000, + 92880.05000000, + 92890.92000000, + 92901.79000000, + 92912.65000000, + 92923.51000000, + 92934.36000000, + 92945.20000000, + 92956.04000000, + 92966.88000000, + 92977.70000000, + 92988.53000000, + 92999.34000000, + 93010.16000000, + 93020.96000000, + 93031.76000000, + 93042.56000000, + 93053.35000000, + 93064.13000000, + 93074.91000000, + 93085.68000000, + 93096.45000000, + 93107.21000000, + 93117.97000000, + 93128.72000000, + 93139.46000000, + 93150.20000000, + 93160.94000000, + 93171.66000000, + 93182.39000000, + 93193.10000000, + 93203.81000000, + 93214.52000000, + 93225.22000000, + 93235.91000000, + 93246.60000000, + 93257.28000000, + 93267.96000000, + 93278.63000000, + 93289.30000000, + 93299.96000000, + 93310.61000000, + 93321.26000000, + 93331.91000000, + 93342.54000000, + 93353.18000000, + 93363.80000000, + 93374.42000000, + 93385.04000000, + 93395.65000000, + 93406.25000000, + 93416.85000000, + 93427.44000000, + 93438.03000000, + 93448.61000000, + 93459.19000000, + 93469.76000000, + 93480.32000000, + 93490.88000000, + 93501.43000000, + 93511.98000000, + 93522.52000000, + 93533.05000000, + 93543.58000000, + 93554.11000000, + 93564.62000000, + 93575.14000000, + 93585.64000000, + 93596.14000000, + 93606.64000000, + 93617.13000000, + 93627.61000000, + 93638.09000000, + 93648.56000000, + 93659.03000000, + 93669.49000000, + 93679.95000000, + 93690.40000000, + 93700.84000000, + 93711.28000000, + 93721.71000000, + 93732.13000000, + 93742.56000000, + 93752.97000000, + 93763.38000000, + 93773.78000000, + 93784.18000000, + 93794.57000000, + 93804.96000000, + 93815.34000000, + 93825.71000000, + 93836.08000000, + 93846.44000000, + 93856.80000000, + 93867.15000000, + 93877.49000000, + 93887.83000000, + 93898.17000000, + 93908.49000000, + 93918.81000000, + 93929.13000000, + 93939.44000000, + 93949.74000000, + 93960.04000000, + 93970.34000000, + 93980.62000000, + 93990.90000000, + 94001.18000000, + 94011.45000000, + 94021.71000000, + 94031.97000000, + 94042.22000000, + 94052.46000000, + 94062.70000000, + 94072.94000000, + 94083.17000000, + 94093.39000000, + 94103.60000000, + 94113.81000000, + 94124.02000000, + 94134.22000000, + 94144.41000000, + 94154.60000000, + 94164.78000000, + 94174.95000000, + 94185.12000000, + 94195.28000000, + 94205.44000000, + 94215.59000000, + 94225.74000000, + 94235.87000000, + 94246.01000000, + 94256.14000000, + 94266.26000000, + 94276.37000000, + 94286.48000000, + 94296.59000000, + 94306.68000000, + 94316.77000000, + 94326.86000000, + 94336.94000000, + 94347.01000000, + 94357.08000000, + 94367.14000000, + 94377.20000000, + 94387.25000000, + 94397.29000000, + 94407.33000000, + 94417.36000000, + 94427.39000000, + 94437.41000000, + 94447.42000000, + 94457.43000000, + 94467.43000000, + 94477.42000000, + 94487.41000000, + 94497.40000000, + 94507.38000000, + 94517.35000000, + 94527.31000000, + 94537.27000000, + 94547.22000000, + 94557.17000000, + 94567.11000000, + 94577.05000000, + 94586.98000000, + 94596.90000000, + 94606.82000000, + 94616.73000000, + 94626.63000000, + 94636.53000000, + 94646.42000000, + 94656.31000000, + 94666.19000000, + 94676.07000000, + 94685.93000000, + 94695.80000000, + 94705.65000000, + 94715.50000000, + 94725.35000000, + 94735.18000000, + 94745.02000000, + 94754.84000000, + 94764.66000000, + 94774.47000000, + 94784.28000000, + 94794.08000000, + 94803.88000000, + 94813.67000000, + 94823.45000000, + 94833.22000000, + 94842.99000000, + 94852.76000000, + 94862.52000000, + 94872.27000000, + 94882.01000000, + 94891.75000000, + 94901.49000000, + 94911.21000000, + 94920.93000000, + 94930.65000000, + 94940.36000000, + 94950.06000000, + 94959.75000000, + 94969.44000000, + 94979.13000000, + 94988.80000000, + 94998.48000000, + 95008.14000000, + 95017.80000000, + 95027.45000000, + 95037.10000000, + 95046.74000000, + 95056.37000000, + 95066.00000000, + 95075.62000000, + 95085.23000000, + 95094.84000000, + 95104.44000000, + 95114.04000000, + 95123.63000000, + 95133.21000000, + 95142.79000000, + 95152.36000000, + 95161.93000000, + 95171.48000000, + 95181.04000000, + 95190.58000000, + 95200.12000000, + 95209.65000000, + 95219.18000000, + 95228.70000000, + 95238.22000000, + 95247.72000000, + 95257.22000000, + 95266.72000000, + 95276.21000000, + 95285.69000000, + 95295.17000000, + 95304.64000000, + 95314.10000000, + 95323.56000000, + 95333.01000000, + 95342.45000000, + 95351.89000000, + 95361.32000000, + 95370.75000000, + 95380.16000000, + 95389.58000000, + 95398.98000000, + 95408.38000000, + 95417.77000000, + 95427.16000000, + 95436.54000000, + 95445.91000000, + 95455.28000000, + 95464.64000000, + 95474.00000000, + 95483.35000000, + 95492.69000000, + 95502.02000000, + 95511.35000000, + 95520.67000000, + 95529.99000000, + 95539.30000000, + 95548.60000000, + 95557.90000000, + 95567.19000000, + 95576.47000000, + 95585.75000000, + 95595.02000000, + 95604.28000000, + 95613.54000000, + 95622.79000000, + 95632.03000000, + 95641.27000000, + 95650.50000000, + 95659.73000000, + 95668.95000000, + 95678.16000000, + 95687.36000000, + 95696.56000000, + 95705.75000000, + 95714.94000000, + 95724.12000000, + 95733.29000000, + 95742.46000000, + 95751.62000000, + 95760.77000000, + 95769.92000000, + 95779.05000000, + 95788.19000000, + 95797.31000000, + 95806.43000000, + 95815.55000000, + 95824.65000000, + 95833.75000000, + 95842.85000000, + 95851.93000000, + 95861.01000000, + 95870.09000000, + 95879.15000000, + 95888.21000000, + 95897.27000000, + 95906.31000000, + 95915.35000000, + 95924.39000000, + 95933.41000000, + 95942.43000000, + 95951.45000000, + 95960.46000000, + 95969.46000000, + 95978.45000000, + 95987.44000000, + 95996.42000000, + 96005.39000000, + 96014.36000000, + 96023.31000000, + 96032.27000000, + 96041.21000000, + 96050.15000000, + 96059.09000000, + 96068.01000000, + 96076.93000000, + 96085.85000000, + 96094.75000000, + 96103.65000000, + 96112.54000000, + 96121.43000000, + 96130.31000000, + 96139.18000000, + 96148.04000000, + 96156.90000000, + 96165.75000000, + 96174.60000000, + 96183.44000000, + 96192.27000000, + 96201.09000000, + 96209.91000000, + 96218.72000000, + 96227.53000000, + 96236.32000000, + 96245.11000000, + 96253.90000000, + 96262.67000000, + 96271.44000000, + 96280.21000000, + 96288.96000000, + 96297.71000000, + 96306.45000000, + 96315.19000000, + 96323.92000000, + 96332.64000000, + 96341.35000000, + 96350.06000000, + 96358.76000000, + 96367.45000000, + 96376.14000000, + 96384.82000000, + 96393.49000000, + 96402.16000000, + 96410.82000000, + 96419.47000000, + 96428.12000000, + 96436.75000000, + 96445.39000000, + 96454.01000000, + 96462.63000000, + 96471.24000000, + 96479.84000000, + 96488.44000000, + 96497.03000000, + 96505.61000000, + 96514.18000000, + 96522.75000000, + 96531.31000000, + 96539.87000000, + 96548.41000000, + 96556.95000000, + 96565.49000000, + 96574.01000000, + 96582.53000000, + 96591.04000000, + 96599.55000000, + 96608.04000000, + 96616.54000000, + 96625.02000000, + 96633.50000000, + 96641.96000000, + 96650.43000000, + 96658.88000000, + 96667.33000000, + 96675.77000000, + 96684.20000000, + 96692.63000000, + 96701.05000000, + 96709.46000000, + 96717.87000000, + 96726.26000000, + 96734.65000000, + 96743.04000000, + 96751.41000000, + 96759.78000000, + 96768.15000000, + 96776.50000000, + 96784.85000000, + 96793.19000000, + 96801.52000000, + 96809.85000000, + 96818.16000000, + 96826.48000000, + 96834.78000000, + 96843.08000000, + 96851.37000000, + 96859.65000000, + 96867.92000000, + 96876.19000000, + 96884.45000000, + 96892.70000000, + 96900.95000000, + 96909.19000000, + 96917.42000000, + 96925.64000000, + 96933.86000000, + 96942.07000000, + 96950.27000000, + 96958.47000000, + 96966.65000000, + 96974.83000000, + 96983.01000000, + 96991.17000000, + 96999.33000000, + 97007.48000000, + 97015.62000000, + 97023.76000000, + 97031.89000000, + 97040.01000000, + 97048.12000000, + 97056.23000000, + 97064.32000000, + 97072.41000000, + 97080.50000000, + 97088.57000000, + 97096.64000000, + 97104.70000000, + 97112.76000000, + 97120.80000000, + 97128.84000000, + 97136.87000000, + 97144.90000000, + 97152.91000000, + 97160.92000000, + 97168.92000000, + 97176.92000000, + 97184.90000000, + 97192.88000000, + 97200.85000000, + 97208.82000000, + 97216.77000000, + 97224.72000000, + 97232.66000000, + 97240.59000000, + 97248.52000000, + 97256.44000000, + 97264.35000000, + 97272.25000000, + 97280.15000000, + 97288.03000000, + 97295.91000000, + 97303.78000000, + 97311.65000000, + 97319.51000000, + 97327.36000000, + 97335.20000000, + 97343.03000000, + 97350.86000000, + 97358.68000000, + 97366.49000000, + 97374.29000000, + 97382.08000000, + 97389.87000000, + 97397.65000000, + 97405.42000000, + 97413.19000000, + 97420.94000000, + 97428.69000000, + 97436.43000000, + 97444.17000000, + 97451.89000000, + 97459.61000000, + 97467.32000000, + 97475.02000000, + 97482.72000000, + 97490.40000000, + 97498.08000000, + 97505.75000000, + 97513.42000000, + 97521.07000000, + 97528.72000000, + 97536.36000000, + 97543.99000000, + 97551.61000000, + 97559.23000000, + 97566.83000000, + 97574.43000000, + 97582.03000000, + 97589.61000000, + 97597.19000000, + 97604.75000000, + 97612.31000000, + 97619.87000000, + 97627.41000000, + 97634.95000000, + 97642.47000000, + 97649.99000000, + 97657.51000000, + 97665.01000000, + 97672.51000000, + 97679.99000000, + 97687.47000000, + 97694.94000000, + 97702.41000000, + 97709.86000000, + 97717.31000000, + 97724.75000000, + 97732.18000000, + 97739.61000000, + 97747.02000000, + 97754.43000000, + 97761.83000000, + 97769.22000000, + 97776.60000000, + 97783.97000000, + 97791.34000000, + 97798.70000000, + 97806.05000000, + 97813.39000000, + 97820.72000000, + 97828.05000000, + 97835.37000000, + 97842.68000000, + 97849.98000000, + 97857.27000000, + 97864.55000000, + 97871.83000000, + 97879.10000000, + 97886.36000000, + 97893.61000000, + 97900.85000000, + 97908.08000000, + 97915.31000000, + 97922.53000000, + 97929.74000000, + 97936.94000000, + 97944.13000000, + 97951.31000000, + 97958.49000000, + 97965.66000000, + 97972.82000000, + 97979.97000000, + 97987.11000000, + 97994.24000000, + 98001.37000000, + 98008.48000000, + 98015.59000000, + 98022.69000000, + 98029.78000000, + 98036.87000000, + 98043.94000000, + 98051.01000000, + 98058.06000000, + 98065.11000000, + 98072.15000000, + 98079.18000000, + 98086.21000000, + 98093.22000000, + 98100.23000000, + 98107.23000000, + 98114.21000000, + 98121.19000000, + 98128.17000000, + 98135.13000000, + 98142.08000000, + 98149.03000000, + 98155.96000000, + 98162.89000000, + 98169.81000000, + 98176.72000000, + 98183.63000000, + 98190.52000000, + 98197.40000000, + 98204.28000000, + 98211.15000000, + 98218.00000000, + 98224.85000000, + 98231.69000000, + 98238.53000000, + 98245.35000000, + 98252.16000000, + 98258.97000000, + 98265.76000000, + 98272.55000000, + 98279.33000000, + 98286.10000000, + 98292.86000000, + 98299.61000000, + 98306.36000000, + 98313.09000000, + 98319.82000000, + 98326.53000000, + 98333.24000000, + 98339.94000000, + 98346.63000000, + 98353.31000000, + 98359.98000000, + 98366.64000000, + 98373.29000000, + 98379.94000000, + 98386.57000000, + 98393.20000000, + 98399.82000000, + 98406.42000000, + 98413.02000000, + 98419.61000000, + 98426.19000000, + 98432.76000000, + 98439.33000000, + 98445.88000000, + 98452.42000000, + 98458.96000000, + 98465.48000000, + 98472.00000000, + 98478.51000000, + 98485.00000000, + 98491.49000000, + 98497.97000000, + 98504.44000000, + 98510.90000000, + 98517.35000000, + 98523.79000000, + 98530.22000000, + 98536.65000000, + 98543.06000000, + 98549.46000000, + 98555.86000000, + 98562.24000000, + 98568.62000000, + 98574.99000000, + 98581.34000000, + 98587.69000000, + 98594.03000000, + 98600.36000000, + 98606.68000000, + 98612.99000000, + 98619.29000000, + 98625.58000000, + 98631.86000000, + 98638.13000000, + 98644.39000000, + 98650.64000000, + 98656.88000000, + 98663.12000000, + 98669.34000000, + 98675.55000000, + 98681.76000000, + 98687.95000000, + 98694.13000000, + 98700.31000000, + 98706.47000000, + 98712.63000000, + 98718.77000000, + 98724.91000000, + 98731.03000000, + 98737.15000000, + 98743.25000000, + 98749.35000000, + 98755.44000000, + 98761.51000000, + 98767.58000000, + 98773.63000000, + 98779.68000000, + 98785.72000000, + 98791.74000000, + 98797.76000000, + 98803.76000000, + 98809.76000000, + 98815.75000000, + 98821.72000000, + 98827.69000000, + 98833.64000000, + 98839.59000000, + 98845.53000000, + 98851.45000000, + 98857.37000000, + 98863.27000000, + 98869.17000000, + 98875.05000000, + 98880.93000000, + 98886.79000000, + 98892.64000000, + 98898.49000000, + 98904.32000000, + 98910.14000000, + 98915.96000000, + 98921.76000000, + 98927.55000000, + 98933.33000000, + 98939.10000000, + 98944.86000000, + 98950.61000000, + 98956.35000000, + 98962.08000000, + 98967.80000000, + 98973.51000000, + 98979.21000000, + 98984.89000000, + 98990.57000000, + 98996.23000000, + 99001.89000000, + 99007.53000000, + 99013.17000000, + 99018.79000000, + 99024.40000000, + 99030.00000000, + 99035.59000000, + 99041.17000000, + 99046.74000000, + 99052.30000000, + 99057.85000000, + 99063.39000000, + 99068.91000000, + 99074.43000000, + 99079.93000000, + 99085.42000000, + 99090.91000000, + 99096.38000000, + 99101.84000000, + 99107.29000000, + 99112.72000000, + 99118.15000000, + 99123.57000000, + 99128.97000000, + 99134.36000000, + 99139.75000000, + 99145.12000000, + 99150.48000000, + 99155.83000000, + 99161.16000000, + 99166.49000000, + 99171.80000000, + 99177.11000000, + 99182.40000000, + 99187.68000000, + 99192.95000000, + 99198.21000000, + 99203.45000000, + 99208.69000000, + 99213.91000000, + 99219.12000000, + 99224.32000000, + 99229.51000000, + 99234.69000000, + 99239.85000000, + 99245.01000000, + 99250.15000000, + 99255.28000000, + 99260.40000000, + 99265.50000000, + 99270.60000000, + 99275.68000000, + 99280.75000000, + 99285.81000000, + 99290.86000000, + 99295.89000000, + 99300.92000000, + 99305.93000000, + 99310.93000000, + 99315.91000000, + 99320.89000000, + 99325.85000000, + 99330.80000000, + 99335.74000000, + 99340.67000000, + 99345.58000000, + 99350.48000000, + 99355.37000000, + 99360.25000000, + 99365.12000000, + 99369.97000000, + 99374.81000000, + 99379.64000000, + 99384.45000000, + 99389.26000000, + 99394.05000000, + 99398.82000000, + 99403.59000000, + 99408.34000000, + 99413.08000000, + 99417.81000000, + 99422.52000000, + 99427.22000000, + 99431.91000000, + 99436.59000000, + 99441.25000000, + 99445.90000000, + 99450.54000000, + 99455.16000000, + 99459.77000000, + 99464.37000000, + 99468.95000000, + 99473.52000000, + 99478.08000000, + 99482.63000000, + 99487.16000000, + 99491.68000000, + 99496.18000000, + 99500.67000000, + 99505.15000000, + 99509.62000000, + 99514.07000000, + 99518.51000000, + 99522.93000000, + 99527.34000000, + 99531.74000000, + 99536.12000000, + 99540.49000000, + 99544.84000000, + 99549.19000000, + 99553.51000000, + 99557.83000000, + 99562.13000000, + 99566.41000000, + 99570.69000000, + 99574.94000000, + 99579.19000000, + 99583.42000000, + 99587.63000000, + 99591.83000000, + 99596.02000000, + 99600.19000000, + 99604.35000000, + 99608.49000000, + 99612.62000000, + 99616.73000000, + 99620.83000000, + 99624.92000000, + 99628.99000000, + 99633.04000000, + 99637.08000000, + 99641.11000000, + 99645.12000000, + 99649.11000000, + 99653.09000000, + 99657.05000000, + 99661.00000000, + 99664.94000000, + 99668.86000000, + 99672.76000000, + 99676.65000000, + 99680.52000000, + 99684.38000000, + 99688.22000000, + 99692.04000000, + 99695.85000000, + 99699.65000000, + 99703.43000000, + 99707.19000000, + 99710.93000000, + 99714.66000000, + 99718.38000000, + 99722.08000000, + 99725.76000000, + 99729.42000000, + 99733.07000000, + 99736.70000000, + 99740.32000000, + 99743.91000000, + 99747.50000000, + 99751.06000000, + 99754.61000000, + 99758.14000000, + 99761.65000000, + 99765.15000000, + 99768.63000000, + 99772.09000000, + 99775.53000000, + 99778.96000000, + 99782.37000000, + 99785.76000000, + 99789.14000000, + 99792.49000000, + 99795.83000000, + 99799.15000000, + 99802.45000000, + 99805.73000000, + 99809.00000000, + 99812.25000000, + 99815.47000000, + 99818.68000000, + 99821.87000000, + 99825.04000000, + 99828.20000000, + 99831.33000000, + 99834.44000000, + 99837.54000000, + 99840.61000000, + 99843.67000000, + 99846.70000000, + 99849.72000000, + 99852.72000000, + 99855.69000000, + 99858.65000000, + 99861.58000000, + 99864.49000000, + 99867.39000000, + 99870.26000000, + 99873.11000000, + 99875.94000000, + 99878.75000000, + 99881.54000000, + 99884.30000000, + 99887.05000000, + 99889.77000000, + 99892.47000000, + 99895.15000000, + 99897.80000000, + 99900.43000000, + 99903.04000000, + 99905.63000000, + 99908.19000000, + 99910.73000000, + 99913.24000000, + 99915.73000000, + 99918.20000000, + 99920.64000000, + 99923.06000000, + 99925.45000000, + 99927.82000000, + 99930.16000000, + 99932.47000000, + 99934.76000000, + 99937.02000000, + 99939.26000000, + 99941.47000000, + 99943.65000000, + 99945.80000000, + 99947.93000000, + 99950.02000000, + 99952.09000000, + 99954.13000000, + 99956.14000000, + 99958.12000000, + 99960.07000000, + 99961.98000000, + 99963.87000000, + 99965.72000000, + 99967.54000000, + 99969.33000000, + 99971.08000000, + 99972.80000000, + 99974.48000000, + 99976.12000000, + 99977.73000000, + 99979.31000000, + 99980.84000000, + 99982.33000000, + 99983.78000000, + 99985.19000000, + 99986.56000000, + 99987.88000000, + 99989.16000000, + 99990.38000000, + 99991.56000000, + 99992.68000000, + 99993.75000000, + 99994.76000000, + 99995.71000000, + 99996.60000000, + 99997.41000000, + 99998.15000000, + 99998.80000000, + 99999.35000000, + 99999.77000000 +] + +access(all) let flash_crash_moderate_agents: [SimAgent] = [ + SimAgent( + count: 150, + initialHF: 1.15000000, + rebalancingHF: 1.05000000, + targetHF: 1.08000000, + debtPerAgent: 133333.00000000, + totalSystemDebt: 20000000.00000000 + ) +] + +access(all) let flash_crash_moderate_pools: {String: SimPool} = { + "moet_yt": SimPool( + size: 500000.00000000, + concentration: 0.95000000, + feeTier: 0.00050000 + ), + "moet_btc": SimPool( + size: 5000000.00000000, + concentration: 0.80000000, + feeTier: 0.00300000 + ) +} + +access(all) let flash_crash_moderate_constants: SimConstants = SimConstants( + btcCollateralFactor: 0.80000000, + btcLiquidationThreshold: 0.85000000, + yieldAPR: 0.10000000, + directMintYT: true +) + +access(all) let flash_crash_moderate_expectedLiquidationCount: Int = 0 +access(all) let flash_crash_moderate_expectedAllAgentsSurvive: Bool = true + +access(all) let flash_crash_moderate_durationMinutes: Int = 2880 +access(all) let flash_crash_moderate_notes: String = "20% BTC crash over 5 min, floor for 20 min, exponential recovery. No oracle manipulation, no liquidity evaporation, no random noise." diff --git a/cadence/tests/forked_flash_crash_moderate_test.cdc b/cadence/tests/forked_flash_crash_moderate_test.cdc new file mode 100644 index 00000000..d02cc65f --- /dev/null +++ b/cadence/tests/forked_flash_crash_moderate_test.cdc @@ -0,0 +1,457 @@ +#test_fork(network: "mainnet-fork", height: 143292255) + +import Test +import BlockchainHelpers + +import "test_helpers.cdc" +import "evm_state_helpers.cdc" +import "flash_crash_moderate_helpers.cdc" + +import "FlowYieldVaults" +import "FlowToken" +import "MOET" +import "FlowYieldVaultsStrategiesV2" +import "FlowALPv0" + +// ============================================================================ +// 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 seconds) to reset the MOET/FUSDEV pool price to peg. + /// Simulates the ALM arbitrage agent from the Python sims. + /// 0 = reset every tick + /// 43200 = every 12h (matches Python sim ALM interval) + access(all) let poolResetInterval: UFix64 + /// Python sim HF thresholds — for reference/logging only. + /// TODO: These should be applied to the on-chain FlowALP Position (via Position.setMinHealth, + /// setTargetHealth, setMaxHealth) but the Position is embedded inside FUSDEVStrategy with + /// access(self) and no passthrough exists. Current on-chain defaults are: + /// minHealth=1.1, targetHealth=1.3, maxHealth=1.5 + /// Python sim values (flash crash): rebalancingHF=1.05, targetHF=1.08, initialHF=1.15 + /// To fix: either expose setters on FUSDEVStrategy, or add an EGovernance method to Pool. + access(all) let initialHF: UFix64 + access(all) let rebalancingHF: UFix64 + access(all) let targetHF: UFix64 + + init( + prices: [UFix64], + tickIntervalSeconds: UFix64, + numAgents: Int, + fundingPerAgent: UFix64, + yieldAPR: UFix64, + expectedLiquidationCount: Int, + poolResetInterval: UFix64, + initialHF: UFix64, + rebalancingHF: UFix64, + targetHF: UFix64 + ) { + self.prices = prices + self.tickIntervalSeconds = tickIntervalSeconds + self.numAgents = numAgents + self.fundingPerAgent = fundingPerAgent + self.yieldAPR = yieldAPR + self.expectedLiquidationCount = expectedLiquidationCount + self.poolResetInterval = poolResetInterval + self.initialHF = initialHF + self.rebalancingHF = rebalancingHF + self.targetHF = targetHF + } +} + +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) + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: moetAddress, + tokenBAddress: morphoVaultAddress, + fee: 100, + priceTokenBPerTokenA: 1.0, + tokenABalanceSlot: moetBalanceSlot, + tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount + ) + + // 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 = flash_crash_moderate_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, external market pool, and vault share price each tick. +/// This does NOT touch the MOET/FUSDEV pool — that's controlled by resetYieldPool(). +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 + ) +} + +/// Reset the MOET/FUSDEV (yield token) pool price to peg. +access(all) fun resetYieldPool(ytPrice: UFix64) { + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: moetAddress, + tokenBAddress: morphoVaultAddress, + fee: 100, + priceTokenBPerTokenA: UFix128(ytPrice), + tokenABalanceSlot: moetBalanceSlot, + tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount + ) +} + +// ============================================================================ +// SIMULATION RUNNER +// ============================================================================ + +access(all) fun runSimulation(config: SimConfig): SimResult { + let prices = config.prices + let initialPrice = prices[0] + + // 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] + + users.append(user) + pids.append(pid) + vaultIds.append(vaultId) + + log(" Agent \(i): pid=\(pid) vaultId=\(vaultId)") + i = i + 1 + } + + log("\n=== SIMULATION ===") + 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)") + + // Run simulation + var rebalanceCount = 0 + var liquidationCount = 0 + var lowestHF: UFix64 = 100.0 + var lowestPrice: UFix64 = 999999999.0 + var previousBTCPrice: UFix64 = initialPrice + 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 != previousBTCPrice { + 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]) + + // Reset yield pool on interval (simulates ALM arb agent) + let elapsedSeconds = UFix64(step) * config.tickIntervalSeconds + if config.poolResetInterval == 0.0 || elapsedSeconds % config.poolResetInterval == 0.0 { + resetYieldPool(ytPrice: ytPrice) + } + + // Rebalance all agents + 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 + } + rebalanceCount = rebalanceCount + 1 + + // Check health factor for all agents + a = 0 + while a < config.numAgents { + let btcCollateral = getBTCCollateralFromPosition(pid: pids[a]) + let btcCollateralValue = btcCollateral * absolutePrice + let debt = getMOETDebtFromPosition(pid: pids[a]) + + if debt > 0.0 { + let hf = btcCollateralValue / debt + if hf < lowestHF { + lowestHF = hf + } + + if a == 0 { + log(" [t=\(step)] price=\(absolutePrice) yt=\(ytPrice) HF=\(hf) collateral=\(btcCollateralValue) debt=\(debt)") + } + + if hf < 1.0 { + liquidationCount = liquidationCount + 1 + log(" *** LIQUIDATION agent=\(a) at t=\(step)! HF=\(hf) ***") + } + } + a = a + 1 + } + + previousBTCPrice = absolutePrice + } + + step = step + 1 + } + + // Final state from agent 0 + let finalBTCCollateral = getBTCCollateralFromPosition(pid: pids[0]) + let finalDebt = getMOETDebtFromPosition(pid: pids[0]) + let finalHF = (finalBTCCollateral * previousBTCPrice) / finalDebt + let finalPrice = prices[prices.length - 1] + + log("\n=== SIMULATION RESULTS ===") + log("Agents: \(config.numAgents)") + log("Rebalance events: \(rebalanceCount)") + log("Liquidation count: \(liquidationCount)") + log("Lowest price: \(lowestPrice)") + log("Lowest HF observed: \(lowestHF)") + log("Final price: \(finalPrice)") + log("Final HF (agent 0): \(finalHF)") + log("===========================\n") + + return SimResult( + rebalanceCount: rebalanceCount, + liquidationCount: liquidationCount, + lowestHF: lowestHF, + finalHF: finalHF, + lowestPrice: lowestPrice, + finalPrice: finalPrice + ) +} + +// ============================================================================ +// TEST +// ============================================================================ + +access(all) +fun test_FlashCrashModerate_ZeroLiquidations() { + let result = runSimulation(config: SimConfig( + prices: flash_crash_moderate_prices, + tickIntervalSeconds: 5.0, + numAgents: 5, + fundingPerAgent: 1.0, + yieldAPR: flash_crash_moderate_constants.yieldAPR, + expectedLiquidationCount: 0, + poolResetInterval: 43200.0, // ALM arb every 12h (43200 seconds) + initialHF: 1.15, + rebalancingHF: 1.05, + targetHF: 1.08 + )) + + 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: Zero liquidations under flash crash ===") +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 568b8a13..ff8a3141 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -778,8 +778,11 @@ fun setBandOraclePrices(signer: Test.TestAccount, symbolPrices: {String: UFix64} for symbol in symbolPrices.keys { // BandOracle uses 1e9 multiplier for prices // e.g., $1.00 = 1_000_000_000, $0.50 = 500_000_000 + // Split into whole + fractional to avoid UFix64 overflow for large prices (e.g. BTC > $184) let price = symbolPrices[symbol]! - symbolsRates[symbol] = UInt64(price * 1_000_000_000.0) + let whole = UInt64(price) + let frac = price - UFix64(whole) + symbolsRates[symbol] = whole * 1_000_000_000 + UInt64(frac * 1_000_000_000.0) } let setRes = _executeTransaction( @@ -921,6 +924,81 @@ fun transferFlow(signer: Test.TestAccount, recipient: Address, amount: UFix64) { Test.expect(transferResult, Test.beSucceeded()) } +access(all) +fun setupGenericVault(signer: Test.TestAccount, vaultIdentifier: String) { + let setupResult = _executeTransaction( + "../../lib/flow-evm-bridge/cadence/transactions/example-assets/setup/setup_generic_vault.cdc", + [vaultIdentifier], + signer + ) + Test.expect(setupResult, Test.beSucceeded()) +} + +access(all) +fun transferBTC(signer: Test.TestAccount, recipient: Test.TestAccount, amount: UFix64) { + setupGenericVault( + signer: recipient, + vaultIdentifier: "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" + ) + let transferResult = _executeTransaction( + "transactions/transfer_wbtc.cdc", + [recipient.address, amount], + signer + ) + Test.expect(transferResult, Test.beSucceeded()) +} + +access(all) +fun setERC20Balance( + signer: Test.TestAccount, + tokenAddress: String, + holderAddress: String, + balanceSlot: UInt256, + amount: UInt256 +) { + let res = _executeTransaction( + "transactions/set_erc20_balance.cdc", + [tokenAddress, holderAddress, balanceSlot, amount], + signer + ) + Test.expect(res, Test.beSucceeded()) +} + +access(all) +fun mintBTC(signer: Test.TestAccount, amount: UFix64) { + let wbtcAddress = "0x717dae2baf7656be9a9b01dee31d571a9d4c9579" + let wbtcTokenId = "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" + let wbtcBalanceSlot: UInt256 = 5 + + // Ensure signer has a COA (needs some FLOW for gas) + if getCOA(signer.address) == nil { + createCOA(signer, fundingAmount: 1.0) + } + let coaAddress = getCOA(signer.address)! + + // Set wBTC ERC20 balance for the signer's COA on EVM + // wBTC has 8 decimals, so multiply amount by 1e8 + // Split to avoid UFix64 overflow for large amounts + let whole = UInt256(amount) + let frac = amount - UFix64(UInt64(amount)) + let amountSmallestUnit = whole * 100_000_000 + UInt256(frac * 100_000_000.0) + setERC20Balance( + signer: signer, + tokenAddress: wbtcAddress, + holderAddress: coaAddress, + balanceSlot: wbtcBalanceSlot, + amount: amountSmallestUnit + ) + + // Bridge wBTC from EVM to Cadence + let bridgeRes = _executeTransaction( + "../../lib/flow-evm-bridge/cadence/transactions/bridge/tokens/bridge_tokens_from_evm.cdc", + [wbtcTokenId, amountSmallestUnit], + signer + ) + Test.expect(bridgeRes, Test.beSucceeded()) +} + access(all) fun createCOA(_ signer: Test.TestAccount, fundingAmount: UFix64) { let createCOAResult = _executeTransaction( diff --git a/cadence/tests/transactions/set_erc20_balance.cdc b/cadence/tests/transactions/set_erc20_balance.cdc new file mode 100644 index 00000000..24af3423 --- /dev/null +++ b/cadence/tests/transactions/set_erc20_balance.cdc @@ -0,0 +1,36 @@ +import EVM from "MockEVM" + +/// Sets the ERC20 balanceOf for a given holder address via direct storage manipulation. +/// +/// @param tokenAddress: hex EVM address of the ERC20 contract +/// @param holderAddress: hex EVM address whose balance to set +/// @param balanceSlot: the storage slot index of the _balances mapping in the ERC20 contract +/// @param amount: the raw balance value to write (in smallest token units, e.g. satoshis for wBTC) +/// +transaction(tokenAddress: String, holderAddress: String, balanceSlot: UInt256, amount: UInt256) { + prepare(signer: auth(Storage) &Account) { + let token = EVM.addressFromString(tokenAddress) + + var addrHex = holderAddress + if holderAddress.slice(from: 0, upTo: 2) == "0x" { + addrHex = holderAddress.slice(from: 2, upTo: holderAddress.length) + } + let addrBytes = addrHex.decodeHex() + let holder = EVM.EVMAddress(bytes: addrBytes.toConstantSized<[UInt8; 20]>()!) + + let encoded = EVM.encodeABI([holder, balanceSlot]) + let slotHash = String.encodeHex(HashAlgorithm.KECCAK_256.hash(encoded)) + + let raw = amount.toBigEndianBytes() + var padded: [UInt8] = [] + var padCount = 32 - raw.length + while padCount > 0 { + padded.append(0) + padCount = padCount - 1 + } + padded = padded.concat(raw) + let valueHex = String.encodeHex(padded) + + EVM.store(target: token, slot: slotHash, value: valueHex) + } +} diff --git a/cadence/tests/transactions/transfer_wbtc.cdc b/cadence/tests/transactions/transfer_wbtc.cdc new file mode 100644 index 00000000..f3be55aa --- /dev/null +++ b/cadence/tests/transactions/transfer_wbtc.cdc @@ -0,0 +1,30 @@ +import "FungibleToken" + +transaction(recipient: Address, amount: UFix64) { + + let providerVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + let receiver: &{FungibleToken.Receiver} + + let storagePath: StoragePath + let receiverPath: PublicPath + + prepare(signer: auth(BorrowValue) &Account) { + self.storagePath = /storage/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Vault + self.receiverPath = /public/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Receiver + + self.providerVault = signer.storage.borrow( + from: self.storagePath + ) ?? panic("Could not borrow wBTC vault reference from signer") + + self.receiver = getAccount(recipient).capabilities.borrow<&{FungibleToken.Receiver}>(self.receiverPath) + ?? panic("Could not borrow receiver reference from recipient") + } + + execute { + self.receiver.deposit( + from: <-self.providerVault.withdraw( + amount: amount + ) + ) + } +} From d2aeb579b83e97533c57e3e5fb656e9f656bfef9 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 26 Mar 2026 22:44:39 -0700 Subject: [PATCH 5/5] Add scheduled trransactions reset --- .../mocks/FlowTransactionScheduler.cdc | 1454 +++++++++++++++++ .../forked_flash_crash_moderate_test.cdc | 3 + cadence/tests/test_helpers.cdc | 15 +- .../tests/transactions/reset_scheduler.cdc | 12 + flow.json | 8 + 5 files changed, 1491 insertions(+), 1 deletion(-) create mode 100644 cadence/contracts/mocks/FlowTransactionScheduler.cdc create mode 100644 cadence/tests/transactions/reset_scheduler.cdc diff --git a/cadence/contracts/mocks/FlowTransactionScheduler.cdc b/cadence/contracts/mocks/FlowTransactionScheduler.cdc new file mode 100644 index 00000000..642b938f --- /dev/null +++ b/cadence/contracts/mocks/FlowTransactionScheduler.cdc @@ -0,0 +1,1454 @@ +import "FungibleToken" +import "FlowToken" +import "FlowFees" +import "FlowStorageFees" +import "ViewResolver" + +/// FlowTransactionScheduler enables smart contracts to schedule autonomous execution in the future. +/// +/// This contract implements FLIP 330's scheduled transaction system, allowing contracts to "wake up" and execute +/// logic at predefined times without external triggers. +/// +/// Scheduled transactions are prioritized (High/Medium/Low) with different execution guarantees and fee multipliers: +/// - High priority guarantees first-block execution at the exact requested timestamp (fails if slot is full). +/// - Medium priority provides best-effort scheduling, shifting to the next available slot if the requested one is full. +/// - Low priority shifts to the next available slot if the requested one is full. +/// +/// Each priority level has its own independent effort pool per time slot with no shared capacity between priorities. +/// Low priority transactions are never rescheduled by higher priority transactions. +/// +/// The system uses time slots with execution effort limits to manage network resources, +/// ensuring predictable performance while enabling novel autonomous blockchain patterns like recurring +/// payments, automated arbitrage, and time-based contract logic. +access(all) contract FlowTransactionScheduler { + + /// singleton instance used to store all scheduled transaction data + /// and route all scheduled transaction functionality + access(self) var sharedScheduler: Capability + + /// storage path for the singleton scheduler resource + access(all) let storagePath: StoragePath + + /// Enums + + /// Priority + access(all) enum Priority: UInt8 { + access(all) case High + access(all) case Medium + access(all) case Low + } + + /// Status + access(all) enum Status: UInt8 { + /// unknown statuses are used for handling historic scheduled transactions with null statuses + access(all) case Unknown + /// mutable status + access(all) case Scheduled + /// finalized statuses + access(all) case Executed + access(all) case Canceled + } + + /// Events + + /// Emitted when a transaction is scheduled + access(all) event Scheduled( + id: UInt64, + priority: UInt8, + timestamp: UFix64, + executionEffort: UInt64, + fees: UFix64, + transactionHandlerOwner: Address, + transactionHandlerTypeIdentifier: String, + transactionHandlerUUID: UInt64, + + // The public path of the transaction handler that can be used to resolve views + // DISCLAIMER: There is no guarantee that the public path is accurate + transactionHandlerPublicPath: PublicPath? + ) + + /// Emitted when a scheduled transaction's scheduled timestamp is reached and it is ready for execution + access(all) event PendingExecution( + id: UInt64, + priority: UInt8, + executionEffort: UInt64, + fees: UFix64, + transactionHandlerOwner: Address, + transactionHandlerTypeIdentifier: String + ) + + /// Emitted when a scheduled transaction is executed by the FVM + access(all) event Executed( + id: UInt64, + priority: UInt8, + executionEffort: UInt64, + transactionHandlerOwner: Address, + transactionHandlerTypeIdentifier: String, + transactionHandlerUUID: UInt64, + + // The public path of the transaction handler that can be used to resolve views + // DISCLAIMER: There is no guarantee that the public path is accurate + transactionHandlerPublicPath: PublicPath? + ) + + /// Emitted when a scheduled transaction is canceled by the creator of the transaction + access(all) event Canceled( + id: UInt64, + priority: UInt8, + feesReturned: UFix64, + feesDeducted: UFix64, + transactionHandlerOwner: Address, + transactionHandlerTypeIdentifier: String + ) + + /// Emitted when a collection limit is reached + /// The limit that was reached is non-nil and is the limit that was reached + /// The other limit that was not reached is nil + access(all) event CollectionLimitReached( + collectionEffortLimit: UInt64?, + collectionTransactionsLimit: Int? + ) + + /// Emitted when the limit on the number of transactions that can be removed in process() is reached + access(all) event RemovalLimitReached() + + // Emitted when one or more of the configuration details fields are updated + // Event listeners can listen to this and query the new configuration + // if they need to + access(all) event ConfigUpdated() + + // Emitted when a critical issue is encountered + access(all) event CriticalIssue(message: String) + + /// Entitlements + access(all) entitlement Execute + access(all) entitlement Process + access(all) entitlement Cancel + access(all) entitlement UpdateConfig + + /// Interfaces + + /// TransactionHandler is an interface that defines a single method executeTransaction that + /// must be implemented by the resource that contains the logic to be executed by the scheduled transaction. + /// An authorized capability to this resource is provided when scheduling a transaction. + /// The transaction scheduler uses this capability to execute the transaction when its scheduled timestamp arrives. + access(all) resource interface TransactionHandler: ViewResolver.Resolver { + + access(all) view fun getViews(): [Type] { + return [] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + return nil + } + + /// Executes the implemented transaction logic + /// + /// @param id: The id of the scheduled transaction (this can be useful for any internal tracking) + /// @param data: The data that was passed when the transaction was originally scheduled + /// that may be useful for the execution of the transaction logic + access(Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) + } + + /// Structs + + /// ScheduledTransaction is the resource that the user receives after scheduling a transaction. + /// It allows them to get the status of their transaction and can be passed back + /// to the scheduler contract to cancel the transaction if it has not yet been executed. + access(all) resource ScheduledTransaction { + access(all) let id: UInt64 + access(all) let timestamp: UFix64 + access(all) let handlerTypeIdentifier: String + + access(all) view fun status(): Status? { + return FlowTransactionScheduler.sharedScheduler.borrow()!.getStatus(id: self.id) + } + + init( + id: UInt64, + timestamp: UFix64, + handlerTypeIdentifier: String + ) { + self.id = id + self.timestamp = timestamp + self.handlerTypeIdentifier = handlerTypeIdentifier + } + + // event emitted when the resource is destroyed + access(all) event ResourceDestroyed(id: UInt64 = self.id, timestamp: UFix64 = self.timestamp, handlerTypeIdentifier: String = self.handlerTypeIdentifier) + } + + /// EstimatedScheduledTransaction contains data for estimating transaction scheduling. + access(all) struct EstimatedScheduledTransaction { + /// flowFee is the estimated fee in Flow for the transaction to be scheduled + access(all) let flowFee: UFix64? + /// timestamp is estimated timestamp that the transaction will be executed at + access(all) let timestamp: UFix64? + /// error is an optional error message if the transaction cannot be scheduled + access(all) let error: String? + + access(contract) view init(flowFee: UFix64?, timestamp: UFix64?, error: String?) { + self.flowFee = flowFee + self.timestamp = timestamp + self.error = error + } + } + + /// Transaction data is a representation of a scheduled transaction + /// It is the source of truth for an individual transaction and stores the + /// capability to the handler that contains the logic that will be executed by the transaction. + access(all) struct TransactionData { + access(all) let id: UInt64 + access(all) let priority: Priority + access(all) let executionEffort: UInt64 + access(all) var status: Status + + /// Fee amount to pay for the transaction + access(all) let fees: UFix64 + + /// The timestamp that the transaction is scheduled for + /// For medium priority transactions, it may be different than the requested timestamp + /// For low priority transactions, it is the requested timestamp, + /// but the timestamp where the transaction is actually executed may be different + access(all) var scheduledTimestamp: UFix64 + + /// Capability to the logic that the transaction will execute + access(contract) let handler: Capability + + /// Type identifier of the transaction handler + access(all) let handlerTypeIdentifier: String + access(all) let handlerAddress: Address + + /// Optional data that can be passed to the handler + /// This data is publicly accessible, so make sure it does not contain + /// any privileged information or functionality + access(contract) let data: AnyStruct? + + access(contract) init( + id: UInt64, + handler: Capability, + scheduledTimestamp: UFix64, + data: AnyStruct?, + priority: Priority, + executionEffort: UInt64, + fees: UFix64, + ) { + self.id = id + self.handler = handler + self.data = data + self.priority = priority + self.executionEffort = executionEffort + self.fees = fees + self.status = Status.Scheduled + let handlerRef = handler.borrow() + ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler") + self.handlerAddress = handler.address + self.handlerTypeIdentifier = handlerRef.getType().identifier + self.scheduledTimestamp = scheduledTimestamp + } + + /// setStatus updates the status of the transaction. + /// It panics if the transaction status is already finalized. + access(contract) fun setStatus(newStatus: Status) { + pre { + newStatus != Status.Unknown: "Invalid status: New status cannot be Unknown" + self.status != Status.Executed && self.status != Status.Canceled: + "Invalid status: Transaction with id \(self.id) is already finalized" + newStatus == Status.Executed ? self.status == Status.Scheduled : true: + "Invalid status: Transaction with id \(self.id) can only be set as Executed if it is Scheduled" + newStatus == Status.Canceled ? self.status == Status.Scheduled : true: + "Invalid status: Transaction with id \(self.id) can only be set as Canceled if it is Scheduled" + } + + self.status = newStatus + } + + /// setScheduledTimestamp updates the scheduled timestamp of the transaction. + /// It panics if the transaction status is already finalized. + access(contract) fun setScheduledTimestamp(newTimestamp: UFix64) { + pre { + self.status != Status.Executed && self.status != Status.Canceled: + "Invalid status: Transaction with id \(self.id) is already finalized" + } + self.scheduledTimestamp = newTimestamp + } + + /// payAndRefundFees withdraws fees from the transaction based on the refund multiplier. + /// It deposits any leftover fees to the FlowFees vault to be used to pay node operator rewards + /// like any other transaction on the Flow network. + access(contract) fun payAndRefundFees(refundMultiplier: UFix64): @FlowToken.Vault { + pre { + refundMultiplier >= 0.0 && refundMultiplier <= 1.0: + "Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)" + } + if refundMultiplier == 0.0 { + FlowFees.deposit(from: <-FlowTransactionScheduler.withdrawFees(amount: self.fees)) + return <-FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) + } else { + let amountToReturn = self.fees * refundMultiplier + let amountToKeep = self.fees - amountToReturn + let feesToReturn <- FlowTransactionScheduler.withdrawFees(amount: amountToReturn) + FlowFees.deposit(from: <-FlowTransactionScheduler.withdrawFees(amount: amountToKeep)) + return <-feesToReturn + } + } + + /// getData copies and returns the data field + access(all) view fun getData(): AnyStruct? { + return self.data + } + + /// borrowHandler returns an un-entitled reference to the transaction handler + /// This allows users to query metadata views about the handler + /// @return: An un-entitled reference to the transaction handler + access(all) view fun borrowHandler(): &{TransactionHandler} { + return self.handler.borrow() as? &{TransactionHandler} + ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler") + } + } + + /// Struct interface representing all the base configuration details in the Scheduler contract + /// that is used for governing the protocol + /// This is an interface to allow for the configuration details to be updated in the future + access(all) struct interface SchedulerConfig { + + /// maximum effort that can be used for any transaction + access(all) var maximumIndividualEffort: UInt64 + + /// minimum execution effort is the minimum effort that can be + /// used for any transaction + access(all) var minimumExecutionEffort: UInt64 + + /// slot total effort limit is the maximum effort that can be + /// cumulatively allocated to one timeslot by all priorities + access(all) var slotTotalEffortLimit: UInt64 + + /// slot shared effort limit is the maximum effort + /// that can be allocated to high and medium priority + /// transactions combined after their exclusive effort reserves have been filled + access(all) var slotSharedEffortLimit: UInt64 + + /// priority effort reserve is the amount of effort that is + /// reserved exclusively for each priority + access(all) var priorityEffortReserve: {Priority: UInt64} + + /// priority effort limit is the maximum cumulative effort per priority in a timeslot + access(all) var priorityEffortLimit: {Priority: UInt64} + + /// max data size is the maximum data size that can be stored for a transaction + access(all) var maxDataSizeMB: UFix64 + + /// priority fee multipliers are values we use to calculate the added + /// processing fee for each priority + access(all) var priorityFeeMultipliers: {Priority: UFix64} + + /// refund multiplier is the portion of the fees that are refunded when any transaction is cancelled + access(all) var refundMultiplier: UFix64 + + /// canceledTransactionsLimit is the maximum number of canceled transactions + /// to keep in the canceledTransactions array + access(all) var canceledTransactionsLimit: UInt + + /// collectionEffortLimit is the maximum effort that can be used for all transactions in a collection + access(all) var collectionEffortLimit: UInt64 + + /// collectionTransactionsLimit is the maximum number of transactions that can be processed in a collection + access(all) var collectionTransactionsLimit: Int + + access(all) init( + maximumIndividualEffort: UInt64, + minimumExecutionEffort: UInt64, + priorityEffortLimit: {Priority: UInt64}, + maxDataSizeMB: UFix64, + priorityFeeMultipliers: {Priority: UFix64}, + refundMultiplier: UFix64, + canceledTransactionsLimit: UInt, + collectionEffortLimit: UInt64, + collectionTransactionsLimit: Int, + txRemovalLimit: UInt + ) { + post { + self.refundMultiplier >= 0.0 && self.refundMultiplier <= 1.0: + "Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)" + self.priorityFeeMultipliers[Priority.Low]! >= 1.0: + "Invalid priority fee multiplier: Low priority multiplier must be greater than or equal to 1.0 but got \(self.priorityFeeMultipliers[Priority.Low]!)" + self.priorityFeeMultipliers[Priority.Medium]! > self.priorityFeeMultipliers[Priority.Low]!: + "Invalid priority fee multiplier: Medium priority multiplier must be greater than or equal to \(priorityFeeMultipliers[Priority.Low]!) but got \(priorityFeeMultipliers[Priority.Medium]!)" + self.priorityFeeMultipliers[Priority.High]! > self.priorityFeeMultipliers[Priority.Medium]!: + "Invalid priority fee multiplier: High priority multiplier must be greater than or equal to \(priorityFeeMultipliers[Priority.Medium]!) but got \(priorityFeeMultipliers[Priority.High]!)" + self.priorityEffortLimit[Priority.Low]! > 0: + "Invalid priority effort limit: Low priority effort limit must be greater than 0" + self.priorityEffortLimit[Priority.Medium]! > self.priorityEffortLimit[Priority.Low]!: + "Invalid priority effort limit: Medium priority effort limit must be greater than the low priority effort limit of \(priorityEffortLimit[Priority.Low]!)" + self.priorityEffortLimit[Priority.High]! > self.priorityEffortLimit[Priority.Medium]!: + "Invalid priority effort limit: High priority effort limit must be greater than the medium priority effort limit of \(priorityEffortLimit[Priority.Medium]!)" + self.collectionTransactionsLimit >= 0: + "Invalid collection transactions limit: Collection transactions limit must be greater than or equal to 0 but got \(collectionTransactionsLimit)" + self.canceledTransactionsLimit >= 1: + "Invalid canceled transactions limit: Canceled transactions limit must be greater than or equal to 1 but got \(canceledTransactionsLimit)" + self.collectionEffortLimit > self.slotTotalEffortLimit: + "Invalid collection effort limit: Collection effort limit must be greater than \(self.slotTotalEffortLimit) but got \(self.collectionEffortLimit)" + } + } + + access(all) view fun getTxRemovalLimit(): UInt + } + + /// Concrete implementation of the SchedulerConfig interface + /// This struct is used to store the configuration details in the Scheduler contract + access(all) struct Config: SchedulerConfig { + access(all) var maximumIndividualEffort: UInt64 + access(all) var minimumExecutionEffort: UInt64 + access(all) var slotTotalEffortLimit: UInt64 + access(all) var slotSharedEffortLimit: UInt64 + access(all) var priorityEffortReserve: {Priority: UInt64} + access(all) var priorityEffortLimit: {Priority: UInt64} + access(all) var maxDataSizeMB: UFix64 + access(all) var priorityFeeMultipliers: {Priority: UFix64} + access(all) var refundMultiplier: UFix64 + access(all) var canceledTransactionsLimit: UInt + access(all) var collectionEffortLimit: UInt64 + access(all) var collectionTransactionsLimit: Int + + access(all) init( + maximumIndividualEffort: UInt64, + minimumExecutionEffort: UInt64, + priorityEffortLimit: {Priority: UInt64}, + maxDataSizeMB: UFix64, + priorityFeeMultipliers: {Priority: UFix64}, + refundMultiplier: UFix64, + canceledTransactionsLimit: UInt, + collectionEffortLimit: UInt64, + collectionTransactionsLimit: Int, + txRemovalLimit: UInt + ) { + self.maximumIndividualEffort = maximumIndividualEffort + self.minimumExecutionEffort = minimumExecutionEffort + self.priorityEffortLimit = priorityEffortLimit + // Legacy fields kept for storage compatibility; not used by scheduling logic + self.slotTotalEffortLimit = priorityEffortLimit[Priority.High]! + priorityEffortLimit[Priority.Medium]! + priorityEffortLimit[Priority.Low]! + self.slotSharedEffortLimit = 0 + self.priorityEffortReserve = {Priority.High: priorityEffortLimit[Priority.High]!, Priority.Medium: priorityEffortLimit[Priority.Medium]!, Priority.Low: priorityEffortLimit[Priority.Low]!} + self.maxDataSizeMB = maxDataSizeMB + self.priorityFeeMultipliers = priorityFeeMultipliers + self.refundMultiplier = refundMultiplier + self.canceledTransactionsLimit = canceledTransactionsLimit + self.collectionEffortLimit = collectionEffortLimit + self.collectionTransactionsLimit = collectionTransactionsLimit + } + + access(all) view fun getTxRemovalLimit(): UInt { + return FlowTransactionScheduler.account.storage.copy(from: /storage/txRemovalLimit) + ?? 200 + } + } + + + /// SortedTimestamps maintains timestamps sorted in ascending order for efficient processing + /// It encapsulates all operations related to maintaining and querying sorted timestamps + access(all) struct SortedTimestamps { + /// Internal sorted array of timestamps + access(self) var timestamps: [UFix64] + + access(all) init() { + self.timestamps = [] + } + + /// bisect is a function that finds the index to insert a new timestamp in the sorted array. + /// taken from bisect_right in pthon https://stackoverflow.com/questions/2945017/javas-equivalent-to-bisect-in-python + /// @param new: The new timestamp to insert + /// @return: The index to insert the new timestamp at or nil if the timestamp is already in the array + access(all) fun bisect(new: UFix64): Int? { + var high = self.timestamps.length + var low = 0 + while low < high { + let mid = (low+high)/2 + let midTimestamp = self.timestamps[mid] + + if midTimestamp == new { + return nil + } else if midTimestamp > new { + high = mid + } else { + low = mid + 1 + } + } + return low + } + + /// Add a timestamp to the sorted array maintaining sorted order + access(all) fun add(timestamp: UFix64) { + // Only insert if the timestamp is not already in the array + if let insertIndex = self.bisect(new: timestamp) { + self.timestamps.insert(at: insertIndex, timestamp) + } + } + + /// Remove a timestamp from the sorted array + access(all) fun remove(timestamp: UFix64) { + // Only remove if the timestamp is in the array + if let index = self.timestamps.firstIndex(of: timestamp) { + self.timestamps.remove(at: index) + } + } + + /// Get all timestamps that are in the past (less than or equal to current timestamp) + access(all) fun getBefore(current: UFix64): [UFix64] { + let pastTimestamps: [UFix64] = [] + for timestamp in self.timestamps { + if timestamp <= current { + pastTimestamps.append(timestamp) + } else { + break // No need to check further since array is sorted + } + } + return pastTimestamps + } + + /// Check if there are any timestamps that need processing + /// Returns true if processing is needed, false for early exit + access(all) fun hasBefore(current: UFix64): Bool { + return self.timestamps.length > 0 && self.timestamps[0] <= current + } + + /// Get the whole array of timestamps + access(all) fun getAll(): [UFix64] { + return self.timestamps + } + } + + /// Resources + + /// Shared scheduler is a resource that is used as a singleton in the scheduler contract and contains + /// all the functionality to schedule, process and execute transactions as well as the internal state. + access(all) resource SharedScheduler { + /// nextID contains the next transaction ID to be assigned + /// This the ID is monotonically increasing and is used to identify each transaction + access(contract) var nextID: UInt64 + + /// transactions is a map of transaction IDs to TransactionData structs + access(contract) var transactions: {UInt64: TransactionData} + + /// slot queue is a map of timestamps to Priorities to transaction IDs and their execution efforts + access(contract) var slotQueue: {UFix64: {Priority: {UInt64: UInt64}}} + + /// slot used effort is a map of timestamps map of priorities and + /// efforts that has been used for the timeslot + access(contract) var slotUsedEffort: {UFix64: {Priority: UInt64}} + + /// sorted timestamps manager for efficient processing + access(contract) var sortedTimestamps: SortedTimestamps + + /// canceled transactions keeps a record of canceled transaction IDs up to a canceledTransactionsLimit + access(contract) var canceledTransactions: [UInt64] + + /// Struct that contains all the configuration details for the transaction scheduler protocol + /// Can be updated by the owner of the contract + access(contract) var config: {SchedulerConfig} + + access(all) init() { + self.nextID = 1 + self.canceledTransactions = [0 as UInt64] + + self.transactions = {} + self.slotUsedEffort = {} + self.slotQueue = {} + self.sortedTimestamps = SortedTimestamps() + + /* Default slot efforts - each priority has its own independent pool: + + Timestamp Slot (25kee total) + ┌─────────────────────────┐ + │ ┌─────────────────────┐ │ High: 15kee — fail if full + │ │ High Pool 15kee │ │ + │ └─────────────────────┘ │ + │ ┌─────────────────────┐ │ Medium: 7.5kee — shift to next slot if full + │ │ Medium Pool 7.5kee │ │ + │ └─────────────────────┘ │ + │ ┌─────────────────────┐ │ Low: 2.5kee — shift to next slot if full + │ │ Low Pool 2.5kee │ │ + │ └─────────────────────┘ │ + └─────────────────────────┘ + */ + + self.config = Config( + maximumIndividualEffort: 9999, + minimumExecutionEffort: 100, + priorityEffortLimit: { + Priority.High: 15_000, + Priority.Medium: 7_500, + Priority.Low: 2_500 + }, + maxDataSizeMB: 0.001, + priorityFeeMultipliers: { + Priority.High: 10.0, + Priority.Medium: 5.0, + Priority.Low: 2.0 + }, + refundMultiplier: 0.5, + canceledTransactionsLimit: 1000, + collectionEffortLimit: 500_000, // Maximum effort for all transactions in a collection + collectionTransactionsLimit: 150, // Maximum number of transactions in a collection + txRemovalLimit: 200 + ) + } + + /// Gets a copy of the struct containing all the configuration details + /// of the Scheduler resource + access(contract) view fun getConfig(): {SchedulerConfig} { + return self.config + } + + /// sets all the configuration details for the Scheduler resource + /// NOTE: This function is guarded by the UpdateConfig entitlement, which is an admin-only + /// capability. It is not callable by regular users. Any configuration changes (including + /// txRemovalLimit) require explicit authorization from the contract administrator. + access(UpdateConfig) fun setConfig(newConfig: {SchedulerConfig}, txRemovalLimit: UInt) { + self.config = newConfig + FlowTransactionScheduler.account.storage.load(from: /storage/txRemovalLimit) + FlowTransactionScheduler.account.storage.save(txRemovalLimit, to: /storage/txRemovalLimit) + emit ConfigUpdated() + } + + /// getTransaction returns a copy of the specified transaction + access(contract) view fun getTransaction(id: UInt64): TransactionData? { + return self.transactions[id] + } + + /// borrowTransaction borrows a reference to the specified transaction + access(contract) view fun borrowTransaction(id: UInt64): &TransactionData? { + return &self.transactions[id] + } + + /// getCanceledTransactions returns a copy of the canceled transactions array + access(contract) view fun getCanceledTransactions(): [UInt64] { + return self.canceledTransactions + } + + /// getTransactionsForTimeframe returns a dictionary of transactions scheduled within a specified time range, + /// organized by timestamp and priority with arrays of transaction IDs. + /// WARNING: If you provide a time range that is too large, the function will likely fail to complete + /// because the function will run out of gas. Keep the time range small. + /// + /// @param startTimestamp: The start timestamp (inclusive) for the time range + /// @param endTimestamp: The end timestamp (inclusive) for the time range + /// @return {UFix64: {Priority: [UInt64]}}: A dictionary mapping timestamps to priorities to arrays of transaction IDs + access(contract) fun getTransactionsForTimeframe(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: {UInt8: [UInt64]}} { + var transactionsInTimeframe: {UFix64: {UInt8: [UInt64]}} = {} + + // Validate input parameters + if startTimestamp > endTimestamp { + return transactionsInTimeframe + } + + // Get all timestamps that fall within the specified range + let allTimestampsBeforeEnd = self.sortedTimestamps.getBefore(current: endTimestamp) + + for timestamp in allTimestampsBeforeEnd { + // Check if this timestamp falls within our range + if timestamp < startTimestamp { continue } + + let transactionPriorities = self.slotQueue[timestamp] ?? {} + + var timestampTransactions: {UInt8: [UInt64]} = {} + + for priority in transactionPriorities.keys { + let transactionIDs = transactionPriorities[priority] ?? {} + var priorityTransactions: [UInt64] = [] + + for id in transactionIDs.keys { + priorityTransactions.append(id) + } + + if priorityTransactions.length > 0 { + timestampTransactions[priority.rawValue] = priorityTransactions + } + } + + if timestampTransactions.keys.length > 0 { + transactionsInTimeframe[timestamp] = timestampTransactions + } + + } + + return transactionsInTimeframe + } + + /// calculate fee by converting execution effort to a fee in Flow tokens. + /// @param executionEffort: The execution effort of the transaction + /// @param priority: The priority of the transaction + /// @param dataSizeMB: The size of the data that was passed when the transaction was originally scheduled + /// @return UFix64: The fee in Flow tokens that is required to pay for the transaction + access(contract) fun calculateFee(executionEffort: UInt64, priority: Priority, dataSizeMB: UFix64): UFix64 { + // Use the official FlowFees calculation + let baseFee = FlowFees.computeFees(inclusionEffort: 1.0, executionEffort: UFix64(executionEffort)/100000000.0) + + // Scale the execution fee by the multiplier for the priority + let scaledExecutionFee = baseFee * self.config.priorityFeeMultipliers[priority]! + + // Calculate the FLOW required to pay for storage of the transaction data + let storageFee = FlowStorageFees.storageCapacityToFlow(dataSizeMB) + + // Add inclusion Flow fee for scheduled transactions + let inclusionFee = 0.00001 + + return scaledExecutionFee + storageFee + inclusionFee + } + + /// getNextIDAndIncrement returns the next ID and increments the ID counter + access(self) fun getNextIDAndIncrement(): UInt64 { + let nextID = self.nextID + self.nextID = self.nextID + 1 + return nextID + } + + /// get status of the scheduled transaction + /// @param id: The ID of the transaction to get the status of + /// @return Status: The status of the transaction, if the transaction is not found Unknown is returned. + access(contract) view fun getStatus(id: UInt64): Status? { + // if the transaction ID is greater than the next ID, it is not scheduled yet and has never existed + if id == 0 as UInt64 || id >= self.nextID { + return nil + } + + // This should always return Scheduled or Executed + if let tx = self.borrowTransaction(id: id) { + return tx.status + } + + // if the transaction was canceled and it is still not pruned from + // list return canceled status + if self.canceledTransactions.contains(id) { + return Status.Canceled + } + + // If we reach this point, the transaction is not in the active transactions map + // and not in canceledTransactions. Since Scheduled transactions always remain in + // the transactions map until execution, a transaction can only reach this code path + // after it has been executed and aged out. The inference below uses the sorted + // canceledTransactions array as a lower-bound anchor: if the requested ID is greater + // than the oldest known canceled ID, it must have been executed (not canceled), + // because any cancellation would have added it to the canceledTransactions array. + // NOTE: Scheduled (future) transactions cannot be incorrectly reported as Executed + // here — they are still in the transactions map and are returned as Scheduled above. + let firstCanceledID = self.canceledTransactions[0] + if id > firstCanceledID { + return Status.Executed + } + + // the transaction list was pruned and the transaction status might be + // either canceled or execute so we return unknown + return Status.Unknown + } + + /// schedule is the primary entry point for scheduling a new transaction within the scheduler contract. + /// If scheduling the transaction is not possible either due to invalid arguments or due to + /// unavailable slots, the function panics. + // + /// The schedule function accepts the following arguments: + /// @param: transaction: A capability to a resource in storage that implements the transaction handler + /// interface. This handler will be invoked at execution time and will receive the specified data payload. + /// @param: timestamp: Specifies the earliest block timestamp at which the transaction is eligible for execution + /// (Unix timestamp so fractional seconds values are ignored). It must be set in the future. + /// @param: priority: An enum value (`High`, `Medium`, or `Low`) that influences the scheduling behavior and determines + /// how soon after the timestamp the transaction will be executed. + /// @param: executionEffort: Defines the maximum computational resources allocated to the transaction. This also determines + /// the fee charged. Unused execution effort is not refunded. + /// @param: fees: A Vault resource containing sufficient funds to cover the required execution effort. + access(contract) fun schedule( + handlerCap: Capability, + data: AnyStruct?, + timestamp: UFix64, + priority: Priority, + executionEffort: UInt64, + fees: @FlowToken.Vault + ): @ScheduledTransaction { + + // Use the estimate function to validate inputs + let estimate = self.estimate( + data: data, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort + ) + + if estimate.error != nil { + panic(estimate.error!) + } + + assert ( + fees.balance >= estimate.flowFee!, + message: "Insufficient fees: The Fee balance of \(fees.balance) is not sufficient to pay the required amount of \(estimate.flowFee!) for execution of the transaction." + ) + + let transactionID = self.getNextIDAndIncrement() + let transactionData = TransactionData( + id: transactionID, + handler: handlerCap, + scheduledTimestamp: estimate.timestamp!, + data: data, + priority: priority, + executionEffort: executionEffort, + fees: fees.balance, + ) + + // Deposit the fees to the service account's vault + FlowTransactionScheduler.depositFees(from: <-fees) + + let handlerRef = handlerCap.borrow() + ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler") + + let handlerPublicPath = handlerRef.resolveView(Type()) as? PublicPath + + emit Scheduled( + id: transactionData.id, + priority: transactionData.priority.rawValue, + timestamp: transactionData.scheduledTimestamp, + executionEffort: transactionData.executionEffort, + fees: transactionData.fees, + transactionHandlerOwner: transactionData.handler.address, + transactionHandlerTypeIdentifier: transactionData.handlerTypeIdentifier, + transactionHandlerUUID: handlerRef.uuid, + transactionHandlerPublicPath: handlerPublicPath + ) + + // Add the transaction to the slot queue and update the internal state + self.addTransaction(slot: estimate.timestamp!, txData: transactionData) + + return <-create ScheduledTransaction( + id: transactionID, + timestamp: estimate.timestamp!, + handlerTypeIdentifier: transactionData.handlerTypeIdentifier + ) + } + + /// The estimate function calculates the required fee in Flow and expected execution timestamp for + /// a transaction based on the requested timestamp, priority, and execution effort. + /// + /// If the provided arguments are invalid or the transaction cannot be scheduled (e.g., due to + /// insufficient computation effort or unavailable time slots) the estimate function + /// returns an EstimatedScheduledTransaction struct with a non-nil error message. + /// + /// This helps developers ensure sufficient funding and preview the expected scheduling window, + /// reducing the risk of unnecessary cancellations. + /// + /// V2: Each priority has its own independent pool. Low priority transactions receive a valid + /// timestamp estimate just like High and Medium priority transactions. + /// + /// @param data: The data that was passed when the transaction was originally scheduled + /// @param timestamp: The requested timestamp for the transaction + /// @param priority: The priority of the transaction + /// @param executionEffort: The execution effort of the transaction + /// @return EstimatedScheduledTransaction: A struct containing the estimated fee, timestamp, and error message + access(contract) fun estimate( + data: AnyStruct?, + timestamp: UFix64, + priority: Priority, + executionEffort: UInt64 + ): EstimatedScheduledTransaction { + // Remove fractional values from the timestamp + let sanitizedTimestamp = UFix64(UInt64(timestamp)) + + if sanitizedTimestamp <= getCurrentBlock().timestamp { + return EstimatedScheduledTransaction( + flowFee: nil, + timestamp: nil, + error: "Invalid timestamp: \(sanitizedTimestamp) is in the past, current timestamp: \(getCurrentBlock().timestamp)" + ) + } + + if executionEffort > self.config.maximumIndividualEffort { + return EstimatedScheduledTransaction( + flowFee: nil, + timestamp: nil, + error: "Invalid execution effort: \(executionEffort) is greater than the maximum transaction effort of \(self.config.maximumIndividualEffort)" + ) + } + + if executionEffort > self.config.priorityEffortLimit[priority]! { + return EstimatedScheduledTransaction( + flowFee: nil, + timestamp: nil, + error: "Invalid execution effort: \(executionEffort) is greater than the priority's max effort of \(self.config.priorityEffortLimit[priority]!)" + ) + } + + if executionEffort < self.config.minimumExecutionEffort { + return EstimatedScheduledTransaction( + flowFee: nil, + timestamp: nil, + error: "Invalid execution effort: \(executionEffort) is less than the minimum execution effort of \(self.config.minimumExecutionEffort)" + ) + } + + let dataSizeMB = FlowTransactionScheduler.getSizeOfData(data) + if dataSizeMB > self.config.maxDataSizeMB { + return EstimatedScheduledTransaction( + flowFee: nil, + timestamp: nil, + error: "Invalid data size: \(dataSizeMB) is greater than the maximum data size of \(self.config.maxDataSizeMB)MB" + ) + } + + let fee = self.calculateFee(executionEffort: executionEffort, priority: priority, dataSizeMB: dataSizeMB) + + let scheduledTimestamp = self.calculateScheduledTimestamp( + timestamp: sanitizedTimestamp, + priority: priority, + executionEffort: executionEffort + ) + + if scheduledTimestamp == nil { + return EstimatedScheduledTransaction( + flowFee: nil, + timestamp: nil, + error: "Invalid execution effort: \(executionEffort) is greater than the priority's available effort for the requested timestamp." + ) + } + + return EstimatedScheduledTransaction(flowFee: fee, timestamp: scheduledTimestamp, error: nil) + } + + /// calculateScheduledTimestamp calculates the timestamp at which a transaction + /// can be scheduled. It takes into account the priority of the transaction and + /// the execution effort. + /// - If the transaction is high priority, it returns the timestamp if there is enough + /// space or nil if there is no space left. + /// - If the transaction is medium or low priority and there is space left in the requested timestamp, + /// it returns the requested timestamp. If there is not enough space, it finds the next timestamp with space. + /// + /// @param timestamp: The requested timestamp for the transaction + /// @param priority: The priority of the transaction + /// @param executionEffort: The execution effort of the transaction + /// @return UFix64?: The timestamp at which the transaction can be scheduled, or nil if there is no space left for a high priority transaction + access(contract) view fun calculateScheduledTimestamp( + timestamp: UFix64, + priority: Priority, + executionEffort: UInt64 + ): UFix64? { + + var timestampToSearch = timestamp + + // If no available timestamps are found, this will eventually reach the gas limit and fail + // This is extremely unlikely + while true { + + let used = self.slotUsedEffort[timestampToSearch] + // if nothing is scheduled at this timestamp, we can schedule at provided timestamp + if used == nil { + return timestampToSearch + } + + let available = self.getSlotAvailableEffort(sanitizedTimestamp: timestampToSearch, priority: priority) + // if theres enough space, we can tentatively schedule at provided timestamp + if executionEffort <= available { + return timestampToSearch + } + + if priority == Priority.High { + // high priority demands scheduling at exact timestamp or failing + return nil + } + + timestampToSearch = timestampToSearch + 1.0 + } + + // should never happen + return nil + } + + /// slot available effort returns the amount of effort that is available for a given timestamp and priority. + /// Each priority has its own independent pool with no shared capacity between priorities. + /// @param sanitizedTimestamp: The timestamp to get the available effort for. It should already have been sanitized + /// in the calling function + /// @param priority: The priority to get the available effort for + /// @return UInt64: The amount of effort that is available for the given timestamp and priority + access(contract) view fun getSlotAvailableEffort(sanitizedTimestamp: UFix64, priority: Priority): UInt64 { + let limit = self.config.priorityEffortLimit[priority]! + + if !self.slotUsedEffort.containsKey(sanitizedTimestamp) { + return limit + } + + let slotPriorityEffortsUsed = &self.slotUsedEffort[sanitizedTimestamp]! as &{Priority: UInt64} + let used = slotPriorityEffortsUsed[priority] ?? 0 + return limit.saturatingSubtract(used) + } + + /// add transaction to the queue and updates all the internal state + access(self) fun addTransaction(slot: UFix64, txData: TransactionData) { + + // If nothing is in the queue for this slot, initialize the slot + if self.slotQueue[slot] == nil { + self.slotQueue[slot] = {} + + // This also means that the used effort record for this slot has not been initialized + self.slotUsedEffort[slot] = { + Priority.High: 0, + Priority.Medium: 0, + Priority.Low: 0 + } + + self.sortedTimestamps.add(timestamp: slot) + } + + // Add this transaction id to the slot + let transactionsForSlot = self.slotQueue[slot]! + if let priorityQueue = transactionsForSlot[txData.priority] { + priorityQueue[txData.id] = txData.executionEffort + transactionsForSlot[txData.priority] = priorityQueue + } else { + transactionsForSlot[txData.priority] = { + txData.id: txData.executionEffort + } + } + self.slotQueue[slot] = transactionsForSlot + + // Add the execution effort for this transaction to the per-priority total for the slot. + // NOTE: This addition cannot overflow in practice. executionEffort is validated against + // maximumIndividualEffort and priorityEffortLimit before reaching this point (in estimate()), + // and the cumulative slot total is bounded by priorityEffortLimit[priority] which is + // checked on every schedule() call. UInt64 max (~1.8e19) far exceeds any reachable sum. + let slotEfforts = &self.slotUsedEffort[slot]! as auth(Mutate) &{Priority: UInt64} + slotEfforts[txData.priority] = slotEfforts[txData.priority]! + txData.executionEffort + + // Store the transaction in the transactions map + self.transactions[txData.id] = txData + } + + /// remove the transaction from the slot queue. + access(self) fun removeTransaction(txData: &TransactionData): TransactionData { + + let transactionID = txData.id + let slot = txData.scheduledTimestamp + let transactionPriority = txData.priority + + // remove transaction object + let transactionObject = self.transactions.remove(key: transactionID)! + + // garbage collect slots + let transactionQueue = self.slotQueue[slot]! + + if let priorityQueue = transactionQueue[transactionPriority] { + priorityQueue[transactionID] = nil + if priorityQueue.keys.length == 0 { + transactionQueue.remove(key: transactionPriority) + } else { + transactionQueue[transactionPriority] = priorityQueue + } + } + self.slotQueue[slot] = transactionQueue + + // if the slot is now empty remove it from the maps + if transactionQueue.keys.length == 0 { + self.slotQueue.remove(key: slot) + self.slotUsedEffort.remove(key: slot) + + self.sortedTimestamps.remove(timestamp: slot) + } + + return transactionObject + } + + /// pendingQueue creates a list of transactions that are ready for execution. + /// For transaction to be ready for execution it must be scheduled. + /// + /// The queue is sorted by timestamp and then by priority (high, medium, low). + /// The queue will contain transactions from all timestamps that are in the past. + /// Low priority transactions will only be added if there is effort available in the slot. + /// The return value can be empty if there are no transactions ready for execution. + access(Process) fun pendingQueue(): [&TransactionData] { + let currentTimestamp = getCurrentBlock().timestamp + var pendingTransactions: [&TransactionData] = [] + + // total effort across different timestamps guards collection being over the effort limit + var collectionAvailableEffort = self.config.collectionEffortLimit + var transactionsAvailableCount = self.config.collectionTransactionsLimit + + // Collect past timestamps efficiently from sorted array + let pastTimestamps = self.sortedTimestamps.getBefore(current: currentTimestamp) + + for timestamp in pastTimestamps { + let transactionPriorities = self.slotQueue[timestamp] ?? {} + var high: [&TransactionData] = [] + var medium: [&TransactionData] = [] + var low: [&TransactionData] = [] + + for priority in transactionPriorities.keys { + let transactionIDs = transactionPriorities[priority] ?? {} + for id in transactionIDs.keys { + let tx = self.borrowTransaction(id: id) + if tx == nil { + emit CriticalIssue(message: "Invalid ID: \(id) transaction not found while preparing pending queue") + continue + } + + // Only add scheduled transactions to the queue + if tx!.status != Status.Scheduled { + continue + } + + // this is safeguard to prevent collection growing too large in case of block production slowdown + if tx!.executionEffort >= collectionAvailableEffort || transactionsAvailableCount == 0 { + emit CollectionLimitReached( + collectionEffortLimit: transactionsAvailableCount == 0 ? nil : self.config.collectionEffortLimit, + collectionTransactionsLimit: transactionsAvailableCount == 0 ? self.config.collectionTransactionsLimit : nil + ) + break + } + + collectionAvailableEffort = collectionAvailableEffort.saturatingSubtract(tx!.executionEffort) + transactionsAvailableCount = transactionsAvailableCount - 1 + + switch tx!.priority { + case Priority.High: + high.append(tx!) + case Priority.Medium: + medium.append(tx!) + case Priority.Low: + low.append(tx!) + } + } + } + + pendingTransactions = pendingTransactions + .concat(high) + .concat(medium) + .concat(low) + } + + return pendingTransactions + } + + /// removeExecutedTransactions removes all transactions that are marked as executed. + access(self) fun removeExecutedTransactions(_ currentTimestamp: UFix64) { + let pastTimestamps = self.sortedTimestamps.getBefore(current: currentTimestamp) + var numRemoved = 0 + let removalLimit = self.config.getTxRemovalLimit() + + for timestamp in pastTimestamps { + let transactionPriorities = self.slotQueue[timestamp] ?? {} + + for priority in transactionPriorities.keys { + let transactionIDs = transactionPriorities[priority] ?? {} + for id in transactionIDs.keys { + + numRemoved = numRemoved + 1 + + if UInt(numRemoved) >= removalLimit { + emit RemovalLimitReached() + return + } + + let tx = self.borrowTransaction(id: id) + if tx == nil { + emit CriticalIssue(message: "Invalid ID: \(id) transaction not found while removing executed transactions") + continue + } + + // Only remove executed transactions + if tx!.status != Status.Executed { + continue + } + + // charge the full fee for transaction execution + destroy tx!.payAndRefundFees(refundMultiplier: 0.0) + + self.removeTransaction(txData: tx!) + } + } + } + } + + /// process scheduled transactions and prepare them for execution. + /// + /// First, it removes transactions that have already been executed. + /// Then, it iterates over past timestamps in the queue and processes the transactions that are + /// eligible for execution. It also emits an event for each transaction that is processed. + /// + /// This function is only called by the FVM to process transactions. + access(Process) fun process() { + let currentTimestamp = getCurrentBlock().timestamp + // Early exit if no timestamps need processing + if !self.sortedTimestamps.hasBefore(current: currentTimestamp) { + return + } + + self.removeExecutedTransactions(currentTimestamp) + + let pendingTransactions = self.pendingQueue() + + for tx in pendingTransactions { + + emit PendingExecution( + id: tx.id, + priority: tx.priority.rawValue, + executionEffort: tx.executionEffort, + fees: tx.fees, + transactionHandlerOwner: tx.handler.address, + // Cannot use the real type identifier here because if + // the handler contract is broken, it could cause the process function to fail + transactionHandlerTypeIdentifier: "" + ) + + // after pending execution event is emitted we set the transaction as executed because we + // must rely on execution node to actually execute it. Execution of the transaction is + // done in a separate transaction that calls executeTransaction(id) function. + // Executing the transaction can not update the status of transaction or any other shared state, + // since that blocks concurrent transaction execution. + // Therefore an optimistic update to executed is made here to avoid race condition. + tx.setStatus(newStatus: Status.Executed) + } + } + + /// cancel a scheduled transaction and return a portion of the fees that were paid. + /// + /// @param id: The ID of the transaction to cancel + /// @return: The fees to be returned to the caller + access(Cancel) fun cancel(id: UInt64): @FlowToken.Vault { + let tx = self.borrowTransaction(id: id) ?? + panic("Invalid ID: \(id) transaction not found") + + assert( + tx.status == Status.Scheduled, + message: "Transaction must be in a scheduled state in order to be canceled" + ) + + // Subtract the execution effort for this transaction from the slot's priority + let slotEfforts = self.slotUsedEffort[tx.scheduledTimestamp]! + slotEfforts[tx.priority] = slotEfforts[tx.priority]!.saturatingSubtract(tx.executionEffort) + self.slotUsedEffort[tx.scheduledTimestamp] = slotEfforts + + let totalFees = tx.fees + let refundedFees <- tx.payAndRefundFees(refundMultiplier: self.config.refundMultiplier) + + // if the transaction was canceled, add it to the canceled transactions array + // maintain sorted order by inserting at the correct position + var high = self.canceledTransactions.length + var low = 0 + while low < high { + let mid = (low+high)/2 + let midCanceledID = self.canceledTransactions[mid] + + if midCanceledID == id { + emit CriticalIssue(message: "Invalid ID: \(id) transaction already in canceled transactions array") + break + } else if midCanceledID > id { + high = mid + } else { + low = mid + 1 + } + } + self.canceledTransactions.insert(at: low, id) + + // keep the array under the limit + if UInt(self.canceledTransactions.length) > self.config.canceledTransactionsLimit { + self.canceledTransactions.remove(at: 0) + } + + emit Canceled( + id: tx.id, + priority: tx.priority.rawValue, + feesReturned: refundedFees.balance, + feesDeducted: totalFees - refundedFees.balance, + transactionHandlerOwner: tx.handler.address, + transactionHandlerTypeIdentifier: tx.handlerTypeIdentifier + ) + + self.removeTransaction(txData: tx) + + return <-refundedFees + } + + /// execute transaction is a system function that is called by FVM to execute a transaction by ID. + /// The transaction must be found and in correct state or the function panics and this is a fatal error + /// + /// This function is only called by the FVM to execute transactions. + /// WARNING: this function should not change any shared state, it will be run concurrently and it must not be blocking. + access(Execute) fun executeTransaction(id: UInt64) { + let tx = self.borrowTransaction(id: id) ?? + panic("Invalid ID: Transaction with id \(id) not found") + + assert ( + tx.status == Status.Executed, + message: "Invalid ID: Cannot execute transaction with id \(id) because it has incorrect status \(tx.status.rawValue)" + ) + + let transactionHandler = tx.handler.borrow() + ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler") + + let handlerPublicPath = transactionHandler.resolveView(Type()) as? PublicPath + + emit Executed( + id: tx.id, + priority: tx.priority.rawValue, + executionEffort: tx.executionEffort, + transactionHandlerOwner: tx.handler.address, + transactionHandlerTypeIdentifier: transactionHandler.getType().identifier, + transactionHandlerUUID: transactionHandler.uuid, + transactionHandlerPublicPath: handlerPublicPath + + ) + + transactionHandler.executeTransaction(id: id, data: tx.getData()) + } + + /// Clears all queued/scheduled transactions, resetting the scheduler to an empty state. + access(Cancel) fun reset() { + self.transactions = {} + self.slotQueue = {} + self.slotUsedEffort = {} + self.sortedTimestamps = SortedTimestamps() + self.canceledTransactions = [0 as UInt64] + } + } + + /// Deposit fees to this contract's account's vault + access(contract) fun depositFees(from: @FlowToken.Vault) { + let vaultRef = self.account.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault) + ?? panic("Unable to borrow reference to the default token vault") + vaultRef.deposit(from: <-from) + } + + /// Withdraw fees from this contract's account's vault + access(contract) fun withdrawFees(amount: UFix64): @FlowToken.Vault { + let vaultRef = self.account.storage.borrow(from: /storage/flowTokenVault) + ?? panic("Unable to borrow reference to the default token vault") + + return <-vaultRef.withdraw(amount: amount) as! @FlowToken.Vault + } + + access(all) fun schedule( + handlerCap: Capability, + data: AnyStruct?, + timestamp: UFix64, + priority: Priority, + executionEffort: UInt64, + fees: @FlowToken.Vault + ): @ScheduledTransaction { + return <-self.sharedScheduler.borrow()!.schedule( + handlerCap: handlerCap, + data: data, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort, + fees: <-fees + ) + } + + access(all) fun estimate( + data: AnyStruct?, + timestamp: UFix64, + priority: Priority, + executionEffort: UInt64 + ): EstimatedScheduledTransaction { + return self.sharedScheduler.borrow()! + .estimate( + data: data, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort, + ) + } + + /// Allows users to calculate the fee for a scheduled transaction without having to call the expensive estimate function + /// @param executionEffort: The execution effort of the transaction + /// @param priority: The priority of the transaction + /// @param dataSizeMB: The size of the data to be stored with the scheduled transaction + /// The user must calculate this data size themselves before calling this function + /// But should be done in a separate script or transaction to avoid the expensive getSizeOfData function + /// @return UFix64: The fee in Flow tokens that is required to pay for the transaction + access(all) fun calculateFee(executionEffort: UInt64, priority: Priority, dataSizeMB: UFix64): UFix64 { + return self.sharedScheduler.borrow()!.calculateFee(executionEffort: executionEffort, priority: priority, dataSizeMB: dataSizeMB) + } + + access(all) fun cancel(scheduledTx: @ScheduledTransaction): @FlowToken.Vault { + let id = scheduledTx.id + destroy scheduledTx + return <-self.sharedScheduler.borrow()!.cancel(id: id) + } + + /// getTransactionData returns the transaction data for a given ID + /// This function can only get the data for a transaction that is currently scheduled or pending execution + /// because finalized transaction metadata is not stored in the contract + /// @param id: The ID of the transaction to get the data for + /// @return: The transaction data for the given ID + access(all) view fun getTransactionData(id: UInt64): TransactionData? { + return self.sharedScheduler.borrow()!.getTransaction(id: id) + } + + /// borrowHandlerForID returns an un-entitled reference to the transaction handler for a given ID + /// The handler reference can be used to resolve views to get info about the handler and see where it is stored + /// @param id: The ID of the transaction to get the handler for + /// @return: An un-entitled reference to the transaction handler for the given ID + access(all) view fun borrowHandlerForID(_ id: UInt64): &{TransactionHandler}? { + return self.getTransactionData(id: id)?.borrowHandler() + } + + /// getCanceledTransactions returns the IDs of the transactions that have been canceled + /// @return: The IDs of the transactions that have been canceled + access(all) view fun getCanceledTransactions(): [UInt64] { + return self.sharedScheduler.borrow()!.getCanceledTransactions() + } + + + access(all) view fun getStatus(id: UInt64): Status? { + return self.sharedScheduler.borrow()!.getStatus(id: id) + } + + /// getTransactionsForTimeframe returns the IDs of the transactions that are scheduled for a given timeframe + /// @param startTimestamp: The start timestamp to get the IDs for + /// @param endTimestamp: The end timestamp to get the IDs for + /// @return: The IDs of the transactions that are scheduled for the given timeframe + access(all) fun getTransactionsForTimeframe(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: {UInt8: [UInt64]}} { + return self.sharedScheduler.borrow()!.getTransactionsForTimeframe(startTimestamp: startTimestamp, endTimestamp: endTimestamp) + } + + access(all) view fun getSlotAvailableEffort(timestamp: UFix64, priority: Priority): UInt64 { + // Remove fractional values from the timestamp + let sanitizedTimestamp = UFix64(UInt64(timestamp)) + return self.sharedScheduler.borrow()!.getSlotAvailableEffort(sanitizedTimestamp: sanitizedTimestamp, priority: priority) + } + + access(all) fun getConfig(): {SchedulerConfig} { + return self.sharedScheduler.borrow()!.getConfig() + } + + /// getSizeOfData takes a transaction's data + /// argument and stores it in the contract account's storage, + /// checking storage used before and after to see how large the data is in MB + /// If data is nil, the function returns 0.0 + access(all) fun getSizeOfData(_ data: AnyStruct?): UFix64 { + if data == nil { + return 0.0 + } else { + let type = data!.getType() + if type.isSubtype(of: Type()) + || type.isSubtype(of: Type()) + || type.isSubtype(of: Type
()) + || type.isSubtype(of: Type()) + || type.isSubtype(of: Type()) + { + return 0.0 + } + } + let storagePath = /storage/dataTemp + let storageUsedBefore = self.account.storage.used + self.account.storage.save(data!, to: storagePath) + let storageUsedAfter = self.account.storage.used + self.account.storage.load(from: storagePath) + + return FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(storageUsedAfter.saturatingSubtract(storageUsedBefore)) + } + + access(all) init() { + self.storagePath = /storage/sharedScheduler + let scheduler <- create SharedScheduler() + let oldScheduler <- self.account.storage.load<@AnyResource>(from: self.storagePath) + destroy oldScheduler + self.account.storage.save(<-scheduler, to: self.storagePath) + + self.sharedScheduler = self.account.capabilities.storage + .issue(self.storagePath) + } +} \ No newline at end of file diff --git a/cadence/tests/forked_flash_crash_moderate_test.cdc b/cadence/tests/forked_flash_crash_moderate_test.cdc index d02cc65f..d2c18272 100644 --- a/cadence/tests/forked_flash_crash_moderate_test.cdc +++ b/cadence/tests/forked_flash_crash_moderate_test.cdc @@ -281,6 +281,9 @@ access(all) fun runSimulation(config: SimConfig): 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) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index ff8a3141..9b06651c 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -226,7 +226,10 @@ access(all) fun deployContractsForFork() { // Deploy EVM mock var err = Test.deployContract(name: "EVM", path: "../contracts/mocks/EVM.cdc", arguments: []) - + + // Redeploy FlowTransactionScheduler mock (replaces forked mainnet contract with reset-capable version) + err = Test.deployContract(name: "FlowTransactionScheduler", path: "../contracts/mocks/FlowTransactionScheduler.cdc", arguments: []) + _deploy(config: config) } @@ -650,6 +653,16 @@ fun rebalancePosition(signer: Test.TestAccount, pid: UInt64, force: Bool, beFail Test.expect(rebalanceRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun resetTransactionScheduler() { + let result = _executeTransaction( + "transactions/reset_scheduler.cdc", + [], + serviceAccount + ) + Test.expect(result, Test.beSucceeded()) +} + access(all) fun setupMoetVault(_ signer: Test.TestAccount, beFailed: Bool) { let setupRes = _executeTransaction("../../lib/FlowALP/cadence/transactions/moet/setup_vault.cdc", [], signer) diff --git a/cadence/tests/transactions/reset_scheduler.cdc b/cadence/tests/transactions/reset_scheduler.cdc new file mode 100644 index 00000000..7087e883 --- /dev/null +++ b/cadence/tests/transactions/reset_scheduler.cdc @@ -0,0 +1,12 @@ +import FlowTransactionScheduler from "MockFlowTransactionScheduler" + +/// Clears all queued/scheduled transactions from the shared scheduler. +transaction { + prepare(signer: auth(BorrowValue) &Account) { + let scheduler = signer.storage.borrow( + from: FlowTransactionScheduler.storagePath + ) ?? panic("Could not borrow SharedScheduler from signer's storage") + + scheduler.reset() + } +} diff --git a/flow.json b/flow.json index 13ddca51..5cc4e229 100644 --- a/flow.json +++ b/flow.json @@ -268,6 +268,14 @@ "testnet": "8c5303eaa26202d6" } }, + "MockFlowTransactionScheduler": { + "source": "./cadence/contracts/mocks/FlowTransactionScheduler.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, "MockOracle": { "source": "cadence/contracts/mocks/MockOracle.cdc", "aliases": {