diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml index 039a0c5e..4e9b1ba1 100644 --- a/.github/workflows/cadence_tests.yml +++ b/.github/workflows/cadence_tests.yml @@ -30,7 +30,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + 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 1801af75..f349df89 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -30,7 +30,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + 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 b2a269b8..98d3191e 100644 --- a/.github/workflows/incrementfi_tests.yml +++ b/.github/workflows/incrementfi_tests.yml @@ -20,7 +20,7 @@ jobs: token: ${{ secrets.GH_PAT }} submodules: recursive - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + 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 af61fca4..47f38a7c 100644 --- a/.github/workflows/punchswap.yml +++ b/.github/workflows/punchswap.yml @@ -26,7 +26,7 @@ jobs: cache-dependency-path: | **/go.sum - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + 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 97ad33a2..6c226f16 100644 --- a/.github/workflows/scheduled_rebalance_tests.yml +++ b/.github/workflows/scheduled_rebalance_tests.yml @@ -30,7 +30,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Install Flow CLI - run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + 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/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..7a3c7fec --- /dev/null +++ b/cadence/tests/evm_state_helpers.cdc @@ -0,0 +1,53 @@ +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, + totalSupplySlot: UInt256, + vaultTotalAssetsSlot: UInt256, + baseAssets: UFix64, + priceMultiplier: UFix64, + signer: Test.TestAccount +) { + let result = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/set_erc4626_vault_price.cdc"), + authorizers: [signer.address], + signers: [signer], + arguments: [vaultAddress, assetAddress, assetBalanceSlot, totalSupplySlot, vaultTotalAssetsSlot, baseAssets, priceMultiplier] + ) + ) + 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 +) { + let seedResult = Test.executeTransaction( + Test.Transaction( + code: Test.readFile("transactions/set_uniswap_v3_pool_price.cdc"), + authorizers: [signer.address], + signers: [signer], + arguments: [factoryAddress, tokenAAddress, tokenBAddress, fee, priceTokenBPerTokenA, tokenABalanceSlot, tokenBBalanceSlot] + ) + ) + Test.expect(seedResult, Test.beSucceeded()) +} diff --git a/cadence/tests/evm_state_helpers_test.cdc b/cadence/tests/evm_state_helpers_test.cdc new file mode 100644 index 00000000..d8648fda --- /dev/null +++ b/cadence/tests/evm_state_helpers_test.cdc @@ -0,0 +1,149 @@ +// Tests that EVM state helpers correctly set Uniswap V3 pool price and ERC4626 vault price +#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 = 100 + +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 = flowAmount * 2.0 + 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())" + ) +} + +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 + 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())" + ) +} 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..5f81e389 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,59 @@ 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) + + var err = Test.deployContract( + name: "MockStrategies", + path: "../contracts/mocks/MockStrategies.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + 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) +} +access(self) fun _deploy(config: DeploymentConfig) { // DeFiActions contracts var err = Test.deployContract( name: "DeFiActionsUtils", @@ -161,6 +237,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", @@ -324,36 +401,18 @@ 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()) + 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()) + } - // 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 +428,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 +443,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 +542,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 +692,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 +1012,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/cadence/tests/transactions/set_erc4626_vault_price.cdc b/cadence/tests/transactions/set_erc4626_vault_price.cdc new file mode 100644 index 00000000..79e0852f --- /dev/null +++ b/cadence/tests/transactions/set_erc4626_vault_price.cdc @@ -0,0 +1,123 @@ +import EVM from "MockEVM" +import "ERC4626Utils" +import "FlowEVMBridgeUtils" + +// 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 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 +transaction( + vaultAddress: String, + assetAddress: String, + assetBalanceSlot: UInt256, + totalSupplySlot: UInt256, + vaultTotalAssetsSlot: UInt256, + baseAssets: UFix64, + priceMultiplier: UFix64 +) { + prepare(signer: &Account) {} + + execute { + let vault = EVM.addressFromString(vaultAddress) + let asset = EVM.addressFromString(assetAddress) + + // 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) + + // 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() + var multiplierUInt64: UInt64 = 0 + for byte in multiplierBytes { + multiplierUInt64 = (multiplierUInt64 << 8) + UInt64(byte) + } + let finalTargetAssets = (targetAssets * UInt256(multiplierUInt64)) / UInt256(100000000) + + // For a 1:1 price (1 share = 1 asset), we need: + // totalAssets (in assetDecimals) / totalSupply (vault decimals) = 1 + // 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 = vaultDecimals - assetDecimals + let supplyMultiplier = FlowEVMBridgeUtils.pow(base: 10, exponent: decimalDifference) + let finalTargetSupply = targetAssets * supplyMultiplier + + 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 = String.encodeHex(finalTargetAssets.toBigEndianBytes()) + EVM.store(target: asset, slot: vaultBalanceSlot, value: targetAssetsValue) + + // 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 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 { + 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 = 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 new file mode 100644 index 00000000..147ebd22 --- /dev/null +++ b/cadence/tests/transactions/set_uniswap_v3_pool_price.cdc @@ -0,0 +1,845 @@ +import EVM from "MockEVM" + +// 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 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, + tokenAAddress: String, + tokenBAddress: String, + fee: UInt64, + priceTokenBPerTokenA: UFix64, + tokenABalanceSlot: UInt256, + tokenBBalanceSlot: UInt256 +) { + 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(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 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 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( + "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 + 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 = 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 poolAddress = poolAddr.toString() + + // Read pool parameters (tickSpacing) + let tickSpacingCalldata = EVM.encodeABIWithSignature("tickSpacing()", []) + let spacingResult = self.coa.dryCall( + to: poolAddr, + data: tickSpacingCalldata, + gasLimit: 100000, + value: EVM.Balance(attoflow: 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 + + // Recalculate sqrtPriceX96 from the aligned tick so it matches slot0 + 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 + 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 = 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 = 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: "0", value: slot0Value) + + // Verify what we stored by reading it back + 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): + // - 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 = String.encodeHex(obs0Bytes) + EVM.store(target: poolAddr, slot: "8", value: obs0Value) + + // Set feeGrowthGlobal0X128 and feeGrowthGlobal1X128 + EVM.store(target: poolAddr, slot: "1", value: "0000000000000000000000000000000000000000000000000000000000000000") + EVM.store(target: poolAddr, slot: "2", value: "0000000000000000000000000000000000000000000000000000000000000000") + + // 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 (storage layout below) + + // Lower tick + let tickLowerSlot = computeMappingSlot([tickLower, 5]) + + // Slot 0: liquidityGross=1e24 (lower 128 bits), liquidityNet=+1e24 (upper 128 bits) + let tickLowerData0 = "000000000000d3c21bcecceda1000000000000000000d3c21bcecceda1000000" + + // ASSERTION: Verify tick data is 32 bytes + 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.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 = "" + 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: "0000000000000000000000000000000000000000000000000000000000000000") + + // Slot 2: feeGrowthOutside1X128 = 0 + let tickLowerSlot2Bytes = (tickLowerSlotNum + 2).toBigEndianBytes() + 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: "0000000000000000000000000000000000000000000000000000000000000000") + + // Slot 3: tickCumulativeOutside=0, secondsPerLiquidity=0, secondsOutside=0, initialized=true(0x01) + let tickLowerSlot3Bytes = (tickLowerSlotNum + 3).toBigEndianBytes() + 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: "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 = "ffffffffffff2c3de43133125f000000000000000000d3c21bcecceda1000000" + + // ASSERTION: Verify tick upper data is 32 bytes + 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.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 = "" + 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: "0000000000000000000000000000000000000000000000000000000000000000") + + let tickUpperSlot2Bytes = (tickUpperSlotNum + 2).toBigEndianBytes() + 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: "0000000000000000000000000000000000000000000000000000000000000000") + + let tickUpperSlot3Bytes = (tickUpperSlotNum + 3).toBigEndianBytes() + 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: "0100000000000000000000000000000000000000000000000000000000000000") + + // Set tick bitmap + + 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 = "" + 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 == 64, message: "bitmap must be 64 hex chars = 64 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 = "" + 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 == 64, message: "bitmap must be 64 hex chars = 64 chars total") + + EVM.store(target: poolAddr, slot: bitmapUpperSlot, value: bitmapUpperValue) + + // Create position + + 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 = 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 = String.encodeHex(positionSlotHash) + + // Set position liquidity = 1e24 (matching global liquidity) + let positionLiquidityValue = "00000000000000000000000000000000000000000000d3c21bcecceda1000000" + + // ASSERTION: Verify position liquidity value is 32 bytes + assert(positionLiquidityValue.length == 64, message: "Position liquidity must be 64 hex chars = 64 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 = "" + 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: "0000000000000000000000000000000000000000000000000000000000000000") + + // Slot 2: feeGrowthInside1LastX128 = 0 + let positionSlot2Bytes = (positionSlotNum + 2).toBigEndianBytes() + 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: "0000000000000000000000000000000000000000000000000000000000000000") + + // Slot 3: tokensOwed0 = 0, tokensOwed1 = 0 + let positionSlot3Bytes = (positionSlotNum + 3).toBigEndianBytes() + 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: "0000000000000000000000000000000000000000000000000000000000000000") + + // 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 + } + + // Convert to hex and pad to 32 bytes + let token0BalanceHex = String.encodeHex(token0Balance.toBigEndianBytes()) + let token1BalanceHex = String.encodeHex(token1Balance.toBigEndianBytes()) + + // Set token0 balance + let token0BalanceSlotComputed = computeBalanceOfSlot(holderAddress: poolAddress, balanceSlot: token0BalanceSlot) + 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: token1BalanceHex) + } +} + +/// Calculate sqrtPriceX96 from tick using Uniswap V3's formula +/// 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 + // = exp(tick * ln(sqrt(1.0001))) * 2^96 + + // 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 exp(exponent / 10^18) * scaleFactor using Taylor series + let expValue = expInt256(x: exponent, scaleFactor: scaleFactor) + + // 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 twoTo96 = (UInt256(1) << 96) + let sqrtPriceX96 = (expValue * twoTo96) / scaleFactor + + 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 +/// 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 + for byte in priceBytes { + priceUInt64 = (priceUInt64 << 8) + UInt64(byte) + } + + // priceUInt64 is price * 10^8 + // + // 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 (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(adjusted_price) / ln(1.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 dd1f2ee7..d0764ea7 100644 --- a/flow.json +++ b/flow.json @@ -212,9 +212,19 @@ "source": "./lib/FlowALP/cadence/contracts/mocks/MockDexSwapper.cdc", "aliases": { "emulator": "045a1763c93006ca", + "mainnet": "b1d63873c3cc9f79", + "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000007" } }, + "MockEVM": { + "source": "./cadence/contracts/mocks/EVM.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, "MockOracle": { "source": "cadence/contracts/mocks/MockOracle.cdc", "aliases": {