Skip to content

Commit bd1e86c

Browse files
committed
Add EVM State Manipulation Helpers For Forked Simulations
1 parent 61bd08f commit bd1e86c

File tree

10 files changed

+1069
-5
lines changed

10 files changed

+1069
-5
lines changed

.github/workflows/cadence_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
restore-keys: |
2929
${{ runner.os }}-go-
3030
- name: Install Flow CLI
31-
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
31+
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0
3232
- name: Flow CLI Version
3333
run: flow version
3434
- name: Update PATH

.github/workflows/e2e_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
restore-keys: |
2929
${{ runner.os }}-go-
3030
- name: Install Flow CLI
31-
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
31+
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0
3232
- name: Flow CLI Version
3333
run: flow version
3434
- name: Update PATH

.github/workflows/incrementfi_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
token: ${{ secrets.GH_PAT }}
1919
submodules: recursive
2020
- name: Install Flow CLI
21-
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
21+
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0
2222
- name: Flow CLI Version
2323
run: flow version
2424
- name: Update PATH

.github/workflows/punchswap.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
cache-dependency-path: |
2525
**/go.sum
2626
- name: Install Flow CLI
27-
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
27+
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0
2828
- name: Flow CLI Version
2929
run: flow version
3030
- name: Update PATH

.github/workflows/scheduled_rebalance_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
restore-keys: |
3030
${{ runner.os }}-go-
3131
- name: Install Flow CLI
32-
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
32+
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0
3333
- name: Flow CLI Version
3434
run: flow version
3535
- name: Update PATH
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import Test
2+
import "EVM"
3+
4+
/* --- ERC4626 Vault State Manipulation --- */
5+
6+
/// Set vault share price by setting totalAssets to a specific base value, then multiplying by the price multiplier
7+
/// Manipulates both asset.balanceOf(vault) and vault._totalAssets to bypass maxRate capping
8+
/// Caller should provide baseAssets large enough to prevent slippage during price changes
9+
access(all) fun setVaultSharePrice(
10+
vaultAddress: String,
11+
assetAddress: String,
12+
assetBalanceSlot: UInt256,
13+
vaultTotalAssetsSlot: String,
14+
baseAssets: UFix64,
15+
priceMultiplier: UFix64,
16+
signer: Test.TestAccount
17+
) {
18+
// Convert UFix64 baseAssets to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8)
19+
let baseAssetsBytes = baseAssets.toBigEndianBytes()
20+
var baseAssetsUInt64: UInt64 = 0
21+
for byte in baseAssetsBytes {
22+
baseAssetsUInt64 = (baseAssetsUInt64 << 8) + UInt64(byte)
23+
}
24+
let baseAssetsUInt256 = UInt256(baseAssetsUInt64)
25+
26+
// Calculate target: baseAssets * multiplier
27+
let multiplierBytes = priceMultiplier.toBigEndianBytes()
28+
var multiplierUInt64: UInt64 = 0
29+
for byte in multiplierBytes {
30+
multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte)
31+
}
32+
let targetAssets = (baseAssetsUInt256 * UInt256(multiplierUInt64)) / UInt256(100000000)
33+
34+
let result = Test.executeTransaction(
35+
Test.Transaction(
36+
code: Test.readFile("transactions/set_erc4626_vault_price.cdc"),
37+
authorizers: [signer.address],
38+
signers: [signer],
39+
arguments: [vaultAddress, assetAddress, assetBalanceSlot, vaultTotalAssetsSlot, priceMultiplier, targetAssets]
40+
)
41+
)
42+
Test.expect(result, Test.beSucceeded())
43+
}
44+
45+
/* --- Uniswap V3 Pool State Manipulation --- */
46+
47+
/// Set Uniswap V3 pool to a specific price via EVM.store
48+
/// Creates pool if it doesn't exist, then manipulates state
49+
access(all) fun setPoolToPrice(
50+
factoryAddress: String,
51+
tokenAAddress: String,
52+
tokenBAddress: String,
53+
fee: UInt64,
54+
priceTokenBPerTokenA: UFix64,
55+
tokenABalanceSlot: UInt256,
56+
tokenBBalanceSlot: UInt256,
57+
signer: Test.TestAccount
58+
) {
59+
// Sort tokens (Uniswap V3 requires token0 < token1)
60+
let token0 = tokenAAddress < tokenBAddress ? tokenAAddress : tokenBAddress
61+
let token1 = tokenAAddress < tokenBAddress ? tokenBAddress : tokenAAddress
62+
let token0BalanceSlot = tokenAAddress < tokenBAddress ? tokenABalanceSlot : tokenBBalanceSlot
63+
let token1BalanceSlot = tokenAAddress < tokenBAddress ? tokenBBalanceSlot : tokenABalanceSlot
64+
65+
let poolPrice = tokenAAddress < tokenBAddress ? priceTokenBPerTokenA : 1.0 / priceTokenBPerTokenA
66+
67+
let targetSqrtPriceX96 = calculateSqrtPriceX96(price: poolPrice)
68+
let targetTick = calculateTick(price: poolPrice)
69+
70+
let createResult = Test.executeTransaction(
71+
Test.Transaction(
72+
code: Test.readFile("transactions/ensure_uniswap_pool_exists.cdc"),
73+
authorizers: [signer.address],
74+
signers: [signer],
75+
arguments: [factoryAddress, token0, token1, fee, targetSqrtPriceX96]
76+
)
77+
)
78+
Test.expect(createResult, Test.beSucceeded())
79+
80+
let seedResult = Test.executeTransaction(
81+
Test.Transaction(
82+
code: Test.readFile("transactions/set_uniswap_v3_pool_price.cdc"),
83+
authorizers: [signer.address],
84+
signers: [signer],
85+
arguments: [factoryAddress, token0, token1, fee, targetSqrtPriceX96, targetTick, token0BalanceSlot, token1BalanceSlot]
86+
)
87+
)
88+
Test.expect(seedResult, Test.beSucceeded())
89+
}
90+
91+
/* --- Internal Math Utilities --- */
92+
93+
/// Calculate sqrtPriceX96 from a price ratio
94+
/// Returns sqrt(price) * 2^96 as a string for Uniswap V3 pool initialization
95+
access(self) fun calculateSqrtPriceX96(price: UFix64): String {
96+
// Convert UFix64 to UInt256 (UFix64 has 8 decimal places)
97+
// price is stored as integer * 10^8 internally
98+
let priceBytes = price.toBigEndianBytes()
99+
var priceUInt64: UInt64 = 0
100+
for byte in priceBytes {
101+
priceUInt64 = (priceUInt64 << 8) + UInt64(byte)
102+
}
103+
let priceScaled = UInt256(priceUInt64) // This is price * 10^8
104+
105+
// We want: sqrt(price) * 2^96
106+
// = sqrt(priceScaled / 10^8) * 2^96
107+
// = sqrt(priceScaled) * 2^96 / sqrt(10^8)
108+
// = sqrt(priceScaled) * 2^96 / 10^4
109+
110+
// Calculate sqrt(priceScaled) with scale factor 2^48 for precision
111+
// sqrt(priceScaled) * 2^48
112+
let sqrtPriceScaled = sqrt(n: priceScaled, scaleFactor: UInt256(1) << 48)
113+
114+
// Now we have: sqrt(priceScaled) * 2^48
115+
// We want: sqrt(priceScaled) * 2^96 / 10^4
116+
// = (sqrt(priceScaled) * 2^48) * 2^48 / 10^4
117+
118+
let sqrtPriceX96 = (sqrtPriceScaled * (UInt256(1) << 48)) / UInt256(10000)
119+
120+
return sqrtPriceX96.toString()
121+
}
122+
123+
/// Calculate tick from price ratio
124+
/// Returns tick = floor(log_1.0001(price)) for Uniswap V3 tick spacing
125+
access(self) fun calculateTick(price: UFix64): Int256 {
126+
// Convert UFix64 to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8)
127+
let priceBytes = price.toBigEndianBytes()
128+
var priceUInt64: UInt64 = 0
129+
for byte in priceBytes {
130+
priceUInt64 = (priceUInt64 << 8) + UInt64(byte)
131+
}
132+
133+
// priceUInt64 is price * 10^8
134+
// Scale to 10^18 for precision: price * 10^18 = priceUInt64 * 10^10
135+
let priceScaled = UInt256(priceUInt64) * UInt256(10000000000) // 10^10
136+
let scaleFactor = UInt256(1000000000000000000) // 10^18
137+
138+
// Calculate ln(price) * 10^18
139+
let lnPrice = ln(x: priceScaled, scaleFactor: scaleFactor)
140+
141+
// ln(1.0001) * 10^18 ≈ 99995000333083
142+
let ln1_0001 = Int256(99995000333083)
143+
144+
// tick = ln(price) / ln(1.0001)
145+
// lnPrice is already scaled by 10^18
146+
// ln1_0001 is already scaled by 10^18
147+
// So: tick = (lnPrice * 10^18) / (ln1_0001 * 10^18) = lnPrice / ln1_0001
148+
149+
let tick = lnPrice / ln1_0001
150+
151+
return tick
152+
}
153+
154+
/// Calculate square root using Newton's method for UInt256
155+
/// Returns sqrt(n) * scaleFactor to maintain precision
156+
access(self) fun sqrt(n: UInt256, scaleFactor: UInt256): UInt256 {
157+
if n == UInt256(0) {
158+
return UInt256(0)
159+
}
160+
161+
// Initial guess: n/2 (scaled)
162+
var x = (n * scaleFactor) / UInt256(2)
163+
var prevX = UInt256(0)
164+
165+
// Newton's method: x_new = (x + n*scale^2/x) / 2
166+
// Iterate until convergence (max 50 iterations for safety)
167+
var iterations = 0
168+
while x != prevX && iterations < 50 {
169+
prevX = x
170+
// x_new = (x + (n * scaleFactor^2) / x) / 2
171+
let nScaled = n * scaleFactor * scaleFactor
172+
x = (x + nScaled / x) / UInt256(2)
173+
iterations = iterations + 1
174+
}
175+
176+
return x
177+
}
178+
179+
/// Calculate natural logarithm using Taylor series
180+
/// ln(x) for x > 0, returns ln(x) * scaleFactor for precision
181+
access(self) fun ln(x: UInt256, scaleFactor: UInt256): Int256 {
182+
if x == UInt256(0) {
183+
panic("ln(0) is undefined")
184+
}
185+
186+
// For better convergence, reduce x to range [0.5, 1.5] using:
187+
// ln(x) = ln(2^n * y) = n*ln(2) + ln(y) where y is in [0.5, 1.5]
188+
189+
var value = x
190+
var n = 0
191+
192+
// Scale down if x > 1.5 * scaleFactor
193+
let threshold = (scaleFactor * UInt256(3)) / UInt256(2)
194+
while value > threshold {
195+
value = value / UInt256(2)
196+
n = n + 1
197+
}
198+
199+
// Scale up if x < 0.5 * scaleFactor
200+
let lowerThreshold = scaleFactor / UInt256(2)
201+
while value < lowerThreshold {
202+
value = value * UInt256(2)
203+
n = n - 1
204+
}
205+
206+
// Now value is in [0.5*scale, 1.5*scale], compute ln(value/scale)
207+
// Use Taylor series: ln(1+z) = z - z^2/2 + z^3/3 - z^4/4 + ...
208+
// where z = value/scale - 1
209+
210+
let z = value > scaleFactor
211+
? Int256(value - scaleFactor)
212+
: -Int256(scaleFactor - value)
213+
214+
// Calculate Taylor series terms until convergence
215+
var result = z // First term: z
216+
var term = z
217+
var i = 2
218+
var prevResult = Int256(0)
219+
220+
// Calculate terms until convergence (term becomes negligible or result stops changing)
221+
// Max 50 iterations for safety
222+
while i <= 50 && result != prevResult {
223+
prevResult = result
224+
225+
// term = term * z / scaleFactor
226+
term = (term * z) / Int256(scaleFactor)
227+
228+
// Add or subtract term/i based on sign
229+
if i % 2 == 0 {
230+
result = result - term / Int256(i)
231+
} else {
232+
result = result + term / Int256(i)
233+
}
234+
i = i + 1
235+
}
236+
237+
// Add n * ln(2) * scaleFactor
238+
// ln(2) ≈ 0.693147180559945309417232121458
239+
// ln(2) * 10^18 ≈ 693147180559945309
240+
let ln2Scaled = Int256(693147180559945309)
241+
let nScaled = Int256(n) * ln2Scaled
242+
243+
// Scale to our scaleFactor (assuming scaleFactor is 10^18)
244+
result = result + nScaled
245+
246+
return result
247+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Test
2+
import "evm_state_helpers.cdc"
3+
4+
// Simple smoke test to verify helpers are importable and functional
5+
access(all) fun testHelpersExist() {
6+
// Just verify we can import the helpers without errors
7+
// Actual usage will be tested in the forked rebalance tests
8+
Test.assertEqual(true, true)
9+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Transaction to ensure Uniswap V3 pool exists (creates if needed)
2+
import "EVM"
3+
4+
transaction(
5+
factoryAddress: String,
6+
token0Address: String,
7+
token1Address: String,
8+
fee: UInt64,
9+
sqrtPriceX96: String
10+
) {
11+
let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount
12+
13+
prepare(signer: auth(Storage) &Account) {
14+
self.coa = signer.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm)
15+
?? panic("Could not borrow COA")
16+
}
17+
18+
execute {
19+
let factory = EVM.addressFromString(factoryAddress)
20+
let token0 = EVM.addressFromString(token0Address)
21+
let token1 = EVM.addressFromString(token1Address)
22+
23+
// First check if pool already exists
24+
var getPoolCalldata = EVM.encodeABIWithSignature(
25+
"getPool(address,address,uint24)",
26+
[token0, token1, UInt256(fee)]
27+
)
28+
var getPoolResult = self.coa.dryCall(
29+
to: factory,
30+
data: getPoolCalldata,
31+
gasLimit: 100000,
32+
value: EVM.Balance(attoflow: 0)
33+
)
34+
35+
assert(getPoolResult.status == EVM.Status.successful, message: "Failed to query pool from factory")
36+
37+
// Decode pool address
38+
let poolAddress = (EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: getPoolResult.data)[0] as! EVM.EVMAddress)
39+
let zeroAddress = EVM.EVMAddress(bytes: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])
40+
41+
// If pool already exists, we're done (idempotent behavior)
42+
if poolAddress.bytes != zeroAddress.bytes {
43+
return
44+
}
45+
46+
// Pool doesn't exist, create it
47+
var calldata = EVM.encodeABIWithSignature(
48+
"createPool(address,address,uint24)",
49+
[token0, token1, UInt256(fee)]
50+
)
51+
var result = self.coa.call(
52+
to: factory,
53+
data: calldata,
54+
gasLimit: 5000000,
55+
value: EVM.Balance(attoflow: 0)
56+
)
57+
58+
assert(result.status == EVM.Status.successful, message: "Pool creation failed")
59+
60+
// Get the newly created pool address
61+
getPoolResult = self.coa.dryCall(to: factory, data: getPoolCalldata, gasLimit: 100000, value: EVM.Balance(attoflow: 0))
62+
63+
assert(getPoolResult.status == EVM.Status.successful && getPoolResult.data.length >= 20, message: "Failed to get pool address after creation")
64+
65+
var poolAddrBytes: [UInt8] = []
66+
var i = getPoolResult.data.length - 20
67+
while i < getPoolResult.data.length {
68+
poolAddrBytes.append(getPoolResult.data[i])
69+
i = i + 1
70+
}
71+
let poolAddr = EVM.addressFromString("0x\(String.encodeHex(poolAddrBytes))")
72+
73+
// Initialize the pool with the target price
74+
let initPrice = UInt256.fromString(sqrtPriceX96)!
75+
calldata = EVM.encodeABIWithSignature(
76+
"initialize(uint160)",
77+
[initPrice]
78+
)
79+
result = self.coa.call(
80+
to: poolAddr,
81+
data: calldata,
82+
gasLimit: 5000000,
83+
value: EVM.Balance(attoflow: 0)
84+
)
85+
86+
assert(result.status == EVM.Status.successful, message: "Pool initialization failed")
87+
}
88+
}

0 commit comments

Comments
 (0)