From eae9d1ed7ca09a91bcaf8fbb5cf6cab80efa5c98 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 13 Feb 2026 10:14:57 -0800 Subject: [PATCH 01/14] Add EVM State Manipulation Helpers For Forked Simulations --- .github/workflows/cadence_tests.yml | 2 +- .github/workflows/e2e_tests.yml | 2 +- .github/workflows/incrementfi_tests.yml | 2 +- .github/workflows/punchswap.yml | 2 +- .../workflows/scheduled_rebalance_tests.yml | 2 +- cadence/contracts/mocks/EVM.cdc | 1000 +++++++++++++++++ cadence/tests/evm_state_helpers.cdc | 209 ++++ .../ensure_uniswap_pool_exists.cdc | 84 ++ .../transactions/set_erc4626_vault_price.cdc | 123 ++ .../set_uniswap_v3_pool_price.cdc | 560 +++++++++ flow.json | 8 + 11 files changed, 1989 insertions(+), 5 deletions(-) create mode 100644 cadence/contracts/mocks/EVM.cdc create mode 100644 cadence/tests/evm_state_helpers.cdc create mode 100644 cadence/tests/transactions/ensure_uniswap_pool_exists.cdc create mode 100644 cadence/tests/transactions/set_erc4626_vault_price.cdc create mode 100644 cadence/tests/transactions/set_uniswap_v3_pool_price.cdc diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml index ceec0582..978f123f 100644 --- a/.github/workflows/cadence_tests.yml +++ b/.github/workflows/cadence_tests.yml @@ -28,7 +28,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)" + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0 - 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 d2504456..d16c20d0 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -28,7 +28,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)" + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0 - 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 647d1cd4..d74879cd 100644 --- a/.github/workflows/incrementfi_tests.yml +++ b/.github/workflows/incrementfi_tests.yml @@ -18,7 +18,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)" + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0 - name: Flow CLI Version run: flow version - name: Update PATH diff --git a/.github/workflows/punchswap.yml b/.github/workflows/punchswap.yml index a7591245..0183f7ab 100644 --- a/.github/workflows/punchswap.yml +++ b/.github/workflows/punchswap.yml @@ -24,7 +24,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)" + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0 - 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 d504ae69..d3567e4a 100644 --- a/.github/workflows/scheduled_rebalance_tests.yml +++ b/.github/workflows/scheduled_rebalance_tests.yml @@ -29,7 +29,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)" + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.0 - name: Flow CLI Version run: flow version - name: Update PATH diff --git a/cadence/contracts/mocks/EVM.cdc b/cadence/contracts/mocks/EVM.cdc new file mode 100644 index 00000000..f62e4c9f --- /dev/null +++ b/cadence/contracts/mocks/EVM.cdc @@ -0,0 +1,1000 @@ +import Crypto +import "NonFungibleToken" +import "FungibleToken" +import "FlowToken" + +access(all) +contract EVM { + + // Entitlements enabling finer-grained access control on a CadenceOwnedAccount + access(all) entitlement Validate + access(all) entitlement Withdraw + access(all) entitlement Call + access(all) entitlement Deploy + access(all) entitlement Owner + access(all) entitlement Bridge + + /// Block executed event is emitted when a new block is created, + /// which always happens when a transaction is executed. + access(all) + event BlockExecuted( + // height or number of the block + height: UInt64, + // hash of the block + hash: [UInt8; 32], + // timestamp of the block creation + timestamp: UInt64, + // total Flow supply + totalSupply: Int, + // all gas used in the block by transactions included + totalGasUsed: UInt64, + // parent block hash + parentHash: [UInt8; 32], + // root hash of all the transaction receipts + receiptRoot: [UInt8; 32], + // root hash of all the transaction hashes + transactionHashRoot: [UInt8; 32], + /// value returned for PREVRANDAO opcode + prevrandao: [UInt8; 32], + ) + + /// Transaction executed event is emitted every time a transaction + /// is executed by the EVM (even if failed). + access(all) + event TransactionExecuted( + // hash of the transaction + hash: [UInt8; 32], + // index of the transaction in a block + index: UInt16, + // type of the transaction + type: UInt8, + // RLP encoded transaction payload + payload: [UInt8], + // code indicating a specific validation (201-300) or execution (301-400) error + errorCode: UInt16, + // a human-readable message about the error (if any) + errorMessage: String, + // the amount of gas transaction used + gasConsumed: UInt64, + // if transaction was a deployment contains a newly deployed contract address + contractAddress: String, + // RLP encoded logs + logs: [UInt8], + // block height in which transaction was included + blockHeight: UInt64, + /// captures the hex encoded data that is returned from + /// the evm. For contract deployments + /// it returns the code deployed to + /// the address provided in the contractAddress field. + /// in case of revert, the smart contract custom error message + /// is also returned here (see EIP-140 for more details). + returnedData: [UInt8], + /// captures the input and output of the calls (rlp encoded) to the extra + /// precompiled contracts (e.g. Cadence Arch) during the transaction execution. + /// This data helps to replay the transactions without the need to + /// have access to the full cadence state data. + precompiledCalls: [UInt8], + /// stateUpdateChecksum provides a mean to validate + /// the updates to the storage when re-executing a transaction off-chain. + stateUpdateChecksum: [UInt8; 4] + ) + + access(all) + event CadenceOwnedAccountCreated(address: String) + + /// FLOWTokensDeposited is emitted when FLOW tokens is bridged + /// into the EVM environment. Note that this event is not emitted + /// for transfer of flow tokens between two EVM addresses. + /// Similar to the FungibleToken.Deposited event + /// this event includes a depositedUUID that captures the + /// uuid of the source vault. + access(all) + event FLOWTokensDeposited( + address: String, + amount: UFix64, + depositedUUID: UInt64, + balanceAfterInAttoFlow: UInt + ) + + /// FLOWTokensWithdrawn is emitted when FLOW tokens are bridged + /// out of the EVM environment. Note that this event is not emitted + /// for transfer of flow tokens between two EVM addresses. + /// similar to the FungibleToken.Withdrawn events + /// this event includes a withdrawnUUID that captures the + /// uuid of the returning vault. + access(all) + event FLOWTokensWithdrawn( + address: String, + amount: UFix64, + withdrawnUUID: UInt64, + balanceAfterInAttoFlow: UInt + ) + + /// BridgeAccessorUpdated is emitted when the BridgeAccessor Capability + /// is updated in the stored BridgeRouter along with identifying + /// information about both. + access(all) + event BridgeAccessorUpdated( + routerType: Type, + routerUUID: UInt64, + routerAddress: Address, + accessorType: Type, + accessorUUID: UInt64, + accessorAddress: Address + ) + + /// EVMAddress is an EVM-compatible address + access(all) + struct EVMAddress { + + /// Bytes of the address + access(all) + let bytes: [UInt8; 20] + + /// Constructs a new EVM address from the given byte representation + view init(bytes: [UInt8; 20]) { + self.bytes = bytes + } + + /// Balance of the address + access(all) + view fun balance(): Balance { + let balance = InternalEVM.balance( + address: self.bytes + ) + return Balance(attoflow: balance) + } + + /// Nonce of the address + access(all) + fun nonce(): UInt64 { + return InternalEVM.nonce( + address: self.bytes + ) + } + + /// Code of the address + access(all) + fun code(): [UInt8] { + return InternalEVM.code( + address: self.bytes + ) + } + + /// CodeHash of the address + access(all) + fun codeHash(): [UInt8] { + return InternalEVM.codeHash( + address: self.bytes + ) + } + + /// Deposits the given vault into the EVM account with the given address + access(all) + fun deposit(from: @FlowToken.Vault) { + let amount = from.balance + if amount == 0.0 { + panic("calling deposit function with an empty vault is not allowed") + } + let depositedUUID = from.uuid + InternalEVM.deposit( + from: <-from, + to: self.bytes + ) + emit FLOWTokensDeposited( + address: self.toString(), + amount: amount, + depositedUUID: depositedUUID, + balanceAfterInAttoFlow: self.balance().attoflow + ) + } + + /// Serializes the address to a hex string without the 0x prefix + /// Future implementations should pass data to InternalEVM for native serialization + access(all) + view fun toString(): String { + return String.encodeHex(self.bytes.toVariableSized()) + } + + /// Compares the address with another address + access(all) + view fun equals(_ other: EVMAddress): Bool { + return self.bytes == other.bytes + } + } + + /// EVMBytes is a type wrapper used for ABI encoding/decoding into + /// Solidity `bytes` type + access(all) + struct EVMBytes { + + /// Byte array representing the `bytes` value + access(all) + let value: [UInt8] + + view init(value: [UInt8]) { + self.value = value + } + } + + /// EVMBytes4 is a type wrapper used for ABI encoding/decoding into + /// Solidity `bytes4` type + access(all) + struct EVMBytes4 { + + /// Byte array representing the `bytes4` value + access(all) + let value: [UInt8; 4] + + view init(value: [UInt8; 4]) { + self.value = value + } + } + + /// EVMBytes32 is a type wrapper used for ABI encoding/decoding into + /// Solidity `bytes32` type + access(all) + struct EVMBytes32 { + + /// Byte array representing the `bytes32` value + access(all) + let value: [UInt8; 32] + + view init(value: [UInt8; 32]) { + self.value = value + } + } + + /// Converts a hex string to an EVM address if the string is a valid hex string + /// Future implementations should pass data to InternalEVM for native deserialization + access(all) + fun addressFromString(_ asHex: String): EVMAddress { + pre { + asHex.length == 40 || asHex.length == 42: "Invalid hex string length for an EVM address" + } + // Strip the 0x prefix if it exists + var withoutPrefix = (asHex[1] == "x" ? asHex.slice(from: 2, upTo: asHex.length) : asHex).toLower() + let bytes = withoutPrefix.decodeHex().toConstantSized<[UInt8; 20]>()! + return EVMAddress(bytes: bytes) + } + + access(all) + struct Balance { + + /// The balance in atto-FLOW + /// Atto-FLOW is the smallest denomination of FLOW (1e18 FLOW) + /// that is used to store account balances inside EVM + /// similar to the way WEI is used to store ETH divisible to 18 decimal places. + access(all) + var attoflow: UInt + + /// Constructs a new balance + access(all) + view init(attoflow: UInt) { + self.attoflow = attoflow + } + + /// Sets the balance by a UFix64 (8 decimal points), the format + /// that is used in Cadence to store FLOW tokens. + access(all) + fun setFLOW(flow: UFix64){ + self.attoflow = InternalEVM.castToAttoFLOW(balance: flow) + } + + /// Casts the balance to a UFix64 (rounding down) + /// Warning! casting a balance to a UFix64 which supports a lower level of precision + /// (8 decimal points in compare to 18) might result in rounding down error. + /// Use the toAttoFlow function if you care need more accuracy. + access(all) + view fun inFLOW(): UFix64 { + return InternalEVM.castToFLOW(balance: self.attoflow) + } + + /// Returns the balance in Atto-FLOW + access(all) + view fun inAttoFLOW(): UInt { + return self.attoflow + } + + /// Returns true if the balance is zero + access(all) + fun isZero(): Bool { + return self.attoflow == 0 + } + } + + /// reports the status of evm execution. + access(all) enum Status: UInt8 { + /// is (rarely) returned when status is unknown + /// and something has gone very wrong. + access(all) case unknown + + /// is returned when execution of an evm transaction/call + /// has failed at the validation step (e.g. nonce mismatch). + /// An invalid transaction/call is rejected to be executed + /// or be included in a block. + access(all) case invalid + + /// is returned when execution of an evm transaction/call + /// has been successful but the vm has reported an error as + /// the outcome of execution (e.g. running out of gas). + /// A failed tx/call is included in a block. + /// Note that resubmission of a failed transaction would + /// result in invalid status in the second attempt, given + /// the nonce would be come invalid. + access(all) case failed + + /// is returned when execution of an evm transaction/call + /// has been successful and no error is reported by the vm. + access(all) case successful + } + + /// reports the outcome of evm transaction/call execution attempt + access(all) struct Result { + /// status of the execution + access(all) + let status: Status + + /// error code (error code zero means no error) + access(all) + let errorCode: UInt64 + + /// error message + access(all) + let errorMessage: String + + /// returns the amount of gas metered during + /// evm execution + access(all) + let gasUsed: UInt64 + + /// returns the data that is returned from + /// the evm for the call. For coa.deploy + /// calls it returns the code deployed to + /// the address provided in the contractAddress field. + /// in case of revert, the smart contract custom error message + /// is also returned here (see EIP-140 for more details). + access(all) + let data: [UInt8] + + /// returns the newly deployed contract address + /// if the transaction caused such a deployment + /// otherwise the value is nil. + access(all) + let deployedContract: EVMAddress? + + init( + status: Status, + errorCode: UInt64, + errorMessage: String, + gasUsed: UInt64, + data: [UInt8], + contractAddress: [UInt8; 20]? + ) { + self.status = status + self.errorCode = errorCode + self.errorMessage = errorMessage + self.gasUsed = gasUsed + self.data = data + + if let addressBytes = contractAddress { + self.deployedContract = EVMAddress(bytes: addressBytes) + } else { + self.deployedContract = nil + } + } + } + + access(all) + resource interface Addressable { + /// The EVM address + access(all) + view fun address(): EVMAddress + } + + access(all) + resource CadenceOwnedAccount: Addressable { + + access(self) + var addressBytes: [UInt8; 20] + + init() { + // address is initially set to zero + // but updated through initAddress later + // we have to do this since we need resource id (uuid) + // to calculate the EVM address for this cadence owned account + self.addressBytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + } + + access(contract) + fun initAddress(addressBytes: [UInt8; 20]) { + // only allow set address for the first time + // check address is empty + for item in self.addressBytes { + assert(item == 0, message: "address byte is not empty") + } + self.addressBytes = addressBytes + } + + /// The EVM address of the cadence owned account + access(all) + view fun address(): EVMAddress { + // Always create a new EVMAddress instance + return EVMAddress(bytes: self.addressBytes) + } + + /// Get balance of the cadence owned account + access(all) + view fun balance(): Balance { + return self.address().balance() + } + + /// Deposits the given vault into the cadence owned account's balance + access(all) + fun deposit(from: @FlowToken.Vault) { + self.address().deposit(from: <-from) + } + + /// The EVM address of the cadence owned account behind an entitlement, acting as proof of access + access(Owner | Validate) + view fun protectedAddress(): EVMAddress { + return self.address() + } + + /// Withdraws the balance from the cadence owned account's balance + /// Note that amounts smaller than 10nF (10e-8) can't be withdrawn + /// given that Flow Token Vaults use UFix64s to store balances. + /// If the given balance conversion to UFix64 results in + /// rounding error, this function would fail. + access(Owner | Withdraw) + fun withdraw(balance: Balance): @FlowToken.Vault { + if balance.isZero() { + panic("calling withdraw function with zero balance is not allowed") + } + let vault <- InternalEVM.withdraw( + from: self.addressBytes, + amount: balance.attoflow + ) as! @FlowToken.Vault + emit FLOWTokensWithdrawn( + address: self.address().toString(), + amount: balance.inFLOW(), + withdrawnUUID: vault.uuid, + balanceAfterInAttoFlow: self.balance().attoflow + ) + return <-vault + } + + /// Deploys a contract to the EVM environment. + /// Returns the result which contains address of + /// the newly deployed contract + access(Owner | Deploy) + fun deploy( + code: [UInt8], + gasLimit: UInt64, + value: Balance + ): Result { + return InternalEVM.deploy( + from: self.addressBytes, + code: code, + gasLimit: gasLimit, + value: value.attoflow + ) as! Result + } + + /// Calls a function with the given data. + /// The execution is limited by the given amount of gas + access(Owner | Call) + fun call( + to: EVMAddress, + data: [UInt8], + gasLimit: UInt64, + value: Balance + ): Result { + return InternalEVM.call( + from: self.addressBytes, + to: to.bytes, + data: data, + gasLimit: gasLimit, + value: value.attoflow + ) as! Result + } + + /// Calls a contract function with the given data. + /// The execution is limited by the given amount of gas. + /// The transaction state changes are not persisted. + access(all) + fun dryCall( + to: EVMAddress, + data: [UInt8], + gasLimit: UInt64, + value: Balance, + ): Result { + return InternalEVM.dryCall( + from: self.addressBytes, + to: to.bytes, + data: data, + gasLimit: gasLimit, + value: value.attoflow + ) as! Result + } + + /// Bridges the given NFT to the EVM environment, requiring a Provider from which to withdraw a fee to fulfill + /// the bridge request + access(all) + fun depositNFT( + nft: @{NonFungibleToken.NFT}, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + EVM.borrowBridgeAccessor().depositNFT(nft: <-nft, to: self.address(), feeProvider: feeProvider) + } + + /// Bridges the given NFT from the EVM environment, requiring a Provider from which to withdraw a fee to fulfill + /// the bridge request. Note: the caller should own the requested NFT in EVM + access(Owner | Bridge) + fun withdrawNFT( + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{NonFungibleToken.NFT} { + return <- EVM.borrowBridgeAccessor().withdrawNFT( + caller: &self as auth(Call) &CadenceOwnedAccount, + type: type, + id: id, + feeProvider: feeProvider + ) + } + + /// Bridges the given Vault to the EVM environment, requiring a Provider from which to withdraw a fee to fulfill + /// the bridge request + access(all) + fun depositTokens( + vault: @{FungibleToken.Vault}, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + EVM.borrowBridgeAccessor().depositTokens(vault: <-vault, to: self.address(), feeProvider: feeProvider) + } + + /// Bridges the given fungible tokens from the EVM environment, requiring a Provider from which to withdraw a + /// fee to fulfill the bridge request. Note: the caller should own the requested tokens & sufficient balance of + /// requested tokens in EVM + access(Owner | Bridge) + fun withdrawTokens( + type: Type, + amount: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{FungibleToken.Vault} { + return <- EVM.borrowBridgeAccessor().withdrawTokens( + caller: &self as auth(Call) &CadenceOwnedAccount, + type: type, + amount: amount, + feeProvider: feeProvider + ) + } + } + + /// Creates a new cadence owned account + access(all) + fun createCadenceOwnedAccount(): @CadenceOwnedAccount { + let acc <-create CadenceOwnedAccount() + let addr = InternalEVM.createCadenceOwnedAccount(uuid: acc.uuid) + acc.initAddress(addressBytes: addr) + + emit CadenceOwnedAccountCreated(address: acc.address().toString()) + return <-acc + } + + /// Runs an a RLP-encoded EVM transaction, deducts the gas fees, + /// and deposits the gas fees into the provided coinbase address. + access(all) + fun run(tx: [UInt8], coinbase: EVMAddress): Result { + return InternalEVM.run( + tx: tx, + coinbase: coinbase.bytes + ) as! Result + } + + /// mustRun runs the transaction using EVM.run yet it + /// rollback if the tx execution status is unknown or invalid. + /// Note that this method does not rollback if transaction + /// is executed but an vm error is reported as the outcome + /// of the execution (status: failed). + access(all) + fun mustRun(tx: [UInt8], coinbase: EVMAddress): Result { + let runResult = self.run(tx: tx, coinbase: coinbase) + assert( + runResult.status == Status.failed || runResult.status == Status.successful, + message: "tx is not valid for execution" + ) + return runResult + } + + /// Simulates running unsigned RLP-encoded transaction using + /// the from address as the signer. + /// The transaction state changes are not persisted. + /// This is useful for gas estimation or calling view contract functions. + access(all) + fun dryRun(tx: [UInt8], from: EVMAddress): Result { + return InternalEVM.dryRun( + tx: tx, + from: from.bytes, + ) as! Result + } + + /// Calls a contract function with the given data. + /// The execution is limited by the given amount of gas. + /// The transaction state changes are not persisted. + access(all) + fun dryCall( + from: EVMAddress, + to: EVMAddress, + data: [UInt8], + gasLimit: UInt64, + value: Balance, + ): Result { + return InternalEVM.dryCall( + from: from.bytes, + to: to.bytes, + data: data, + gasLimit: gasLimit, + value: value.attoflow + ) as! Result + } + + /// Runs a batch of RLP-encoded EVM transactions, deducts the gas fees, + /// and deposits the gas fees into the provided coinbase address. + /// An invalid transaction is not executed and not included in the block. + access(all) + fun batchRun(txs: [[UInt8]], coinbase: EVMAddress): [Result] { + return InternalEVM.batchRun( + txs: txs, + coinbase: coinbase.bytes, + ) as! [Result] + } + + access(all) + fun encodeABI(_ values: [AnyStruct]): [UInt8] { + return InternalEVM.encodeABI(values) + } + + access(all) + fun decodeABI(types: [Type], data: [UInt8]): [AnyStruct] { + return InternalEVM.decodeABI(types: types, data: data) + } + + access(all) + fun encodeABIWithSignature( + _ signature: String, + _ values: [AnyStruct] + ): [UInt8] { + let methodID = HashAlgorithm.KECCAK_256.hash( + signature.utf8 + ).slice(from: 0, upTo: 4) + let arguments = InternalEVM.encodeABI(values) + + return methodID.concat(arguments) + } + + access(all) + fun decodeABIWithSignature( + _ signature: String, + types: [Type], + data: [UInt8] + ): [AnyStruct] { + let methodID = HashAlgorithm.KECCAK_256.hash( + signature.utf8 + ).slice(from: 0, upTo: 4) + + for byte in methodID { + if byte != data.removeFirst() { + panic("signature mismatch") + } + } + + return InternalEVM.decodeABI(types: types, data: data) + } + + /// ValidationResult returns the result of COA ownership proof validation + access(all) + struct ValidationResult { + access(all) + let isValid: Bool + + access(all) + let problem: String? + + init(isValid: Bool, problem: String?) { + self.isValid = isValid + self.problem = problem + } + } + + /// validateCOAOwnershipProof validates a COA ownership proof + access(all) + fun validateCOAOwnershipProof( + address: Address, + path: PublicPath, + signedData: [UInt8], + keyIndices: [UInt64], + signatures: [[UInt8]], + evmAddress: [UInt8; 20] + ): ValidationResult { + // make signature set first + // check number of signatures matches number of key indices + if keyIndices.length != signatures.length { + return ValidationResult( + isValid: false, + problem: "key indices size doesn't match the signatures" + ) + } + + // fetch account + let acc = getAccount(address) + + var signatureSet: [Crypto.KeyListSignature] = [] + let keyList = Crypto.KeyList() + var keyListLength = 0 + let seenAccountKeyIndices: {Int: Int} = {} + for signatureIndex, signature in signatures{ + // index of the key on the account + let accountKeyIndex = Int(keyIndices[signatureIndex]!) + // index of the key in the key list + var keyListIndex = 0 + + if !seenAccountKeyIndices.containsKey(accountKeyIndex) { + // fetch account key with accountKeyIndex + if let key = acc.keys.get(keyIndex: accountKeyIndex) { + if key.isRevoked { + return ValidationResult( + isValid: false, + problem: "account key is revoked" + ) + } + + keyList.add( + key.publicKey, + hashAlgorithm: key.hashAlgorithm, + // normalization factor. We need to divide by 1000 because the + // `Crypto.KeyList.verify()` function expects the weight to be + // in the range [0, 1]. 1000 is the key weight threshold. + weight: key.weight / 1000.0, + ) + + keyListIndex = keyListLength + keyListLength = keyListLength + 1 + seenAccountKeyIndices[accountKeyIndex] = keyListIndex + } else { + return ValidationResult( + isValid: false, + problem: "invalid key index" + ) + } + } else { + // if we have already seen this accountKeyIndex, use the keyListIndex + // that was previously assigned to it + // `Crypto.KeyList.verify()` knows how to handle duplicate keys + keyListIndex = seenAccountKeyIndices[accountKeyIndex]! + } + + signatureSet.append(Crypto.KeyListSignature( + keyIndex: keyListIndex, + signature: signature + )) + } + + let isValid = keyList.verify( + signatureSet: signatureSet, + signedData: signedData, + domainSeparationTag: "FLOW-V0.0-user" + ) + + if !isValid{ + return ValidationResult( + isValid: false, + problem: "the given signatures are not valid or provide enough weight" + ) + } + + let coaRef = acc.capabilities.borrow<&EVM.CadenceOwnedAccount>(path) + if coaRef == nil { + return ValidationResult( + isValid: false, + problem: "could not borrow bridge account's resource" + ) + } + + // verify evm address matching + var addr = coaRef!.address() + for index, item in coaRef!.address().bytes { + if item != evmAddress[index] { + return ValidationResult( + isValid: false, + problem: "evm address mismatch" + ) + } + } + + return ValidationResult( + isValid: true, + problem: nil + ) + } + + /// Block returns information about the latest executed block. + access(all) + struct EVMBlock { + access(all) + let height: UInt64 + + access(all) + let hash: String + + access(all) + let totalSupply: Int + + access(all) + let timestamp: UInt64 + + init(height: UInt64, hash: String, totalSupply: Int, timestamp: UInt64) { + self.height = height + self.hash = hash + self.totalSupply = totalSupply + self.timestamp = timestamp + } + } + + /// Returns the latest executed block. + access(all) + fun getLatestBlock(): EVMBlock { + return InternalEVM.getLatestBlock() as! EVMBlock + } + + /// Interface for a resource which acts as an entrypoint to the VM bridge + access(all) + resource interface BridgeAccessor { + + /// Endpoint enabling the bridging of an NFT to EVM + access(Bridge) + fun depositNFT( + nft: @{NonFungibleToken.NFT}, + to: EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + + /// Endpoint enabling the bridging of an NFT from EVM + access(Bridge) + fun withdrawNFT( + caller: auth(Call) &CadenceOwnedAccount, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{NonFungibleToken.NFT} + + /// Endpoint enabling the bridging of a fungible token vault to EVM + access(Bridge) + fun depositTokens( + vault: @{FungibleToken.Vault}, + to: EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + + /// Endpoint enabling the bridging of fungible tokens from EVM + access(Bridge) + fun withdrawTokens( + caller: auth(Call) &CadenceOwnedAccount, + type: Type, + amount: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{FungibleToken.Vault} + } + + /// Interface which captures a Capability to the bridge Accessor, saving it within the BridgeRouter resource + access(all) + resource interface BridgeRouter { + + /// Returns a reference to the BridgeAccessor designated for internal bridge requests + access(Bridge) view fun borrowBridgeAccessor(): auth(Bridge) &{BridgeAccessor} + + /// Sets the BridgeAccessor Capability in the BridgeRouter + access(Bridge) fun setBridgeAccessor(_ accessor: Capability) { + pre { + accessor.check(): "Invalid BridgeAccessor Capability provided" + emit BridgeAccessorUpdated( + routerType: self.getType(), + routerUUID: self.uuid, + routerAddress: self.owner?.address ?? panic("Router must have an owner to be identified"), + accessorType: accessor.borrow()!.getType(), + accessorUUID: accessor.borrow()!.uuid, + accessorAddress: accessor.address + ) + } + } + } + + /// Returns a reference to the BridgeAccessor designated for internal bridge requests + access(self) + view fun borrowBridgeAccessor(): auth(Bridge) &{BridgeAccessor} { + return self.account.storage.borrow(from: /storage/evmBridgeRouter) + ?.borrowBridgeAccessor() + ?? panic("Could not borrow reference to the EVM bridge") + } + + /// The Heartbeat resource controls the block production. + /// It is stored in the storage and used in the Flow protocol to call the heartbeat function once per block. + access(all) + resource Heartbeat { + /// heartbeat calls commit block proposals and forms new blocks including all the + /// recently executed transactions. + /// The Flow protocol makes sure to call this function once per block as a system call. + access(all) + fun heartbeat() { + InternalEVM.commitBlockProposal() + } + } + + access(all) + fun call( + from: String, + to: String, + data: [UInt8], + gasLimit: UInt64, + value: UInt + ): Result { + return InternalEVM.call( + from: EVM.addressFromString(from).bytes, + to: EVM.addressFromString(to).bytes, + data: data, + gasLimit: gasLimit, + value: value + ) as! Result + } + + /// Stores a value to an address' storage slot. + access(all) + fun store(target: EVM.EVMAddress, slot: String, value: String) { + InternalEVM.store(target: target.bytes, slot: slot, value: value) + } + + /// Loads a storage slot from an address. + access(all) + fun load(target: EVM.EVMAddress, slot: String): [UInt8] { + return InternalEVM.load(target: target.bytes, slot: slot) + } + + /// Runs a transaction by setting the call's `msg.sender` to be the `from` address. + access(all) + fun runTxAs( + from: EVM.EVMAddress, + to: EVM.EVMAddress, + data: [UInt8], + gasLimit: UInt64, + value: EVM.Balance, + ): Result { + return InternalEVM.call( + from: from.bytes, + to: to.bytes, + data: data, + gasLimit: gasLimit, + value: value.attoflow + ) as! Result + } + + /// setupHeartbeat creates a heartbeat resource and saves it to storage. + /// The function is called once during the contract initialization. + /// + /// The heartbeat resource is used to control the block production, + /// and used in the Flow protocol to call the heartbeat function once per block. + /// + /// The function can be called by anyone, but only once: + /// the function will fail if the resource already exists. + /// + /// The resulting resource is stored in the account storage, + /// and is only accessible by the account, not the caller of the function. + access(all) + fun setupHeartbeat() { + self.account.storage.save(<-create Heartbeat(), to: /storage/EVMHeartbeat) + } + + init() { + self.setupHeartbeat() + } +} \ No newline at end of file diff --git a/cadence/tests/evm_state_helpers.cdc b/cadence/tests/evm_state_helpers.cdc new file mode 100644 index 00000000..ed7f55b5 --- /dev/null +++ b/cadence/tests/evm_state_helpers.cdc @@ -0,0 +1,209 @@ +import Test +import "EVM" + +/* --- ERC4626 Vault State Manipulation --- */ + +/// Set vault share price by setting totalAssets to a specific base value, then multiplying by the price multiplier +/// Manipulates both asset.balanceOf(vault) and vault._totalAssets to bypass maxRate capping +/// Caller should provide baseAssets large enough to prevent slippage during price changes +access(all) fun setVaultSharePrice( + vaultAddress: String, + assetAddress: String, + assetBalanceSlot: UInt256, + vaultTotalAssetsSlot: String, + baseAssets: UFix64, + priceMultiplier: UFix64, + signer: Test.TestAccount +) { + // Convert UFix64 baseAssets to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8) + let baseAssetsBytes = baseAssets.toBigEndianBytes() + var baseAssetsUInt64: UInt64 = 0 + for byte in baseAssetsBytes { + baseAssetsUInt64 = (baseAssetsUInt64 << 8) + UInt64(byte) + } + let baseAssetsUInt256 = UInt256(baseAssetsUInt64) + + // Calculate target: baseAssets * multiplier + let multiplierBytes = priceMultiplier.toBigEndianBytes() + var multiplierUInt64: UInt64 = 0 + for byte in multiplierBytes { + multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte) + } + let targetAssets = (baseAssetsUInt256 * UInt256(multiplierUInt64)) / UInt256(100000000) + + let result = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/set_erc4626_vault_price.cdc"), + authorizers: [signer.address], + signers: [signer], + arguments: [vaultAddress, assetAddress, assetBalanceSlot, vaultTotalAssetsSlot, priceMultiplier, targetAssets] + ) + ) + Test.expect(result, Test.beSucceeded()) +} + +/* --- Uniswap V3 Pool State Manipulation --- */ + +/// Set Uniswap V3 pool to a specific price via EVM.store +/// Creates pool if it doesn't exist, then manipulates state +access(all) fun setPoolToPrice( + factoryAddress: String, + tokenAAddress: String, + tokenBAddress: String, + fee: UInt64, + priceTokenBPerTokenA: UFix64, + tokenABalanceSlot: UInt256, + tokenBBalanceSlot: UInt256, + signer: Test.TestAccount +) { + // Sort tokens (Uniswap V3 requires token0 < token1) + let token0 = tokenAAddress < tokenBAddress ? tokenAAddress : tokenBAddress + let token1 = tokenAAddress < tokenBAddress ? tokenBAddress : tokenAAddress + let token0BalanceSlot = tokenAAddress < tokenBAddress ? tokenABalanceSlot : tokenBBalanceSlot + let token1BalanceSlot = tokenAAddress < tokenBAddress ? tokenBBalanceSlot : tokenABalanceSlot + + let poolPrice = tokenAAddress < tokenBAddress ? priceTokenBPerTokenA : 1.0 / priceTokenBPerTokenA + + let targetSqrtPriceX96 = calculateSqrtPriceX96(price: poolPrice) + let targetTick = calculateTick(price: poolPrice) + + let createResult = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/ensure_uniswap_pool_exists.cdc"), + authorizers: [signer.address], + signers: [signer], + arguments: [factoryAddress, token0, token1, fee, targetSqrtPriceX96] + ) + ) + Test.expect(createResult, Test.beSucceeded()) + + let seedResult = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/set_uniswap_v3_pool_price.cdc"), + authorizers: [signer.address], + signers: [signer], + arguments: [factoryAddress, token0, token1, fee, targetSqrtPriceX96, targetTick, token0BalanceSlot, token1BalanceSlot] + ) + ) + Test.expect(seedResult, Test.beSucceeded()) +} + +/* --- Internal Math Utilities --- */ + +/// Calculate sqrtPriceX96 from a price ratio +/// Returns sqrt(price) * 2^96 as a string for Uniswap V3 pool initialization +access(self) fun calculateSqrtPriceX96(price: UFix64): String { + // Convert UFix64 to UInt256 (UFix64 has 8 decimal places) + // price is stored as integer * 10^8 internally + let priceBytes = price.toBigEndianBytes() + var priceUInt64: UInt64 = 0 + for byte in priceBytes { + priceUInt64 = (priceUInt64 << 8) + UInt64(byte) + } + let priceScaled = UInt256(priceUInt64) // This is price * 10^8 + + // sqrt(price) * 2^96, adjusted for UFix64 scaling + let sqrtPriceScaled = sqrt(n: priceScaled, scaleFactor: UInt256(1) << 48) + let sqrtPriceX96 = (sqrtPriceScaled * (UInt256(1) << 48)) / UInt256(10000) + + return sqrtPriceX96.toString() +} + +/// Calculate tick from price ratio +/// Returns tick = floor(log_1.0001(price)) for Uniswap V3 tick spacing +access(self) fun calculateTick(price: UFix64): Int256 { + // Convert UFix64 to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8) + let priceBytes = price.toBigEndianBytes() + var priceUInt64: UInt64 = 0 + for byte in priceBytes { + priceUInt64 = (priceUInt64 << 8) + UInt64(byte) + } + + // priceUInt64 is price * 10^8 + // Scale to 10^18 for precision: price * 10^18 = priceUInt64 * 10^10 + let priceScaled = UInt256(priceUInt64) * UInt256(10000000000) // 10^10 + let scaleFactor = UInt256(1000000000000000000) // 10^18 + + // Calculate ln(price) * 10^18 + let lnPrice = ln(x: priceScaled, scaleFactor: scaleFactor) + + // ln(1.0001) * 10^18 ≈ 99995000333083 + let ln1_0001 = Int256(99995000333083) + + // tick = ln(price) / ln(1.0001) + let tick = lnPrice / ln1_0001 + + return tick +} + +/// Calculate square root using Newton's method +/// Returns sqrt(n) * scaleFactor for precision +access(self) fun sqrt(n: UInt256, scaleFactor: UInt256): UInt256 { + if n == UInt256(0) { + return UInt256(0) + } + + var x = (n * scaleFactor) / UInt256(2) + var prevX = UInt256(0) + var iterations = 0 + + while x != prevX && iterations < 50 { + prevX = x + let nScaled = n * scaleFactor * scaleFactor + x = (x + nScaled / x) / UInt256(2) + iterations = iterations + 1 + } + + return x +} + +/// Calculate natural logarithm using Taylor series +/// Returns ln(x) * scaleFactor for precision +access(self) fun ln(x: UInt256, scaleFactor: UInt256): Int256 { + if x == UInt256(0) { + panic("ln(0) is undefined") + } + + // Reduce x to range [0.5, 1.5] for better convergence + var value = x + var n = 0 + + let threshold = (scaleFactor * UInt256(3)) / UInt256(2) + while value > threshold { + value = value / UInt256(2) + n = n + 1 + } + + let lowerThreshold = scaleFactor / UInt256(2) + while value < lowerThreshold { + value = value * UInt256(2) + n = n - 1 + } + + // Taylor series: ln(1+z) = z - z^2/2 + z^3/3 - ... + let z = value > scaleFactor + ? Int256(value - scaleFactor) + : -Int256(scaleFactor - value) + + var result = z + var term = z + var i = 2 + var prevResult = Int256(0) + + while i <= 50 && result != prevResult { + prevResult = result + term = (term * z) / Int256(scaleFactor) + if i % 2 == 0 { + result = result - term / Int256(i) + } else { + result = result + term / Int256(i) + } + i = i + 1 + } + + // Adjust for range reduction: ln(2^n * y) = n*ln(2) + ln(y) + let ln2Scaled = Int256(693147180559945309) // ln(2) * 10^18 + result = result + Int256(n) * ln2Scaled + + return result +} diff --git a/cadence/tests/transactions/ensure_uniswap_pool_exists.cdc b/cadence/tests/transactions/ensure_uniswap_pool_exists.cdc new file mode 100644 index 00000000..5012dcc1 --- /dev/null +++ b/cadence/tests/transactions/ensure_uniswap_pool_exists.cdc @@ -0,0 +1,84 @@ +// Transaction to ensure Uniswap V3 pool exists (creates if needed) +import "EVM" + +transaction( + factoryAddress: String, + token0Address: String, + token1Address: String, + fee: UInt64, + sqrtPriceX96: String +) { + let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount + + prepare(signer: auth(Storage) &Account) { + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA") + } + + execute { + let factory = EVM.addressFromString(factoryAddress) + let token0 = EVM.addressFromString(token0Address) + let token1 = EVM.addressFromString(token1Address) + + // First check if pool already exists + var getPoolCalldata = EVM.encodeABIWithSignature( + "getPool(address,address,uint24)", + [token0, token1, UInt256(fee)] + ) + var getPoolResult = self.coa.dryCall( + to: factory, + data: getPoolCalldata, + gasLimit: 100000, + value: EVM.Balance(attoflow: 0) + ) + + assert(getPoolResult.status == EVM.Status.successful, message: "Failed to query pool from factory") + + // Decode pool address + let poolAddress = (EVM.decodeABI(types: [Type()], data: getPoolResult.data)[0] as! EVM.EVMAddress) + 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]) + + // If pool already exists, we're done (idempotent behavior) + if poolAddress.bytes != zeroAddress.bytes { + return + } + + // Pool doesn't exist, create it + var calldata = EVM.encodeABIWithSignature( + "createPool(address,address,uint24)", + [token0, token1, UInt256(fee)] + ) + var result = self.coa.call( + to: factory, + data: calldata, + gasLimit: 5000000, + value: EVM.Balance(attoflow: 0) + ) + + assert(result.status == EVM.Status.successful, message: "Pool creation failed") + + // Get the newly created pool address + getPoolResult = self.coa.dryCall(to: factory, data: getPoolCalldata, gasLimit: 100000, value: EVM.Balance(attoflow: 0)) + + assert(getPoolResult.status == EVM.Status.successful && getPoolResult.data.length >= 20, message: "Failed to get pool address after creation") + + // Extract last 20 bytes as pool address + let poolAddrBytes = getPoolResult.data.slice(from: getPoolResult.data.length - 20, upTo: getPoolResult.data.length) + let poolAddr = EVM.addressFromString("0x\(String.encodeHex(poolAddrBytes))") + + // Initialize the pool with the target price + let initPrice = UInt256.fromString(sqrtPriceX96)! + calldata = EVM.encodeABIWithSignature( + "initialize(uint160)", + [initPrice] + ) + result = self.coa.call( + to: poolAddr, + data: calldata, + gasLimit: 5000000, + value: EVM.Balance(attoflow: 0) + ) + + assert(result.status == EVM.Status.successful, message: "Pool initialization failed") + } +} diff --git a/cadence/tests/transactions/set_erc4626_vault_price.cdc b/cadence/tests/transactions/set_erc4626_vault_price.cdc new file mode 100644 index 00000000..4681cd02 --- /dev/null +++ b/cadence/tests/transactions/set_erc4626_vault_price.cdc @@ -0,0 +1,123 @@ +import "EVM" + +// Helper: Compute Solidity mapping storage slot +access(all) fun computeMappingSlot(_ values: [AnyStruct]): String { + let encoded = EVM.encodeABI(values) + let hashBytes = HashAlgorithm.KECCAK_256.hash(encoded) + return "0x\(String.encodeHex(hashBytes))" +} + +// Helper: Compute ERC20 balanceOf storage slot +access(all) fun computeBalanceOfSlot(holderAddress: String, balanceSlot: UInt256): String { + var addrHex = holderAddress + if holderAddress.slice(from: 0, upTo: 2) == "0x" { + addrHex = holderAddress.slice(from: 2, upTo: holderAddress.length) + } + let addrBytes = addrHex.decodeHex() + let address = EVM.EVMAddress(bytes: addrBytes.toConstantSized<[UInt8; 20]>()!) + return computeMappingSlot([address, balanceSlot]) +} + +// Atomically set ERC4626 vault share price +// This manipulates both the underlying asset balance and vault's _totalAssets storage slot +// If targetTotalAssets is 0, multiplies current totalAssets by priceMultiplier +// If targetTotalAssets is non-zero, uses it directly (priceMultiplier is ignored) +transaction( + vaultAddress: String, + assetAddress: String, + assetBalanceSlot: UInt256, + vaultTotalAssetsSlot: String, + priceMultiplier: UFix64, + targetTotalAssets: UInt256 +) { + prepare(signer: &Account) {} + + execute { + let vault = EVM.addressFromString(vaultAddress) + let asset = EVM.addressFromString(assetAddress) + + var targetAssets: UInt256 = targetTotalAssets + + // If targetTotalAssets is 0, calculate from current assets * multiplier + if targetTotalAssets == UInt256(0) { + // Read current totalAssets from vault via EVM call + let totalAssetsCalldata = EVM.encodeABIWithSignature("totalAssets()", []) + let totalAssetsResult = EVM.call( + from: vaultAddress, + to: vaultAddress, + data: totalAssetsCalldata, + gasLimit: 100000, + value: 0 + ) + + assert(totalAssetsResult.status == EVM.Status.successful, message: "Failed to read totalAssets") + + let currentAssets = (EVM.decodeABI(types: [Type()], data: totalAssetsResult.data)[0] as! UInt256) + + // Calculate target assets (currentAssets * multiplier / 1e8) + // priceMultiplier is UFix64, so convert to UInt64 via big-endian bytes + let multiplierBytes = priceMultiplier.toBigEndianBytes() + var multiplierUInt64: UInt64 = 0 + for byte in multiplierBytes { + multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte) + } + targetAssets = (currentAssets * UInt256(multiplierUInt64)) / UInt256(100000000) + } + + // Update asset.balanceOf(vault) to targetAssets + let vaultBalanceSlot = computeBalanceOfSlot(holderAddress: vaultAddress, balanceSlot: assetBalanceSlot) + + // Pad targetAssets to 32 bytes + let targetAssetsBytes = targetAssets.toBigEndianBytes() + var paddedTargetAssets: [UInt8] = [] + var padCount = 32 - targetAssetsBytes.length + while padCount > 0 { + paddedTargetAssets.append(0) + padCount = padCount - 1 + } + paddedTargetAssets.appendAll(targetAssetsBytes) + + let targetAssetsValue = "0x".concat(String.encodeHex(paddedTargetAssets)) + EVM.store(target: asset, slot: vaultBalanceSlot, value: targetAssetsValue) + + // Read current vault storage slot (contains lastUpdate, maxRate, and totalAssets packed) + let slotBytes = EVM.load(target: vault, slot: vaultTotalAssetsSlot) + + assert(slotBytes.length == 32, message: "Vault storage slot must be 32 bytes") + + // Extract maxRate (bytes 8-15, 8 bytes) + let maxRateBytes = slotBytes.slice(from: 8, upTo: 16) + + // Get current block timestamp for lastUpdate (bytes 0-7, 8 bytes) + let currentTimestamp = UInt64(getCurrentBlock().timestamp) + let lastUpdateBytes = currentTimestamp.toBigEndianBytes() + + // Pad targetAssets to 16 bytes for the slot (bytes 16-31, 16 bytes in slot) + // Re-get bytes from targetAssets to avoid using the 32-byte padded version + let assetsBytesForSlot = targetAssets.toBigEndianBytes() + var paddedAssets: [UInt8] = [] + var assetsPadCount = 16 - assetsBytesForSlot.length + while assetsPadCount > 0 { + paddedAssets.append(0) + assetsPadCount = assetsPadCount - 1 + } + // Only take last 16 bytes if assetsBytesForSlot is somehow longer than 16 + if assetsBytesForSlot.length <= 16 { + paddedAssets.appendAll(assetsBytesForSlot) + } else { + // Take last 16 bytes if longer + paddedAssets.appendAll(assetsBytesForSlot.slice(from: assetsBytesForSlot.length - 16, upTo: assetsBytesForSlot.length)) + } + + // Pack the slot: [lastUpdate(8)] [maxRate(8)] [totalAssets(16)] + var newSlotBytes: [UInt8] = [] + newSlotBytes.appendAll(lastUpdateBytes) + newSlotBytes.appendAll(maxRateBytes) + newSlotBytes.appendAll(paddedAssets) + + 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 = "0x".concat(String.encodeHex(newSlotBytes)) + EVM.store(target: vault, slot: 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 new file mode 100644 index 00000000..3e3ae2d4 --- /dev/null +++ b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc @@ -0,0 +1,560 @@ +import "EVM" + +// Helper: Compute Solidity mapping storage slot +access(all) fun computeMappingSlot(_ values: [AnyStruct]): String { + let encoded = EVM.encodeABI(values) + let hashBytes = HashAlgorithm.KECCAK_256.hash(encoded) + return "0x\(String.encodeHex(hashBytes))" +} + +// Helper: Compute ERC20 balanceOf storage slot +access(all) fun computeBalanceOfSlot(holderAddress: String, balanceSlot: UInt256): String { + var addrHex = holderAddress + if holderAddress.slice(from: 0, upTo: 2) == "0x" { + addrHex = holderAddress.slice(from: 2, upTo: holderAddress.length) + } + let addrBytes = addrHex.decodeHex() + let address = EVM.EVMAddress(bytes: addrBytes.toConstantSized<[UInt8; 20]>()!) + return computeMappingSlot([address, balanceSlot]) +} + +// Properly seed Uniswap V3 pool with STRUCTURALLY VALID state +// This creates: slot0, observations, liquidity, ticks (with initialized flag), bitmap, and token balances +transaction( + factoryAddress: String, + token0Address: String, + token1Address: String, + fee: UInt64, + targetSqrtPriceX96: String, + targetTick: Int256, + token0BalanceSlot: UInt256, + token1BalanceSlot: UInt256 +) { + prepare(signer: &Account) {} + + execute { + let factory = EVM.addressFromString(factoryAddress) + let token0 = EVM.addressFromString(token0Address) + let token1 = EVM.addressFromString(token1Address) + + // Get pool address from factory + let getPoolCalldata = EVM.encodeABIWithSignature( + "getPool(address,address,uint24)", + [token0, token1, UInt256(fee)] + ) + let getPoolResult = EVM.call( + from: factoryAddress, + to: factoryAddress, + data: getPoolCalldata, + gasLimit: 100000, + value: 0 + ) + + if getPoolResult.status != EVM.Status.successful { + panic("Failed to get pool address") + } + + let decoded = EVM.decodeABI(types: [Type()], data: getPoolResult.data) + let poolAddr = decoded[0] as! EVM.EVMAddress + let poolAddress = poolAddr.toString() + + // Check pool exists + var isZero = true + for byte in poolAddr.bytes { + if byte != 0 { + isZero = false + break + } + } + assert(!isZero, message: "Pool does not exist - create it first") + + // Read pool parameters (tickSpacing is CRITICAL) + let tickSpacingCalldata = EVM.encodeABIWithSignature("tickSpacing()", []) + let spacingResult = EVM.call( + from: poolAddress, + to: poolAddress, + data: tickSpacingCalldata, + gasLimit: 100000, + value: 0 + ) + assert(spacingResult.status == EVM.Status.successful, message: "Failed to read tickSpacing") + + let tickSpacing = (EVM.decodeABI(types: [Type()], data: spacingResult.data)[0] as! Int256) + + // Round targetTick to nearest tickSpacing multiple + // NOTE: In real Uniswap V3, slot0.tick doesn't need to be on tickSpacing boundaries + // (only initialized ticks with liquidity do). However, rounding here ensures consistency + // and avoids potential edge cases. The price difference is minimal (e.g., ~0.16% for tick + // 6931→6900). We may revisit this if exact prices become critical. + // TODO: Consider passing unrounded tick to slot0 if precision matters + let targetTickAligned = (targetTick / tickSpacing) * tickSpacing + + // Calculate full-range ticks (MUST be multiples of tickSpacing!) + let tickLower = (-887272 as Int256) / tickSpacing * tickSpacing + let tickUpper = (887272 as Int256) / tickSpacing * tickSpacing + + // Set slot0 with target price + // slot0 packing (from lowest to highest bits): + // sqrtPriceX96 (160 bits) + // tick (24 bits, signed) + // observationIndex (16 bits) + // observationCardinality (16 bits) + // observationCardinalityNext (16 bits) + // feeProtocol (8 bits) + // unlocked (8 bits) + + // Pack slot0 correctly for Solidity storage layout + // In Solidity, the struct is packed right-to-left (LSB to MSB): + // sqrtPriceX96 (160 bits) | tick (24 bits) | observationIndex (16 bits) | + // observationCardinality (16 bits) | observationCardinalityNext (16 bits) | + // feeProtocol (8 bits) | unlocked (8 bits) + // + // Storage is a 32-byte (256-bit) word, packed from right to left. + // We build the byte array in BIG-ENDIAN order (as it will be stored). + + // Parse sqrtPriceX96 as UInt256 + let sqrtPriceU256 = UInt256.fromString(targetSqrtPriceX96)! + + // Convert tick to 24-bit representation (with two's complement for negative) + let tickMask = UInt256(((1 as Int256) << 24) - 1) // 0xFFFFFF + let tickU = UInt256( + targetTickAligned < 0 + ? ((1 as Int256) << 24) + targetTickAligned // Two's complement for negative + : targetTickAligned + ) & tickMask + + // Now pack everything into a UInt256 + // Formula: value = sqrtPrice + (tick << 160) + (obsIndex << 184) + (obsCard << 200) + + // (obsCardNext << 216) + (feeProtocol << 232) + (unlocked << 240) + + var packedValue = sqrtPriceU256 // sqrtPriceX96 in bits [0:159] + + // Add tick at bits [160:183] + packedValue = packedValue + (tickU << 160) + + // Add observationIndex = 0 at bits [184:199] - already 0 + // Add observationCardinality = 1 at bits [200:215] + packedValue = packedValue + (1 << 200) + + // Add observationCardinalityNext = 1 at bits [216:231] + packedValue = packedValue + (1 << 216) + + // Add feeProtocol = 0 at bits [232:239] - already 0 + + // Add unlocked = 1 (bool, 8 bits) at bits [240:247] + packedValue = packedValue + (1 << 240) + + // Convert to 32-byte hex string + let packedBytes = packedValue.toBigEndianBytes() + var slot0Bytes: [UInt8] = [] + + // Pad to exactly 32 bytes + var padCount = 32 - packedBytes.length + while padCount > 0 { + slot0Bytes.append(0) + padCount = padCount - 1 + } + slot0Bytes = slot0Bytes.concat(packedBytes) + + let slot0Value = "0x\(String.encodeHex(slot0Bytes))" + + // ASSERTION: Verify slot0 is exactly 32 bytes + assert(slot0Bytes.length == 32, message: "slot0 must be exactly 32 bytes") + + EVM.store(target: poolAddr, slot: "0x0", value: slot0Value) + + // Verify what we stored by reading it back + let readBack = EVM.load(target: poolAddr, slot: "0x0") + let readBackHex = "0x\(String.encodeHex(readBack))" + + // ASSERTION: Verify EVM.store/load round-trip works + assert(readBackHex == slot0Value, message: "slot0 read-back mismatch - storage corruption!") + assert(readBack.length == 32, message: "slot0 read-back wrong size") + + // Initialize observations[0] (REQUIRED or swaps will revert!) + // Observations array structure (slot 8): + // Solidity packs from LSB to MSB (right-to-left in big-endian hex): + // - blockTimestamp: uint32 (4 bytes) - lowest/rightmost + // - tickCumulative: int56 (7 bytes) + // - secondsPerLiquidityCumulativeX128: uint160 (20 bytes) + // - initialized: bool (1 byte) - highest/leftmost + // + // So in storage (big-endian), the 32-byte word is: + // [initialized(1)] [secondsPerLiquidity(20)] [tickCumulative(7)] [blockTimestamp(4)] + + // Get current block timestamp for observations[0] + let currentTimestamp = UInt32(getCurrentBlock().timestamp) + + var obs0Bytes: [UInt8] = [] + + // initialized = true (1 byte, highest/leftmost) + obs0Bytes.append(1) + + // secondsPerLiquidityCumulativeX128 (uint160, 20 bytes) = 0 + obs0Bytes.appendAll([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]) + + // tickCumulative (int56, 7 bytes) = 0 + obs0Bytes.appendAll([0,0,0,0,0,0,0]) + + // blockTimestamp (uint32, big-endian, 4 bytes, lowest/rightmost) + let tsBytes = currentTimestamp.toBigEndianBytes() + obs0Bytes.appendAll(tsBytes) + + // ASSERTION: Verify observations[0] is exactly 32 bytes + assert(obs0Bytes.length == 32, message: "observations[0] must be exactly 32 bytes") + assert(obs0Bytes[0] == 1, message: "initialized must be at byte 0 and = 1") + + let obs0Value = "0x\(String.encodeHex(obs0Bytes))" + EVM.store(target: poolAddr, slot: "0x8", value: obs0Value) + + // Set feeGrowthGlobal0X128 and feeGrowthGlobal1X128 (CRITICAL for swaps!) + EVM.store(target: poolAddr, slot: "0x1", value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: "0x2", value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + // Set protocolFees (CRITICAL) + EVM.store(target: poolAddr, slot: "0x3", value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + // Set massive liquidity + let liquidityValue = "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" + EVM.store(target: poolAddr, slot: "0x4", value: liquidityValue) + + // Initialize boundary ticks with CORRECT storage layout + + // Lower tick + let tickLowerSlot = computeMappingSlot([tickLower, 5]) + + // Slot 0: liquidityGross=1e24 (lower 128 bits), liquidityNet=+1e24 (upper 128 bits) + let tickLowerData0 = "0x000000000000d3c21bcecceda1000000000000000000d3c21bcecceda1000000" + + // ASSERTION: Verify tick data is 32 bytes + assert(tickLowerData0.length == 66, message: "Tick data must be 0x + 64 hex chars = 66 chars total") + + EVM.store(target: poolAddr, slot: tickLowerSlot, value: tickLowerData0) + + // Calculate slot offsets by parsing the base slot and adding 1, 2, 3 + let tickLowerSlotBytes = tickLowerSlot.slice(from: 2, upTo: tickLowerSlot.length).decodeHex() + var tickLowerSlotNum = 0 as UInt256 + for byte in tickLowerSlotBytes { + tickLowerSlotNum = tickLowerSlotNum * 256 + UInt256(byte) + } + + // Slot 1: feeGrowthOutside0X128 = 0 + let tickLowerSlot1Bytes = (tickLowerSlotNum + 1).toBigEndianBytes() + var tickLowerSlot1Hex = "0x" + var padCount1 = 32 - tickLowerSlot1Bytes.length + while padCount1 > 0 { + tickLowerSlot1Hex = "\(tickLowerSlot1Hex)00" + padCount1 = padCount1 - 1 + } + tickLowerSlot1Hex = "\(tickLowerSlot1Hex)\(String.encodeHex(tickLowerSlot1Bytes))" + EVM.store(target: poolAddr, slot: tickLowerSlot1Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + // Slot 2: feeGrowthOutside1X128 = 0 + let tickLowerSlot2Bytes = (tickLowerSlotNum + 2).toBigEndianBytes() + var tickLowerSlot2Hex = "0x" + var padCount2 = 32 - tickLowerSlot2Bytes.length + while padCount2 > 0 { + tickLowerSlot2Hex = "\(tickLowerSlot2Hex)00" + padCount2 = padCount2 - 1 + } + tickLowerSlot2Hex = "\(tickLowerSlot2Hex)\(String.encodeHex(tickLowerSlot2Bytes))" + EVM.store(target: poolAddr, slot: tickLowerSlot2Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + // Slot 3: tickCumulativeOutside=0, secondsPerLiquidity=0, secondsOutside=0, initialized=true(0x01) + let tickLowerSlot3Bytes = (tickLowerSlotNum + 3).toBigEndianBytes() + var tickLowerSlot3Hex = "0x" + var padCount3 = 32 - tickLowerSlot3Bytes.length + while padCount3 > 0 { + tickLowerSlot3Hex = "\(tickLowerSlot3Hex)00" + padCount3 = padCount3 - 1 + } + tickLowerSlot3Hex = "\(tickLowerSlot3Hex)\(String.encodeHex(tickLowerSlot3Bytes))" + EVM.store(target: poolAddr, slot: tickLowerSlot3Hex, value: "0x0100000000000000000000000000000000000000000000000000000000000000") + + // Upper tick (liquidityNet is NEGATIVE for upper tick) + let tickUpperSlot = computeMappingSlot([tickUpper, 5]) + + // Slot 0: liquidityGross=1e24 (lower 128 bits), liquidityNet=-1e24 (upper 128 bits, two's complement) + // CRITICAL: Must be exactly 64 hex chars = 32 bytes + // -1e24 in 128-bit two's complement: ffffffffffff2c3de43133125f000000 (32 chars = 16 bytes) + // liquidityGross: 000000000000d3c21bcecceda1000000 (32 chars = 16 bytes) + // Storage layout: [liquidityNet (upper 128)] [liquidityGross (lower 128)] + let tickUpperData0 = "0xffffffffffff2c3de43133125f000000000000000000d3c21bcecceda1000000" + + // ASSERTION: Verify tick upper data is 32 bytes + assert(tickUpperData0.length == 66, message: "Tick upper data must be 0x + 64 hex chars = 66 chars total") + + EVM.store(target: poolAddr, slot: tickUpperSlot, value: tickUpperData0) + + let tickUpperSlotBytes = tickUpperSlot.slice(from: 2, upTo: tickUpperSlot.length).decodeHex() + var tickUpperSlotNum = 0 as UInt256 + for byte in tickUpperSlotBytes { + tickUpperSlotNum = tickUpperSlotNum * 256 + UInt256(byte) + } + + // Slot 1, 2, 3 same as lower + let tickUpperSlot1Bytes = (tickUpperSlotNum + 1).toBigEndianBytes() + var tickUpperSlot1Hex = "0x" + var padCount4 = 32 - tickUpperSlot1Bytes.length + while padCount4 > 0 { + tickUpperSlot1Hex = "\(tickUpperSlot1Hex)00" + padCount4 = padCount4 - 1 + } + tickUpperSlot1Hex = "\(tickUpperSlot1Hex)\(String.encodeHex(tickUpperSlot1Bytes))" + EVM.store(target: poolAddr, slot: tickUpperSlot1Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + let tickUpperSlot2Bytes = (tickUpperSlotNum + 2).toBigEndianBytes() + var tickUpperSlot2Hex = "0x" + var padCount5 = 32 - tickUpperSlot2Bytes.length + while padCount5 > 0 { + tickUpperSlot2Hex = "\(tickUpperSlot2Hex)00" + padCount5 = padCount5 - 1 + } + tickUpperSlot2Hex = "\(tickUpperSlot2Hex)\(String.encodeHex(tickUpperSlot2Bytes))" + EVM.store(target: poolAddr, slot: tickUpperSlot2Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + let tickUpperSlot3Bytes = (tickUpperSlotNum + 3).toBigEndianBytes() + var tickUpperSlot3Hex = "0x" + var padCount6 = 32 - tickUpperSlot3Bytes.length + while padCount6 > 0 { + tickUpperSlot3Hex = "\(tickUpperSlot3Hex)00" + padCount6 = padCount6 - 1 + } + tickUpperSlot3Hex = "\(tickUpperSlot3Hex)\(String.encodeHex(tickUpperSlot3Bytes))" + EVM.store(target: poolAddr, slot: tickUpperSlot3Hex, value: "0x0100000000000000000000000000000000000000000000000000000000000000") + + // Set tick bitmap (CRITICAL for tick crossing!) + + let compressedLower = tickLower / tickSpacing + let wordPosLower = compressedLower / 256 + var bitPosLower = compressedLower % 256 + if bitPosLower < 0 { + bitPosLower = bitPosLower + 256 + } + + let compressedUpper = tickUpper / tickSpacing + let wordPosUpper = compressedUpper / 256 + var bitPosUpper = compressedUpper % 256 + if bitPosUpper < 0 { + bitPosUpper = bitPosUpper + 256 + } + + // Set bitmap for lower tick + let bitmapLowerSlot = computeMappingSlot([wordPosLower, 6]) + + // ASSERTION: Verify bitPos is valid + assert(bitPosLower >= 0 && bitPosLower < 256, message: "bitPosLower must be 0-255, got \(bitPosLower.toString())") + + var bitmapLowerValue = "0x" + var byteIdx = 0 + while byteIdx < 32 { + let byteIndexFromRight = Int(bitPosLower) / 8 + let targetByteIdx = 31 - byteIndexFromRight + let bitInByte = Int(bitPosLower) % 8 + + // ASSERTION: Verify byte index is valid + assert(targetByteIdx >= 0 && targetByteIdx < 32, message: "targetByteIdx must be 0-31, got \(targetByteIdx)") + + var byteVal: UInt8 = 0 + if byteIdx == targetByteIdx { + byteVal = 1 << UInt8(bitInByte) + } + + let byteHex = String.encodeHex([byteVal]) + bitmapLowerValue = "\(bitmapLowerValue)\(byteHex)" + byteIdx = byteIdx + 1 + } + + // ASSERTION: Verify bitmap value is correct length + assert(bitmapLowerValue.length == 66, message: "bitmap must be 0x + 64 hex chars = 66 chars total") + + EVM.store(target: poolAddr, slot: bitmapLowerSlot, value: bitmapLowerValue) + + // Set bitmap for upper tick + let bitmapUpperSlot = computeMappingSlot([wordPosUpper, UInt256(6)]) + + // ASSERTION: Verify bitPos is valid + assert(bitPosUpper >= 0 && bitPosUpper < 256, message: "bitPosUpper must be 0-255, got \(bitPosUpper.toString())") + + var bitmapUpperValue = "0x" + byteIdx = 0 + while byteIdx < 32 { + let byteIndexFromRight = Int(bitPosUpper) / 8 + let targetByteIdx = 31 - byteIndexFromRight + let bitInByte = Int(bitPosUpper) % 8 + + // ASSERTION: Verify byte index is valid + assert(targetByteIdx >= 0 && targetByteIdx < 32, message: "targetByteIdx must be 0-31, got \(targetByteIdx)") + + var byteVal: UInt8 = 0 + if byteIdx == targetByteIdx { + byteVal = 1 << UInt8(bitInByte) + } + + let byteHex = String.encodeHex([byteVal]) + bitmapUpperValue = "\(bitmapUpperValue)\(byteHex)" + byteIdx = byteIdx + 1 + } + + // ASSERTION: Verify bitmap value is correct length + assert(bitmapUpperValue.length == 66, message: "bitmap must be 0x + 64 hex chars = 66 chars total") + + EVM.store(target: poolAddr, slot: bitmapUpperSlot, value: bitmapUpperValue) + + // CREATE POSITION (CRITICAL) + + var positionKeyData: [UInt8] = [] + + // Add pool address (20 bytes) + positionKeyData.appendAll(poolAddr.bytes.toVariableSized()) + + // Add tickLower (int24, 3 bytes, big-endian, two's complement) + let tickLowerU256 = tickLower < 0 + ? ((1 as Int256) << 24) + tickLower // Two's complement for negative + : tickLower + let tickLowerBytes = tickLowerU256.toBigEndianBytes() + + // Pad to exactly 3 bytes (left-pad with 0x00) + var tickLower3Bytes: [UInt8] = [] + let tickLowerLen = tickLowerBytes.length + if tickLowerLen < 3 { + // Left-pad with zeros + var padCount = 3 - tickLowerLen + while padCount > 0 { + tickLower3Bytes.append(0) + padCount = padCount - 1 + } + for byte in tickLowerBytes { + tickLower3Bytes.append(byte) + } + } else { + // Take last 3 bytes if longer + tickLower3Bytes = [ + tickLowerBytes[tickLowerLen-3], + tickLowerBytes[tickLowerLen-2], + tickLowerBytes[tickLowerLen-1] + ] + } + + // ASSERTION: Verify tickLower is exactly 3 bytes + assert(tickLower3Bytes.length == 3, message: "tickLower must be exactly 3 bytes for abi.encodePacked, got \(tickLower3Bytes.length)") + + for byte in tickLower3Bytes { + positionKeyData.append(byte) + } + + // Add tickUpper (int24, 3 bytes, big-endian, two's complement) + let tickUpperU256 = tickUpper < 0 + ? ((1 as Int256) << 24) + tickUpper + : tickUpper + let tickUpperBytes = tickUpperU256.toBigEndianBytes() + + // Pad to exactly 3 bytes (left-pad with 0x00) + var tickUpper3Bytes: [UInt8] = [] + let tickUpperLen = tickUpperBytes.length + if tickUpperLen < 3 { + // Left-pad with zeros + var padCount = 3 - tickUpperLen + while padCount > 0 { + tickUpper3Bytes.append(0) + padCount = padCount - 1 + } + for byte in tickUpperBytes { + tickUpper3Bytes.append(byte) + } + } else { + // Take last 3 bytes if longer + tickUpper3Bytes = [ + tickUpperBytes[tickUpperLen-3], + tickUpperBytes[tickUpperLen-2], + tickUpperBytes[tickUpperLen-1] + ] + } + + // ASSERTION: Verify tickUpper is exactly 3 bytes + assert(tickUpper3Bytes.length == 3, message: "tickUpper must be exactly 3 bytes for abi.encodePacked, got \(tickUpper3Bytes.length)") + + for byte in tickUpper3Bytes { + positionKeyData.append(byte) + } + + // ASSERTION: Verify total position key data is exactly 26 bytes (20 + 3 + 3) + assert(positionKeyData.length == 26, message: "Position key data must be 26 bytes (20 + 3 + 3), got \(positionKeyData.length.toString())") + + let positionKeyHash = HashAlgorithm.KECCAK_256.hash(positionKeyData) + let positionKeyHex = "0x".concat(String.encodeHex(positionKeyHash)) + + // Now compute storage slot: keccak256(positionKey . slot7) + var positionSlotData: [UInt8] = [] + positionSlotData = positionSlotData.concat(positionKeyHash) + + // Add slot 7 as 32-byte value (31 zeros + 7) + var slotBytes: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7] + positionSlotData = positionSlotData.concat(slotBytes) + + // ASSERTION: Verify position slot data is 64 bytes (32 + 32) + assert(positionSlotData.length == 64, message: "Position slot data must be 64 bytes (32 key + 32 slot), got \(positionSlotData.length)") + + let positionSlotHash = HashAlgorithm.KECCAK_256.hash(positionSlotData) + let positionSlot = "0x\(String.encodeHex(positionSlotHash))" + + // Set position liquidity = 1e24 (matching global liquidity) + let positionLiquidityValue = "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" + + // ASSERTION: Verify position liquidity value is 32 bytes + assert(positionLiquidityValue.length == 66, message: "Position liquidity must be 0x + 64 hex chars = 66 chars total") + + EVM.store(target: poolAddr, slot: positionSlot, value: positionLiquidityValue) + + // Calculate slot+1, slot+2, slot+3 + let positionSlotBytes = positionSlotHash + var positionSlotNum = 0 as UInt256 + for byte in positionSlotBytes { + positionSlotNum = positionSlotNum * 256 + UInt256(byte) + } + + // Slot 1: feeGrowthInside0LastX128 = 0 + let positionSlot1Bytes = (positionSlotNum + 1).toBigEndianBytes() + var positionSlot1Hex = "0x" + var posPadCount1 = 32 - positionSlot1Bytes.length + while posPadCount1 > 0 { + positionSlot1Hex = "\(positionSlot1Hex)00" + posPadCount1 = posPadCount1 - 1 + } + positionSlot1Hex = "\(positionSlot1Hex)\(String.encodeHex(positionSlot1Bytes))" + EVM.store(target: poolAddr, slot: positionSlot1Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + // Slot 2: feeGrowthInside1LastX128 = 0 + let positionSlot2Bytes = (positionSlotNum + 2).toBigEndianBytes() + var positionSlot2Hex = "0x" + var posPadCount2 = 32 - positionSlot2Bytes.length + while posPadCount2 > 0 { + positionSlot2Hex = "\(positionSlot2Hex)00" + posPadCount2 = posPadCount2 - 1 + } + positionSlot2Hex = "\(positionSlot2Hex)\(String.encodeHex(positionSlot2Bytes))" + EVM.store(target: poolAddr, slot: positionSlot2Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + // Slot 3: tokensOwed0 = 0, tokensOwed1 = 0 + let positionSlot3Bytes = (positionSlotNum + 3).toBigEndianBytes() + var positionSlot3Hex = "0x" + var posPadCount3 = 32 - positionSlot3Bytes.length + while posPadCount3 > 0 { + positionSlot3Hex = "\(positionSlot3Hex)00" + posPadCount3 = posPadCount3 - 1 + } + positionSlot3Hex = "\(positionSlot3Hex)\(String.encodeHex(positionSlot3Bytes))" + EVM.store(target: poolAddr, slot: positionSlot3Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + + // Fund pool with massive token balances + let hugeBalance = "0x000000000000000000000000af298d050e4395d69670b12b7f41000000000000" + + // Set token0 balance + let token0BalanceSlotComputed = computeBalanceOfSlot(holderAddress: poolAddress, balanceSlot: token0BalanceSlot) + EVM.store(target: token0, slot: token0BalanceSlotComputed, value: hugeBalance) + + // Set token1 balance + let token1BalanceSlotComputed = computeBalanceOfSlot(holderAddress: poolAddress, balanceSlot: token1BalanceSlot) + EVM.store(target: token1, slot: token1BalanceSlotComputed, value: hugeBalance) + } +} diff --git a/flow.json b/flow.json index 632353f9..9f5c914e 100644 --- a/flow.json +++ b/flow.json @@ -1,5 +1,13 @@ { "contracts": { + "EVM": { + "source": "./cadence/contracts/mocks/EVM.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, "BandOracleConnectors": { "source": "./lib/FlowCreditMarket/FlowActions/cadence/contracts/connectors/band-oracle/BandOracleConnectors.cdc", "aliases": { From c405a2cf09fbfb23d98ce96756d35c6faa7112ad Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 20 Feb 2026 10:11:00 -0800 Subject: [PATCH 02/14] Fix bugs with decimal offsets & cleanup --- cadence/tests/evm_state_helpers.cdc | 166 +-------- .../ensure_uniswap_pool_exists.cdc | 84 ----- .../transactions/set_erc4626_vault_price.cdc | 118 +++--- .../set_uniswap_v3_pool_price.cdc | 343 +++++++++++++++--- flow.json | 2 +- 5 files changed, 370 insertions(+), 343 deletions(-) delete mode 100644 cadence/tests/transactions/ensure_uniswap_pool_exists.cdc diff --git a/cadence/tests/evm_state_helpers.cdc b/cadence/tests/evm_state_helpers.cdc index ed7f55b5..7a3c7fec 100644 --- a/cadence/tests/evm_state_helpers.cdc +++ b/cadence/tests/evm_state_helpers.cdc @@ -10,33 +10,18 @@ access(all) fun setVaultSharePrice( vaultAddress: String, assetAddress: String, assetBalanceSlot: UInt256, - vaultTotalAssetsSlot: String, + totalSupplySlot: UInt256, + vaultTotalAssetsSlot: UInt256, baseAssets: UFix64, priceMultiplier: UFix64, signer: Test.TestAccount ) { - // Convert UFix64 baseAssets to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8) - let baseAssetsBytes = baseAssets.toBigEndianBytes() - var baseAssetsUInt64: UInt64 = 0 - for byte in baseAssetsBytes { - baseAssetsUInt64 = (baseAssetsUInt64 << 8) + UInt64(byte) - } - let baseAssetsUInt256 = UInt256(baseAssetsUInt64) - - // Calculate target: baseAssets * multiplier - let multiplierBytes = priceMultiplier.toBigEndianBytes() - var multiplierUInt64: UInt64 = 0 - for byte in multiplierBytes { - multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte) - } - let targetAssets = (baseAssetsUInt256 * UInt256(multiplierUInt64)) / UInt256(100000000) - let result = Test.executeTransaction( Test.Transaction( code: Test.readFile("transactions/set_erc4626_vault_price.cdc"), authorizers: [signer.address], signers: [signer], - arguments: [vaultAddress, assetAddress, assetBalanceSlot, vaultTotalAssetsSlot, priceMultiplier, targetAssets] + arguments: [vaultAddress, assetAddress, assetBalanceSlot, totalSupplySlot, vaultTotalAssetsSlot, baseAssets, priceMultiplier] ) ) Test.expect(result, Test.beSucceeded()) @@ -55,155 +40,14 @@ access(all) fun setPoolToPrice( tokenABalanceSlot: UInt256, tokenBBalanceSlot: UInt256, signer: Test.TestAccount -) { - // Sort tokens (Uniswap V3 requires token0 < token1) - let token0 = tokenAAddress < tokenBAddress ? tokenAAddress : tokenBAddress - let token1 = tokenAAddress < tokenBAddress ? tokenBAddress : tokenAAddress - let token0BalanceSlot = tokenAAddress < tokenBAddress ? tokenABalanceSlot : tokenBBalanceSlot - let token1BalanceSlot = tokenAAddress < tokenBAddress ? tokenBBalanceSlot : tokenABalanceSlot - - let poolPrice = tokenAAddress < tokenBAddress ? priceTokenBPerTokenA : 1.0 / priceTokenBPerTokenA - - let targetSqrtPriceX96 = calculateSqrtPriceX96(price: poolPrice) - let targetTick = calculateTick(price: poolPrice) - - let createResult = Test.executeTransaction( - Test.Transaction( - code: Test.readFile("transactions/ensure_uniswap_pool_exists.cdc"), - authorizers: [signer.address], - signers: [signer], - arguments: [factoryAddress, token0, token1, fee, targetSqrtPriceX96] - ) - ) - Test.expect(createResult, Test.beSucceeded()) - +) { let seedResult = Test.executeTransaction( Test.Transaction( code: Test.readFile("transactions/set_uniswap_v3_pool_price.cdc"), authorizers: [signer.address], signers: [signer], - arguments: [factoryAddress, token0, token1, fee, targetSqrtPriceX96, targetTick, token0BalanceSlot, token1BalanceSlot] + arguments: [factoryAddress, tokenAAddress, tokenBAddress, fee, priceTokenBPerTokenA, tokenABalanceSlot, tokenBBalanceSlot] ) ) Test.expect(seedResult, Test.beSucceeded()) } - -/* --- Internal Math Utilities --- */ - -/// Calculate sqrtPriceX96 from a price ratio -/// Returns sqrt(price) * 2^96 as a string for Uniswap V3 pool initialization -access(self) fun calculateSqrtPriceX96(price: UFix64): String { - // Convert UFix64 to UInt256 (UFix64 has 8 decimal places) - // price is stored as integer * 10^8 internally - let priceBytes = price.toBigEndianBytes() - var priceUInt64: UInt64 = 0 - for byte in priceBytes { - priceUInt64 = (priceUInt64 << 8) + UInt64(byte) - } - let priceScaled = UInt256(priceUInt64) // This is price * 10^8 - - // sqrt(price) * 2^96, adjusted for UFix64 scaling - let sqrtPriceScaled = sqrt(n: priceScaled, scaleFactor: UInt256(1) << 48) - let sqrtPriceX96 = (sqrtPriceScaled * (UInt256(1) << 48)) / UInt256(10000) - - return sqrtPriceX96.toString() -} - -/// Calculate tick from price ratio -/// Returns tick = floor(log_1.0001(price)) for Uniswap V3 tick spacing -access(self) fun calculateTick(price: UFix64): Int256 { - // Convert UFix64 to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8) - let priceBytes = price.toBigEndianBytes() - var priceUInt64: UInt64 = 0 - for byte in priceBytes { - priceUInt64 = (priceUInt64 << 8) + UInt64(byte) - } - - // priceUInt64 is price * 10^8 - // Scale to 10^18 for precision: price * 10^18 = priceUInt64 * 10^10 - let priceScaled = UInt256(priceUInt64) * UInt256(10000000000) // 10^10 - let scaleFactor = UInt256(1000000000000000000) // 10^18 - - // Calculate ln(price) * 10^18 - let lnPrice = ln(x: priceScaled, scaleFactor: scaleFactor) - - // ln(1.0001) * 10^18 ≈ 99995000333083 - let ln1_0001 = Int256(99995000333083) - - // tick = ln(price) / ln(1.0001) - let tick = lnPrice / ln1_0001 - - return tick -} - -/// Calculate square root using Newton's method -/// Returns sqrt(n) * scaleFactor for precision -access(self) fun sqrt(n: UInt256, scaleFactor: UInt256): UInt256 { - if n == UInt256(0) { - return UInt256(0) - } - - var x = (n * scaleFactor) / UInt256(2) - var prevX = UInt256(0) - var iterations = 0 - - while x != prevX && iterations < 50 { - prevX = x - let nScaled = n * scaleFactor * scaleFactor - x = (x + nScaled / x) / UInt256(2) - iterations = iterations + 1 - } - - return x -} - -/// Calculate natural logarithm using Taylor series -/// Returns ln(x) * scaleFactor for precision -access(self) fun ln(x: UInt256, scaleFactor: UInt256): Int256 { - if x == UInt256(0) { - panic("ln(0) is undefined") - } - - // Reduce x to range [0.5, 1.5] for better convergence - var value = x - var n = 0 - - let threshold = (scaleFactor * UInt256(3)) / UInt256(2) - while value > threshold { - value = value / UInt256(2) - n = n + 1 - } - - let lowerThreshold = scaleFactor / UInt256(2) - while value < lowerThreshold { - value = value * UInt256(2) - n = n - 1 - } - - // Taylor series: ln(1+z) = z - z^2/2 + z^3/3 - ... - let z = value > scaleFactor - ? Int256(value - scaleFactor) - : -Int256(scaleFactor - value) - - var result = z - var term = z - var i = 2 - var prevResult = Int256(0) - - while i <= 50 && result != prevResult { - prevResult = result - term = (term * z) / Int256(scaleFactor) - if i % 2 == 0 { - result = result - term / Int256(i) - } else { - result = result + term / Int256(i) - } - i = i + 1 - } - - // Adjust for range reduction: ln(2^n * y) = n*ln(2) + ln(y) - let ln2Scaled = Int256(693147180559945309) // ln(2) * 10^18 - result = result + Int256(n) * ln2Scaled - - return result -} diff --git a/cadence/tests/transactions/ensure_uniswap_pool_exists.cdc b/cadence/tests/transactions/ensure_uniswap_pool_exists.cdc deleted file mode 100644 index 5012dcc1..00000000 --- a/cadence/tests/transactions/ensure_uniswap_pool_exists.cdc +++ /dev/null @@ -1,84 +0,0 @@ -// Transaction to ensure Uniswap V3 pool exists (creates if needed) -import "EVM" - -transaction( - factoryAddress: String, - token0Address: String, - token1Address: String, - fee: UInt64, - sqrtPriceX96: String -) { - let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount - - prepare(signer: auth(Storage) &Account) { - self.coa = signer.storage.borrow(from: /storage/evm) - ?? panic("Could not borrow COA") - } - - execute { - let factory = EVM.addressFromString(factoryAddress) - let token0 = EVM.addressFromString(token0Address) - let token1 = EVM.addressFromString(token1Address) - - // First check if pool already exists - var getPoolCalldata = EVM.encodeABIWithSignature( - "getPool(address,address,uint24)", - [token0, token1, UInt256(fee)] - ) - var getPoolResult = self.coa.dryCall( - to: factory, - data: getPoolCalldata, - gasLimit: 100000, - value: EVM.Balance(attoflow: 0) - ) - - assert(getPoolResult.status == EVM.Status.successful, message: "Failed to query pool from factory") - - // Decode pool address - let poolAddress = (EVM.decodeABI(types: [Type()], data: getPoolResult.data)[0] as! EVM.EVMAddress) - 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]) - - // If pool already exists, we're done (idempotent behavior) - if poolAddress.bytes != zeroAddress.bytes { - return - } - - // Pool doesn't exist, create it - var calldata = EVM.encodeABIWithSignature( - "createPool(address,address,uint24)", - [token0, token1, UInt256(fee)] - ) - var result = self.coa.call( - to: factory, - data: calldata, - gasLimit: 5000000, - value: EVM.Balance(attoflow: 0) - ) - - assert(result.status == EVM.Status.successful, message: "Pool creation failed") - - // Get the newly created pool address - getPoolResult = self.coa.dryCall(to: factory, data: getPoolCalldata, gasLimit: 100000, value: EVM.Balance(attoflow: 0)) - - assert(getPoolResult.status == EVM.Status.successful && getPoolResult.data.length >= 20, message: "Failed to get pool address after creation") - - // Extract last 20 bytes as pool address - let poolAddrBytes = getPoolResult.data.slice(from: getPoolResult.data.length - 20, upTo: getPoolResult.data.length) - let poolAddr = EVM.addressFromString("0x\(String.encodeHex(poolAddrBytes))") - - // Initialize the pool with the target price - let initPrice = UInt256.fromString(sqrtPriceX96)! - calldata = EVM.encodeABIWithSignature( - "initialize(uint160)", - [initPrice] - ) - result = self.coa.call( - to: poolAddr, - data: calldata, - gasLimit: 5000000, - value: EVM.Balance(attoflow: 0) - ) - - assert(result.status == EVM.Status.successful, message: "Pool initialization failed") - } -} diff --git a/cadence/tests/transactions/set_erc4626_vault_price.cdc b/cadence/tests/transactions/set_erc4626_vault_price.cdc index 4681cd02..a5d10c85 100644 --- a/cadence/tests/transactions/set_erc4626_vault_price.cdc +++ b/cadence/tests/transactions/set_erc4626_vault_price.cdc @@ -1,4 +1,6 @@ -import "EVM" +import EVM from "MockEVM" +import "ERC4626Utils" +import "FlowEVMBridgeUtils" // Helper: Compute Solidity mapping storage slot access(all) fun computeMappingSlot(_ values: [AnyStruct]): String { @@ -20,15 +22,14 @@ access(all) fun computeBalanceOfSlot(holderAddress: String, balanceSlot: UInt256 // Atomically set ERC4626 vault share price // This manipulates both the underlying asset balance and vault's _totalAssets storage slot -// If targetTotalAssets is 0, multiplies current totalAssets by priceMultiplier -// If targetTotalAssets is non-zero, uses it directly (priceMultiplier is ignored) transaction( vaultAddress: String, assetAddress: String, assetBalanceSlot: UInt256, - vaultTotalAssetsSlot: String, - priceMultiplier: UFix64, - targetTotalAssets: UInt256 + totalSupplySlot: UInt256, + vaultTotalAssetsSlot: UInt256, + baseAssets: UFix64, + priceMultiplier: UFix64 ) { prepare(signer: &Account) {} @@ -36,65 +37,72 @@ transaction( let vault = EVM.addressFromString(vaultAddress) let asset = EVM.addressFromString(assetAddress) - var targetAssets: UInt256 = targetTotalAssets - - // If targetTotalAssets is 0, calculate from current assets * multiplier - if targetTotalAssets == UInt256(0) { - // Read current totalAssets from vault via EVM call - let totalAssetsCalldata = EVM.encodeABIWithSignature("totalAssets()", []) - let totalAssetsResult = EVM.call( - from: vaultAddress, - to: vaultAddress, - data: totalAssetsCalldata, - gasLimit: 100000, - value: 0 - ) - - assert(totalAssetsResult.status == EVM.Status.successful, message: "Failed to read totalAssets") - - let currentAssets = (EVM.decodeABI(types: [Type()], data: totalAssetsResult.data)[0] as! UInt256) - - // Calculate target assets (currentAssets * multiplier / 1e8) - // priceMultiplier is UFix64, so convert to UInt64 via big-endian bytes - let multiplierBytes = priceMultiplier.toBigEndianBytes() - var multiplierUInt64: UInt64 = 0 - for byte in multiplierBytes { - multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte) - } - targetAssets = (currentAssets * UInt256(multiplierUInt64)) / UInt256(100000000) + // Helper to convert UInt256 to hex string for EVM.store + let toSlotString = fun (_ slot: UInt256): String { + return "0x".concat(String.encodeHex(slot.toBigEndianBytes())) } - // Update asset.balanceOf(vault) to targetAssets - let vaultBalanceSlot = computeBalanceOfSlot(holderAddress: vaultAddress, balanceSlot: assetBalanceSlot) + // Query asset decimals from the ERC20 contract + let zeroAddress = EVM.addressFromString("0x0000000000000000000000000000000000000000") + let decimalsCalldata = EVM.encodeABIWithSignature("decimals()", []) + let decimalsResult = EVM.dryCall( + from: zeroAddress, + to: asset, + data: decimalsCalldata, + gasLimit: 100000, + value: EVM.Balance(attoflow: 0) + ) + assert(decimalsResult.status == EVM.Status.successful, message: "Failed to query asset decimals") + let assetDecimals = (EVM.decodeABI(types: [Type()], data: decimalsResult.data)[0] as! UInt8) - // Pad targetAssets to 32 bytes - let targetAssetsBytes = targetAssets.toBigEndianBytes() - var paddedTargetAssets: [UInt8] = [] - var padCount = 32 - targetAssetsBytes.length - while padCount > 0 { - paddedTargetAssets.append(0) - padCount = padCount - 1 + // Convert baseAssets to asset decimals and apply multiplier + let targetAssets = FlowEVMBridgeUtils.ufix64ToUInt256(value: baseAssets, decimals: assetDecimals) + log("SET_VAULT_PRICE: baseAssets=".concat(baseAssets.toString()) + .concat(", assetDecimals=").concat(assetDecimals.toString()) + .concat(", targetAssets=").concat(targetAssets.toString())) + let multiplierBytes = priceMultiplier.toBigEndianBytes() + var multiplierUInt64: UInt64 = 0 + for byte in multiplierBytes { + multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte) } - paddedTargetAssets.appendAll(targetAssetsBytes) + let finalTargetAssets = (targetAssets * UInt256(multiplierUInt64)) / UInt256(100000000) + log("SET_VAULT_PRICE: multiplierUInt64=".concat(multiplierUInt64.toString()) + .concat(", finalTargetAssets=").concat(finalTargetAssets.toString())) - let targetAssetsValue = "0x".concat(String.encodeHex(paddedTargetAssets)) - EVM.store(target: asset, slot: vaultBalanceSlot, value: targetAssetsValue) + // For a 1:1 price (1 share = 1 asset), we need: + // totalAssets (in assetDecimals) / totalSupply (vault decimals) = 1 + // Morpho vaults use 18 decimals for shares regardless of underlying asset decimals + // So: supply_raw = assets_raw * 10^(18 - assetDecimals) + // IMPORTANT: Supply should be based on BASE assets, not multiplied assets (to change price per share) + let decimalDifference = UInt8(18) - assetDecimals + let supplyMultiplier = FlowEVMBridgeUtils.pow(base: 10, exponent: decimalDifference) + let finalTargetSupply = targetAssets * supplyMultiplier - // Read current vault storage slot (contains lastUpdate, maxRate, and totalAssets packed) - let slotBytes = EVM.load(target: vault, slot: vaultTotalAssetsSlot) + log("SET_VAULT_PRICE: assetDecimals=".concat(assetDecimals.toString()) + .concat(", finalTargetAssets=").concat(finalTargetAssets.toString()) + .concat(", decimalDifference=").concat(decimalDifference.toString()) + .concat(", supplyMultiplier=").concat(supplyMultiplier.toString()) + .concat(", finalTargetSupply=").concat(finalTargetSupply.toString())) - assert(slotBytes.length == 32, message: "Vault storage slot must be 32 bytes") + let supplyValue = "0x".concat(String.encodeHex(finalTargetSupply.toBigEndianBytes())) + EVM.store(target: vault, slot: toSlotString(totalSupplySlot), value: supplyValue) + log("SET_VAULT_PRICE: Stored totalSupply at slot ".concat(toSlotString(totalSupplySlot)).concat(" = ").concat(supplyValue)) - // Extract maxRate (bytes 8-15, 8 bytes) - let maxRateBytes = slotBytes.slice(from: 8, upTo: 16) + // Update asset.balanceOf(vault) to finalTargetAssets + let vaultBalanceSlot = computeBalanceOfSlot(holderAddress: vaultAddress, balanceSlot: assetBalanceSlot) + let targetAssetsValue = "0x".concat(String.encodeHex(finalTargetAssets.toBigEndianBytes())) + EVM.store(target: asset, slot: vaultBalanceSlot, value: targetAssetsValue) + log("SET_VAULT_PRICE: Stored asset balance at vault = ".concat(targetAssetsValue)) - // Get current block timestamp for lastUpdate (bytes 0-7, 8 bytes) + // Set vault storage slot (lastUpdate, maxRate, totalAssets packed) + // For testing, we'll set maxRate to 0 to disable interest rate caps let currentTimestamp = UInt64(getCurrentBlock().timestamp) let lastUpdateBytes = currentTimestamp.toBigEndianBytes() + let maxRateBytes: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0] // maxRate = 0 - // Pad targetAssets to 16 bytes for the slot (bytes 16-31, 16 bytes in slot) - // Re-get bytes from targetAssets to avoid using the 32-byte padded version - let assetsBytesForSlot = targetAssets.toBigEndianBytes() + // Pad finalTargetAssets to 16 bytes for the slot (bytes 16-31, 16 bytes in slot) + // Re-get bytes from finalTargetAssets to avoid using the 32-byte padded version + let assetsBytesForSlot = finalTargetAssets.toBigEndianBytes() var paddedAssets: [UInt8] = [] var assetsPadCount = 16 - assetsBytesForSlot.length while assetsPadCount > 0 { @@ -118,6 +126,8 @@ 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 = "0x".concat(String.encodeHex(newSlotBytes)) - EVM.store(target: vault, slot: vaultTotalAssetsSlot, value: newSlotValue) + EVM.store(target: vault, slot: toSlotString(vaultTotalAssetsSlot), value: newSlotValue) + log("SET_VAULT_PRICE: Stored packed slot at ".concat(toSlotString(vaultTotalAssetsSlot)).concat(" = ").concat(newSlotValue)) + log("SET_VAULT_PRICE: COMPLETE - Share price should be 1:1") } } diff --git a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc index 3e3ae2d4..dfbd9058 100644 --- a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc +++ b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc @@ -1,4 +1,4 @@ -import "EVM" +import EVM from "MockEVM" // Helper: Compute Solidity mapping storage slot access(all) fun computeMappingSlot(_ values: [AnyStruct]): String { @@ -22,60 +22,126 @@ access(all) fun computeBalanceOfSlot(holderAddress: String, balanceSlot: UInt256 // This creates: slot0, observations, liquidity, ticks (with initialized flag), bitmap, and token balances transaction( factoryAddress: String, - token0Address: String, - token1Address: String, + tokenAAddress: String, + tokenBAddress: String, fee: UInt64, - targetSqrtPriceX96: String, - targetTick: Int256, - token0BalanceSlot: UInt256, - token1BalanceSlot: UInt256 + priceTokenBPerTokenA: UFix64, + tokenABalanceSlot: UInt256, + tokenBBalanceSlot: UInt256 ) { - prepare(signer: &Account) {} + let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount + prepare(signer: auth(Storage) &Account) { + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA") + } execute { + // Sort tokens (Uniswap V3 requires token0 < token1) let factory = EVM.addressFromString(factoryAddress) - let token0 = EVM.addressFromString(token0Address) - let token1 = EVM.addressFromString(token1Address) + let token0 = EVM.addressFromString(tokenAAddress < tokenBAddress ? tokenAAddress : tokenBAddress) + let token1 = EVM.addressFromString(tokenAAddress < tokenBAddress ? tokenBAddress : tokenAAddress) + let token0BalanceSlot = tokenAAddress < tokenBAddress ? tokenABalanceSlot : tokenBBalanceSlot + let token1BalanceSlot = tokenAAddress < tokenBAddress ? tokenBBalanceSlot : tokenABalanceSlot + + let poolPrice = tokenAAddress < tokenBAddress ? priceTokenBPerTokenA : 1.0 / priceTokenBPerTokenA + + // Read decimals from EVM + let token0Decimals = getTokenDecimals(evmContractAddress: token0) + let token1Decimals = getTokenDecimals(evmContractAddress: token1) + let decOffset = Int(token1Decimals) - Int(token0Decimals) + + // Calculate base price/tick + var targetSqrtPriceX96 = calculateSqrtPriceX96(price: poolPrice) + var targetTick = calculateTick(price: poolPrice) + + // Apply decimal offset if needed + if decOffset != 0 { + // Adjust sqrtPriceX96: multiply/divide by 10^(decOffset/2) + var sqrtPriceU256 = UInt256.fromString(targetSqrtPriceX96)! + let absHalfOffset = decOffset < 0 ? (-decOffset) / 2 : decOffset / 2 + var pow10: UInt256 = 1 + var i = 0 + while i < absHalfOffset { + pow10 = pow10 * 10 + i = i + 1 + } + if decOffset > 0 { + sqrtPriceU256 = sqrtPriceU256 * pow10 + } else { + sqrtPriceU256 = sqrtPriceU256 / pow10 + } + targetSqrtPriceX96 = sqrtPriceU256.toString() + + // Adjust tick: add/subtract decOffset * 23026 (ticks per decimal) + targetTick = targetTick + Int256(decOffset) * 23026 + } - // Get pool address from factory - let getPoolCalldata = EVM.encodeABIWithSignature( + // First check if pool already exists + var getPoolCalldata = EVM.encodeABIWithSignature( "getPool(address,address,uint24)", [token0, token1, UInt256(fee)] ) - let getPoolResult = EVM.call( - from: factoryAddress, - to: factoryAddress, + var getPoolResult = self.coa.dryCall( + to: factory, data: getPoolCalldata, gasLimit: 100000, - value: 0 + value: EVM.Balance(attoflow: 0) ) - if getPoolResult.status != EVM.Status.successful { - panic("Failed to get pool address") + assert(getPoolResult.status == EVM.Status.successful, message: "Failed to query pool from factory") + + // Decode pool address + var poolAddr = (EVM.decodeABI(types: [Type()], data: getPoolResult.data)[0] as! EVM.EVMAddress) + 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]) + + // If pool doesn't exist, create and initialize it + if poolAddr.bytes == zeroAddress.bytes { + // Pool doesn't exist, create it + var calldata = EVM.encodeABIWithSignature( + "createPool(address,address,uint24)", + [token0, token1, UInt256(fee)] + ) + var result = self.coa.call( + to: factory, + data: calldata, + gasLimit: 5000000, + value: EVM.Balance(attoflow: 0) + ) + + assert(result.status == EVM.Status.successful, message: "Pool creation failed") + + // Get the newly created pool address + getPoolResult = self.coa.dryCall(to: factory, data: getPoolCalldata, gasLimit: 100000, value: EVM.Balance(attoflow: 0)) + + assert(getPoolResult.status == EVM.Status.successful && getPoolResult.data.length >= 20, message: "Failed to get pool address after creation") + + poolAddr = (EVM.decodeABI(types: [Type()], data: getPoolResult.data)[0] as! EVM.EVMAddress) + + // Initialize the pool with the target price + let initPrice = UInt256.fromString(targetSqrtPriceX96)! + calldata = EVM.encodeABIWithSignature( + "initialize(uint160)", + [initPrice] + ) + result = self.coa.call( + to: poolAddr, + data: calldata, + gasLimit: 5000000, + value: EVM.Balance(attoflow: 0) + ) + + assert(result.status == EVM.Status.successful, message: "Pool initialization failed") } - let decoded = EVM.decodeABI(types: [Type()], data: getPoolResult.data) - let poolAddr = decoded[0] as! EVM.EVMAddress let poolAddress = poolAddr.toString() - // Check pool exists - var isZero = true - for byte in poolAddr.bytes { - if byte != 0 { - isZero = false - break - } - } - assert(!isZero, message: "Pool does not exist - create it first") - // Read pool parameters (tickSpacing is CRITICAL) let tickSpacingCalldata = EVM.encodeABIWithSignature("tickSpacing()", []) - let spacingResult = EVM.call( - from: poolAddress, - to: poolAddress, + let spacingResult = self.coa.dryCall( + to: poolAddr, data: tickSpacingCalldata, gasLimit: 100000, - value: 0 + value: EVM.Balance(attoflow: 0) ) assert(spacingResult.status == EVM.Status.successful, message: "Failed to read tickSpacing") @@ -89,10 +155,13 @@ transaction( // TODO: Consider passing unrounded tick to slot0 if precision matters let targetTickAligned = (targetTick / tickSpacing) * tickSpacing - // Calculate full-range ticks (MUST be multiples of tickSpacing!) + // Use FULL RANGE ticks (min/max for Uniswap V3) + // This ensures liquidity is available at any price let tickLower = (-887272 as Int256) / tickSpacing * tickSpacing let tickUpper = (887272 as Int256) / tickSpacing * tickSpacing + log("Tick range: tickLower=\(tickLower), tick=\(targetTickAligned), tickUpper=\(tickUpper)") + // Set slot0 with target price // slot0 packing (from lowest to highest bits): // sqrtPriceX96 (160 bits) @@ -275,10 +344,6 @@ transaction( let tickUpperSlot = computeMappingSlot([tickUpper, 5]) // Slot 0: liquidityGross=1e24 (lower 128 bits), liquidityNet=-1e24 (upper 128 bits, two's complement) - // CRITICAL: Must be exactly 64 hex chars = 32 bytes - // -1e24 in 128-bit two's complement: ffffffffffff2c3de43133125f000000 (32 chars = 16 bytes) - // liquidityGross: 000000000000d3c21bcecceda1000000 (32 chars = 16 bytes) - // Storage layout: [liquidityNet (upper 128)] [liquidityGross (lower 128)] let tickUpperData0 = "0xffffffffffff2c3de43133125f000000000000000000d3c21bcecceda1000000" // ASSERTION: Verify tick upper data is 32 bytes @@ -546,15 +611,207 @@ transaction( positionSlot3Hex = "\(positionSlot3Hex)\(String.encodeHex(positionSlot3Bytes))" EVM.store(target: poolAddr, slot: positionSlot3Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") - // Fund pool with massive token balances - let hugeBalance = "0x000000000000000000000000af298d050e4395d69670b12b7f41000000000000" + // Fund pool with balanced token amounts (1 billion logical tokens for each) + // Need to account for decimal differences between tokens + + // Calculate 1 billion tokens in each token's decimal format + // 1,000,000,000 * 10^decimals + var token0Balance: UInt256 = 1000000000 + var i: UInt8 = 0 + while i < token0Decimals { + token0Balance = token0Balance * 10 + i = i + 1 + } + + var token1Balance: UInt256 = 1000000000 + i = 0 + while i < token1Decimals { + token1Balance = token1Balance * 10 + i = i + 1 + } + + log("Setting pool balances: token0=\(token0Balance.toString()) (\(token0Decimals) decimals), token1=\(token1Balance.toString()) (\(token1Decimals) decimals)") + + // Convert to hex and pad to 32 bytes + let token0BalanceHex = "0x".concat(String.encodeHex(token0Balance.toBigEndianBytes())) + let token1BalanceHex = "0x".concat(String.encodeHex(token1Balance.toBigEndianBytes())) // Set token0 balance let token0BalanceSlotComputed = computeBalanceOfSlot(holderAddress: poolAddress, balanceSlot: token0BalanceSlot) - EVM.store(target: token0, slot: token0BalanceSlotComputed, value: hugeBalance) + EVM.store(target: token0, slot: token0BalanceSlotComputed, value: token0BalanceHex) // Set token1 balance let token1BalanceSlotComputed = computeBalanceOfSlot(holderAddress: poolAddress, balanceSlot: token1BalanceSlot) - EVM.store(target: token1, slot: token1BalanceSlotComputed, value: hugeBalance) + EVM.store(target: token1, slot: token1BalanceSlotComputed, value: token1BalanceHex) } } + +/// Calculate sqrtPriceX96 from a price ratio +/// Returns sqrt(price) * 2^96 as a string for Uniswap V3 pool initialization +access(self) fun calculateSqrtPriceX96(price: UFix64): String { + // Convert UFix64 to UInt256 (UFix64 has 8 decimal places) + // price is stored as integer * 10^8 internally + let priceBytes = price.toBigEndianBytes() + var priceUInt64: UInt64 = 0 + for byte in priceBytes { + priceUInt64 = (priceUInt64 << 8) + UInt64(byte) + } + let priceScaled = UInt256(priceUInt64) // This is price * 10^8 + + // We want: sqrt(price) * 2^96 + // = sqrt(priceScaled / 10^8) * 2^96 + // = sqrt(priceScaled) * 2^96 / sqrt(10^8) + // = sqrt(priceScaled) * 2^96 / 10^4 + + // Calculate sqrt(priceScaled) with scale factor 2^48 for precision + // sqrt(priceScaled) * 2^48 + let sqrtPriceScaled = sqrtUInt256(n: priceScaled, scaleFactor: UInt256(1) << 48) + + // Now we have: sqrt(priceScaled) * 2^48 + // We want: sqrt(priceScaled) * 2^96 / 10^4 + // = (sqrt(priceScaled) * 2^48) * 2^48 / 10^4 + + let sqrtPriceX96 = (sqrtPriceScaled * (UInt256(1) << 48)) / UInt256(10000) + + return sqrtPriceX96.toString() +} + +/// Calculate tick from price ratio +/// Returns tick = floor(log_1.0001(price)) for Uniswap V3 tick spacing +access(self) fun calculateTick(price: UFix64): Int256 { + // Convert UFix64 to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8) + let priceBytes = price.toBigEndianBytes() + var priceUInt64: UInt64 = 0 + for byte in priceBytes { + priceUInt64 = (priceUInt64 << 8) + UInt64(byte) + } + + // priceUInt64 is price * 10^8 + // Scale to 10^18 for precision: price * 10^18 = priceUInt64 * 10^10 + let priceScaled = UInt256(priceUInt64) * UInt256(10000000000) // 10^10 + let scaleFactor = UInt256(1000000000000000000) // 10^18 + + // Calculate ln(price) * 10^18 + let lnPrice = lnUInt256(x: priceScaled, scaleFactor: scaleFactor) + + // ln(1.0001) * 10^18 ≈ 99995000333083 + let ln1_0001 = Int256(99995000333083) + + // tick = ln(price) / ln(1.0001) + // lnPrice is already scaled by 10^18 + // ln1_0001 is already scaled by 10^18 + // So: tick = (lnPrice * 10^18) / (ln1_0001 * 10^18) = lnPrice / ln1_0001 + + let tick = lnPrice / ln1_0001 + + return tick +} + +/// Calculate square root using Newton's method for UInt256 +/// Returns sqrt(n) * scaleFactor to maintain precision +access(self) fun sqrtUInt256(n: UInt256, scaleFactor: UInt256): UInt256 { + if n == UInt256(0) { + return UInt256(0) + } + + // Initial guess: n/2 (scaled) + var x = (n * scaleFactor) / UInt256(2) + var prevX = UInt256(0) + + // Newton's method: x_new = (x + n*scale^2/x) / 2 + // Iterate until convergence (max 50 iterations for safety) + var iterations = 0 + while x != prevX && iterations < 50 { + prevX = x + // x_new = (x + (n * scaleFactor^2) / x) / 2 + let nScaled = n * scaleFactor * scaleFactor + x = (x + nScaled / x) / UInt256(2) + iterations = iterations + 1 + } + + return x +} + +/// Calculate natural logarithm using Taylor series +/// ln(x) for x > 0, returns ln(x) * scaleFactor for precision +access(self) fun lnUInt256(x: UInt256, scaleFactor: UInt256): Int256 { + if x == UInt256(0) { + panic("ln(0) is undefined") + } + + // For better convergence, reduce x to range [0.5, 1.5] using: + // ln(x) = ln(2^n * y) = n*ln(2) + ln(y) where y is in [0.5, 1.5] + + var value = x + var n = 0 + + // Scale down if x > 1.5 * scaleFactor + let threshold = (scaleFactor * UInt256(3)) / UInt256(2) + while value > threshold { + value = value / UInt256(2) + n = n + 1 + } + + // Scale up if x < 0.5 * scaleFactor + let lowerThreshold = scaleFactor / UInt256(2) + while value < lowerThreshold { + value = value * UInt256(2) + n = n - 1 + } + + // Now value is in [0.5*scale, 1.5*scale], compute ln(value/scale) + // Use Taylor series: ln(1+z) = z - z^2/2 + z^3/3 - z^4/4 + ... + // where z = value/scale - 1 + + let z = value > scaleFactor + ? Int256(value - scaleFactor) + : -Int256(scaleFactor - value) + + // Calculate Taylor series terms until convergence + var result = z // First term: z + var term = z + var i = 2 + var prevResult = Int256(0) + + // Calculate terms until convergence (term becomes negligible or result stops changing) + // Max 50 iterations for safety + while i <= 50 && result != prevResult { + prevResult = result + + // term = term * z / scaleFactor + term = (term * z) / Int256(scaleFactor) + + // Add or subtract term/i based on sign + if i % 2 == 0 { + result = result - term / Int256(i) + } else { + result = result + term / Int256(i) + } + i = i + 1 + } + + // Add n * ln(2) * scaleFactor + // ln(2) ≈ 0.693147180559945309417232121458 + // ln(2) * 10^18 ≈ 693147180559945309 + let ln2Scaled = Int256(693147180559945309) + let nScaled = Int256(n) * ln2Scaled + + // Scale to our scaleFactor (assuming scaleFactor is 10^18) + result = result + nScaled + + return result +} + +access(all) fun getTokenDecimals(evmContractAddress: EVM.EVMAddress): UInt8 { + let zeroAddress = EVM.addressFromString("0x0000000000000000000000000000000000000000") + let callResult = EVM.dryCall( + from: zeroAddress, + to: evmContractAddress, + data: EVM.encodeABIWithSignature("decimals()", []), + gasLimit: 100000, + value: EVM.Balance(attoflow: 0) + ) + + assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset decimals failed") + return (EVM.decodeABI(types: [Type()], data: callResult.data)[0] as! UInt8) +} diff --git a/flow.json b/flow.json index 8307b789..2e9be8e1 100644 --- a/flow.json +++ b/flow.json @@ -1,6 +1,6 @@ { "contracts": { - "EVM": { + "MockEVM": { "source": "./cadence/contracts/mocks/EVM.cdc", "aliases": { "emulator": "f8d6e0586b0a20c7", From af63f675bddd74a27202191ccccef19b06055d47 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 20 Feb 2026 11:45:40 -0800 Subject: [PATCH 03/14] tidy comments --- .../transactions/set_erc4626_vault_price.cdc | 15 --------------- .../transactions/set_uniswap_v3_pool_price.cdc | 4 ---- 2 files changed, 19 deletions(-) diff --git a/cadence/tests/transactions/set_erc4626_vault_price.cdc b/cadence/tests/transactions/set_erc4626_vault_price.cdc index a5d10c85..8b96aa0d 100644 --- a/cadence/tests/transactions/set_erc4626_vault_price.cdc +++ b/cadence/tests/transactions/set_erc4626_vault_price.cdc @@ -57,17 +57,12 @@ transaction( // Convert baseAssets to asset decimals and apply multiplier let targetAssets = FlowEVMBridgeUtils.ufix64ToUInt256(value: baseAssets, decimals: assetDecimals) - log("SET_VAULT_PRICE: baseAssets=".concat(baseAssets.toString()) - .concat(", assetDecimals=").concat(assetDecimals.toString()) - .concat(", targetAssets=").concat(targetAssets.toString())) let multiplierBytes = priceMultiplier.toBigEndianBytes() var multiplierUInt64: UInt64 = 0 for byte in multiplierBytes { multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte) } let finalTargetAssets = (targetAssets * UInt256(multiplierUInt64)) / UInt256(100000000) - log("SET_VAULT_PRICE: multiplierUInt64=".concat(multiplierUInt64.toString()) - .concat(", finalTargetAssets=").concat(finalTargetAssets.toString())) // For a 1:1 price (1 share = 1 asset), we need: // totalAssets (in assetDecimals) / totalSupply (vault decimals) = 1 @@ -78,21 +73,13 @@ transaction( let supplyMultiplier = FlowEVMBridgeUtils.pow(base: 10, exponent: decimalDifference) let finalTargetSupply = targetAssets * supplyMultiplier - log("SET_VAULT_PRICE: assetDecimals=".concat(assetDecimals.toString()) - .concat(", finalTargetAssets=").concat(finalTargetAssets.toString()) - .concat(", decimalDifference=").concat(decimalDifference.toString()) - .concat(", supplyMultiplier=").concat(supplyMultiplier.toString()) - .concat(", finalTargetSupply=").concat(finalTargetSupply.toString())) - let supplyValue = "0x".concat(String.encodeHex(finalTargetSupply.toBigEndianBytes())) EVM.store(target: vault, slot: toSlotString(totalSupplySlot), value: supplyValue) - log("SET_VAULT_PRICE: Stored totalSupply at slot ".concat(toSlotString(totalSupplySlot)).concat(" = ").concat(supplyValue)) // Update asset.balanceOf(vault) to finalTargetAssets let vaultBalanceSlot = computeBalanceOfSlot(holderAddress: vaultAddress, balanceSlot: assetBalanceSlot) let targetAssetsValue = "0x".concat(String.encodeHex(finalTargetAssets.toBigEndianBytes())) EVM.store(target: asset, slot: vaultBalanceSlot, value: targetAssetsValue) - log("SET_VAULT_PRICE: Stored asset balance at vault = ".concat(targetAssetsValue)) // Set vault storage slot (lastUpdate, maxRate, totalAssets packed) // For testing, we'll set maxRate to 0 to disable interest rate caps @@ -127,7 +114,5 @@ transaction( let newSlotValue = "0x".concat(String.encodeHex(newSlotBytes)) EVM.store(target: vault, slot: toSlotString(vaultTotalAssetsSlot), value: newSlotValue) - log("SET_VAULT_PRICE: Stored packed slot at ".concat(toSlotString(vaultTotalAssetsSlot)).concat(" = ").concat(newSlotValue)) - log("SET_VAULT_PRICE: COMPLETE - Share price should be 1:1") } } diff --git a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc index dfbd9058..541c8d15 100644 --- a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc +++ b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc @@ -160,8 +160,6 @@ transaction( let tickLower = (-887272 as Int256) / tickSpacing * tickSpacing let tickUpper = (887272 as Int256) / tickSpacing * tickSpacing - log("Tick range: tickLower=\(tickLower), tick=\(targetTickAligned), tickUpper=\(tickUpper)") - // Set slot0 with target price // slot0 packing (from lowest to highest bits): // sqrtPriceX96 (160 bits) @@ -630,8 +628,6 @@ transaction( i = i + 1 } - log("Setting pool balances: token0=\(token0Balance.toString()) (\(token0Decimals) decimals), token1=\(token1Balance.toString()) (\(token1Decimals) decimals)") - // Convert to hex and pad to 32 bytes let token0BalanceHex = "0x".concat(String.encodeHex(token0Balance.toBigEndianBytes())) let token1BalanceHex = "0x".concat(String.encodeHex(token1Balance.toBigEndianBytes())) From bb1752775ce0af97252e97af05cfce19600f97e2 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Sat, 21 Feb 2026 21:12:36 -0800 Subject: [PATCH 04/14] Cleanup test helpers & fix precision --- .../transactions/set_erc4626_vault_price.cdc | 33 ++- .../set_uniswap_v3_pool_price.cdc | 251 ++++++++++-------- 2 files changed, 162 insertions(+), 122 deletions(-) diff --git a/cadence/tests/transactions/set_erc4626_vault_price.cdc b/cadence/tests/transactions/set_erc4626_vault_price.cdc index 8b96aa0d..79e0852f 100644 --- a/cadence/tests/transactions/set_erc4626_vault_price.cdc +++ b/cadence/tests/transactions/set_erc4626_vault_price.cdc @@ -6,7 +6,7 @@ import "FlowEVMBridgeUtils" access(all) fun computeMappingSlot(_ values: [AnyStruct]): String { let encoded = EVM.encodeABI(values) let hashBytes = HashAlgorithm.KECCAK_256.hash(encoded) - return "0x\(String.encodeHex(hashBytes))" + return String.encodeHex(hashBytes) } // Helper: Compute ERC20 balanceOf storage slot @@ -37,11 +37,6 @@ transaction( let vault = EVM.addressFromString(vaultAddress) let asset = EVM.addressFromString(assetAddress) - // Helper to convert UInt256 to hex string for EVM.store - let toSlotString = fun (_ slot: UInt256): String { - return "0x".concat(String.encodeHex(slot.toBigEndianBytes())) - } - // Query asset decimals from the ERC20 contract let zeroAddress = EVM.addressFromString("0x0000000000000000000000000000000000000000") let decimalsCalldata = EVM.encodeABIWithSignature("decimals()", []) @@ -55,6 +50,17 @@ transaction( assert(decimalsResult.status == EVM.Status.successful, message: "Failed to query asset decimals") let assetDecimals = (EVM.decodeABI(types: [Type()], data: decimalsResult.data)[0] as! UInt8) + // Query vault decimals + let vaultDecimalsResult = EVM.dryCall( + from: zeroAddress, + to: vault, + data: decimalsCalldata, + gasLimit: 100000, + value: EVM.Balance(attoflow: 0) + ) + assert(vaultDecimalsResult.status == EVM.Status.successful, message: "Failed to query vault decimals") + let vaultDecimals = (EVM.decodeABI(types: [Type()], data: vaultDecimalsResult.data)[0] as! UInt8) + // Convert baseAssets to asset decimals and apply multiplier let targetAssets = FlowEVMBridgeUtils.ufix64ToUInt256(value: baseAssets, decimals: assetDecimals) let multiplierBytes = priceMultiplier.toBigEndianBytes() @@ -66,19 +72,18 @@ transaction( // For a 1:1 price (1 share = 1 asset), we need: // totalAssets (in assetDecimals) / totalSupply (vault decimals) = 1 - // Morpho vaults use 18 decimals for shares regardless of underlying asset decimals - // So: supply_raw = assets_raw * 10^(18 - assetDecimals) + // So: supply_raw = assets_raw * 10^(vaultDecimals - assetDecimals) // IMPORTANT: Supply should be based on BASE assets, not multiplied assets (to change price per share) - let decimalDifference = UInt8(18) - assetDecimals + let decimalDifference = vaultDecimals - assetDecimals let supplyMultiplier = FlowEVMBridgeUtils.pow(base: 10, exponent: decimalDifference) let finalTargetSupply = targetAssets * supplyMultiplier - let supplyValue = "0x".concat(String.encodeHex(finalTargetSupply.toBigEndianBytes())) - EVM.store(target: vault, slot: toSlotString(totalSupplySlot), value: supplyValue) + let supplyValue = String.encodeHex(finalTargetSupply.toBigEndianBytes()) + EVM.store(target: vault, slot: String.encodeHex(totalSupplySlot.toBigEndianBytes()), value: supplyValue) // Update asset.balanceOf(vault) to finalTargetAssets let vaultBalanceSlot = computeBalanceOfSlot(holderAddress: vaultAddress, balanceSlot: assetBalanceSlot) - let targetAssetsValue = "0x".concat(String.encodeHex(finalTargetAssets.toBigEndianBytes())) + let targetAssetsValue = String.encodeHex(finalTargetAssets.toBigEndianBytes()) EVM.store(target: asset, slot: vaultBalanceSlot, value: targetAssetsValue) // Set vault storage slot (lastUpdate, maxRate, totalAssets packed) @@ -112,7 +117,7 @@ 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 = "0x".concat(String.encodeHex(newSlotBytes)) - EVM.store(target: vault, slot: toSlotString(vaultTotalAssetsSlot), value: newSlotValue) + let newSlotValue = String.encodeHex(newSlotBytes) + EVM.store(target: vault, slot: String.encodeHex(vaultTotalAssetsSlot.toBigEndianBytes()), 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 541c8d15..1627ef3e 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 "0x\(String.encodeHex(hashBytes))" + return String.encodeHex(hashBytes) } // Helper: Compute ERC20 balanceOf storage slot @@ -43,38 +43,17 @@ transaction( let token0BalanceSlot = tokenAAddress < tokenBAddress ? tokenABalanceSlot : tokenBBalanceSlot let token1BalanceSlot = tokenAAddress < tokenBAddress ? tokenBBalanceSlot : tokenABalanceSlot - let poolPrice = tokenAAddress < tokenBAddress ? priceTokenBPerTokenA : 1.0 / priceTokenBPerTokenA + let poolPriceHuman = tokenAAddress < tokenBAddress ? priceTokenBPerTokenA : 1.0 / priceTokenBPerTokenA // Read decimals from EVM let token0Decimals = getTokenDecimals(evmContractAddress: token0) let token1Decimals = getTokenDecimals(evmContractAddress: token1) let decOffset = Int(token1Decimals) - Int(token0Decimals) - // Calculate base price/tick - var targetSqrtPriceX96 = calculateSqrtPriceX96(price: poolPrice) - var targetTick = calculateTick(price: poolPrice) - - // Apply decimal offset if needed - if decOffset != 0 { - // Adjust sqrtPriceX96: multiply/divide by 10^(decOffset/2) - var sqrtPriceU256 = UInt256.fromString(targetSqrtPriceX96)! - let absHalfOffset = decOffset < 0 ? (-decOffset) / 2 : decOffset / 2 - var pow10: UInt256 = 1 - var i = 0 - while i < absHalfOffset { - pow10 = pow10 * 10 - i = i + 1 - } - if decOffset > 0 { - sqrtPriceU256 = sqrtPriceU256 * pow10 - } else { - sqrtPriceU256 = sqrtPriceU256 / pow10 - } - targetSqrtPriceX96 = sqrtPriceU256.toString() - - // Adjust tick: add/subtract decOffset * 23026 (ticks per decimal) - targetTick = targetTick + Int256(decOffset) * 23026 - } + // Calculate tick from decimal-adjusted price, then derive sqrtPriceX96 from tick + // This ensures they satisfy Uniswap's invariant: sqrtPriceX96 = getSqrtRatioAtTick(tick) + let targetTick = calculateTick(price: poolPriceHuman, decimalOffset: decOffset) + var targetSqrtPriceX96 = calculateSqrtPriceX96FromTick(tick: targetTick) // First check if pool already exists var getPoolCalldata = EVM.encodeABIWithSignature( @@ -118,7 +97,7 @@ transaction( poolAddr = (EVM.decodeABI(types: [Type()], data: getPoolResult.data)[0] as! EVM.EVMAddress) // Initialize the pool with the target price - let initPrice = UInt256.fromString(targetSqrtPriceX96)! + let initPrice = targetSqrtPriceX96 calldata = EVM.encodeABIWithSignature( "initialize(uint160)", [initPrice] @@ -155,6 +134,10 @@ transaction( // TODO: Consider passing unrounded tick to slot0 if precision matters let targetTickAligned = (targetTick / tickSpacing) * tickSpacing + // CRITICAL: Recalculate sqrtPriceX96 from the ALIGNED tick to ensure consistency + // After rounding tick to tickSpacing, sqrtPriceX96 must match the aligned tick + targetSqrtPriceX96 = calculateSqrtPriceX96FromTick(tick: targetTickAligned) + // Use FULL RANGE ticks (min/max for Uniswap V3) // This ensures liquidity is available at any price let tickLower = (-887272 as Int256) / tickSpacing * tickSpacing @@ -180,7 +163,7 @@ transaction( // We build the byte array in BIG-ENDIAN order (as it will be stored). // Parse sqrtPriceX96 as UInt256 - let sqrtPriceU256 = UInt256.fromString(targetSqrtPriceX96)! + let sqrtPriceU256 = targetSqrtPriceX96 // Convert tick to 24-bit representation (with two's complement for negative) let tickMask = UInt256(((1 as Int256) << 24) - 1) // 0xFFFFFF @@ -223,21 +206,21 @@ transaction( } slot0Bytes = slot0Bytes.concat(packedBytes) - let slot0Value = "0x\(String.encodeHex(slot0Bytes))" + let slot0Value = String.encodeHex(slot0Bytes) // ASSERTION: Verify slot0 is exactly 32 bytes assert(slot0Bytes.length == 32, message: "slot0 must be exactly 32 bytes") - EVM.store(target: poolAddr, slot: "0x0", value: slot0Value) + EVM.store(target: poolAddr, slot: "0", value: slot0Value) // Verify what we stored by reading it back - let readBack = EVM.load(target: poolAddr, slot: "0x0") - let readBackHex = "0x\(String.encodeHex(readBack))" + let readBack = EVM.load(target: poolAddr, slot: "0") + let readBackHex = String.encodeHex(readBack) // ASSERTION: Verify EVM.store/load round-trip works assert(readBackHex == slot0Value, message: "slot0 read-back mismatch - storage corruption!") assert(readBack.length == 32, message: "slot0 read-back wrong size") - + // Initialize observations[0] (REQUIRED or swaps will revert!) // Observations array structure (slot 8): // Solidity packs from LSB to MSB (right-to-left in big-endian hex): @@ -271,19 +254,19 @@ transaction( assert(obs0Bytes.length == 32, message: "observations[0] must be exactly 32 bytes") assert(obs0Bytes[0] == 1, message: "initialized must be at byte 0 and = 1") - let obs0Value = "0x\(String.encodeHex(obs0Bytes))" - EVM.store(target: poolAddr, slot: "0x8", value: obs0Value) + let obs0Value = String.encodeHex(obs0Bytes) + EVM.store(target: poolAddr, slot: "8", value: obs0Value) // Set feeGrowthGlobal0X128 and feeGrowthGlobal1X128 (CRITICAL for swaps!) - EVM.store(target: poolAddr, slot: "0x1", value: "0x0000000000000000000000000000000000000000000000000000000000000000") - EVM.store(target: poolAddr, slot: "0x2", value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: "1", value: "0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: "2", value: "0000000000000000000000000000000000000000000000000000000000000000") // Set protocolFees (CRITICAL) - EVM.store(target: poolAddr, slot: "0x3", value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: "3", value: "0000000000000000000000000000000000000000000000000000000000000000") // Set massive liquidity - let liquidityValue = "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" - EVM.store(target: poolAddr, slot: "0x4", value: liquidityValue) + let liquidityValue = "00000000000000000000000000000000000000000000d3c21bcecceda1000000" + EVM.store(target: poolAddr, slot: "4", value: liquidityValue) // Initialize boundary ticks with CORRECT storage layout @@ -291,15 +274,15 @@ transaction( let tickLowerSlot = computeMappingSlot([tickLower, 5]) // Slot 0: liquidityGross=1e24 (lower 128 bits), liquidityNet=+1e24 (upper 128 bits) - let tickLowerData0 = "0x000000000000d3c21bcecceda1000000000000000000d3c21bcecceda1000000" + let tickLowerData0 = "000000000000d3c21bcecceda1000000000000000000d3c21bcecceda1000000" // ASSERTION: Verify tick data is 32 bytes - assert(tickLowerData0.length == 66, message: "Tick data must be 0x + 64 hex chars = 66 chars total") + assert(tickLowerData0.length == 64, message: "Tick data must be 64 hex chars = 64 chars total") EVM.store(target: poolAddr, slot: tickLowerSlot, value: tickLowerData0) // Calculate slot offsets by parsing the base slot and adding 1, 2, 3 - let tickLowerSlotBytes = tickLowerSlot.slice(from: 2, upTo: tickLowerSlot.length).decodeHex() + let tickLowerSlotBytes = tickLowerSlot.decodeHex() var tickLowerSlotNum = 0 as UInt256 for byte in tickLowerSlotBytes { tickLowerSlotNum = tickLowerSlotNum * 256 + UInt256(byte) @@ -307,49 +290,49 @@ transaction( // Slot 1: feeGrowthOutside0X128 = 0 let tickLowerSlot1Bytes = (tickLowerSlotNum + 1).toBigEndianBytes() - var tickLowerSlot1Hex = "0x" + var tickLowerSlot1Hex = "" var padCount1 = 32 - tickLowerSlot1Bytes.length while padCount1 > 0 { tickLowerSlot1Hex = "\(tickLowerSlot1Hex)00" padCount1 = padCount1 - 1 } tickLowerSlot1Hex = "\(tickLowerSlot1Hex)\(String.encodeHex(tickLowerSlot1Bytes))" - EVM.store(target: poolAddr, slot: tickLowerSlot1Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: tickLowerSlot1Hex, value: "0000000000000000000000000000000000000000000000000000000000000000") // Slot 2: feeGrowthOutside1X128 = 0 let tickLowerSlot2Bytes = (tickLowerSlotNum + 2).toBigEndianBytes() - var tickLowerSlot2Hex = "0x" + var tickLowerSlot2Hex = "" var padCount2 = 32 - tickLowerSlot2Bytes.length while padCount2 > 0 { tickLowerSlot2Hex = "\(tickLowerSlot2Hex)00" padCount2 = padCount2 - 1 } tickLowerSlot2Hex = "\(tickLowerSlot2Hex)\(String.encodeHex(tickLowerSlot2Bytes))" - EVM.store(target: poolAddr, slot: tickLowerSlot2Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: tickLowerSlot2Hex, value: "0000000000000000000000000000000000000000000000000000000000000000") // Slot 3: tickCumulativeOutside=0, secondsPerLiquidity=0, secondsOutside=0, initialized=true(0x01) let tickLowerSlot3Bytes = (tickLowerSlotNum + 3).toBigEndianBytes() - var tickLowerSlot3Hex = "0x" + var tickLowerSlot3Hex = "" var padCount3 = 32 - tickLowerSlot3Bytes.length while padCount3 > 0 { tickLowerSlot3Hex = "\(tickLowerSlot3Hex)00" padCount3 = padCount3 - 1 } tickLowerSlot3Hex = "\(tickLowerSlot3Hex)\(String.encodeHex(tickLowerSlot3Bytes))" - EVM.store(target: poolAddr, slot: tickLowerSlot3Hex, value: "0x0100000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: tickLowerSlot3Hex, value: "0100000000000000000000000000000000000000000000000000000000000000") // Upper tick (liquidityNet is NEGATIVE for upper tick) let tickUpperSlot = computeMappingSlot([tickUpper, 5]) // Slot 0: liquidityGross=1e24 (lower 128 bits), liquidityNet=-1e24 (upper 128 bits, two's complement) - let tickUpperData0 = "0xffffffffffff2c3de43133125f000000000000000000d3c21bcecceda1000000" + let tickUpperData0 = "ffffffffffff2c3de43133125f000000000000000000d3c21bcecceda1000000" // ASSERTION: Verify tick upper data is 32 bytes - assert(tickUpperData0.length == 66, message: "Tick upper data must be 0x + 64 hex chars = 66 chars total") + assert(tickUpperData0.length == 64, message: "Tick upper data must be 64 hex chars = 64 chars total") EVM.store(target: poolAddr, slot: tickUpperSlot, value: tickUpperData0) - let tickUpperSlotBytes = tickUpperSlot.slice(from: 2, upTo: tickUpperSlot.length).decodeHex() + let tickUpperSlotBytes = tickUpperSlot.decodeHex() var tickUpperSlotNum = 0 as UInt256 for byte in tickUpperSlotBytes { tickUpperSlotNum = tickUpperSlotNum * 256 + UInt256(byte) @@ -357,34 +340,34 @@ transaction( // Slot 1, 2, 3 same as lower let tickUpperSlot1Bytes = (tickUpperSlotNum + 1).toBigEndianBytes() - var tickUpperSlot1Hex = "0x" + var tickUpperSlot1Hex = "" var padCount4 = 32 - tickUpperSlot1Bytes.length while padCount4 > 0 { tickUpperSlot1Hex = "\(tickUpperSlot1Hex)00" padCount4 = padCount4 - 1 } tickUpperSlot1Hex = "\(tickUpperSlot1Hex)\(String.encodeHex(tickUpperSlot1Bytes))" - EVM.store(target: poolAddr, slot: tickUpperSlot1Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: tickUpperSlot1Hex, value: "0000000000000000000000000000000000000000000000000000000000000000") let tickUpperSlot2Bytes = (tickUpperSlotNum + 2).toBigEndianBytes() - var tickUpperSlot2Hex = "0x" + var tickUpperSlot2Hex = "" var padCount5 = 32 - tickUpperSlot2Bytes.length while padCount5 > 0 { tickUpperSlot2Hex = "\(tickUpperSlot2Hex)00" padCount5 = padCount5 - 1 } tickUpperSlot2Hex = "\(tickUpperSlot2Hex)\(String.encodeHex(tickUpperSlot2Bytes))" - EVM.store(target: poolAddr, slot: tickUpperSlot2Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: tickUpperSlot2Hex, value: "0000000000000000000000000000000000000000000000000000000000000000") let tickUpperSlot3Bytes = (tickUpperSlotNum + 3).toBigEndianBytes() - var tickUpperSlot3Hex = "0x" + var tickUpperSlot3Hex = "" var padCount6 = 32 - tickUpperSlot3Bytes.length while padCount6 > 0 { tickUpperSlot3Hex = "\(tickUpperSlot3Hex)00" padCount6 = padCount6 - 1 } tickUpperSlot3Hex = "\(tickUpperSlot3Hex)\(String.encodeHex(tickUpperSlot3Bytes))" - EVM.store(target: poolAddr, slot: tickUpperSlot3Hex, value: "0x0100000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: tickUpperSlot3Hex, value: "0100000000000000000000000000000000000000000000000000000000000000") // Set tick bitmap (CRITICAL for tick crossing!) @@ -408,7 +391,7 @@ transaction( // ASSERTION: Verify bitPos is valid assert(bitPosLower >= 0 && bitPosLower < 256, message: "bitPosLower must be 0-255, got \(bitPosLower.toString())") - var bitmapLowerValue = "0x" + var bitmapLowerValue = "" var byteIdx = 0 while byteIdx < 32 { let byteIndexFromRight = Int(bitPosLower) / 8 @@ -429,7 +412,7 @@ transaction( } // ASSERTION: Verify bitmap value is correct length - assert(bitmapLowerValue.length == 66, message: "bitmap must be 0x + 64 hex chars = 66 chars total") + assert(bitmapLowerValue.length == 64, message: "bitmap must be 64 hex chars = 64 chars total") EVM.store(target: poolAddr, slot: bitmapLowerSlot, value: bitmapLowerValue) @@ -439,7 +422,7 @@ transaction( // ASSERTION: Verify bitPos is valid assert(bitPosUpper >= 0 && bitPosUpper < 256, message: "bitPosUpper must be 0-255, got \(bitPosUpper.toString())") - var bitmapUpperValue = "0x" + var bitmapUpperValue = "" byteIdx = 0 while byteIdx < 32 { let byteIndexFromRight = Int(bitPosUpper) / 8 @@ -460,7 +443,7 @@ transaction( } // ASSERTION: Verify bitmap value is correct length - assert(bitmapUpperValue.length == 66, message: "bitmap must be 0x + 64 hex chars = 66 chars total") + assert(bitmapUpperValue.length == 64, message: "bitmap must be 64 hex chars = 64 chars total") EVM.store(target: poolAddr, slot: bitmapUpperSlot, value: bitmapUpperValue) @@ -545,7 +528,7 @@ transaction( assert(positionKeyData.length == 26, message: "Position key data must be 26 bytes (20 + 3 + 3), got \(positionKeyData.length.toString())") let positionKeyHash = HashAlgorithm.KECCAK_256.hash(positionKeyData) - let positionKeyHex = "0x".concat(String.encodeHex(positionKeyHash)) + let positionKeyHex = String.encodeHex(positionKeyHash) // Now compute storage slot: keccak256(positionKey . slot7) var positionSlotData: [UInt8] = [] @@ -559,13 +542,13 @@ transaction( assert(positionSlotData.length == 64, message: "Position slot data must be 64 bytes (32 key + 32 slot), got \(positionSlotData.length)") let positionSlotHash = HashAlgorithm.KECCAK_256.hash(positionSlotData) - let positionSlot = "0x\(String.encodeHex(positionSlotHash))" + let positionSlot = String.encodeHex(positionSlotHash) // Set position liquidity = 1e24 (matching global liquidity) - let positionLiquidityValue = "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" + let positionLiquidityValue = "00000000000000000000000000000000000000000000d3c21bcecceda1000000" // ASSERTION: Verify position liquidity value is 32 bytes - assert(positionLiquidityValue.length == 66, message: "Position liquidity must be 0x + 64 hex chars = 66 chars total") + assert(positionLiquidityValue.length == 64, message: "Position liquidity must be 64 hex chars = 64 chars total") EVM.store(target: poolAddr, slot: positionSlot, value: positionLiquidityValue) @@ -578,36 +561,36 @@ transaction( // Slot 1: feeGrowthInside0LastX128 = 0 let positionSlot1Bytes = (positionSlotNum + 1).toBigEndianBytes() - var positionSlot1Hex = "0x" + var positionSlot1Hex = "" var posPadCount1 = 32 - positionSlot1Bytes.length while posPadCount1 > 0 { positionSlot1Hex = "\(positionSlot1Hex)00" posPadCount1 = posPadCount1 - 1 } positionSlot1Hex = "\(positionSlot1Hex)\(String.encodeHex(positionSlot1Bytes))" - EVM.store(target: poolAddr, slot: positionSlot1Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: positionSlot1Hex, value: "0000000000000000000000000000000000000000000000000000000000000000") // Slot 2: feeGrowthInside1LastX128 = 0 let positionSlot2Bytes = (positionSlotNum + 2).toBigEndianBytes() - var positionSlot2Hex = "0x" + var positionSlot2Hex = "" var posPadCount2 = 32 - positionSlot2Bytes.length while posPadCount2 > 0 { positionSlot2Hex = "\(positionSlot2Hex)00" posPadCount2 = posPadCount2 - 1 } positionSlot2Hex = "\(positionSlot2Hex)\(String.encodeHex(positionSlot2Bytes))" - EVM.store(target: poolAddr, slot: positionSlot2Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: positionSlot2Hex, value: "0000000000000000000000000000000000000000000000000000000000000000") // Slot 3: tokensOwed0 = 0, tokensOwed1 = 0 let positionSlot3Bytes = (positionSlotNum + 3).toBigEndianBytes() - var positionSlot3Hex = "0x" + var positionSlot3Hex = "" var posPadCount3 = 32 - positionSlot3Bytes.length while posPadCount3 > 0 { positionSlot3Hex = "\(positionSlot3Hex)00" posPadCount3 = posPadCount3 - 1 } positionSlot3Hex = "\(positionSlot3Hex)\(String.encodeHex(positionSlot3Bytes))" - EVM.store(target: poolAddr, slot: positionSlot3Hex, value: "0x0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: positionSlot3Hex, value: "0000000000000000000000000000000000000000000000000000000000000000") // Fund pool with balanced token amounts (1 billion logical tokens for each) // Need to account for decimal differences between tokens @@ -629,8 +612,8 @@ transaction( } // Convert to hex and pad to 32 bytes - let token0BalanceHex = "0x".concat(String.encodeHex(token0Balance.toBigEndianBytes())) - let token1BalanceHex = "0x".concat(String.encodeHex(token1Balance.toBigEndianBytes())) + let token0BalanceHex = String.encodeHex(token0Balance.toBigEndianBytes()) + let token1BalanceHex = String.encodeHex(token1Balance.toBigEndianBytes()) // Set token0 balance let token0BalanceSlotComputed = computeBalanceOfSlot(holderAddress: poolAddress, balanceSlot: token0BalanceSlot) @@ -642,39 +625,75 @@ transaction( } } -/// Calculate sqrtPriceX96 from a price ratio -/// Returns sqrt(price) * 2^96 as a string for Uniswap V3 pool initialization -access(self) fun calculateSqrtPriceX96(price: UFix64): String { - // Convert UFix64 to UInt256 (UFix64 has 8 decimal places) - // price is stored as integer * 10^8 internally - let priceBytes = price.toBigEndianBytes() - var priceUInt64: UInt64 = 0 - for byte in priceBytes { - priceUInt64 = (priceUInt64 << 8) + UInt64(byte) - } - let priceScaled = UInt256(priceUInt64) // This is price * 10^8 +/// Calculate sqrtPriceX96 from tick using Uniswap V3's formula +/// sqrtPriceX96 = 1.0001^(tick/2) * 2^96 +/// This ensures consistency with Uniswap's tick-to-price conversion +/// NOTE: This is kept for reference but not used - we calculate sqrtPriceX96 directly from price +access(self) fun calculateSqrtPriceX96FromTick(tick: Int256): UInt256 { + // sqrtPriceX96 = 1.0001^(tick/2) * 2^96 + // = exp(tick/2 * ln(1.0001)) * 2^96 + // = exp(tick * ln(sqrt(1.0001))) * 2^96 - // We want: sqrt(price) * 2^96 - // = sqrt(priceScaled / 10^8) * 2^96 - // = sqrt(priceScaled) * 2^96 / sqrt(10^8) - // = sqrt(priceScaled) * 2^96 / 10^4 + // ln(sqrt(1.0001)) = ln(1.0001) / 2 ≈ 0.00009999500033 / 2 ≈ 0.000049997500165 + // ln(sqrt(1.0001)) * 10^18 ≈ 49997500166541 + let lnSqrt1_0001 = Int256(49997500166541) + let scaleFactor = UInt256(1000000000000000000) // 10^18 + + // Calculate tick * ln(sqrt(1.0001)) + let exponent = tick * lnSqrt1_0001 // This is scaled by 10^18 - // Calculate sqrt(priceScaled) with scale factor 2^48 for precision - // sqrt(priceScaled) * 2^48 - let sqrtPriceScaled = sqrtUInt256(n: priceScaled, scaleFactor: UInt256(1) << 48) + // Calculate exp(exponent / 10^18) * scaleFactor using Taylor series + let expValue = expInt256(x: exponent, scaleFactor: scaleFactor) - // Now we have: sqrt(priceScaled) * 2^48 - // We want: sqrt(priceScaled) * 2^96 / 10^4 - // = (sqrt(priceScaled) * 2^48) * 2^48 / 10^4 + // expValue is now exp(tick * ln(sqrt(1.0001))) * 10^18 + // We want: exp(...) * 2^96 + // = (expValue / 10^18) * 2^96 + // = expValue * 2^96 / 10^18 - let sqrtPriceX96 = (sqrtPriceScaled * (UInt256(1) << 48)) / UInt256(10000) + let twoTo96 = (UInt256(1) << 96) + let sqrtPriceX96 = (expValue * twoTo96) / scaleFactor - return sqrtPriceX96.toString() + return sqrtPriceX96 +} + +/// Calculate e^x for Int256 x (can be negative) using Taylor series +/// Returns e^(x/scaleFactor) * scaleFactor +access(self) fun expInt256(x: Int256, scaleFactor: UInt256): UInt256 { + // Handle negative exponents: e^(-x) = 1 / e^x + if x < 0 { + let posExp = expInt256(x: -x, scaleFactor: scaleFactor) + // Return scaleFactor^2 / posExp + return (scaleFactor * scaleFactor) / posExp + } + + // For positive x, use Taylor series: e^x = 1 + x + x^2/2! + x^3/3! + ... + // x is already scaled by scaleFactor + let xU = UInt256(x) + + var sum = scaleFactor // Start with 1 * scaleFactor + var term = scaleFactor // Current term in series + var i = UInt256(1) + + // Calculate up to 50 terms for precision + while i <= 50 && term > 0 { + // term = term * x / (i * scaleFactor) + term = (term * xU) / (i * scaleFactor) + sum = sum + term + i = i + 1 + + // Stop if term becomes negligible + if term < scaleFactor / UInt256(1000000000000) { + break + } + } + + return sum } /// Calculate tick from price ratio /// Returns tick = floor(log_1.0001(price)) for Uniswap V3 tick spacing -access(self) fun calculateTick(price: UFix64): Int256 { +/// decimalOffset: (token1Decimals - token0Decimals) to adjust for raw EVM units +access(self) fun calculateTick(price: UFix64, decimalOffset: Int): Int256 { // Convert UFix64 to UInt256 (UFix64 has 8 decimal places, stored as int * 10^8) let priceBytes = price.toBigEndianBytes() var priceUInt64: UInt64 = 0 @@ -683,21 +702,37 @@ access(self) fun calculateTick(price: UFix64): Int256 { } // priceUInt64 is price * 10^8 - // Scale to 10^18 for precision: price * 10^18 = priceUInt64 * 10^10 - let priceScaled = UInt256(priceUInt64) * UInt256(10000000000) // 10^10 + // + // For decimal offset adjustment: + // - If decOffset > 0: multiply price (token1 has MORE decimals than token0) + // - If decOffset < 0: divide price (token1 has FEWER decimals than token0) + // + // To avoid underflow when dividing, we adjust using logarithm properties + // For example, with decOffset = -12: + // - Raw price = human_price / 10^12 + // - ln(raw_price) = ln(human_price / 10^12) = ln(human_price) - ln(10^12) + // - ln(10^12) = 12 * ln(10) = 12 * 2.302585093... ≈ 27.631021115... + // - ln(10) * 10^18 ≈ 2302585092994045684 (for scale factor 10^18) + + let priceScaled = UInt256(priceUInt64) * UInt256(10000000000) // price * 10^18 let scaleFactor = UInt256(1000000000000000000) // 10^18 - // Calculate ln(price) * 10^18 - let lnPrice = lnUInt256(x: priceScaled, scaleFactor: scaleFactor) + // Calculate ln(price) * 10^18 (without decimal adjustment yet) + var lnPrice = lnUInt256(x: priceScaled, scaleFactor: scaleFactor) + + // Apply decimal offset adjustment to ln(price) + // ln(price * 10^decOffset) = ln(price) + decOffset * ln(10) + if decimalOffset != 0 { + // ln(10) * 10^18 ≈ 2302585092994045684 + let ln10 = Int256(2302585092994045684) + let adjustment = Int256(decimalOffset) * ln10 + lnPrice = lnPrice + adjustment + } // ln(1.0001) * 10^18 ≈ 99995000333083 let ln1_0001 = Int256(99995000333083) - // tick = ln(price) / ln(1.0001) - // lnPrice is already scaled by 10^18 - // ln1_0001 is already scaled by 10^18 - // So: tick = (lnPrice * 10^18) / (ln1_0001 * 10^18) = lnPrice / ln1_0001 - + // tick = ln(adjusted_price) / ln(1.0001) let tick = lnPrice / ln1_0001 return tick From fb8b266b3f8b5766a497b696299d0714dfea7f66 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 23 Feb 2026 10:10:06 -0800 Subject: [PATCH 05/14] Update to newer CLI --- .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 978f123f..f6f31429 100644 --- a/.github/workflows/cadence_tests.yml +++ b/.github/workflows/cadence_tests.yml @@ -28,7 +28,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.0 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 - 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 d16c20d0..25f5f47c 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -28,7 +28,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.0 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 - 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 d74879cd..794db9b4 100644 --- a/.github/workflows/incrementfi_tests.yml +++ b/.github/workflows/incrementfi_tests.yml @@ -18,7 +18,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.0 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 - name: Flow CLI Version run: flow version - name: Update PATH diff --git a/.github/workflows/punchswap.yml b/.github/workflows/punchswap.yml index 0183f7ab..c9cea84f 100644 --- a/.github/workflows/punchswap.yml +++ b/.github/workflows/punchswap.yml @@ -24,7 +24,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.0 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 - 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 d3567e4a..ccf206b1 100644 --- a/.github/workflows/scheduled_rebalance_tests.yml +++ b/.github/workflows/scheduled_rebalance_tests.yml @@ -29,7 +29,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.0 + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v2.14.2-evm-manipulation-poc.1 - name: Flow CLI Version run: flow version - name: Update PATH From dc143d08c117ef3a2338d994637071e0b8d103f3 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 23 Feb 2026 13:06:05 -0800 Subject: [PATCH 06/14] address feedback --- .../set_uniswap_v3_pool_price.cdc | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc index 1627ef3e..147ebd22 100644 --- a/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc +++ b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc @@ -114,7 +114,7 @@ transaction( let poolAddress = poolAddr.toString() - // Read pool parameters (tickSpacing is CRITICAL) + // Read pool parameters (tickSpacing) let tickSpacingCalldata = EVM.encodeABIWithSignature("tickSpacing()", []) let spacingResult = self.coa.dryCall( to: poolAddr, @@ -134,8 +134,7 @@ transaction( // TODO: Consider passing unrounded tick to slot0 if precision matters let targetTickAligned = (targetTick / tickSpacing) * tickSpacing - // CRITICAL: Recalculate sqrtPriceX96 from the ALIGNED tick to ensure consistency - // After rounding tick to tickSpacing, sqrtPriceX96 must match the aligned tick + // Recalculate sqrtPriceX96 from the aligned tick so it matches slot0 targetSqrtPriceX96 = calculateSqrtPriceX96FromTick(tick: targetTickAligned) // Use FULL RANGE ticks (min/max for Uniswap V3) @@ -257,18 +256,18 @@ transaction( let obs0Value = String.encodeHex(obs0Bytes) EVM.store(target: poolAddr, slot: "8", value: obs0Value) - // Set feeGrowthGlobal0X128 and feeGrowthGlobal1X128 (CRITICAL for swaps!) + // Set feeGrowthGlobal0X128 and feeGrowthGlobal1X128 EVM.store(target: poolAddr, slot: "1", value: "0000000000000000000000000000000000000000000000000000000000000000") EVM.store(target: poolAddr, slot: "2", value: "0000000000000000000000000000000000000000000000000000000000000000") - // Set protocolFees (CRITICAL) + // protocolFees (slot 3): collected fees only; swap fee rate unchanged, fees still charged on swaps EVM.store(target: poolAddr, slot: "3", value: "0000000000000000000000000000000000000000000000000000000000000000") // Set massive liquidity let liquidityValue = "00000000000000000000000000000000000000000000d3c21bcecceda1000000" EVM.store(target: poolAddr, slot: "4", value: liquidityValue) - // Initialize boundary ticks with CORRECT storage layout + // Initialize boundary ticks (storage layout below) // Lower tick let tickLowerSlot = computeMappingSlot([tickLower, 5]) @@ -369,7 +368,7 @@ transaction( tickUpperSlot3Hex = "\(tickUpperSlot3Hex)\(String.encodeHex(tickUpperSlot3Bytes))" EVM.store(target: poolAddr, slot: tickUpperSlot3Hex, value: "0100000000000000000000000000000000000000000000000000000000000000") - // Set tick bitmap (CRITICAL for tick crossing!) + // Set tick bitmap let compressedLower = tickLower / tickSpacing let wordPosLower = compressedLower / 256 @@ -447,7 +446,7 @@ transaction( EVM.store(target: poolAddr, slot: bitmapUpperSlot, value: bitmapUpperValue) - // CREATE POSITION (CRITICAL) + // Create position var positionKeyData: [UInt8] = [] @@ -626,9 +625,7 @@ transaction( } /// Calculate sqrtPriceX96 from tick using Uniswap V3's formula -/// sqrtPriceX96 = 1.0001^(tick/2) * 2^96 -/// This ensures consistency with Uniswap's tick-to-price conversion -/// NOTE: This is kept for reference but not used - we calculate sqrtPriceX96 directly from price +/// sqrtPriceX96 = 1.0001^(tick/2) * 2^96; used for the aligned tick so slot0 is consistent. access(self) fun calculateSqrtPriceX96FromTick(tick: Int256): UInt256 { // sqrtPriceX96 = 1.0001^(tick/2) * 2^96 // = exp(tick/2 * ln(1.0001)) * 2^96 From 5939f17e2b8a4a568e767a3669d123fee30d608e Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 23 Feb 2026 16:58:28 -0800 Subject: [PATCH 07/14] Add test for EVM state helpers --- cadence/tests/evm_state_helpers_test.cdc | 148 +++++++++++ .../scripts/get_bridged_vault_balance.cdc | 24 ++ cadence/tests/test_helpers.cdc | 233 +++++++++++++----- .../transactions/deposit_flow_to_coa.cdc | 16 ++ .../transactions/execute_morpho_deposit.cdc | 72 ++++++ .../tests/transactions/execute_univ3_swap.cdc | 90 +++++++ .../transactions/set_coa_token_balance.cdc | 61 +++++ flow.json | 1 + 8 files changed, 579 insertions(+), 66 deletions(-) create mode 100644 cadence/tests/evm_state_helpers_test.cdc create mode 100644 cadence/tests/scripts/get_bridged_vault_balance.cdc create mode 100644 cadence/tests/transactions/deposit_flow_to_coa.cdc create mode 100644 cadence/tests/transactions/execute_morpho_deposit.cdc create mode 100644 cadence/tests/transactions/execute_univ3_swap.cdc create mode 100644 cadence/tests/transactions/set_coa_token_balance.cdc diff --git a/cadence/tests/evm_state_helpers_test.cdc b/cadence/tests/evm_state_helpers_test.cdc new file mode 100644 index 00000000..fc5e6851 --- /dev/null +++ b/cadence/tests/evm_state_helpers_test.cdc @@ -0,0 +1,148 @@ +// Tests that EVM state helpers correctly set Uniswap V3 pool price and ERC4626 vault price, +// verified by executing a swap (UniV3) and a deposit (ERC4626) using the same fork/setup as scenario3c. +#test_fork(network: "mainnet-fork", height: 142251136) + +import Test +import BlockchainHelpers + +import "test_helpers.cdc" +import "evm_state_helpers.cdc" + +import "FlowToken" + +// Mainnet addresses (same as forked_rebalance_scenario3c_test.cdc) +access(all) let whaleFlowAccount = Test.getAccount(0x92674150c9213fc9) +access(all) let coaOwnerAccount = Test.getAccount(0xe467b9dd11fa00df) + +access(all) let factoryAddress = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" +access(all) let routerAddress = "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" +access(all) let quoterAddress = "0x370A8DF17742867a44e56223EC20D82092242C85" + +access(all) let morphoVaultAddress = "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D" +access(all) let pyusd0Address = "0x99aF3EeA856556646C98c8B9b2548Fe815240750" +access(all) let wflowAddress = "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" + +access(all) let pyusd0BalanceSlot = 1 as UInt256 +access(all) let fusdevBalanceSlot = 12 as UInt256 +access(all) let wflowBalanceSlot = 3 as UInt256 +access(all) let morphoVaultTotalSupplySlot = 11 as UInt256 +access(all) let morphoVaultTotalAssetsSlot = 15 as UInt256 + +// Bridged vault type identifiers (service account prefix may vary; use deployment) +access(all) let pyusd0VaultTypeId = "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" +access(all) let fusdevVaultTypeId = "A.1e4aa0b87d10b141.EVMVMBridgedToken_d069d989e2f44b70c65347d1853c0c67e10a9f8d.Vault" + +access(all) +fun setup() { + deployContractsForFork() + transferFlow(signer: whaleFlowAccount, recipient: coaOwnerAccount.address, amount: 1000.0) + + // Deposit FLOW to COA to cover bridge/gas fees for swaps (scheduled txs can consume some) + let depositFlowRes = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/deposit_flow_to_coa.cdc"), + authorizers: [coaOwnerAccount.address], + signers: [coaOwnerAccount], + arguments: [5.0] + ) + ) + Test.expect(depositFlowRes, Test.beSucceeded()) +} + +access(all) let univ3PoolFee: UInt64 = 3000 + +access(all) +fun test_UniswapV3PriceSetAndSwap() { + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: wflowAddress, + tokenBAddress: pyusd0Address, + fee: univ3PoolFee, + priceTokenBPerTokenA: 2.0, + tokenABalanceSlot: wflowBalanceSlot, + tokenBBalanceSlot: pyusd0BalanceSlot, + signer: coaOwnerAccount + ) + + // Set COA WFLOW balance to 100.0 for the swap + let flowAmount = 100.0 + let setBalanceRes = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/set_coa_token_balance.cdc"), + authorizers: [coaOwnerAccount.address], + signers: [coaOwnerAccount], + arguments: [wflowAddress, wflowBalanceSlot, flowAmount] + ) + ) + Test.expect(setBalanceRes, Test.beSucceeded()) + + let swapRes = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/execute_univ3_swap.cdc"), + authorizers: [coaOwnerAccount.address], + signers: [coaOwnerAccount], + arguments: [factoryAddress, routerAddress, quoterAddress, wflowAddress, pyusd0Address, univ3PoolFee, flowAmount] + ) + ) + Test.expect(swapRes, Test.beSucceeded()) + + let balanceRes = Test.executeScript( + Test.readFile("scripts/get_bridged_vault_balance.cdc"), + [coaOwnerAccount.address, pyusd0VaultTypeId] + ) + Test.expect(balanceRes, Test.beSucceeded()) + let pyusd0Balance = (balanceRes.returnValue as? UFix64) ?? 0.0 + let expectedOut = 2.0 * flowAmount + Test.assert( + pyusd0Balance >= expectedOut * (1.0 - forkedPercentTolerance), + message: "Expected PYUSD0 balance >= ".concat((expectedOut * (1.0 - forkedPercentTolerance)).toString()).concat(" after swap (price 2.0, 0.01% fee), got ").concat(pyusd0Balance.toString()) + ) +} + +access(all) +fun test_ERC4626PriceSetAndDeposit() { + setVaultSharePrice( + vaultAddress: morphoVaultAddress, + assetAddress: pyusd0Address, + assetBalanceSlot: pyusd0BalanceSlot, + totalSupplySlot: morphoVaultTotalSupplySlot, + vaultTotalAssetsSlot: morphoVaultTotalAssetsSlot, + baseAssets: 1000000000.0, + priceMultiplier: 2.0, + signer: coaOwnerAccount + ) + + // Set COA PYUSD0 balance to 1000000000.0 for the deposit + let fundRes = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/set_coa_token_balance.cdc"), + authorizers: [coaOwnerAccount.address], + signers: [coaOwnerAccount], + arguments: [pyusd0Address, pyusd0BalanceSlot, 1000000000.0] + ) + ) + Test.expect(fundRes, Test.beSucceeded()) + + let amountIn = 1.0 + let depositRes = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/execute_morpho_deposit.cdc"), + authorizers: [coaOwnerAccount.address], + signers: [coaOwnerAccount], + arguments: [pyusd0VaultTypeId, morphoVaultAddress, amountIn] + ) + ) + Test.expect(depositRes, Test.beSucceeded()) + + let balanceRes = Test.executeScript( + Test.readFile("scripts/get_bridged_vault_balance.cdc"), + [coaOwnerAccount.address, fusdevVaultTypeId] + ) + Test.expect(balanceRes, Test.beSucceeded()) + let fusdevBalance = (balanceRes.returnValue as? UFix64) ?? 0.0 + let expectedShares = 0.5 + Test.assert( + fusdevBalance >= expectedShares * (1.0 - forkedPercentTolerance), + message: "Expected FUSDEV shares >= ".concat((expectedShares * (1.0 - forkedPercentTolerance)).toString()).concat(" after deposit, got ").concat(fusdevBalance.toString()) + ) +} diff --git a/cadence/tests/scripts/get_bridged_vault_balance.cdc b/cadence/tests/scripts/get_bridged_vault_balance.cdc new file mode 100644 index 00000000..418ae548 --- /dev/null +++ b/cadence/tests/scripts/get_bridged_vault_balance.cdc @@ -0,0 +1,24 @@ +// Returns the balance of a bridged token vault for an account. +// vaultTypeIdentifier: full type identifier e.g. "A.xxx.EVMVMBridgedToken_d069d989e2f44b70c65347d1853c0c67e10a9f8d.Vault" +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" +import "FlowEVMBridgeUtils" + +access(all) +fun main(address: Address, vaultTypeIdentifier: String): UFix64? { + let vaultType = CompositeType(vaultTypeIdentifier) + ?? panic("Invalid vault type identifier: \(vaultTypeIdentifier)") + let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: vaultType) + ?? panic("No contract address for type") + let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: vaultType) + ?? panic("No contract name for type") + let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName) + ?? panic("No ViewResolver for token contract") + let vaultData = viewResolver.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("No FTVaultData for type") + return getAccount(address).capabilities.borrow<&{FungibleToken.Vault}>(vaultData.receiverPath)?.balance ?? nil +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 87aedce0..dd713133 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -8,7 +8,35 @@ import "FlowALPv0" access(all) let serviceAccount = Test.serviceAccount() +access(all) struct DeploymentConfig { + access(all) let uniswapFactoryAddress: String + access(all) let uniswapRouterAddress: String + access(all) let uniswapQuoterAddress: String + access(all) let pyusd0Address: String + access(all) let morphoVaultAddress: String + access(all) let wflowAddress: String + + init( + uniswapFactoryAddress: String, + uniswapRouterAddress: String, + uniswapQuoterAddress: String, + pyusd0Address: String, + morphoVaultAddress: String, + wflowAddress: String + ) { + self.uniswapFactoryAddress = uniswapFactoryAddress + self.uniswapRouterAddress = uniswapRouterAddress + self.uniswapQuoterAddress = uniswapQuoterAddress + self.pyusd0Address = pyusd0Address + self.morphoVaultAddress = morphoVaultAddress + self.wflowAddress = wflowAddress + } +} + /* --- Test execution helpers --- */ +// tolerance for forked tests +access(all) +let forkedPercentTolerance = 0.05 access(all) fun _executeScript(_ path: String, _ args: [AnyStruct]): Test.ScriptResult { @@ -144,11 +172,92 @@ fun tempUpsertBridgeTemplateChunks(_ serviceAccount: Test.TestAccount) { // Common test setup function that deploys all required contracts access(all) fun deployContracts() { - + let config = DeploymentConfig( + uniswapFactoryAddress: "0x986Cb42b0557159431d48fE0A40073296414d410", + uniswapRouterAddress: "0x92657b195e22b69E4779BBD09Fa3CD46F0CF8e39", + uniswapQuoterAddress: "0x8dd92c8d0C3b304255fF9D98ae59c3385F88360C", + pyusd0Address: "0xaCCF0c4EeD4438Ad31Cd340548f4211a465B6528", + morphoVaultAddress: "0x0000000000000000000000000000000000000000", + wflowAddress: "0x0000000000000000000000000000000000000000" + ) + // TODO: remove this step once the VM bridge templates are updated for test env // see https://github.com/onflow/flow-go/issues/8184 tempUpsertBridgeTemplateChunks(serviceAccount) + + _deploy(config: config) + + // FlowYieldVaultsStrategies V1 (emulator-only, incompatible with mainnet FlowCreditMarket) + var err = Test.deployContract( + name: "FlowYieldVaultsStrategies", + path: "../contracts/FlowYieldVaultsStrategies.cdc", + arguments: [ + config.uniswapFactoryAddress, + config.uniswapRouterAddress, + config.uniswapQuoterAddress, + config.pyusd0Address, + [] as [String], + [] as [UInt32] + ] + ) + Test.expect(err, Test.beNil()) + + // MOET onboarding (emulator-only, already onboarded on mainnet) + let onboarder = Test.createAccount() + transferFlow(signer: serviceAccount, recipient: onboarder.address, amount: 100.0) + let onboardMoet = _executeTransaction( + "../../lib/flow-evm-bridge/cadence/transactions/bridge/onboarding/onboard_by_type.cdc", + [Type<@MOET.Vault>()], + onboarder + ) + Test.expect(onboardMoet, Test.beSucceeded()) + + // MockStrategy (emulator-only) + err = Test.deployContract( + name: "MockStrategy", + path: "../contracts/mocks/MockStrategy.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Emulator-specific setup (already exists on mainnet fork) + let wflowAddress = getEVMAddressAssociated(withType: Type<@FlowToken.Vault>().identifier) + ?? panic("Failed to get WFLOW address via VM Bridge association with FlowToken.Vault") + setupBetaAccess() + setupPunchswap(deployer: serviceAccount, wflowAddress: wflowAddress) +} +access(all) fun deployContractsForFork() { + let config = DeploymentConfig( + uniswapFactoryAddress: "0xca6d7Bb03334bBf135902e1d919a5feccb461632", + uniswapRouterAddress: "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", + uniswapQuoterAddress: "0x370A8DF17742867a44e56223EC20D82092242C85", + pyusd0Address: "0x99aF3EeA856556646C98c8B9b2548Fe815240750", + morphoVaultAddress: "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D", + wflowAddress: "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" + ) + + // Deploy EVM mock + var err = Test.deployContract(name: "EVM", path: "../contracts/mocks/EVM.cdc", arguments: []) + + _deploy(config: config) + + // Deploy Morpho connectors (mainnet-only, depend on real EVM contracts) + err = Test.deployContract( + name: "MorphoERC4626SinkConnectors", + path: "../../lib/FlowCreditMarket/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SinkConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "MorphoERC4626SwapConnectors", + path: "../../lib/FlowCreditMarket/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) +} + +access(self) fun _deploy(config: DeploymentConfig) { // DeFiActions contracts var err = Test.deployContract( name: "DeFiActionsUtils", @@ -161,6 +270,7 @@ access(all) fun deployContracts() { path: "../../lib/FlowALP/cadence/lib/FlowALPMath.cdc", arguments: [] ) + Test.expect(err, Test.beNil()) err = Test.deployContract( name: "DeFiActions", path: "../../lib/FlowALP/FlowActions/cadence/contracts/interfaces/DeFiActions.cdc", @@ -179,6 +289,12 @@ access(all) fun deployContracts() { arguments: [] ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "UniswapV3SwapConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // FlowALPv0 contracts let initialMoetSupply = 0.0 @@ -268,12 +384,7 @@ access(all) fun deployContracts() { arguments: [] ) Test.expect(err, Test.beNil()) - err = Test.deployContract( - name: "UniswapV3SwapConnectors", - path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) + err = Test.deployContract( name: "ERC4626Utils", @@ -324,36 +435,6 @@ access(all) fun deployContracts() { ) Test.expect(err, Test.beNil()) - let onboarder = Test.createAccount() - transferFlow(signer: serviceAccount, recipient: onboarder.address, amount: 100.0) - let onboardMoet = _executeTransaction( - "../../lib/flow-evm-bridge/cadence/transactions/bridge/onboarding/onboard_by_type.cdc", - [Type<@MOET.Vault>()], - onboarder - ) - Test.expect(onboardMoet, Test.beSucceeded()) - - err = Test.deployContract( - name: "MockStrategies", - path: "../contracts/mocks/MockStrategies.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "FlowYieldVaultsStrategiesV2", - path: "../contracts/FlowYieldVaultsStrategiesV2.cdc", - arguments: [ - "0x986Cb42b0557159431d48fE0A40073296414d410", - "0x92657b195e22b69E4779BBD09Fa3CD46F0CF8e39", - "0x8dd92c8d0C3b304255fF9D98ae59c3385F88360C" - ] - ) - - Test.expect(err, Test.beNil()) - - // Deploy Morpho contracts (latest local code) to the forked environment - log("Deploying Morpho contracts...") err = Test.deployContract( name: "ERC4626Utils", path: "../../lib/FlowALP/FlowActions/cadence/contracts/utils/ERC4626Utils.cdc", @@ -369,16 +450,13 @@ access(all) fun deployContracts() { Test.expect(err, Test.beNil()) err = Test.deployContract( - name: "MorphoERC4626SinkConnectors", - path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SinkConnectors.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "MorphoERC4626SwapConnectors", - path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SwapConnectors.cdc", - arguments: [] + name: "FlowYieldVaultsStrategiesV2", + path: "../contracts/FlowYieldVaultsStrategiesV2.cdc", + arguments: [ + config.uniswapFactoryAddress, + config.uniswapRouterAddress, + config.uniswapQuoterAddress + ] ) Test.expect(err, Test.beNil()) @@ -387,27 +465,12 @@ access(all) fun deployContracts() { name: "PMStrategiesV1", path: "../contracts/PMStrategiesV1.cdc", arguments: [ - "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000" + config.uniswapRouterAddress, + config.uniswapQuoterAddress, + config.pyusd0Address ] ) - Test.expect(err, Test.beNil()) - - // Mocked Strategy - err = Test.deployContract( - name: "MockStrategy", - path: "../contracts/mocks/MockStrategy.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - let wflowAddress = getEVMAddressAssociated(withType: Type<@FlowToken.Vault>().identifier) - ?? panic("Failed to get WFLOW address via VM Bridge association with FlowToken.Vault") - - setupBetaAccess() - setupPunchswap(deployer: serviceAccount, wflowAddress: wflowAddress) } access(all) @@ -501,6 +564,9 @@ fun createAndStorePool(signer: Test.TestAccount, defaultTokenIdentifier: String, [defaultTokenIdentifier], signer ) + if createRes.error != nil { + log("createAndStorePool error: ".concat(createRes.error!.message)) + } Test.expect(createRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } @@ -648,6 +714,41 @@ fun equalAmounts(a: UFix64, b: UFix64, tolerance: UFix64): Bool { return b - a <= tolerance } +/// Sets a single BandOracle price +/// +access(all) +fun setBandOraclePrice(signer: Test.TestAccount, symbol: String, price: UFix64) { + // BandOracle uses 1e9 multiplier for prices + // e.g., $1.00 = 1_000_000_000, $0.50 = 500_000_000 + let priceAsUInt64 = UInt64(price * 1_000_000_000.0) + let symbolsRates: {String: UInt64} = { symbol: priceAsUInt64 } + + let setRes = _executeTransaction( + "../../lib/FlowCreditMarket/FlowActions/cadence/tests/transactions/band-oracle/update_data.cdc", + [ symbolsRates ], + signer + ) + Test.expect(setRes, Test.beSucceeded()) +} + +/// Sets multiple BandOracle prices at once +/// +access(all) +fun setBandOraclePrices(signer: Test.TestAccount, symbolPrices: {String: UFix64}) { + let symbolsRates: {String: UInt64} = {} + for symbol in symbolPrices.keys { + let price = symbolPrices[symbol]! + symbolsRates[symbol] = UInt64(price * 1_000_000_000.0) + } + + let setRes = _executeTransaction( + "../../lib/FlowCreditMarket/FlowActions/cadence/tests/transactions/band-oracle/update_data.cdc", + [ symbolsRates ], + signer + ) + Test.expect(setRes, Test.beSucceeded()) +} + /* --- Formatting helpers --- */ access(all) fun formatValue(_ value: UFix64): String { @@ -933,4 +1034,4 @@ fun setupPunchswap(deployer: Test.TestAccount, wflowAddress: String): {String: S swapRouter02Address: swapRouter02Address, punchswapV3FactoryAddress: punchswapV3FactoryAddress } -} +} \ No newline at end of file diff --git a/cadence/tests/transactions/deposit_flow_to_coa.cdc b/cadence/tests/transactions/deposit_flow_to_coa.cdc new file mode 100644 index 00000000..1534312a --- /dev/null +++ b/cadence/tests/transactions/deposit_flow_to_coa.cdc @@ -0,0 +1,16 @@ +// Deposits FLOW from signer's FlowToken vault to the signer's COA (native EVM balance). +// Use before swaps/bridges that need the COA to pay gas or bridge fees. +import "FungibleToken" +import "FlowToken" +import "EVM" + +transaction(amount: UFix64) { + prepare(signer: auth(Storage, BorrowValue) &Account) { + let coa = signer.storage.borrow(from: /storage/evm) + ?? panic("No COA at /storage/evm") + let flowVault = signer.storage.borrow(from: /storage/flowTokenVault) + ?? panic("No FlowToken vault") + let deposit <- flowVault.withdraw(amount: amount) as! @FlowToken.Vault + coa.deposit(from: <-deposit) + } +} diff --git a/cadence/tests/transactions/execute_morpho_deposit.cdc b/cadence/tests/transactions/execute_morpho_deposit.cdc new file mode 100644 index 00000000..b6f673fd --- /dev/null +++ b/cadence/tests/transactions/execute_morpho_deposit.cdc @@ -0,0 +1,72 @@ +// Morpho ERC4626 deposit: asset -> vault shares using MorphoERC4626SwapConnectors. +// Signer must have COA, FlowToken vault (for bridge fees), asset vault with balance, and shares vault (created if missing). +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "MetadataViews" +import "FlowToken" +import "EVM" +import "FlowEVMBridgeConfig" +import "DeFiActions" +import "FungibleTokenConnectors" +import "MorphoERC4626SwapConnectors" + +transaction( + assetVaultIdentifier: String, + erc4626VaultEVMAddressHex: String, + amountIn: UFix64 +) { + prepare(signer: auth(Storage, Capabilities, BorrowValue) &Account) { + let erc4626VaultEVMAddress = EVM.addressFromString(erc4626VaultEVMAddressHex) + let sharesType = FlowEVMBridgeConfig.getTypeAssociated(with: erc4626VaultEVMAddress) + ?? panic("ERC4626 vault not associated with a Cadence type") + + let assetVaultData = MetadataViews.resolveContractViewFromTypeIdentifier( + resourceTypeIdentifier: assetVaultIdentifier, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not resolve FTVaultData for asset") + let sharesVaultData = MetadataViews.resolveContractViewFromTypeIdentifier( + resourceTypeIdentifier: sharesType.identifier, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not resolve FTVaultData for shares") + + if signer.storage.borrow<&{FungibleToken.Vault}>(from: sharesVaultData.storagePath) == nil { + signer.storage.save(<-sharesVaultData.createEmptyVault(), to: sharesVaultData.storagePath) + signer.capabilities.unpublish(sharesVaultData.receiverPath) + signer.capabilities.unpublish(sharesVaultData.metadataPath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(sharesVaultData.storagePath) + signer.capabilities.publish(receiverCap, at: sharesVaultData.receiverPath) + signer.capabilities.publish(receiverCap, at: sharesVaultData.metadataPath) + } + + let coa = signer.capabilities.storage.issue(/storage/evm) + let feeVault = signer.capabilities.storage.issue(/storage/flowTokenVault) + let feeSource = FungibleTokenConnectors.VaultSinkAndSource( + min: nil, + max: nil, + vault: feeVault, + uniqueID: nil + ) + + let swapper = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: erc4626VaultEVMAddress, + coa: coa, + feeSource: feeSource, + uniqueID: nil, + isReversed: false + ) + + let assetVault = signer.storage.borrow(from: assetVaultData.storagePath) + ?? panic("Missing asset vault") + let sharesVault = signer.storage.borrow<&{FungibleToken.Vault}>(from: sharesVaultData.storagePath) + ?? panic("Missing shares vault") + + let inVault <- assetVault.withdraw(amount: amountIn) + let quote = swapper.quoteOut(forProvided: amountIn, reverse: false) + let outVault <- swapper.swap(quote: quote, inVault: <-inVault) + sharesVault.deposit(from: <-outVault) + } + + execute {} +} diff --git a/cadence/tests/transactions/execute_univ3_swap.cdc b/cadence/tests/transactions/execute_univ3_swap.cdc new file mode 100644 index 00000000..54be4017 --- /dev/null +++ b/cadence/tests/transactions/execute_univ3_swap.cdc @@ -0,0 +1,90 @@ +// Generic Uniswap V3 swap: inToken -> outToken on COA. +// Pulls in-token from the COA's EVM balance via EVMTokenConnectors.Source (bridge fee from signer's FlowToken vault), +// then swaps inToken -> outToken. Set the COA's in-token balance first (e.g. set_evm_token_balance for WFLOW). +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" +import "FlowToken" +import "EVM" +import "FlowEVMBridgeUtils" +import "FlowEVMBridgeConfig" +import "DeFiActions" +import "FungibleTokenConnectors" +import "EVMTokenConnectors" +import "UniswapV3SwapConnectors" + +transaction( + factoryAddress: String, + routerAddress: String, + quoterAddress: String, + inTokenAddress: String, + outTokenAddress: String, + poolFee: UInt64, + amountIn: UFix64 +) { + let coaCap: Capability + let tokenSource: {DeFiActions.Source} + let outReceiver: &{FungibleToken.Vault} + + prepare(signer: auth(Storage, Capabilities, BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + self.coaCap = signer.capabilities.storage.issue(/storage/evm) + + let inAddr = EVM.addressFromString(inTokenAddress) + let inType = FlowEVMBridgeConfig.getTypeAssociated(with: inAddr)! + let feeVault = signer.capabilities.storage.issue(/storage/flowTokenVault) + self.tokenSource = FungibleTokenConnectors.VaultSinkAndSource( + min: nil, + max: nil, + vault: feeVault, + uniqueID: nil + ) + + let outAddr = EVM.addressFromString(outTokenAddress) + let outType = FlowEVMBridgeConfig.getTypeAssociated(with: outAddr)! + let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: outType)! + let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: outType)! + let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName)! + let vaultData = viewResolver.resolveContractView( + resourceType: outType, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("No FTVaultData for out token") + if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) == nil { + signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) + signer.capabilities.unpublish(vaultData.receiverPath) + signer.capabilities.unpublish(vaultData.metadataPath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + signer.capabilities.publish(receiverCap, at: vaultData.receiverPath) + signer.capabilities.publish(metadataCap, at: vaultData.metadataPath) + } + self.outReceiver = signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath)! + } + + execute { + let inAddr = EVM.addressFromString(inTokenAddress) + let outAddr = EVM.addressFromString(outTokenAddress) + let inType = FlowEVMBridgeConfig.getTypeAssociated(with: inAddr)! + let outType = FlowEVMBridgeConfig.getTypeAssociated(with: outAddr)! + + let inVault <- self.tokenSource.withdrawAvailable(maxAmount: amountIn) + + let factory = EVM.addressFromString(factoryAddress) + let router = EVM.addressFromString(routerAddress) + let quoter = EVM.addressFromString(quoterAddress) + let swapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: factory, + routerAddress: router, + quoterAddress: quoter, + tokenPath: [inAddr, outAddr], + feePath: [UInt32(poolFee)], + inVault: inType, + outVault: outType, + coaCapability: self.coaCap, + uniqueID: nil + ) + let quote = swapper.quoteOut(forProvided: inVault.balance, reverse: false) + let outVault <- swapper.swap(quote: quote, inVault: <-inVault) + self.outReceiver.deposit(from: <-outVault) + } +} diff --git a/cadence/tests/transactions/set_coa_token_balance.cdc b/cadence/tests/transactions/set_coa_token_balance.cdc new file mode 100644 index 00000000..5ebea57d --- /dev/null +++ b/cadence/tests/transactions/set_coa_token_balance.cdc @@ -0,0 +1,61 @@ +// Sets an ERC20 token balance for the signer's COA on EVM (for fork tests). +import EVM from "MockEVM" +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) +} + +access(all) fun computeBalanceOfSlot(holderAddress: String, balanceSlot: UInt256): String { + var addrHex = holderAddress + if holderAddress.slice(from: 0, upTo: 2) == "0x" { + addrHex = holderAddress.slice(from: 2, upTo: holderAddress.length) + } + let addrBytes = addrHex.decodeHex() + let address = EVM.EVMAddress(bytes: addrBytes.toConstantSized<[UInt8; 20]>()!) + return computeMappingSlot([address, balanceSlot]) +} + +transaction( + tokenAddress: String, + balanceSlot: UInt256, + amount: UFix64 +) { + let holderAddressHex: String + + prepare(signer: auth(Storage) &Account) { + let coa = signer.storage.borrow(from: /storage/evm) + ?? panic("No COA at /storage/evm") + self.holderAddressHex = coa.address().toString() + } + + execute { + let token = EVM.addressFromString(tokenAddress) + let zeroAddress = EVM.addressFromString("0x0000000000000000000000000000000000000000") + let decimalsCalldata = EVM.encodeABIWithSignature("decimals()", []) + let decimalsResult = EVM.dryCall( + from: zeroAddress, + to: token, + data: decimalsCalldata, + gasLimit: 100000, + value: EVM.Balance(attoflow: 0) + ) + assert(decimalsResult.status == EVM.Status.successful, message: "Failed to query token decimals") + let decimals = (EVM.decodeABI(types: [Type()], data: decimalsResult.data)[0] as! UInt8) + + let amountRaw = FlowEVMBridgeUtils.ufix64ToUInt256(value: amount, decimals: decimals) + let rawBytes = amountRaw.toBigEndianBytes() + var paddedBytes: [UInt8] = [] + var padCount = 32 - rawBytes.length + while padCount > 0 { + paddedBytes.append(0) + padCount = padCount - 1 + } + paddedBytes = paddedBytes.concat(rawBytes) + let valueHex = String.encodeHex(paddedBytes) + let slotHex = computeBalanceOfSlot(holderAddress: self.holderAddressHex, balanceSlot: balanceSlot) + EVM.store(target: token, slot: slotHex, value: valueHex) + } +} diff --git a/flow.json b/flow.json index 5e8019ed..ca0be98c 100644 --- a/flow.json +++ b/flow.json @@ -230,6 +230,7 @@ "source": "./lib/FlowALP/cadence/contracts/mocks/MockDexSwapper.cdc", "aliases": { "emulator": "045a1763c93006ca", + "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000007" } }, From 4c1df74e51d963645216def3397e2ac768207ce9 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 23 Feb 2026 17:23:03 -0800 Subject: [PATCH 08/14] cleanup test assertion --- cadence/tests/evm_state_helpers_test.cdc | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cadence/tests/evm_state_helpers_test.cdc b/cadence/tests/evm_state_helpers_test.cdc index fc5e6851..cf5370fc 100644 --- a/cadence/tests/evm_state_helpers_test.cdc +++ b/cadence/tests/evm_state_helpers_test.cdc @@ -92,10 +92,11 @@ fun test_UniswapV3PriceSetAndSwap() { ) Test.expect(balanceRes, Test.beSucceeded()) let pyusd0Balance = (balanceRes.returnValue as? UFix64) ?? 0.0 - let expectedOut = 2.0 * flowAmount + let expectedOut = flowAmount * 2.0 + let tolerance = expectedOut * forkedPercentTolerance Test.assert( - pyusd0Balance >= expectedOut * (1.0 - forkedPercentTolerance), - message: "Expected PYUSD0 balance >= ".concat((expectedOut * (1.0 - forkedPercentTolerance)).toString()).concat(" after swap (price 2.0, 0.01% fee), got ").concat(pyusd0Balance.toString()) + equalAmounts(a: pyusd0Balance, b: expectedOut, tolerance: tolerance), + message: "PYUSD0 balance ".concat(pyusd0Balance.toString()).concat(" not within tolerance of ").concat(expectedOut.toString()) ) } @@ -141,8 +142,9 @@ fun test_ERC4626PriceSetAndDeposit() { Test.expect(balanceRes, Test.beSucceeded()) let fusdevBalance = (balanceRes.returnValue as? UFix64) ?? 0.0 let expectedShares = 0.5 + let tolerance = expectedShares * forkedPercentTolerance Test.assert( - fusdevBalance >= expectedShares * (1.0 - forkedPercentTolerance), - message: "Expected FUSDEV shares >= ".concat((expectedShares * (1.0 - forkedPercentTolerance)).toString()).concat(" after deposit, got ").concat(fusdevBalance.toString()) + equalAmounts(a: fusdevBalance, b: expectedShares, tolerance: tolerance), + message: "FUSDEV shares ".concat(fusdevBalance.toString()).concat(" not within tolerance of ").concat(expectedShares.toString()) ) } From d88f6516cf46b062484de37dfcf83a0d531cf56c Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 23 Feb 2026 19:16:12 -0800 Subject: [PATCH 09/14] cleanup test assertion --- cadence/tests/evm_state_helpers_test.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/tests/evm_state_helpers_test.cdc b/cadence/tests/evm_state_helpers_test.cdc index cf5370fc..67deae41 100644 --- a/cadence/tests/evm_state_helpers_test.cdc +++ b/cadence/tests/evm_state_helpers_test.cdc @@ -96,7 +96,7 @@ fun test_UniswapV3PriceSetAndSwap() { let tolerance = expectedOut * forkedPercentTolerance Test.assert( equalAmounts(a: pyusd0Balance, b: expectedOut, tolerance: tolerance), - message: "PYUSD0 balance ".concat(pyusd0Balance.toString()).concat(" not within tolerance of ").concat(expectedOut.toString()) + message: "PYUSD0 balance \(pyusd0Balance.toString()) not within tolerance of \(expectedOut.toString())" ) } @@ -145,6 +145,6 @@ fun test_ERC4626PriceSetAndDeposit() { let tolerance = expectedShares * forkedPercentTolerance Test.assert( equalAmounts(a: fusdevBalance, b: expectedShares, tolerance: tolerance), - message: "FUSDEV shares ".concat(fusdevBalance.toString()).concat(" not within tolerance of ").concat(expectedShares.toString()) + message: "FUSDEV shares \(fusdevBalance.toString()) not within tolerance of \(expectedShares.toString())" ) } From 4f6c322129aa8b1574b53f5557d7da2468644a6c Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 24 Feb 2026 10:08:11 -0800 Subject: [PATCH 10/14] Fix percent tolerance --- cadence/tests/evm_state_helpers_test.cdc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cadence/tests/evm_state_helpers_test.cdc b/cadence/tests/evm_state_helpers_test.cdc index 67deae41..c11da373 100644 --- a/cadence/tests/evm_state_helpers_test.cdc +++ b/cadence/tests/evm_state_helpers_test.cdc @@ -1,5 +1,4 @@ -// Tests that EVM state helpers correctly set Uniswap V3 pool price and ERC4626 vault price, -// verified by executing a swap (UniV3) and a deposit (ERC4626) using the same fork/setup as scenario3c. +// Tests that EVM state helpers correctly set Uniswap V3 pool price and ERC4626 vault price #test_fork(network: "mainnet-fork", height: 142251136) import Test @@ -93,7 +92,7 @@ fun test_UniswapV3PriceSetAndSwap() { Test.expect(balanceRes, Test.beSucceeded()) let pyusd0Balance = (balanceRes.returnValue as? UFix64) ?? 0.0 let expectedOut = flowAmount * 2.0 - let tolerance = expectedOut * forkedPercentTolerance + let tolerance = expectedOut * forkedPercentTolerance * 0.01 Test.assert( equalAmounts(a: pyusd0Balance, b: expectedOut, tolerance: tolerance), message: "PYUSD0 balance \(pyusd0Balance.toString()) not within tolerance of \(expectedOut.toString())" @@ -142,7 +141,7 @@ fun test_ERC4626PriceSetAndDeposit() { Test.expect(balanceRes, Test.beSucceeded()) let fusdevBalance = (balanceRes.returnValue as? UFix64) ?? 0.0 let expectedShares = 0.5 - let tolerance = expectedShares * forkedPercentTolerance + let tolerance = expectedShares * forkedPercentTolerance * 0.01 Test.assert( equalAmounts(a: fusdevBalance, b: expectedShares, tolerance: tolerance), message: "FUSDEV shares \(fusdevBalance.toString()) not within tolerance of \(expectedShares.toString())" From 90919ce2e751a3ceb79079e7f40568d1c15398d2 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 24 Feb 2026 13:00:00 -0800 Subject: [PATCH 11/14] lower fee --- cadence/tests/evm_state_helpers_test.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/tests/evm_state_helpers_test.cdc b/cadence/tests/evm_state_helpers_test.cdc index c11da373..d8648fda 100644 --- a/cadence/tests/evm_state_helpers_test.cdc +++ b/cadence/tests/evm_state_helpers_test.cdc @@ -48,7 +48,7 @@ fun setup() { Test.expect(depositFlowRes, Test.beSucceeded()) } -access(all) let univ3PoolFee: UInt64 = 3000 +access(all) let univ3PoolFee: UInt64 = 100 access(all) fun test_UniswapV3PriceSetAndSwap() { From dfa2f53212e5a19e01ba7ab243c373a4a8197e10 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 24 Feb 2026 14:20:57 -0800 Subject: [PATCH 12/14] Update dependency hashes --- flow.json | 132 +++++++++++++++++++++++++++--------------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/flow.json b/flow.json index ca0be98c..9d4b8079 100644 --- a/flow.json +++ b/flow.json @@ -1,13 +1,5 @@ { "contracts": { - "MockEVM": { - "source": "./cadence/contracts/mocks/EVM.cdc", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "e467b9dd11fa00df", - "testnet": "8c5303eaa26202d6" - } - }, "BandOracleConnectors": { "source": "./lib/FlowALP/FlowActions/cadence/contracts/connectors/band-oracle/BandOracleConnectors.cdc", "aliases": { @@ -186,16 +178,6 @@ "testnet": "d2580caf2ef07c2f" } }, - "MockStrategies": { - "source": "cadence/contracts/mocks/MockStrategies.cdc", - "aliases": { - "emulator": "045a1763c93006ca", - "mainnet": "b1d63873c3cc9f79", - "mainnet-fork": "b1d63873c3cc9f79", - "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" - } - }, "FlowYieldVaultsStrategiesV2": { "source": "cadence/contracts/FlowYieldVaultsStrategiesV2.cdc", "aliases": { @@ -234,6 +216,14 @@ "testing": "0000000000000007" } }, + "MockEVM": { + "source": "./cadence/contracts/mocks/EVM.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, "MockOracle": { "source": "cadence/contracts/mocks/MockOracle.cdc", "aliases": { @@ -244,6 +234,16 @@ "testnet": "d2580caf2ef07c2f" } }, + "MockStrategies": { + "source": "cadence/contracts/mocks/MockStrategies.cdc", + "aliases": { + "emulator": "045a1763c93006ca", + "mainnet": "b1d63873c3cc9f79", + "mainnet-fork": "b1d63873c3cc9f79", + "testing": "0000000000000009", + "testnet": "d2580caf2ef07c2f" + } + }, "MockStrategy": { "source": "cadence/contracts/mocks/MockStrategy.cdc", "aliases": { @@ -327,7 +327,7 @@ "ArrayUtils": { "source": "mainnet://1e4aa0b87d10b141.ArrayUtils", "hash": "e70ddc2f0c7c72158a3f6c68de3a131e1f49e2908ad83eac0308f9e2953957d5", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -338,7 +338,7 @@ "BandOracle": { "source": "mainnet://6801a6222ebf784a.BandOracle", "hash": "ababa195ef50b63d71520022aa2468656a9703b924c0f5228cfaa51a71db094d", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "045a1763c93006ca", "mainnet": "6801a6222ebf784a", @@ -350,7 +350,7 @@ "Burner": { "source": "mainnet://f233dcee88fe0abe.Burner", "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "f233dcee88fe0abe", @@ -361,7 +361,7 @@ "CrossVMMetadataViews": { "source": "mainnet://1d7e57aa55817448.CrossVMMetadataViews", "hash": "7e79b77b87c750de5b126ebd6fca517c2b905ac7f01c0428e9f3f82838c7f524", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -372,7 +372,7 @@ "CrossVMNFT": { "source": "mainnet://1e4aa0b87d10b141.CrossVMNFT", "hash": "8fe69f487164caffedab68b52a584fa7aa4d54a0061f4f211998c73a619fbea5", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -383,7 +383,7 @@ "CrossVMToken": { "source": "mainnet://1e4aa0b87d10b141.CrossVMToken", "hash": "9f055ad902e7de5619a2b0f2dc91826ac9c4f007afcd6df9f5b8229c0ca94531", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -395,7 +395,7 @@ "EVM": { "source": "mainnet://e467b9dd11fa00df.EVM", "hash": "960b0c7df7ee536956af196fba8c8d5dd4f7a89a4ecc61467e31287c4617b0dd", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -406,7 +406,7 @@ "FlowEVMBridge": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridge", "hash": "9cd0f897b19c0394e9042225e5758d6ae529a0cce19b19ae05bde8e0f14aa10b", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -418,7 +418,7 @@ "FlowEVMBridgeAccessor": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeAccessor", "hash": "888ba0aab5e961924c47b819f4a9f410449c39745e0d3eab20738bf10ef2ed0f", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -430,7 +430,7 @@ "FlowEVMBridgeConfig": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeConfig", "hash": "3c09f74467f22dac7bc02b2fdf462213b2f8ddfb513cd890ad0c2a7016507be3", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -442,7 +442,7 @@ "FlowEVMBridgeCustomAssociationTypes": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeCustomAssociationTypes", "hash": "4651183c3f04f8c5faaa35106b3ab66060ce9868590adb33f3be1900c12ea196", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -454,7 +454,7 @@ "FlowEVMBridgeCustomAssociations": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeCustomAssociations", "hash": "14d1f4ddd347f45d331e543830b94701e1aa1513c56d55c0019c7fac46d8a572", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -466,7 +466,7 @@ "FlowEVMBridgeHandlerInterfaces": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeHandlerInterfaces", "hash": "e32154f2a556e53328a0fce75f1e98b57eefd2a8cb626e803b7d39d452691444", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -478,7 +478,7 @@ "FlowEVMBridgeHandlers": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeHandlers", "hash": "7e8adff1dca0ea1d2e361c17de9eca020f82cabc00a52679078752bf85adb004", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -490,7 +490,7 @@ "FlowEVMBridgeNFTEscrow": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeNFTEscrow", "hash": "30257592838edfd4b72700f43bf0326f6903e879f82ac5ca549561d9863c6fe6", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -502,7 +502,7 @@ "FlowEVMBridgeResolver": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeResolver", "hash": "c1ac18e92828616771df5ff5d6de87866f2742ca4ce196601c11e977e4f63bb3", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -514,7 +514,7 @@ "FlowEVMBridgeTemplates": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeTemplates", "hash": "78b8115eb0ef2be4583acbe655f0c5128c39712084ec23ce47820ea154141898", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -526,7 +526,7 @@ "FlowEVMBridgeTokenEscrow": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeTokenEscrow", "hash": "49df9c8e5d0dd45abd5bf94376d3b9045299b3c2a5ba6caf48092c916362358d", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -538,7 +538,7 @@ "FlowEVMBridgeUtils": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeUtils", "hash": "634ed6dde03eb8f027368aa7861889ce1f5099160903493a7a39a86c9afea14b", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -550,7 +550,7 @@ "FlowFees": { "source": "mainnet://f919ee77447b7497.FlowFees", "hash": "341cc0f3cc847d6b787c390133f6a5e6c867c111784f09c5c0083c47f2f1df64", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "e5a8b7f23e8b548f", "mainnet": "f919ee77447b7497", @@ -561,7 +561,7 @@ "FlowStorageFees": { "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", "hash": "a92c26fb2ea59725441fa703aa4cd811e0fc56ac73d649a8e12c1e72b67a8473", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -572,7 +572,7 @@ "FlowToken": { "source": "mainnet://1654653399040a61.FlowToken", "hash": "f82389e2412624ffa439836b00b42e6605b0c00802a4e485bc95b8930a7eac38", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", @@ -583,7 +583,7 @@ "FlowTransactionScheduler": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", "hash": "23157cf7d70534e45b0ab729133232d0ffb3cdae52661df1744747cb1f8c0495", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -594,7 +594,7 @@ "FlowTransactionSchedulerUtils": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionSchedulerUtils", "hash": "71a1febab6b9ba76abec36dab1e61b1c377e44fbe627e5fac649deb71b727877", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -605,7 +605,7 @@ "FungibleToken": { "source": "mainnet://f233dcee88fe0abe.FungibleToken", "hash": "4b74edfe7d7ddfa70b703c14aa731a0b2e7ce016ce54d998bfd861ada4d240f6", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -616,7 +616,7 @@ "FungibleTokenMetadataViews": { "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", "hash": "70477f80fd7678466c224507e9689f68f72a9e697128d5ea54d19961ec856b3c", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -627,7 +627,7 @@ "IBridgePermissions": { "source": "mainnet://1e4aa0b87d10b141.IBridgePermissions", "hash": "431a51a6cca87773596f79832520b19499fe614297eaef347e49383f2ae809af", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -638,7 +638,7 @@ "ICrossVM": { "source": "mainnet://1e4aa0b87d10b141.ICrossVM", "hash": "b95c36eef516da7cd4d2f507cd48288cc16b1d6605ff03b6fcd18161ff2d82e7", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -650,7 +650,7 @@ "ICrossVMAsset": { "source": "mainnet://1e4aa0b87d10b141.ICrossVMAsset", "hash": "d9c7b2bd9fdcc454180c33b3509a5a060a7fe4bd49bce38818f22fd08acb8ba0", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -662,7 +662,7 @@ "IEVMBridgeNFTMinter": { "source": "mainnet://1e4aa0b87d10b141.IEVMBridgeNFTMinter", "hash": "e2ad15c495ad7fbf4ab744bccaf8c4334dfb843b50f09e9681ce9a5067dbf049", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -674,7 +674,7 @@ "IEVMBridgeTokenMinter": { "source": "mainnet://1e4aa0b87d10b141.IEVMBridgeTokenMinter", "hash": "0ef39c6cb476f0eea2c835900b6a5a83c1ed5f4dbaaeb29cb68ad52c355a40e6", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -686,7 +686,7 @@ "IFlowEVMNFTBridge": { "source": "mainnet://1e4aa0b87d10b141.IFlowEVMNFTBridge", "hash": "2d495e896510a10bbc7307739aca9341633cac4c7fe7dad32488a81f90a39dd9", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -698,7 +698,7 @@ "IFlowEVMTokenBridge": { "source": "mainnet://1e4aa0b87d10b141.IFlowEVMTokenBridge", "hash": "87f7d752da8446e73acd3bf4aa17fe5c279d9641b7976c56561af01bc5240ea4", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -710,7 +710,7 @@ "MetadataViews": { "source": "mainnet://1d7e57aa55817448.MetadataViews", "hash": "b290b7906d901882b4b62e596225fb2f10defb5eaaab4a09368f3aee0e9c18b1", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -721,7 +721,7 @@ "NonFungibleToken": { "source": "mainnet://1d7e57aa55817448.NonFungibleToken", "hash": "a258de1abddcdb50afc929e74aca87161d0083588f6abf2b369672e64cf4a403", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -732,7 +732,7 @@ "ScopedFTProviders": { "source": "mainnet://1e4aa0b87d10b141.ScopedFTProviders", "hash": "77213f9588ec9862d07c4706689424ad7c1d8f043d5970d96bf18764bb936fc3", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -744,7 +744,7 @@ "Serialize": { "source": "mainnet://1e4aa0b87d10b141.Serialize", "hash": "064bb0d7b6c24ee1ed370cbbe9e0cda2a4e0955247de5e3e81f2f3a8a8cabfb7", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -755,7 +755,7 @@ "SerializeMetadata": { "source": "mainnet://1e4aa0b87d10b141.SerializeMetadata", "hash": "e9f84ea07e29cae05ee0d9264596eb281c291fc1090a10ce3de1a042b4d671da", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -766,7 +766,7 @@ "StableSwapFactory": { "source": "mainnet://b063c16cac85dbd1.StableSwapFactory", "hash": "a63b57a5cc91085016abc34c1b49622b385a8f976ac2ba0e646f7a3f780d344e", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f3fcd2c1a78f5eee", "mainnet": "b063c16cac85dbd1", @@ -777,7 +777,7 @@ "StringUtils": { "source": "mainnet://1e4aa0b87d10b141.StringUtils", "hash": "28ac1a744ac7fb97253cba007a520a9ec1c2e14458d1bd1add1424fa19282c03", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", @@ -788,7 +788,7 @@ "SwapConfig": { "source": "mainnet://b78ef7afa52ff906.SwapConfig", "hash": "111f3caa0ab506bed100225a1481f77687f6ac8493d97e49f149fa26a174ef99", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f3fcd2c1a78f5eee", "mainnet": "b78ef7afa52ff906", @@ -799,7 +799,7 @@ "SwapError": { "source": "mainnet://b78ef7afa52ff906.SwapError", "hash": "7d13a652a1308af387513e35c08b4f9a7389a927bddf08431687a846e4c67f21", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f3fcd2c1a78f5eee", "mainnet": "b78ef7afa52ff906", @@ -810,7 +810,7 @@ "SwapFactory": { "source": "mainnet://b063c16cac85dbd1.SwapFactory", "hash": "deea03edbb49877c8c72276e1911cf87bdba4052ae9c3ac54c0d4ac62f3ef511", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f3fcd2c1a78f5eee", "mainnet": "b063c16cac85dbd1", @@ -821,7 +821,7 @@ "SwapInterfaces": { "source": "mainnet://b78ef7afa52ff906.SwapInterfaces", "hash": "e559dff4d914fa12fff7ba482f30d3c575dc3d31587833fd628763d1a4ee96b2", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f3fcd2c1a78f5eee", "mainnet": "b78ef7afa52ff906", @@ -832,7 +832,7 @@ "SwapRouter": { "source": "mainnet://a6850776a94e6551.SwapRouter", "hash": "c0365c01978ca32af94602bfddd0796cfe6375e60a05b927b5de539e608baec5", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f3fcd2c1a78f5eee", "mainnet": "a6850776a94e6551", @@ -843,7 +843,7 @@ "USDCFlow": { "source": "mainnet://f1ab99c82dee3526.USDCFlow", "hash": "da7c21064dc73c06499f0b652caea447233465b49787605ce0f679beca48dee7", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "f1ab99c82dee3526", @@ -854,7 +854,7 @@ "ViewResolver": { "source": "mainnet://1d7e57aa55817448.ViewResolver", "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", - "block_height": 141772866, + "block_height": 143307224, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -1242,4 +1242,4 @@ ] } } -} +} \ No newline at end of file From 823400a9f9665879492e770c6ad623f447c8765b Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 24 Feb 2026 16:12:11 -0800 Subject: [PATCH 13/14] Fix local emulator tests --- cadence/tests/test_helpers.cdc | 66 ++++++++++++---------------------- flow.json | 43 ---------------------- 2 files changed, 22 insertions(+), 87 deletions(-) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index dd713133..5f81e389 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -187,32 +187,13 @@ access(all) fun deployContracts() { _deploy(config: config) - // FlowYieldVaultsStrategies V1 (emulator-only, incompatible with mainnet FlowCreditMarket) var err = Test.deployContract( - name: "FlowYieldVaultsStrategies", - path: "../contracts/FlowYieldVaultsStrategies.cdc", - arguments: [ - config.uniswapFactoryAddress, - config.uniswapRouterAddress, - config.uniswapQuoterAddress, - config.pyusd0Address, - [] as [String], - [] as [UInt32] - ] + name: "MockStrategies", + path: "../contracts/mocks/MockStrategies.cdc", + arguments: [] ) Test.expect(err, Test.beNil()) - - // MOET onboarding (emulator-only, already onboarded on mainnet) - let onboarder = Test.createAccount() - transferFlow(signer: serviceAccount, recipient: onboarder.address, amount: 100.0) - let onboardMoet = _executeTransaction( - "../../lib/flow-evm-bridge/cadence/transactions/bridge/onboarding/onboard_by_type.cdc", - [Type<@MOET.Vault>()], - onboarder - ) - Test.expect(onboardMoet, Test.beSucceeded()) - - // MockStrategy (emulator-only) + err = Test.deployContract( name: "MockStrategy", path: "../contracts/mocks/MockStrategy.cdc", @@ -241,20 +222,6 @@ access(all) fun deployContractsForFork() { var err = Test.deployContract(name: "EVM", path: "../contracts/mocks/EVM.cdc", arguments: []) _deploy(config: config) - - // Deploy Morpho connectors (mainnet-only, depend on real EVM contracts) - err = Test.deployContract( - name: "MorphoERC4626SinkConnectors", - path: "../../lib/FlowCreditMarket/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SinkConnectors.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - err = Test.deployContract( - name: "MorphoERC4626SwapConnectors", - path: "../../lib/FlowCreditMarket/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SwapConnectors.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) } access(self) fun _deploy(config: DeploymentConfig) { @@ -289,12 +256,6 @@ access(self) fun _deploy(config: DeploymentConfig) { arguments: [] ) Test.expect(err, Test.beNil()) - err = Test.deployContract( - name: "UniswapV3SwapConnectors", - path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) // FlowALPv0 contracts let initialMoetSupply = 0.0 @@ -384,7 +345,12 @@ access(self) fun _deploy(config: DeploymentConfig) { arguments: [] ) Test.expect(err, Test.beNil()) - + err = Test.deployContract( + name: "UniswapV3SwapConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) err = Test.deployContract( name: "ERC4626Utils", @@ -435,6 +401,18 @@ access(self) fun _deploy(config: DeploymentConfig) { ) Test.expect(err, Test.beNil()) + let moetAddress = getEVMAddressAssociated(withType: Type<@MOET.Vault>().identifier) + if moetAddress == nil { + let onboarder = Test.createAccount() + transferFlow(signer: serviceAccount, recipient: onboarder.address, amount: 100.0) + let onboardMoet = _executeTransaction( + "../../lib/flow-evm-bridge/cadence/transactions/bridge/onboarding/onboard_by_type.cdc", + [Type<@MOET.Vault>()], + onboarder + ) + Test.expect(onboardMoet, Test.beSucceeded()) + } + err = Test.deployContract( name: "ERC4626Utils", path: "../../lib/FlowALP/FlowActions/cadence/contracts/utils/ERC4626Utils.cdc", diff --git a/flow.json b/flow.json index 31d94448..16088d2e 100644 --- a/flow.json +++ b/flow.json @@ -372,7 +372,6 @@ "CrossVMNFT": { "source": "mainnet://1e4aa0b87d10b141.CrossVMNFT", "hash": "8fe69f487164caffedab68b52a584fa7aa4d54a0061f4f211998c73a619fbea5", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -384,7 +383,6 @@ "CrossVMToken": { "source": "mainnet://1e4aa0b87d10b141.CrossVMToken", "hash": "9f055ad902e7de5619a2b0f2dc91826ac9c4f007afcd6df9f5b8229c0ca94531", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -397,7 +395,6 @@ "EVM": { "source": "mainnet://e467b9dd11fa00df.EVM", "hash": "960b0c7df7ee536956af196fba8c8d5dd4f7a89a4ecc61467e31287c4617b0dd", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -409,7 +406,6 @@ "FlowEVMBridge": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridge", "hash": "9cd0f897b19c0394e9042225e5758d6ae529a0cce19b19ae05bde8e0f14aa10b", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -422,7 +418,6 @@ "FlowEVMBridgeAccessor": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeAccessor", "hash": "888ba0aab5e961924c47b819f4a9f410449c39745e0d3eab20738bf10ef2ed0f", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -435,7 +430,6 @@ "FlowEVMBridgeConfig": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeConfig", "hash": "3c09f74467f22dac7bc02b2fdf462213b2f8ddfb513cd890ad0c2a7016507be3", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -448,7 +442,6 @@ "FlowEVMBridgeCustomAssociationTypes": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeCustomAssociationTypes", "hash": "4651183c3f04f8c5faaa35106b3ab66060ce9868590adb33f3be1900c12ea196", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -461,7 +454,6 @@ "FlowEVMBridgeCustomAssociations": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeCustomAssociations", "hash": "14d1f4ddd347f45d331e543830b94701e1aa1513c56d55c0019c7fac46d8a572", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -474,7 +466,6 @@ "FlowEVMBridgeHandlerInterfaces": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeHandlerInterfaces", "hash": "e32154f2a556e53328a0fce75f1e98b57eefd2a8cb626e803b7d39d452691444", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -487,7 +478,6 @@ "FlowEVMBridgeHandlers": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeHandlers", "hash": "7e8adff1dca0ea1d2e361c17de9eca020f82cabc00a52679078752bf85adb004", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -500,7 +490,6 @@ "FlowEVMBridgeNFTEscrow": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeNFTEscrow", "hash": "30257592838edfd4b72700f43bf0326f6903e879f82ac5ca549561d9863c6fe6", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -513,7 +502,6 @@ "FlowEVMBridgeResolver": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeResolver", "hash": "c1ac18e92828616771df5ff5d6de87866f2742ca4ce196601c11e977e4f63bb3", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -526,7 +514,6 @@ "FlowEVMBridgeTemplates": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeTemplates", "hash": "78b8115eb0ef2be4583acbe655f0c5128c39712084ec23ce47820ea154141898", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -539,7 +526,6 @@ "FlowEVMBridgeTokenEscrow": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeTokenEscrow", "hash": "49df9c8e5d0dd45abd5bf94376d3b9045299b3c2a5ba6caf48092c916362358d", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -552,7 +538,6 @@ "FlowEVMBridgeUtils": { "source": "mainnet://1e4aa0b87d10b141.FlowEVMBridgeUtils", "hash": "634ed6dde03eb8f027368aa7861889ce1f5099160903493a7a39a86c9afea14b", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -565,7 +550,6 @@ "FlowFees": { "source": "mainnet://f919ee77447b7497.FlowFees", "hash": "341cc0f3cc847d6b787c390133f6a5e6c867c111784f09c5c0083c47f2f1df64", - "block_height": 143307913, "aliases": { "emulator": "e5a8b7f23e8b548f", @@ -577,7 +561,6 @@ "FlowStorageFees": { "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", "hash": "a92c26fb2ea59725441fa703aa4cd811e0fc56ac73d649a8e12c1e72b67a8473", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -589,7 +572,6 @@ "FlowToken": { "source": "mainnet://1654653399040a61.FlowToken", "hash": "f82389e2412624ffa439836b00b42e6605b0c00802a4e485bc95b8930a7eac38", - "block_height": 143307913, "aliases": { "emulator": "0ae53cb6e3f42a79", @@ -601,7 +583,6 @@ "FlowTransactionScheduler": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", "hash": "23157cf7d70534e45b0ab729133232d0ffb3cdae52661df1744747cb1f8c0495", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -613,7 +594,6 @@ "FlowTransactionSchedulerUtils": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionSchedulerUtils", "hash": "71a1febab6b9ba76abec36dab1e61b1c377e44fbe627e5fac649deb71b727877", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -625,7 +605,6 @@ "FungibleToken": { "source": "mainnet://f233dcee88fe0abe.FungibleToken", "hash": "4b74edfe7d7ddfa70b703c14aa731a0b2e7ce016ce54d998bfd861ada4d240f6", - "block_height": 143307913, "aliases": { "emulator": "ee82856bf20e2aa6", @@ -637,7 +616,6 @@ "FungibleTokenMetadataViews": { "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", "hash": "70477f80fd7678466c224507e9689f68f72a9e697128d5ea54d19961ec856b3c", - "block_height": 143307913, "aliases": { "emulator": "ee82856bf20e2aa6", @@ -649,7 +627,6 @@ "IBridgePermissions": { "source": "mainnet://1e4aa0b87d10b141.IBridgePermissions", "hash": "431a51a6cca87773596f79832520b19499fe614297eaef347e49383f2ae809af", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -661,7 +638,6 @@ "ICrossVM": { "source": "mainnet://1e4aa0b87d10b141.ICrossVM", "hash": "b95c36eef516da7cd4d2f507cd48288cc16b1d6605ff03b6fcd18161ff2d82e7", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -674,7 +650,6 @@ "ICrossVMAsset": { "source": "mainnet://1e4aa0b87d10b141.ICrossVMAsset", "hash": "d9c7b2bd9fdcc454180c33b3509a5a060a7fe4bd49bce38818f22fd08acb8ba0", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -687,7 +662,6 @@ "IEVMBridgeNFTMinter": { "source": "mainnet://1e4aa0b87d10b141.IEVMBridgeNFTMinter", "hash": "e2ad15c495ad7fbf4ab744bccaf8c4334dfb843b50f09e9681ce9a5067dbf049", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -700,7 +674,6 @@ "IEVMBridgeTokenMinter": { "source": "mainnet://1e4aa0b87d10b141.IEVMBridgeTokenMinter", "hash": "0ef39c6cb476f0eea2c835900b6a5a83c1ed5f4dbaaeb29cb68ad52c355a40e6", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -713,7 +686,6 @@ "IFlowEVMNFTBridge": { "source": "mainnet://1e4aa0b87d10b141.IFlowEVMNFTBridge", "hash": "2d495e896510a10bbc7307739aca9341633cac4c7fe7dad32488a81f90a39dd9", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -726,7 +698,6 @@ "IFlowEVMTokenBridge": { "source": "mainnet://1e4aa0b87d10b141.IFlowEVMTokenBridge", "hash": "87f7d752da8446e73acd3bf4aa17fe5c279d9641b7976c56561af01bc5240ea4", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -739,7 +710,6 @@ "MetadataViews": { "source": "mainnet://1d7e57aa55817448.MetadataViews", "hash": "b290b7906d901882b4b62e596225fb2f10defb5eaaab4a09368f3aee0e9c18b1", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -751,7 +721,6 @@ "NonFungibleToken": { "source": "mainnet://1d7e57aa55817448.NonFungibleToken", "hash": "a258de1abddcdb50afc929e74aca87161d0083588f6abf2b369672e64cf4a403", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -763,7 +732,6 @@ "ScopedFTProviders": { "source": "mainnet://1e4aa0b87d10b141.ScopedFTProviders", "hash": "77213f9588ec9862d07c4706689424ad7c1d8f043d5970d96bf18764bb936fc3", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -776,7 +744,6 @@ "Serialize": { "source": "mainnet://1e4aa0b87d10b141.Serialize", "hash": "064bb0d7b6c24ee1ed370cbbe9e0cda2a4e0955247de5e3e81f2f3a8a8cabfb7", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -788,7 +755,6 @@ "SerializeMetadata": { "source": "mainnet://1e4aa0b87d10b141.SerializeMetadata", "hash": "e9f84ea07e29cae05ee0d9264596eb281c291fc1090a10ce3de1a042b4d671da", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -800,7 +766,6 @@ "StableSwapFactory": { "source": "mainnet://b063c16cac85dbd1.StableSwapFactory", "hash": "a63b57a5cc91085016abc34c1b49622b385a8f976ac2ba0e646f7a3f780d344e", - "block_height": 143307913, "aliases": { "emulator": "f3fcd2c1a78f5eee", @@ -812,7 +777,6 @@ "StringUtils": { "source": "mainnet://1e4aa0b87d10b141.StringUtils", "hash": "28ac1a744ac7fb97253cba007a520a9ec1c2e14458d1bd1add1424fa19282c03", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -824,7 +788,6 @@ "SwapConfig": { "source": "mainnet://b78ef7afa52ff906.SwapConfig", "hash": "111f3caa0ab506bed100225a1481f77687f6ac8493d97e49f149fa26a174ef99", - "block_height": 143307913, "aliases": { "emulator": "f3fcd2c1a78f5eee", @@ -836,7 +799,6 @@ "SwapError": { "source": "mainnet://b78ef7afa52ff906.SwapError", "hash": "7d13a652a1308af387513e35c08b4f9a7389a927bddf08431687a846e4c67f21", - "block_height": 143307913, "aliases": { "emulator": "f3fcd2c1a78f5eee", @@ -848,7 +810,6 @@ "SwapFactory": { "source": "mainnet://b063c16cac85dbd1.SwapFactory", "hash": "deea03edbb49877c8c72276e1911cf87bdba4052ae9c3ac54c0d4ac62f3ef511", - "block_height": 143307913, "aliases": { "emulator": "f3fcd2c1a78f5eee", @@ -860,7 +821,6 @@ "SwapInterfaces": { "source": "mainnet://b78ef7afa52ff906.SwapInterfaces", "hash": "e559dff4d914fa12fff7ba482f30d3c575dc3d31587833fd628763d1a4ee96b2", - "block_height": 143307913, "aliases": { "emulator": "f3fcd2c1a78f5eee", @@ -872,7 +832,6 @@ "SwapRouter": { "source": "mainnet://a6850776a94e6551.SwapRouter", "hash": "c0365c01978ca32af94602bfddd0796cfe6375e60a05b927b5de539e608baec5", - "block_height": 143307913, "aliases": { "emulator": "f3fcd2c1a78f5eee", @@ -884,7 +843,6 @@ "USDCFlow": { "source": "mainnet://f1ab99c82dee3526.USDCFlow", "hash": "da7c21064dc73c06499f0b652caea447233465b49787605ce0f679beca48dee7", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -896,7 +854,6 @@ "ViewResolver": { "source": "mainnet://1d7e57aa55817448.ViewResolver", "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", - "block_height": 143307913, "aliases": { "emulator": "f8d6e0586b0a20c7", From 2ee924a2c23aacc55c4c7aa45d7ca38612cdbc4c Mon Sep 17 00:00:00 2001 From: Jordan Ribbink <17958158+jribbink@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:25:10 -0800 Subject: [PATCH 14/14] Update flow.json Co-authored-by: Raymond Zhang --- flow.json | 1 + 1 file changed, 1 insertion(+) diff --git a/flow.json b/flow.json index 16088d2e..d0764ea7 100644 --- a/flow.json +++ b/flow.json @@ -212,6 +212,7 @@ "source": "./lib/FlowALP/cadence/contracts/mocks/MockDexSwapper.cdc", "aliases": { "emulator": "045a1763c93006ca", + "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000007" }