diff --git a/contracts/evmx/base/AppGatewayBase.sol b/contracts/evmx/base/AppGatewayBase.sol index 2a931237..0c8a1630 100644 --- a/contracts/evmx/base/AppGatewayBase.sol +++ b/contracts/evmx/base/AppGatewayBase.sol @@ -164,6 +164,7 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { /// @param contractId_ The contract ID /// @param chainSlug_ The chain slug /// @return onChainAddress The on-chain address + // TODO:GW: this does not work for Solana, as setAddress() is not called - cos forwarder and solana contract are not deployed with AG function getOnChainAddress( bytes32 contractId_, uint32 chainSlug_ @@ -221,6 +222,8 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { /// @param contractId_ The bytes32 identifier of the contract to be validated /// @param isValid Boolean flag indicating whether the contract is authorized (true) or not (false) /// @dev This function retrieves the onchain address using the contractId_ and chainSlug, then calls the watcher precompile to update the plug's validity status + // TODO:GW: this does not work for Solana, as setAddress() is not called - cos forwarder and solana contract are not deployed with AG + // there is no contractId for solana function _setValidPlug(bool isValid, uint32 chainSlug_, bytes32 contractId_) internal { bytes32 onchainAddress = getOnChainAddress(contractId_, chainSlug_); watcher__().setIsValidPlug(isValid, chainSlug_, onchainAddress); diff --git a/contracts/evmx/fees/Credit.sol b/contracts/evmx/fees/Credit.sol index c9234141..7fbfe747 100644 --- a/contracts/evmx/fees/Credit.sol +++ b/contracts/evmx/fees/Credit.sol @@ -40,6 +40,7 @@ abstract contract FeesManagerStorage is IFeesManager { // slot 52 /// @notice Mapping to track blocked credits for each user /// @dev address => userBlockedCredits + // TODO: this will have to be bytes32 (for solana addresses) mapping(address => uint256) public userBlockedCredits; // slot 53 @@ -53,12 +54,14 @@ abstract contract FeesManagerStorage is IFeesManager { // slot 55 // token pool balances // chainSlug => token address => amount + // TODO: this will have to be bytes32 (for solana tokens) mapping(uint32 => mapping(address => uint256)) public tokenOnChainBalances; // slot 56 /// @notice Mapping to track nonce to whether it has been used /// @dev address => signatureNonce => isNonceUsed /// @dev used by watchers or other users in signatures + // TODO: how about this, do we need to change it to bytes32 ? - how nonces are used here ? mapping(address => mapping(uint256 => bool)) public isNonceUsed; // slot 57 @@ -161,13 +164,17 @@ abstract contract Credit is FeesManagerStorage, Initializable, Ownable, AppGatew } /// @notice Deposits credits and native tokens to a user - /// @param depositTo_ The address to deposit the credits to + /// @param depositTo_ The EVMx address to deposit the credits to (it serves as accounting contract that mirrors real on-chain balances) /// @param chainSlug_ The chain slug /// @param token_ The token address /// @param nativeAmount_ The native amount /// @param creditAmount_ The credit amount + // TODO:4: for Solana handling we will need to have separate function deposit_solana() so that we do not have to change - (?) + // existing function on EVM UserVault which will refer to this one + // - also different handling on transmitter and indexer function deposit( uint32 chainSlug_, + // TODO: token will have to be bytes32 (for solana tokens) - this is the address of token on native chain (could be Solana) address token_, address depositTo_, uint256 nativeAmount_, @@ -177,10 +184,11 @@ abstract contract Credit is FeesManagerStorage, Initializable, Ownable, AppGatew if (!whitelistedReceivers[depositTo_]) revert InvalidReceiver(); tokenOnChainBalances[chainSlug_][token_] += creditAmount_ + nativeAmount_; - // Mint tokens to the user + // Mint tokens to the some evmx accounting contract _mint(depositTo_, creditAmount_); if (nativeAmount_ > 0) { + // TODO: ask: what are native tokens in this context ? - native to real blockchains or to EVMx itself ? // if native transfer fails, add to credit bool success = feesPool.withdraw(depositTo_, nativeAmount_); diff --git a/contracts/evmx/helpers/ForwarderSolana.sol b/contracts/evmx/helpers/ForwarderSolana.sol new file mode 100644 index 00000000..7bb9f2e0 --- /dev/null +++ b/contracts/evmx/helpers/ForwarderSolana.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "solady/utils/Initializable.sol"; +import "./AddressResolverUtil.sol"; +import "../interfaces/IAddressResolver.sol"; +import "../interfaces/IAppGateway.sol"; +import "../interfaces/IForwarder.sol"; +import {QueueParams, OverrideParams, Transaction} from "../../utils/common/Structs.sol"; +import {AsyncModifierNotSet, WatcherNotSet, InvalidOnChainAddress} from "../../utils/common/Errors.sol"; +import "../../utils/RescueFundsLib.sol"; +import {toBytes32Format} from "../../utils/common/Converters.sol"; +import {SolanaInstruction} from "../../utils/common/Structs.sol"; +import {CHAIN_SLUG_SOLANA_MAINNET, CHAIN_SLUG_SOLANA_DEVNET} from "../../utils/common/Constants.sol"; +import {ForwarderStorage} from "./Forwarder.sol"; + +/// @title Forwarder Contract +/// @notice This contract acts as a forwarder for async calls to the on-chain contracts. +contract ForwarderSolana is ForwarderStorage, Initializable, AddressResolverUtil { + error InvalidSolanaChainSlug(); + error AddressResolverNotSet(); + + constructor() { + _disableInitializers(); // disable for implementation + } + + /// @notice Initializer to replace constructor for upgradeable contracts + /// @param chainSlug_ chain slug on which the contract is deployed + //// @param onChainAddress_ on-chain address associated with this forwarder + /// @param addressResolver_ address resolver contract + function initialize( + uint32 chainSlug_, + bytes32 onChainAddress_, // TODO:GW: after demo remove this param, we take target as param in callSolana() + address addressResolver_ + ) public initializer { + if (chainSlug_ == CHAIN_SLUG_SOLANA_MAINNET || chainSlug_ == CHAIN_SLUG_SOLANA_DEVNET) { + chainSlug = chainSlug_; + } else { + revert InvalidSolanaChainSlug(); + } + onChainAddress = onChainAddress_; + _setAddressResolver(addressResolver_); + } + + /// @notice Returns the on-chain address associated with this forwarder. + /// @return The on-chain address. + function getOnChainAddress() external view returns (bytes32) { + return onChainAddress; + } + + /// @notice Returns the chain slug on which the contract is deployed. + /// @return chain slug + function getChainSlug() external view returns (uint32) { + return chainSlug; + } + + /// @notice Fallback function to process the contract calls to onChainAddress + /// @dev It queues the calls in the middleware and deploys the promise contract + // function callSolana(SolanaInstruction memory solanaInstruction, bytes32 switchboardSolana) external { + function callSolana(bytes memory solanaPayload, bytes32 target) external { + if (address(addressResolver__) == address(0)) { + revert AddressResolverNotSet(); + } + if (address(watcher__()) == address(0)) { + revert WatcherNotSet(); + } + + // validates if the async modifier is set + address msgSender = msg.sender; + bool isAsyncModifierSet = IAppGateway(msgSender).isAsyncModifierSet(); + if (!isAsyncModifierSet) revert AsyncModifierNotSet(); + + // fetch the override params from app gateway + (OverrideParams memory overrideParams, bytes32 sbType) = IAppGateway(msgSender) + .getOverrideParams(); + + // TODO:GW: after POC make it work like below + // get the switchboard address from the watcher precompile config + // address switchboard = watcherPrecompileConfig().switchboards(chainSlug, sbType); + + // Queue the call in the middleware. + QueueParams memory queueParams; + queueParams.overrideParams = overrideParams; + queueParams.transaction = Transaction({ + chainSlug: chainSlug, + // target: onChainAddress, // for Solana reads it should be accountToRead + // TODO: Solana forwarder can be a singleton - does not need to store onChainAddress and can use target as param + target: target, + payload: solanaPayload + }); + queueParams.switchboardType = sbType; + watcher__().queue(queueParams, msgSender); + } +} diff --git a/contracts/evmx/interfaces/IConfigurations.sol b/contracts/evmx/interfaces/IConfigurations.sol index 227a18f3..49cc0bbf 100644 --- a/contracts/evmx/interfaces/IConfigurations.sol +++ b/contracts/evmx/interfaces/IConfigurations.sol @@ -33,9 +33,10 @@ interface IConfigurations { /// @return The socket function sockets(uint32 chainSlug_) external view returns (bytes32); - /// @notice Returns the socket for a given chain slug + /// @notice Returns the switchboardId for a given chain slug and switchboard type (1:1 mapping) /// @param chainSlug_ The chain slug - /// @return The socket + /// @param sbType_ The type of switchboard + /// @return switchboardId function switchboards(uint32 chainSlug_, bytes32 sbType_) external view returns (uint64); /// @notice Sets the switchboard for a network diff --git a/contracts/evmx/plugs/FeesPlug.sol b/contracts/evmx/plugs/FeesPlug.sol index b46baab1..e0b2d654 100644 --- a/contracts/evmx/plugs/FeesPlug.sol +++ b/contracts/evmx/plugs/FeesPlug.sol @@ -40,52 +40,54 @@ contract FeesPlug is IFeesPlug, PlugBase, AccessControl { /////////////////////// DEPOSIT AND WITHDRAWAL /////////////////////// function depositCredit( address token_, - address receiver_, + address evmx_receiver_, uint256 amount_, bytes memory data_ ) external override { - _deposit(token_, receiver_, amount_, 0, data_); + _deposit(token_, evmx_receiver_, amount_, 0, data_); } function depositCreditAndNative( address token_, - address receiver_, + address evmx_receiver_, uint256 amount_, bytes memory data_ ) external override { uint256 nativeAmount_ = amount_ / 10; - _deposit(token_, receiver_, amount_ - nativeAmount_, nativeAmount_, data_); + _deposit(token_, evmx_receiver_, amount_ - nativeAmount_, nativeAmount_, data_); } function depositToNative( address token_, - address receiver_, + address evmx_receiver_, uint256 amount_, bytes memory data_ ) external override { - _deposit(token_, receiver_, 0, amount_, data_); + _deposit(token_, evmx_receiver_, 0, amount_, data_); } /// @notice Deposits funds /// @param token_ The token address /// @param creditAmount_ The amount of fees /// @param nativeAmount_ The amount of native tokens - /// @param receiver_ The receiver address + /// @param evmx_receiver_ The evmx receiver address. EVMx tokens will be minted to this address to mirror real on-chain balances. function _deposit( address token_, - address receiver_, + address evmx_receiver_, uint256 creditAmount_, uint256 nativeAmount_, bytes memory data_ ) internal { if (!whitelistedTokens[token_]) revert TokenNotWhitelisted(token_); token_.safeTransferFrom(msg.sender, address(this), creditAmount_ + nativeAmount_); - emit FeesDeposited(token_, receiver_, creditAmount_, nativeAmount_, data_); + emit FeesDeposited(token_, evmx_receiver_, creditAmount_, nativeAmount_, data_); } - /// @notice Withdraws fees + /// @notice Withdraws fees - amount in is represented as 18 decimals token. + /// Before transferring we need to convert to do decimals given token has on this chain. + /// final_amount = input_amount / 10^18 * 10^decimals => input_amount * 10^(-18 + decimals) => input_amount * 10^(decimals - 18) /// @param token_ The token address - /// @param amount_ The amount + /// @param amount_ The input amount (represented as 18 decimals token) /// @param receiver_ The receiver address function withdrawFees( address token_, diff --git a/contracts/evmx/watcher/Configurations.sol b/contracts/evmx/watcher/Configurations.sol index 50bb0999..ddeee64c 100644 --- a/contracts/evmx/watcher/Configurations.sol +++ b/contracts/evmx/watcher/Configurations.sol @@ -44,6 +44,9 @@ abstract contract ConfigurationsStorage is IConfigurations { /// @notice Configuration contract for the Watcher Precompile system /// @dev Handles the mapping between networks, plugs, and app gateways for payload execution contract Configurations is ConfigurationsStorage, Initializable, Ownable, WatcherBase { + // TODO:GW: remove after testing Solana + event VerifyConnectionsSB(bytes32 switchboard, bytes32 switchboardExpected); + /// @notice Emitted when a new plug is configured for an app gateway /// @param appGatewayId The id of the app gateway /// @param chainSlug The identifier of the destination network diff --git a/contracts/evmx/watcher/borsh-serde/BorshDecoder.sol b/contracts/evmx/watcher/borsh-serde/BorshDecoder.sol new file mode 100644 index 00000000..014c80ee --- /dev/null +++ b/contracts/evmx/watcher/borsh-serde/BorshDecoder.sol @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Based on Aurora bridge repo: https://github.com/aurora-is-near/aurora-contracts-sdk/blob/main/aurora-solidity-sdk +pragma solidity ^0.8.21; + +import "../../../utils/common/Structs.sol"; +import "./BorshUtils.sol"; + +library BorshDecoder { + /// Decodes the borsh schema into abi.encode(value) list of params + /// Handles decoding of: + /// 1. u8/u16/u32/u64 Rust types + /// 2. "String" Rust type + /// 3. array/Vec and String numeric Rust types (mentioned in 1) and 2)) + function decodeGenericSchema( + GenericSchema memory schema, + bytes memory encodedData + ) internal pure returns (bytes[] memory) { + bytes[] memory decodedParams = new bytes[](schema.valuesTypeNames.length); + Data memory data = from(encodedData); + + for (uint256 i = 0; i < schema.valuesTypeNames.length; i++) { + string memory typeName = schema.valuesTypeNames[i]; + + if (keccak256(bytes(typeName)) == keccak256(bytes("u8"))) { + uint8 value = data.decodeU8(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16"))) { + uint16 value = data.decodeU16(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32"))) { + uint32 value = data.decodeU32(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64"))) { + uint64 value = data.decodeU64(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128"))) { + uint128 value = data.decodeU128(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("String"))) { + string memory value = data.decodeString(); + decodedParams[i] = abi.encode(value); + } + // Handle Vector types with variable length + else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint8[] memory value; + (length, value) = decodeUint8Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint16[] memory value; + (length, value) = decodeUint16Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint32[] memory value; + (length, value) = decodeUint32Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint64[] memory value; + (length, value) = decodeUint64Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint128[] memory value; + (length, value) = decodeUint128Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + string[] memory value; + (length, value) = decodeStringVec(data); + decodedParams[i] = abi.encode(value); + } + // Handle Array types with fixed length + else if (BorshUtils.startsWith(typeName, "[u8;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint8[] memory value = decodeUint8Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[u16;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint16[] memory value = decodeUint16Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[u32;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint32[] memory value = decodeUint32Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[u64;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint64[] memory value = decodeUint64Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[u128;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint128[] memory value = decodeUint128Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[String;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + string[] memory value = decodeStringArray(data, length); + decodedParams[i] = abi.encode(value); + } else { + revert("Unsupported type"); + } + } + + return decodedParams; + } + + /********* Decode primitive types *********/ + + using BorshDecoder for Data; + + struct Data { + uint256 ptr; + uint256 end; + } + + /********* Helper to manage data pointer *********/ + + function from(bytes memory data) internal pure returns (Data memory res) { + uint256 ptr; + assembly { + ptr := data + } + unchecked { + res.ptr = ptr + 32; + res.end = res.ptr + BorshUtils.readMemory(ptr); + } + } + + // This function assumes that length is reasonably small, so that data.ptr + length will not overflow. In the current code, length is always less than 2^32. + function requireSpace(Data memory data, uint256 length) internal pure { + unchecked { + require(data.ptr + length <= data.end, "Parse error: unexpected EOI"); + } + } + + function read(Data memory data, uint256 length) internal pure returns (bytes32 res) { + data.requireSpace(length); + res = bytes32(BorshUtils.readMemory(data.ptr)); + unchecked { + data.ptr += length; + } + return res; + } + + function done(Data memory data) internal pure { + require(data.ptr == data.end, "Parse error: EOI expected"); + } + + /********* Decoders for primitive types *********/ + + function decodeU8(Data memory data) internal pure returns (uint8) { + return uint8(bytes1(data.read(1))); + } + + function decodeU16(Data memory data) internal pure returns (uint16) { + return BorshUtils.swapBytes2(uint16(bytes2(data.read(2)))); + } + + function decodeU32(Data memory data) internal pure returns (uint32) { + return BorshUtils.swapBytes4(uint32(bytes4(data.read(4)))); + } + + function decodeU64(Data memory data) internal pure returns (uint64) { + return BorshUtils.swapBytes8(uint64(bytes8(data.read(8)))); + } + + function decodeU128(Data memory data) internal pure returns (uint128) { + return BorshUtils.swapBytes16(uint128(bytes16(data.read(16)))); + } + + function decodeU256(Data memory data) internal pure returns (uint256) { + return BorshUtils.swapBytes32(uint256(data.read(32))); + } + + function decodeBytes20(Data memory data) internal pure returns (bytes20) { + return bytes20(data.read(20)); + } + + function decodeBytes32(Data memory data) internal pure returns (bytes32) { + return data.read(32); + } + + function decodeBool(Data memory data) internal pure returns (bool) { + uint8 res = data.decodeU8(); + require(res <= 1, "Parse error: invalid bool"); + return res != 0; + } + + function skipBytes(Data memory data) internal pure { + uint256 length = data.decodeU32(); + data.requireSpace(length); + unchecked { + data.ptr += length; + } + } + + function decodeBytes(Data memory data) internal pure returns (bytes memory res) { + uint256 length = data.decodeU32(); + data.requireSpace(length); + res = BorshUtils.memoryToBytes(data.ptr, length); + unchecked { + data.ptr += length; + } + } + + function decodeString(Data memory data) internal pure returns (string memory) { + bytes memory stringBytes = data.decodeBytes(); + return string(stringBytes); + } + + /********* Decode Vector types with variable length *********/ + + function decodeUint8Vec(Data memory data) internal pure returns (uint32, uint8[] memory) { + uint32 length = data.decodeU32(); + uint8[] memory values = new uint8[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU8(); + } + + return (length, values); + } + + function decodeUint16Vec(Data memory data) internal pure returns (uint32, uint16[] memory) { + uint32 length = data.decodeU32(); + uint16[] memory values = new uint16[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU16(); + } + + return (length, values); + } + + function decodeUint32Vec(Data memory data) internal pure returns (uint32, uint32[] memory) { + uint32 length = data.decodeU32(); + uint32[] memory values = new uint32[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU32(); + } + + return (length, values); + } + + function decodeUint64Vec(Data memory data) internal pure returns (uint32, uint64[] memory) { + uint32 length = data.decodeU32(); + uint64[] memory values = new uint64[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU64(); + } + + return (length, values); + } + + function decodeUint128Vec(Data memory data) internal pure returns (uint32, uint128[] memory) { + uint32 length = data.decodeU32(); + uint128[] memory values = new uint128[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU128(); + } + + return (length, values); + } + + function decodeStringVec(Data memory data) internal pure returns (uint32, string[] memory) { + uint32 length = data.decodeU32(); + string[] memory values = new string[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeString(); + } + + return (length, values); + } + + /********* Decode array types with fixed length *********/ + + function decodeUint8Array(Data memory data, uint256 length) internal pure returns (uint8[] memory) { + uint8[] memory values = new uint8[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU8(); + } + + return values; + } + + function decodeUint16Array(Data memory data, uint256 length) internal pure returns (uint16[] memory) { + uint16[] memory values = new uint16[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU16(); + } + + return values; + } + + function decodeUint32Array(Data memory data, uint256 length) internal pure returns (uint32[] memory) { + uint32[] memory values = new uint32[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU32(); + } + + return values; + } + + function decodeUint64Array(Data memory data, uint256 length) internal pure returns (uint64[] memory) { + uint64[] memory values = new uint64[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU64(); + } + + return values; + } + + function decodeUint128Array(Data memory data, uint256 length) internal pure returns (uint128[] memory) { + uint128[] memory values = new uint128[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU128(); + } + + return values; + } + + function decodeStringArray(Data memory data, uint256 length) internal pure returns (string[] memory) { + string[] memory values = new string[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeString(); + } + + return values; + } +} \ No newline at end of file diff --git a/contracts/evmx/watcher/borsh-serde/BorshEncoder.sol b/contracts/evmx/watcher/borsh-serde/BorshEncoder.sol new file mode 100644 index 00000000..e0b0f9cb --- /dev/null +++ b/contracts/evmx/watcher/borsh-serde/BorshEncoder.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Based on Aurora bridge repo: https://github.com/aurora-is-near/aurora-contracts-sdk/blob/main/aurora-solidity-sdk +pragma solidity ^0.8.21; + +import "../../../utils/common/Structs.sol"; +import "./BorshUtils.sol"; + +library BorshEncoder { + function encodeFunctionArgs( + SolanaInstruction memory instruction + ) internal pure returns (bytes memory) { + bytes memory functionArgsPacked; + for (uint256 i = 0; i < instruction.data.functionArguments.length; i++) { + string memory typeName = instruction.description.functionArgumentTypeNames[i]; + bytes memory data = instruction.data.functionArguments[i]; + + if (keccak256(bytes(typeName)) == keccak256(bytes("u8"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint8 arg = uint8(abiDecodedArg); + bytes1 borshEncodedArg = encodeU8(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint16 arg = uint16(abiDecodedArg); + bytes2 borshEncodedArg = encodeU16(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint32 arg = uint32(abiDecodedArg); + bytes4 borshEncodedArg = encodeU32(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint64 arg = uint64(abiDecodedArg); + bytes8 borshEncodedArg = encodeU64(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint128 arg = uint128(abiDecodedArg); + bytes16 borshEncodedArg = encodeU128(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("String"))) { + string memory abiDecodedArg = abi.decode(data, (string)); + bytes memory borshEncodedArg = encodeString(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } + // Handle array types with fixed length + else if (BorshUtils.startsWith(typeName, "[u8;")) { + uint8[] memory abiDecodedArg = abi.decode(data, (uint8[])); + bytes memory borshEncodedArg = encodeUint8Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16[]"))) { + uint16[] memory abiDecodedArg = abi.decode(data, (uint16[])); + bytes memory borshEncodedArg = encodeUint16Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32[]"))) { + uint32[] memory abiDecodedArg = abi.decode(data, (uint32[])); + bytes memory borshEncodedArg = encodeUint32Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64[]"))) { + uint64[] memory abiDecodedArg = abi.decode(data, (uint64[])); + bytes memory borshEncodedArg = encodeUint64Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128[]"))) { + uint128[] memory abiDecodedArg = abi.decode(data, (uint128[])); + bytes memory borshEncodedArg = encodeUint128Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("String[]"))) { + string[] memory abiDecodedArg = abi.decode(data, (string[])); + bytes memory borshEncodedArg = encodeStringArray(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } + // Handle Vector types with that can have variable length - length prefix is added + else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint8[] memory abiDecodedArg = abi.decode(data, (uint8[])); + bytes memory borshEncodedArg = encodeUint8Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint16[] memory abiDecodedArg = abi.decode(data, (uint16[])); + bytes memory borshEncodedArg = encodeUint16Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32[] memory abiDecodedArg = abi.decode(data, (uint32[])); + bytes memory borshEncodedArg = encodeUint32Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint64[] memory abiDecodedArg = abi.decode(data, (uint64[])); + bytes memory borshEncodedArg = encodeUint64Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint128[] memory abiDecodedArg = abi.decode(data, (uint128[])); + bytes memory borshEncodedArg = encodeUint128Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + string[] memory abiDecodedArg = abi.decode(data, (string[])); + bytes memory borshEncodedArg = encodeStringVec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } + // Handle array types with fixed length - no length prefix, just the bytes + else if (BorshUtils.startsWith(typeName, "[u8;")) { + uint8[] memory abiDecodedArg = abi.decode(data, (uint8[])); + bytes memory borshEncodedArg = encodeUint8Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[u16;")) { + uint16[] memory abiDecodedArg = abi.decode(data, (uint16[])); + bytes memory borshEncodedArg = encodeUint16Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[u32;")) { + uint32[] memory abiDecodedArg = abi.decode(data, (uint32[])); + bytes memory borshEncodedArg = encodeUint32Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[u64;")) { + uint64[] memory abiDecodedArg = abi.decode(data, (uint64[])); + bytes memory borshEncodedArg = encodeUint64Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[u128;")) { + uint128[] memory abiDecodedArg = abi.decode(data, (uint128[])); + bytes memory borshEncodedArg = encodeUint128Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[String;")) { + string[] memory abiDecodedArg = abi.decode(data, (string[])); + bytes memory borshEncodedArg = encodeStringArray(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else { + revert("Unsupported type"); + } + } + return functionArgsPacked; + } + + /********* Encode functions *********/ + + /** Encode primitive types **/ + + function encodeU8(uint8 v) internal pure returns (bytes1) { + return bytes1(v); + } + + function encodeU16(uint16 v) internal pure returns (bytes2) { + return bytes2(BorshUtils.swapBytes2(v)); + } + + function encodeU32(uint32 v) internal pure returns (bytes4) { + return bytes4(BorshUtils.swapBytes4(v)); + } + + function encodeU64(uint64 v) internal pure returns (bytes8) { + return bytes8(BorshUtils.swapBytes8(v)); + } + + function encodeU128(uint128 v) internal pure returns (bytes16) { + return bytes16(BorshUtils.swapBytes16(v)); + } + + /// Encode bytes vector into borsh. Use this method to encode strings as well. + function encodeBytes(bytes memory value) internal pure returns (bytes memory) { + require(value.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + return abi.encodePacked(encodeU32(uint32(value.length)), bytes(value)); + } + + function encodeString(string memory value) internal pure returns (bytes memory) { + bytes memory strBytes = bytes(value); + return bytes.concat(encodeU32(uint32(strBytes.length)), strBytes); + } + + /** Encode Vector types with that can have variable length **/ + + function encodeUint8Vec(uint8[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint8Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint16Vec(uint16[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint16Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint32Vec(uint32[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint32Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint64Vec(uint64[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint64Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint128Vec(uint128[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint128Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeStringVec(string[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "string length overflow (must fit in uint32)"); + bytes memory packed = packStringArray(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + /** Encode array types with fixed length - no length prefix, just the bytes **/ + + function encodeUint8Array(uint8[] memory arr) internal pure returns (bytes memory) { + return packUint8Array(arr); + } + + function encodeUint16Array(uint16[] memory arr) internal pure returns (bytes memory) { + return packUint16Array(arr); + } + + function encodeUint32Array(uint32[] memory arr) internal pure returns (bytes memory) { + return packUint32Array(arr); + } + + function encodeUint64Array(uint64[] memory arr) internal pure returns (bytes memory) { + return packUint64Array(arr); + } + + function encodeUint128Array(uint128[] memory arr) internal pure returns (bytes memory) { + return packUint128Array(arr); + } + + function encodeStringArray(string[] memory arr) internal pure returns (bytes memory) { + return packStringArray(arr); + } + + /********* Packing functions *********/ + + // NOTE: + // When you use abi.encodePacked() on a dynamic array (uint8[]), Solidity applies ABI encoding rules where each array element gets padded to 32 bytes: + // this is why when you have: + //uint8[] memory value = new uint8[](3); + // value[0] = 1; + // value[1] = 2; + // value[2] = 3; + // bytes memory encoded = abi.encodePacked(value); + // console.logBytes(encoded); + // you get: + // 0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003 + // cause each element is padded to 32 bytes + + // --> Below function packs the array into elements without the padding + + function packUint8Array(uint8[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU8(arr[i])); + } + return out; + } + + function packUint16Array(uint16[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU16(arr[i])); + } + return out; + } + + function packUint32Array(uint32[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU32(arr[i])); + } + return out; + } + + function packUint64Array(uint64[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU64(arr[i])); + } + return out; + } + + function packUint128Array(uint128[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU128(arr[i])); + } + return out; + } + + function packStringArray(string[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeString(arr[i])); + } + return out; + } +} diff --git a/contracts/evmx/watcher/borsh-serde/BorshUtils.sol b/contracts/evmx/watcher/borsh-serde/BorshUtils.sol new file mode 100644 index 00000000..06a5d9ad --- /dev/null +++ b/contracts/evmx/watcher/borsh-serde/BorshUtils.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Based on Aurora bridge repo: https://github.com/aurora-is-near/aurora-contracts-sdk/blob/main/aurora-solidity-sdk +pragma solidity ^0.8.21; + + +library BorshUtils { + + function readMemory(uint256 ptr) internal pure returns (uint256 res) { + assembly { + res := mload(ptr) + } + } + + function writeMemory(uint256 ptr, uint256 value) internal pure { + assembly { + mstore(ptr, value) + } + } + + function memoryToBytes(uint256 ptr, uint256 length) internal pure returns (bytes memory res) { + if (length != 0) { + assembly { + // 0x40 is the address of free memory pointer. + res := mload(0x40) + let end := + add(res, and(add(length, 63), 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0)) + // end = res + 32 + 32 * ceil(length / 32). + mstore(0x40, end) + mstore(res, length) + let destPtr := add(res, 32) + // prettier-ignore + for {} 1 {} { + mstore(destPtr, mload(ptr)) + destPtr := add(destPtr, 32) + if eq(destPtr, end) { break } + ptr := add(ptr, 32) + } + } + } + } + + function swapBytes2(uint16 v) internal pure returns (uint16) { + return (v << 8) | (v >> 8); + } + + function swapBytes4(uint32 v) internal pure returns (uint32) { + v = ((v & 0x00ff00ff) << 8) | ((v & 0xff00ff00) >> 8); + return (v << 16) | (v >> 16); + } + + function swapBytes8(uint64 v) internal pure returns (uint64) { + v = ((v & 0x00ff00ff00ff00ff) << 8) | ((v & 0xff00ff00ff00ff00) >> 8); + v = ((v & 0x0000ffff0000ffff) << 16) | ((v & 0xffff0000ffff0000) >> 16); + return (v << 32) | (v >> 32); + } + + function swapBytes16(uint128 v) internal pure returns (uint128) { + v = + ((v & 0x00ff00ff00ff00ff00ff00ff00ff00ff) << 8) | + ((v & 0xff00ff00ff00ff00ff00ff00ff00ff00) >> 8); + v = + ((v & 0x0000ffff0000ffff0000ffff0000ffff) << 16) | + ((v & 0xffff0000ffff0000ffff0000ffff0000) >> 16); + v = + ((v & 0x00000000ffffffff00000000ffffffff) << 32) | + ((v & 0xffffffff00000000ffffffff00000000) >> 32); + return (v << 64) | (v >> 64); + } + + function swapBytes32(uint256 v) internal pure returns (uint256) { + v = + ((v & 0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff) << 8) | + ((v & 0xff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00) >> 8); + v = + ((v & 0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff) << 16) | + ((v & 0xffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000) >> 16); + v = + ((v & 0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff) << 32) | + ((v & 0xffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000) >> 32); + v = + ((v & 0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff) << 64) | + ((v & 0xffffffffffffffff0000000000000000ffffffffffffffff0000000000000000) >> 64); + return (v << 128) | (v >> 128); + } + + function startsWith(string memory str, string memory prefix) internal pure returns (bool) { + bytes memory strBytes = bytes(str); + bytes memory prefixBytes = bytes(prefix); + + if (prefixBytes.length > strBytes.length) return false; + + for (uint256 i = 0; i < prefixBytes.length; i++) { + if (strBytes[i] != prefixBytes[i]) return false; + } + return true; + } + + function extractArrayLength(string memory typeName) internal pure returns (uint256) { + bytes memory typeBytes = bytes(typeName); + uint256 length = 0; + bool foundSemicolon = false; + bool foundDigit = false; + + // Parse patterns like "[u8; 32]" + for (uint256 i = 0; i < typeBytes.length; i++) { + bytes1 char = typeBytes[i]; + + if (char == 0x3B) { // ';' + foundSemicolon = true; + } else if (foundSemicolon && char >= 0x30 && char <= 0x39) { // '0' to '9' + foundDigit = true; + length = length * 10 + uint256(uint8(char)) - 48; // Convert ASCII to number + } else if (foundSemicolon && foundDigit && char == 0x5D) { // ']' + break; // End of array type declaration + } else if (foundSemicolon && foundDigit && char != 0x20) { // Not a space + // If we found digits but hit a non-digit non-space, invalid format + revert("Invalid array length format"); + } + // Skip spaces and other characters before semicolon + } + + require(foundSemicolon && foundDigit && length > 0, "Could not extract array length"); + return length; + } +} \ No newline at end of file diff --git a/contracts/evmx/watcher/precompiles/WritePrecompile.sol b/contracts/evmx/watcher/precompiles/WritePrecompile.sol index 49409261..d51bceca 100644 --- a/contracts/evmx/watcher/precompiles/WritePrecompile.sol +++ b/contracts/evmx/watcher/precompiles/WritePrecompile.sol @@ -10,6 +10,7 @@ import {InvalidIndex, MaxMsgValueLimitExceeded, InvalidPayloadSize} from "../../ import "../../../utils/RescueFundsLib.sol"; import "../WatcherBase.sol"; import {toBytes32Format} from "../../../utils/common/Converters.sol"; +import "../borsh-serde/BorshEncoder.sol"; abstract contract WritePrecompileStorage is IPrecompile { // slots [0-49] reserved for gap @@ -143,7 +144,7 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc queueParams_.overrideParams.writeFinality, queueParams_.overrideParams.gasLimit, queueParams_.overrideParams.value, - configurations__().switchboards( + configurations__().switchboards( // switchboardId queueParams_.transaction.chainSlug, queueParams_.switchboardType ) @@ -169,8 +170,8 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc Transaction memory transaction, , uint256 gasLimit, - uint256 value, - + uint256 value + ,// switchboardId ) = abi.decode( payloadParams.precompileData, (address, Transaction, WriteFinality, uint256, uint256, uint64) @@ -185,21 +186,31 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc uint40(payloadParams.payloadPointer >> 80) ); - // create digest - DigestParams memory digestParams_ = DigestParams( - configurations__().sockets(transaction.chainSlug), - toBytes32Format(transmitter_), - payloadParams.payloadId, - deadline, - payloadParams.callType, - gasLimit, - value, - transaction.payload, - transaction.target, - toBytes32Format(appGateway), - prevBatchDigestHash, - bytes("") - ); + // Construct parameters for digest calculation + DigestParams memory digestParams_; + if (_isSolanaChainSlug(transaction.chainSlug)) { + digestParams_ = _createSolanaDigestParams( + payloadParams, + transaction, + appGateway, + transmitter_, // TODO: we should use here Solana transmitter address + prevBatchDigestHash, + deadline, + gasLimit, + value + ); + } else { + digestParams_ = _createEvmDigestParams( + payloadParams, + transaction, + appGateway, + transmitter_, + prevBatchDigestHash, + deadline, + gasLimit, + value + ); + } // Calculate and store digest from payload parameters bytes32 digest = getDigest(digestParams_); @@ -260,6 +271,80 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc ); } + function _createEvmDigestParams( + PayloadParams memory payloadParams_, + Transaction memory transaction_, + address appGateway_, + address transmitter_, + bytes32 prevBatchDigestHash_, + uint256 deadline_, + uint256 gasLimit_, + uint256 value_ + ) internal view returns (DigestParams memory) { + return + DigestParams( + configurations__().sockets(transaction_.chainSlug), + toBytes32Format(transmitter_), + payloadParams_.payloadId, + deadline_, + payloadParams_.callType, + gasLimit_, + value_, + transaction_.payload, + transaction_.target, + toBytes32Format(appGateway_), + prevBatchDigestHash_, + bytes("") + ); + } + + function _createSolanaDigestParams( + PayloadParams memory payloadParams_, + Transaction memory transaction_, + address appGateway_, + address transmitter_, + bytes32 prevBatchDigestHash_, + uint256 deadline_, + uint256 gasLimit_, + uint256 value_ + ) internal view returns (DigestParams memory) { + SolanaInstruction memory instruction = abi.decode( + transaction_.payload, + (SolanaInstruction) + ); + bytes memory functionArgsPacked = BorshEncoder.encodeFunctionArgs(instruction); + + bytes memory payloadPacked = abi.encodePacked( + instruction.data.programId, + instruction.data.accounts, + instruction.data.instructionDiscriminator, + functionArgsPacked + ); + + // bytes32 of Solana Socket address : 9vFEQ5e3xf4eo17WttfqmXmnqN3gUicrhFGppmmNwyqV + bytes32 hardcodedSocket = 0x84815e8ca2f6dad7e12902c39a51bc72e13c48139b4fb10025d94e7abea2969c; + return + DigestParams( + // watcherPrecompileConfig__.sockets(params_.payloadHeader.getChainSlug()), // TODO: this does not work, for some reason it returns 0x000.... address + hardcodedSocket, + toBytes32Format(transmitter_), + payloadParams_.payloadId, + deadline_, + payloadParams_.callType, + gasLimit_, + value_, + payloadPacked, + transaction_.target, + toBytes32Format(appGateway_), + prevBatchDigestHash_, + bytes("") + ); + } + + function _isSolanaChainSlug(uint32 chainSlug_) internal pure returns (bool) { + return chainSlug_ == CHAIN_SLUG_SOLANA_MAINNET || chainSlug_ == CHAIN_SLUG_SOLANA_DEVNET; + } + /// @notice Marks a write request with a proof on digest /// @param payloadId_ The unique identifier of the request /// @param proof_ The watcher's proof diff --git a/contracts/protocol/base/PlugBase.sol b/contracts/protocol/base/PlugBase.sol index a6eda992..1190d065 100644 --- a/contracts/protocol/base/PlugBase.sol +++ b/contracts/protocol/base/PlugBase.sol @@ -21,6 +21,9 @@ abstract contract PlugBase is IPlug { // overrides encoded in bytes bytes public overrides; + // TODO: why it does not have a switchboardId field if it is used in the _connectSocket function? + // should it not be saved is same way as appGatewayId? + // event emitted when plug is disconnected event ConnectorPlugDisconnected(); diff --git a/contracts/protocol/switchboard/SwitchboardBase.sol b/contracts/protocol/switchboard/SwitchboardBase.sol index 43d5cf52..c8a3bc1d 100644 --- a/contracts/protocol/switchboard/SwitchboardBase.sol +++ b/contracts/protocol/switchboard/SwitchboardBase.sol @@ -53,6 +53,7 @@ abstract contract SwitchboardBase is ISwitchboard, AccessControl { ) external view returns (address transmitter) { transmitter = transmitterSignature_.length > 0 ? _recoverSigner( + // TODO: use api encode packed keccak256(abi.encode(address(socket__), payloadId_)), transmitterSignature_ ) diff --git a/contracts/utils/common/Constants.sol b/contracts/utils/common/Constants.sol index 9b1ee6f5..b6fbf440 100644 --- a/contracts/utils/common/Constants.sol +++ b/contracts/utils/common/Constants.sol @@ -21,5 +21,10 @@ uint16 constant MAX_COPY_BYTES = 2048; // 2KB uint32 constant CHAIN_SLUG_SOLANA_MAINNET = 10000001; uint32 constant CHAIN_SLUG_SOLANA_DEVNET = 10000002; +/**** Solana predefined account schema types ****/ + +bytes32 constant TOKEN_ACCOUNT = keccak256("TokenAccount"); +bytes32 constant MINT_ACCOUNT = keccak256("MintAccount"); + // Constant appGatewayId used on all chains bytes32 constant APP_GATEWAY_ID = 0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcdef; diff --git a/contracts/utils/common/Structs.sol b/contracts/utils/common/Structs.sol index 4503068d..c10b953a 100644 --- a/contracts/utils/common/Structs.sol +++ b/contracts/utils/common/Structs.sol @@ -217,6 +217,10 @@ struct CCTPBatchParams { bytes[] attestations; } +/********* Solana payloads *********/ + +/** Solana write payload - SolanaInstruction **/ + struct SolanaInstruction { SolanaInstructionData data; SolanaInstructionDataDescription description; @@ -226,7 +230,6 @@ struct SolanaInstructionData { bytes32 programId; bytes32[] accounts; bytes8 instructionDiscriminator; - // for now we assume the all functionArguments are simple types (uint256, address, bool, etc.) not complex types (struct, array, etc.) bytes[] functionArguments; } @@ -237,3 +240,23 @@ struct SolanaInstructionDataDescription { // names for function argument types used later in data decoding in watcher and transmitter string[] functionArgumentTypeNames; } + +/** Solana read payload - SolanaReadInstruction **/ + +enum SolanaReadSchemaType { + PREDEFINED, + GENERIC +} + +struct SolanaReadRequest { + bytes32 accountToRead; + SolanaReadSchemaType schemaType; + // keccak256("schema-name") + bytes32 predefinedSchemaNameHash; +} + +// this is only used after getting the data from Solana account +struct GenericSchema { + // list of types recognizable by BorshEncoder that we expect to read from Solana account (data model) + string[] valuesTypeNames; +} \ No newline at end of file diff --git a/hardhat-scripts/deploy/1.deploy.ts b/hardhat-scripts/deploy/1.deploy.ts index 1ed55f34..db8fa465 100644 --- a/hardhat-scripts/deploy/1.deploy.ts +++ b/hardhat-scripts/deploy/1.deploy.ts @@ -5,6 +5,7 @@ import { ethers } from "hardhat"; import { ChainAddressesObj, ChainSlug, + ChainId, Contracts, MESSAGE_TRANSMITTER, } from "../../src"; @@ -46,6 +47,12 @@ config(); let EVMxOwner: string; +// cT9tVQf8NAwHk849ctDqeLhbN2B6JJi3LfR6GfuN751 - super-token test program id +export const mockForwarderSolanaOnChainAddress32Bytes = Buffer.from( + "0914e65e59622aeeefb7f007aef36df62d4c380895553b0643fcc4383c7c2448", + "hex" +); + const main = async () => { logConfig(); await logBalances(); @@ -263,6 +270,27 @@ const deployEVMxContracts = async () => { deployUtils.addresses[Contracts.SchedulePrecompile] = schedulePrecompile.address; + try { + console.log("AddressResolver address:", addressResolver.address); + + deployUtils = await deployContractWithProxy( + Contracts.ForwarderSolana, + `contracts/evmx/helpers/ForwarderSolana.sol`, + [ + ChainId.SOLANA_DEVNET, + mockForwarderSolanaOnChainAddress32Bytes, + addressResolver.address, + ], + proxyFactory, + deployUtils + ); + const forwarderSolanaAddress = + deployUtils.addresses[Contracts.ForwarderSolana]; + console.log("ForwarderSolana Proxy:", forwarderSolanaAddress); + } catch (error) { + console.log("Error deploying ForwarderSolana:", error); + } + deployUtils.addresses.startBlock = (deployUtils.addresses.startBlock ? deployUtils.addresses.startBlock diff --git a/hardhat-scripts/deploy/3.configureChains.ts b/hardhat-scripts/deploy/3.configureChains.ts index 7c6a65de..4cc22acd 100644 --- a/hardhat-scripts/deploy/3.configureChains.ts +++ b/hardhat-scripts/deploy/3.configureChains.ts @@ -168,16 +168,40 @@ async function setOnchainContracts( signer ); - // await updateContractSettings( - // EVMX_CHAIN_ID, - // Contracts.Configurations, - // "switchboards", - // [chain, CCTP_SWITCHBOARD_TYPE], - // cctpSwitchboardId, - // "setSwitchboard", - // [chain, CCTP_SWITCHBOARD_TYPE, cctpSwitchboardId], - // signer - // ); + + // TODO: this was commented out if fee-deposit-hook but it is in old solana deployment scheme + await updateContractSettings( + EVMX_CHAIN_ID, + Contracts.Configurations, + "switchboards", + [chain, CCTP_SWITCHBOARD_TYPE], + cctpSwitchboardId, + "setSwitchboard", + [chain, CCTP_SWITCHBOARD_TYPE, cctpSwitchboardId], + signer + ); + console.log("XXX Setting solana switchboard"); + console.log("FAST_SWITCHBOARD_TYPE: ", FAST_SWITCHBOARD_TYPE); + const solanaSwitchboard = process.env.SWITCHBOARD_SOLANA; + if (!solanaSwitchboard) throw new Error("SWITCHBOARD_SOLANA is not set"); + console.log( + "solanaSwitchboard as bytes32 reversed: ", + Buffer.from(toBytes32Format(solanaSwitchboard)).toString("hex") + ); + await updateContractSettings( + EVMX_CHAIN_ID, + Contracts.Configurations, + "switchboards", + [ChainSlug.SOLANA_DEVNET, FAST_SWITCHBOARD_TYPE], + solanaSwitchboard, + "setSwitchboard", + [ + ChainSlug.SOLANA_DEVNET, + FAST_SWITCHBOARD_TYPE, + toBytes32Format(solanaSwitchboard), + ], + signer + ); await updateContractSettings( EVMX_CHAIN_ID, diff --git a/hardhat-scripts/deploy/6.connect.ts b/hardhat-scripts/deploy/6.connect.ts index 460b4b6f..af8db267 100644 --- a/hardhat-scripts/deploy/6.connect.ts +++ b/hardhat-scripts/deploy/6.connect.ts @@ -14,6 +14,7 @@ import { import { getWatcherSigner, sendWatcherMultiCallWithNonce } from "../utils/sign"; import { isConfigSetOnEVMx, isConfigSetOnSocket } from "../utils"; import pLimit from "p-limit"; +import { mockForwarderSolanaOnChainAddress32Bytes } from "./1.deploy"; const plugs = [ Contracts.ContractFactoryPlug, @@ -172,6 +173,38 @@ export const updateConfigEVMx = async () => { }) ); + //TODO:GW: This is a temporary workaround for th Solana POC + //--- + const appGatewayAddress = process.env.APP_GATEWAY; + if (!appGatewayAddress) throw new Error("APP_GATEWAY is not set"); + const solanaSwitchboard = process.env.SWITCHBOARD_SOLANA!.slice(2); // remove 0x prefix for Buffer from conversion + if (!solanaSwitchboard) throw new Error("SWITCHBOARD_SOLANA is not set"); + + const solanaSwitchboardBytes32 = Buffer.from(solanaSwitchboard, "hex"); + const solanaAppGatewayId = ethers.utils.hexZeroPad(appGatewayAddress, 32); + + console.log("SolanaAppGatewayId: ", solanaAppGatewayId); + console.log( + "SolanaSwitchboardBytes32: ", + solanaSwitchboardBytes32.toString("hex") + ); + + appConfigs.push({ + plugConfig: { + appGatewayId: solanaAppGatewayId, + switchboard: "0x" + solanaSwitchboardBytes32.toString("hex"), + }, + plug: "0x" + mockForwarderSolanaOnChainAddress32Bytes.toString("hex"), + chainSlug: ChainSlug.SOLANA_DEVNET, + }); + // appConfigs.push({ + // plug: "0x" + mockForwarderSolanaOnChainAddress32Bytes.toString("hex"), + // appGatewayId: solanaAppGatewayId, + // switchboard: "0x" + solanaSwitchboardBytes32.toString("hex"), + // chainSlug: ChainSlug.SOLANA_DEVNET, + // }); + //--- + // Update configs if any changes needed if (appConfigs.length > 0) { console.log({ appConfigs }); diff --git a/hardhat.config.ts b/hardhat.config.ts index b005a364..516882f1 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -281,7 +281,7 @@ const config: HardhatUserConfig = { evmVersion: "paris", optimizer: { enabled: true, - runs: 1, + runs: 200, details: { yul: true, yulDetails: { @@ -289,7 +289,7 @@ const config: HardhatUserConfig = { }, }, }, - viaIR: false, + viaIR: true, }, }, }; diff --git a/script/super-token-solana/DeployEVMSolanaApps.s.sol b/script/super-token-solana/DeployEVMSolanaApps.s.sol new file mode 100644 index 00000000..4bfef359 --- /dev/null +++ b/script/super-token-solana/DeployEVMSolanaApps.s.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {EvmSolanaAppGateway} from "../../test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol"; +import {SuperTokenAppGateway} from "../../test/apps/app-gateways/super-token/SuperTokenAppGateway.sol"; +import {ETH_ADDRESS} from "../../contracts/utils/common/Constants.sol"; +import {ForwarderSolana} from "../../contracts/evmx/helpers/ForwarderSolana.sol"; +import {AddressResolver} from "../../contracts/evmx/helpers/AddressResolver.sol"; + +// source .env && forge script script/counter/deployEVMxCounterApp.s.sol --broadcast --skip-simulation --legacy --gas-price 0 +contract DeployEVMSolanaApps is Script { + function run() external { + address addressResolver = vm.envAddress("ADDRESS_RESOLVER"); + // address owner = vm.envAddress("OWNER"); + address owner = vm.envAddress("SENDER_ADDRESS"); // TODO: what address should be used here?– + string memory rpc = vm.envString("EVMX_RPC"); + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // fill with correct values after deployment + bytes32 solanaProgramId = vm.envBytes32("SOLANA_TARGET_PROGRAM"); + address forwarderSolanaAddress = 0x9bBb97c66be06458A63D11ff6025Fdad104DA285; + + // Setting fee payment on Arbitrum Sepolia + uint256 fees = 10 ether; + + EvmSolanaAppGateway gateway = new EvmSolanaAppGateway( + owner, + fees, + EvmSolanaAppGateway.SuperTokenEvmConstructorParams({ + name_: "SuperToken-Evm", + symbol_: "SUPER", + decimals_: 6, + initialSupplyHolder_: owner, + initialSupply_: 100000000000 + }), + solanaProgramId, + forwarderSolanaAddress, + addressResolver + ); + + console.log("Contracts deployed:"); + console.log("EvmSolanaAppGateway:", address(gateway)); + console.log("solanaProgramId:"); + console.logBytes32(solanaProgramId); + console.log("forwarderSolanaAddress:"); + console.logAddress(forwarderSolanaAddress); + + console.log("Forwarder Solana address resolver:"); + console.log(address(ForwarderSolana(forwarderSolanaAddress).addressResolver__())); + console.log("ForwarderSolana chain slug:"); + console.log(ForwarderSolana(forwarderSolanaAddress).chainSlug()); + console.log("ForwarderSolana onChainAddress:"); + console.logBytes32(ForwarderSolana(forwarderSolanaAddress).onChainAddress()); + + console.log("Address resolver from vars:"); + console.log(addressResolver); + + // console.log("Address resolver owner:"); + // console.log(AddressResolver(address(ForwarderSolana(forwarderSolanaAddress).addressResolver__())).owner()); + } +} diff --git a/script/super-token-solana/EvmSolanaOnchainCalls.s.sol b/script/super-token-solana/EvmSolanaOnchainCalls.s.sol new file mode 100644 index 00000000..6bc06955 --- /dev/null +++ b/script/super-token-solana/EvmSolanaOnchainCalls.s.sol @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import { + ETH_ADDRESS, + TOKEN_ACCOUNT, + MINT_ACCOUNT, + CHAIN_SLUG_SOLANA_DEVNET +} from "../../contracts/utils/common/Constants.sol"; +import {EvmSolanaAppGateway} from "../../test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol"; +import { + SolanaInstruction, + SolanaInstructionData, + SolanaInstructionDataDescription, + SolanaReadRequest, + SolanaReadSchemaType, + GenericSchema +} from "../../contracts/utils/common/Structs.sol"; + + +// source .env && forge script script/counter/EvmSolanaOnchainCalls.s.sol --broadcast --skip-simulation --legacy --gas-price 0 +contract EvmSolanaOnchainCalls is Script { + function run() external { + string memory rpc = vm.envString("EVMX_RPC"); + console.log(rpc); + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + EvmSolanaAppGateway appGateway = EvmSolanaAppGateway(vm.envAddress("APP_GATEWAY")); + bytes32 switchboardSolana = vm.envBytes32("SWITCHBOARD_SOLANA"); + address userEvmAddress = vm.envAddress("EVM_TEST_ACCOUNT"); + // bytes32 solanaTargetProgramId = vm.envBytes32("SOLANA_TARGET_PROGRAM"); + + console.log("EvmSolanaAppGateway:", address(appGateway)); + console.log("Switchboard solana:"); + console.logBytes32(switchboardSolana); + console.log("User address: ", userEvmAddress); + + // TODO:GW: fix this so that we can directly use the plug address (changes needed in socket/super-token) + // plug signer pda : aprk6EUeRofYzyABHXLKG8hqJ3GHGV7Uhzbu612UMBD + bytes32 solana_plug_signer = 0x08aa478d7031ac31e2a406e25f1e4dbd00bce5cbd428b7bf5e5b01abfd4b7da8; + // appGateway.setIsValidPlug(CHAIN_SLUG_SOLANA_DEVNET, solanaTargetProgramId); + appGateway.setIsValidPlugForSolana(true, CHAIN_SLUG_SOLANA_DEVNET, solana_plug_signer); + // allow super-token to trigger AppGateway + // appGateway.setIsValidPlug(CHAIN_SLUG_SOLANA_DEVNET, solana_plug_signer); + + uint256 srcAmount = 1000000; + // mintOnEvm(srcAmount, userEvmAddress, appGateway); + mintOnSolana(srcAmount, userEvmAddress, appGateway); + // transferEvmToSolana(srcAmount, userEvmAddress, appGateway); + // readSolanaSuperTokenConfigAccount(appGateway); + // readSolanaTokenAccount(appGateway); + } + + function transferEvmToSolana( + uint256 srcAmount, + address userEvmAddress, + EvmSolanaAppGateway appGateway + ) public { + console.log("Transfer EVM to Solana"); + + EvmSolanaAppGateway.TransferOrderEvmToSolana memory order = EvmSolanaAppGateway + .TransferOrderEvmToSolana({ + srcEvmToken: 0x2A159f24E2562E5874550BE4702CAC3eAe288411, // Forwarder(!!) for Super-token contract on given chain + // mint on local-testnet: BdUzPsaAicEWinR7b14YLtvavwM8zYn8BaHKqGQ8by2q + dstSolanaToken: 0x9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a, + userEvm: userEvmAddress, + // alice super token ata: LVuCmGaoHjAGu54dFppzujS1Ti61CBac57taeQbokUr + destUserTokenAddress: 0x04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb, + srcAmount: srcAmount, + deadline: 1715702400 + }); + + SolanaInstruction memory solanaInstruction = buildMintTokenSolanaInstruction(order); + + bytes memory orderEncoded = abi.encode(order); + + appGateway.transfer(orderEncoded, solanaInstruction); + } + + function mintOnEvm( + uint256 srcAmount, + address userEvmAddress, + EvmSolanaAppGateway appGateway + ) public { + console.log("Mint on EVM"); + + bytes memory order = abi.encode( + EvmSolanaAppGateway.TransferOrderEvmToSolana({ + srcEvmToken: 0x2A159f24E2562E5874550BE4702CAC3eAe288411, // Forwarder(!!) for Super-token contract on given chain + dstSolanaToken: 0x9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a, // irrelevant for EVM minting + userEvm: userEvmAddress, + destUserTokenAddress: 0x04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb, // irrelevant for EVM minting + srcAmount: srcAmount, + deadline: 1715702400 + }) + ); + + EvmSolanaAppGateway.TransferOrderEvmToSolana memory orderObj = abi.decode( + order, + (EvmSolanaAppGateway.TransferOrderEvmToSolana) + ); + console.log("Order srcEvmToken:", orderObj.srcEvmToken); + console.log("Order userEvm:", orderObj.userEvm); + console.log("Order srcAmount:", orderObj.srcAmount); + + appGateway.mintSuperTokenEvm(order); + } + + function mintOnSolana( + uint256 srcAmount, + address userEvmAddress, + EvmSolanaAppGateway appGateway + ) public { + console.log("Mint on Solana"); + + string[] memory returnDataValuesTypeNames = new string[](1); + returnDataValuesTypeNames[0] = "Vec"; + + GenericSchema memory returnDataSchema = GenericSchema({ + valuesTypeNames: returnDataValuesTypeNames + }); + + SolanaInstruction memory solanaInstruction = buildMintTokenSolanaInstruction( + EvmSolanaAppGateway.TransferOrderEvmToSolana({ + srcEvmToken: 0xD4a20b34D0dE11e3382Aaa7E0839844f154B6191, + // mint on local-testnet: BdUzPsaAicEWinR7b14YLtvavwM8zYn8BaHKqGQ8by2q + dstSolanaToken: 0x9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a, + userEvm: userEvmAddress, + // alice super token ata: LVuCmGaoHjAGu54dFppzujS1Ti61CBac57taeQbokUr + destUserTokenAddress: 0x04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb, + srcAmount: srcAmount, + deadline: 1715702400 + }) + ); + + appGateway.mintSuperTokenSolana(solanaInstruction, returnDataSchema); + } + + function readSolanaTokenAccount(EvmSolanaAppGateway appGateway) public { + console.log("Read token account from Solana"); + + // put here token account address to be read + // alice super token ata: LVuCmGaoHjAGu54dFppzujS1Ti61CBac57taeQbokUr + bytes32 accountToRead = 0x04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb; + bytes32 schemaNameHash = TOKEN_ACCOUNT; + + SolanaReadRequest memory readRequest = buildSolanaReadRequestPredefined(accountToRead, schemaNameHash); + + appGateway.readTokenAccount(readRequest); + } + + function readSolanaSuperTokenConfigAccount(EvmSolanaAppGateway appGateway) public { + console.log("Read generic account from Solana"); + + // superTokenConfigPda : GfNcT3X72r8Cmy2itGCNPUrigsoPvTLH4vy3qaYkSqHx + bytes32 accountToRead = 0xe8b3cf7c50f0b707dba43ef8042a34802d4f1768c72798bafa24d741c89f9ccf; + + // TODO:GW: All types recognizable by BorshEncoder must be placed in the constants to avoid hardcoding and confusion with lower/upper case + string[] memory valuesTypeNames = new string[](5); + valuesTypeNames[0] = "[u8;8]"; // account discriminator + valuesTypeNames[1] = "[u8;32]"; // owner + valuesTypeNames[2] = "[u8;32]"; // socket + valuesTypeNames[3] = "[u8;32]"; // mint + valuesTypeNames[4] = "u8"; // bump + + GenericSchema memory genericSchema = GenericSchema({ + valuesTypeNames: valuesTypeNames + }); + + SolanaReadRequest memory readRequest = buildSolanaReadRequestGeneric(accountToRead); + + appGateway.readSuperTokenConfigAccount(readRequest, genericSchema); + } + + function invokeTrigger( + EvmSolanaAppGateway appGateway + ) public { + console.log("Invoke on Solana"); + + SolanaInstruction memory solanaInstruction = buildTriggerTestSolanaInstruction(); + + appGateway.triggerTestSuperTokenSolana(solanaInstruction); + } + + /*************** builder functions ***************/ + + function buildMintTokenSolanaInstruction( + EvmSolanaAppGateway.TransferOrderEvmToSolana memory order + ) internal view returns (SolanaInstruction memory) { + bytes32 solanaTargetProgramId = vm.envBytes32("SOLANA_TARGET_PROGRAM"); + + // May be subject to change + bytes32[] memory accounts = new bytes32[](6); + // -- start here we are missing tmp_data account + // accounts 0 - tmpData pda : EgDiv7JoLPY6CJgfkFKjxsDB6cxC7k9sdS9Baehp1c6L + accounts[0] = 0xcb33f7992e094d6c65e0e95cf54e70c3470840e69d739dfcfcf2e1805fc913d1; + // accounts 1 - superTokenConfigPda : GfNcT3X72r8Cmy2itGCNPUrigsoPvTLH4vy3qaYkSqHx + accounts[1] = 0xe8b3cf7c50f0b707dba43ef8042a34802d4f1768c72798bafa24d741c89f9ccf; + // accounts 2 - mint account + accounts[2] = order.dstSolanaToken; + // accounts 3 - destination user ata + accounts[3] = order.destUserTokenAddress; + // accounts 4 - system programId: 11111111111111111111111111111111 + accounts[4] = 0x0000000000000000000000000000000000000000000000000000000000000000; + // accounts 5 - token programId: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + accounts[5] = 0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9; + + bytes1[] memory accountFlags = new bytes1[](6); + // tmpData pda is writable + accountFlags[0] = bytes1(0x01); // true + // superTokenConfigPda is not writable + accountFlags[1] = bytes1(0x00); // false + // mint is writable + accountFlags[2] = bytes1(0x01); // true + // destination user ata is writable + accountFlags[3] = bytes1(0x01); // true + // system programId is not writable + accountFlags[4] = bytes1(0x00); // false + // token programId is not writable + accountFlags[5] = bytes1(0x00); // false + + // mint instruction discriminator + bytes8 instructionDiscriminator = 0x3339e12fb69289a6; + + bytes[] memory functionArguments = new bytes[](1); + // TODO:GW: in watcher and transmitter we might need to convert this value if on Solana mint has different decimals, for now we assume that both are the same + functionArguments[0] = abi.encode(order.srcAmount); + + string[] memory functionArgumentTypeNames = new string[](1); + functionArgumentTypeNames[0] = "u64"; + + return + SolanaInstruction({ + data: SolanaInstructionData({ + programId: solanaTargetProgramId, + instructionDiscriminator: instructionDiscriminator, + accounts: accounts, + functionArguments: functionArguments + }), + description: SolanaInstructionDataDescription({ + accountFlags: accountFlags, + functionArgumentTypeNames: functionArgumentTypeNames + }) + }); + } + + function buildTriggerTestSolanaInstruction() internal view returns (SolanaInstruction memory) { + bytes32 solanaTargetProgramId = vm.envBytes32("SOLANA_TARGET_PROGRAM"); + + // May be subject to change + bytes32[] memory accounts = new bytes32[](4); + // accounts 0 - plug signer pda : aprk6EUeRofYzyABHXLKG8hqJ3GHGV7Uhzbu612UMBD + accounts[0] = 0x08aa478d7031ac31e2a406e25f1e4dbd00bce5cbd428b7bf5e5b01abfd4b7da8; + // accounts 1 - trigger counter pda : 4EYucSHVdCt5BK9N95peyVCi7weQTBoW5Tf3a2nQbRbf + accounts[1] = 0x300bb0522a1ad67d5c5d8fe3102e0ac9b05f04436d4831609d158067b5bd4cda; + // accounts 2 - socket programId: 9vFEQ5e3xf4eo17WttfqmXmnqN3gUicrhFGppmmNwyqV + accounts[2] = 0x84815e8ca2f6dad7e12902c39a51bc72e13c48139b4fb10025d94e7abea2969c; + // accounts 3 - system programId: 11111111111111111111111111111111 + accounts[3] = 0x0000000000000000000000000000000000000000000000000000000000000000; + + bytes1[] memory accountFlags = new bytes1[](4); + // plug signer is not writable + accountFlags[0] = bytes1(0x00); // false + // trigger counter is writable + accountFlags[1] = bytes1(0x01); // true + // socket programId is not writable + accountFlags[2] = bytes1(0x00); // false + // system programId is not writable + accountFlags[3] = bytes1(0x00); // false + + // trigger_test instruction discriminator + bytes8 instructionDiscriminator = 0x4058d836fd208fb1; + + bytes[] memory functionArguments = new bytes[](0); + + string[] memory functionArgumentTypeNames = new string[](0); + + return + SolanaInstruction({ + data: SolanaInstructionData({ + programId: solanaTargetProgramId, + instructionDiscriminator: instructionDiscriminator, + accounts: accounts, + functionArguments: functionArguments + }), + description: SolanaInstructionDataDescription({ + accountFlags: accountFlags, + functionArgumentTypeNames: functionArgumentTypeNames + }) + }); + } + + function buildSolanaReadRequestPredefined(bytes32 accountToRead, bytes32 schemaNameHash) internal pure returns (SolanaReadRequest memory) { + SolanaReadRequest memory readRequest = SolanaReadRequest({ + schemaType: SolanaReadSchemaType.PREDEFINED, + accountToRead: accountToRead, + predefinedSchemaNameHash: schemaNameHash + }); + return readRequest; + } + + function buildSolanaReadRequestGeneric(bytes32 accountToRead) internal pure returns (SolanaReadRequest memory) { + SolanaReadRequest memory readRequest = SolanaReadRequest({ + schemaType: SolanaReadSchemaType.GENERIC, + accountToRead: accountToRead, + predefinedSchemaNameHash: bytes32(0) + }); + return readRequest; + } + + + /*************** experimental / testing ***************/ + + function buildSolanaInstructionTest( + EvmSolanaAppGateway.TransferOrderEvmToSolana memory order + ) internal view returns (SolanaInstruction memory) { + bytes32 solanaTargetProgramId = vm.envBytes32("SOLANA_TARGET_PROGRAM"); + + // May be subject to change + bytes32[] memory accounts = new bytes32[](5); + // accounts 0 - superTokenConfigPda : GfNcT3X72r8Cmy2itGCNPUrigsoPvTLH4vy3qaYkSqHx + accounts[0] = 0xe8b3cf7c50f0b707dba43ef8042a34802d4f1768c72798bafa24d741c89f9ccf; + // accounts 1 - mint account + accounts[1] = order.dstSolanaToken; + // accounts 2 - destination user ata + accounts[2] = order.destUserTokenAddress; + // accounts 3 - system programId: 11111111111111111111111111111111 + accounts[3] = 0x0000000000000000000000000000000000000000000000000000000000000000; + // accounts 4 - token programId: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + accounts[4] = 0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9; + + bytes[] memory functionArguments = new bytes[](3); + functionArguments[0] = abi.encode(order.srcAmount); + uint256[] memory array = new uint256[](100); + functionArguments[1] = abi.encode(array); + ComplexTestStruct memory complexTestStruct = ComplexTestStruct({ + name: "test", + addr: 0x1234567890123456789012345678901234567890123456789012345678901234, + isActive: true, + value: 100 + }); + functionArguments[2] = abi.encode(complexTestStruct); + + string[] memory functionArgumentTypeNames = new string[](3); + functionArgumentTypeNames[0] = "u64"; + functionArgumentTypeNames[1] = "[u64;100]"; + functionArgumentTypeNames[2] = + '{"ComplexTestStruct": {"name": "string","addr": "[u8;32]","isActive": "boolean","value": "u64"}}'; + + bytes1[] memory accountFlags = new bytes1[](5); + // superTokenConfigPda is not writable + accountFlags[0] = bytes1(0x00); // false + // mint is writable + accountFlags[1] = bytes1(0x01); // true + // destination user ata is writable + accountFlags[2] = bytes1(0x01); // true + // system programId is not writable + accountFlags[3] = bytes1(0x00); // false + // token programId is not writable + accountFlags[4] = bytes1(0x00); // false + + // mint instruction discriminator + bytes8 instructionDiscriminator = 0x3339e12fb69289a6; + + return + SolanaInstruction({ + data: SolanaInstructionData({ + programId: solanaTargetProgramId, + instructionDiscriminator: instructionDiscriminator, + accounts: accounts, + functionArguments: functionArguments + }), + description: SolanaInstructionDataDescription({ + accountFlags: accountFlags, + functionArgumentTypeNames: functionArgumentTypeNames + }) + }); + } + + struct ComplexTestStruct { + string name; + bytes32 addr; + bool isActive; + uint256 value; + } +} diff --git a/src/enums.ts b/src/enums.ts index 487ad142..1cb5a746 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -77,6 +77,7 @@ export enum Contracts { AsyncDeployer = "AsyncDeployer", DeployForwarder = "DeployForwarder", Forwarder = "Forwarder", + ForwarderSolana = "ForwarderSolana", } export enum CallTypeNames { diff --git a/test/BorshDecoderTest.t.sol b/test/BorshDecoderTest.t.sol new file mode 100644 index 00000000..3b7e01ad --- /dev/null +++ b/test/BorshDecoderTest.t.sol @@ -0,0 +1,836 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import {BorshEncoder} from "../contracts/evmx/watcher/borsh-serde/BorshEncoder.sol"; +import {BorshDecoder} from "../contracts/evmx/watcher/borsh-serde/BorshDecoder.sol"; +import "../contracts/utils/common/Structs.sol"; +import "../contracts/utils/common/Constants.sol"; +import "forge-std/console.sol"; + +contract BorshDecoderTest is Test { + using BorshDecoder for BorshDecoder.Data; + + + function testPredefinedSchemaHash() public pure { + console.log("TOKEN_ACCOUNT"); + console.logBytes32(TOKEN_ACCOUNT); + console.log("MINT_ACCOUNT"); + console.logBytes32(MINT_ACCOUNT); + } + + /** Test primitive type decoding **/ + + function testDecodeU8() public pure { + uint8 originalValue = 42; + bytes1 encoded = BorshEncoder.encodeU8(originalValue); + + BorshDecoder.Data memory data = BorshDecoder.from(abi.encodePacked(encoded)); + uint8 decoded = data.decodeU8(); + + assertEq(decoded, originalValue); + } + + function testDecodeU16() public pure { + uint16 originalValue = 0x1234; + bytes2 encoded = BorshEncoder.encodeU16(originalValue); + + BorshDecoder.Data memory data = BorshDecoder.from(abi.encodePacked(encoded)); + uint16 decoded = data.decodeU16(); + + assertEq(decoded, originalValue); + } + + function testDecodeU32() public pure { + uint32 originalValue = 0x12345678; + bytes4 encoded = BorshEncoder.encodeU32(originalValue); + + BorshDecoder.Data memory data = BorshDecoder.from(abi.encodePacked(encoded)); + uint32 decoded = data.decodeU32(); + + assertEq(decoded, originalValue); + } + + function testDecodeU64() public pure { + uint64 originalValue = 0x123456789abcdef0; + bytes8 encoded = BorshEncoder.encodeU64(originalValue); + + BorshDecoder.Data memory data = BorshDecoder.from(abi.encodePacked(encoded)); + uint64 decoded = data.decodeU64(); + + assertEq(decoded, originalValue); + } + + function testDecodeU128() public pure { + uint128 originalValue = 0x123456789abcdef0fedcba9876543210; + bytes16 encoded = BorshEncoder.encodeU128(originalValue); + + BorshDecoder.Data memory data = BorshDecoder.from(abi.encodePacked(encoded)); + uint128 decoded = data.decodeU128(); + + assertEq(decoded, originalValue); + } + + function testDecodeString() public pure { + string memory originalValue = "hello world"; + bytes memory encoded = BorshEncoder.encodeString(originalValue); + + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + string memory decoded = data.decodeString(); + + assertEq(decoded, originalValue); + } + + function testDecodeStringEmpty() public pure { + string memory originalValue = ""; + bytes memory encoded = BorshEncoder.encodeString(originalValue); + + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + string memory decoded = data.decodeString(); + + assertEq(decoded, originalValue); + } + + function testDecodeStringSpecialChars() public pure { + string memory originalValue = "0.1.0"; + bytes memory encoded = BorshEncoder.encodeString(originalValue); + + console.log("encoded 0.1.0"); + console.logBytes(encoded); + + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + string memory decoded = data.decodeString(); + + assertEq(decoded, originalValue); + } + + /** Test Vector type decoding **/ + + function testDecodeUint8Vec() public pure { + uint8[] memory originalValues = new uint8[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint8Vec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, uint8[] memory decoded) = data.decodeUint8Vec(); + + assertEq(length, originalValues.length); + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint8VecEmpty() public pure { + uint8[] memory originalValues = new uint8[](0); + + bytes memory encoded = BorshEncoder.encodeUint8Vec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, uint8[] memory decoded) = data.decodeUint8Vec(); + + assertEq(length, 0); + assertEq(decoded.length, 0); + } + + function testDecodeUint8VecLarge() public pure { + uint8[] memory originalValues = new uint8[](255); + for (uint256 i = 0; i < 255; i++) { + originalValues[i] = uint8(i); + } + + bytes memory encoded = BorshEncoder.encodeUint8Vec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, uint8[] memory decoded) = data.decodeUint8Vec(); + + assertEq(length, originalValues.length); + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint16Vec() public pure { + uint16[] memory originalValues = new uint16[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint16Vec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, uint16[] memory decoded) = data.decodeUint16Vec(); + + assertEq(length, originalValues.length); + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint32Vec() public pure { + uint32[] memory originalValues = new uint32[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint32Vec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, uint32[] memory decoded) = data.decodeUint32Vec(); + + assertEq(length, originalValues.length); + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint64Vec() public pure { + uint64[] memory originalValues = new uint64[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint64Vec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, uint64[] memory decoded) = data.decodeUint64Vec(); + + assertEq(length, originalValues.length); + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint128Vec() public pure { + uint128[] memory originalValues = new uint128[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint128Vec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, uint128[] memory decoded) = data.decodeUint128Vec(); + + assertEq(length, originalValues.length); + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeStringVec() public pure { + string[] memory originalValues = new string[](3); + originalValues[0] = "hello"; + originalValues[1] = "world"; + originalValues[2] = "test"; + + bytes memory encoded = BorshEncoder.encodeStringVec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, string[] memory decoded) = data.decodeStringVec(); + + assertEq(length, originalValues.length); + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeStringVecEmpty() public pure { + string[] memory originalValues = new string[](0); + + bytes memory encoded = BorshEncoder.encodeStringVec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, string[] memory decoded) = data.decodeStringVec(); + + assertEq(length, 0); + assertEq(decoded.length, 0); + } + + function testDecodeStringVecWithEmptyStrings() public pure { + string[] memory originalValues = new string[](3); + originalValues[0] = ""; + originalValues[1] = "hello"; + originalValues[2] = ""; + + bytes memory encoded = BorshEncoder.encodeStringVec(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + (uint32 length, string[] memory decoded) = data.decodeStringVec(); + + assertEq(length, originalValues.length); + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + /** Test Array type decoding **/ + + function testDecodeUint8Array() public pure { + uint8[] memory originalValues = new uint8[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint8Array(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + uint8[] memory decoded = data.decodeUint8Array(3); + + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint8ArrayEmpty() public pure { + uint8[] memory originalValues = new uint8[](0); + + bytes memory encoded = BorshEncoder.encodeUint8Array(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + uint8[] memory decoded = data.decodeUint8Array(0); + + assertEq(decoded.length, 0); + } + + function testDecodeUint8ArrayLarge() public pure { + uint8[] memory originalValues = new uint8[](100); + for (uint256 i = 0; i < 100; i++) { + originalValues[i] = uint8(i % 256); + } + + bytes memory encoded = BorshEncoder.encodeUint8Array(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + uint8[] memory decoded = data.decodeUint8Array(100); + + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint16Array() public pure { + uint16[] memory originalValues = new uint16[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint16Array(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + uint16[] memory decoded = data.decodeUint16Array(3); + + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint32Array() public pure { + uint32[] memory originalValues = new uint32[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint32Array(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + uint32[] memory decoded = data.decodeUint32Array(3); + + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint64Array() public pure { + uint64[] memory originalValues = new uint64[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint64Array(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + uint64[] memory decoded = data.decodeUint64Array(3); + + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeUint128Array() public pure { + uint128[] memory originalValues = new uint128[](3); + originalValues[0] = 1; + originalValues[1] = 2; + originalValues[2] = 3; + + bytes memory encoded = BorshEncoder.encodeUint128Array(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + uint128[] memory decoded = data.decodeUint128Array(3); + + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeStringArray() public pure { + string[] memory originalValues = new string[](3); + originalValues[0] = "hello"; + originalValues[1] = "world"; + originalValues[2] = "test"; + + bytes memory encoded = BorshEncoder.encodeStringArray(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + string[] memory decoded = data.decodeStringArray(3); + + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + function testDecodeStringArrayEmpty() public pure { + string[] memory originalValues = new string[](0); + + bytes memory encoded = BorshEncoder.encodeStringArray(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + string[] memory decoded = data.decodeStringArray(0); + + assertEq(decoded.length, 0); + } + + function testDecodeStringArrayWithEmptyStrings() public pure { + string[] memory originalValues = new string[](2); + originalValues[0] = ""; + originalValues[1] = "test"; + + bytes memory encoded = BorshEncoder.encodeStringArray(originalValues); + BorshDecoder.Data memory data = BorshDecoder.from(encoded); + string[] memory decoded = data.decodeStringArray(2); + + assertEq(decoded.length, originalValues.length); + for (uint256 i = 0; i < decoded.length; i++) { + assertEq(decoded[i], originalValues[i]); + } + } + + /** Test GenericSchema decoding **/ + + function testDecodeGenericSchemaPrimitives() public pure { + GenericSchema memory schema; + schema.valuesTypeNames = new string[](5); + schema.valuesTypeNames[0] = "u8"; + schema.valuesTypeNames[1] = "u16"; + schema.valuesTypeNames[2] = "u32"; + schema.valuesTypeNames[3] = "u64"; + schema.valuesTypeNames[4] = "u128"; + + // Encode test data + bytes memory encodedData = abi.encodePacked( + BorshEncoder.encodeU8(42), + BorshEncoder.encodeU16(1234), + BorshEncoder.encodeU32(0x12345678), + BorshEncoder.encodeU64(0x123456789abcdef0), + BorshEncoder.encodeU128(0x123456789abcdef0fedcba9876543210) + ); + + bytes[] memory decodedParams = BorshDecoder.decodeGenericSchema(schema, encodedData); + + assertEq(decodedParams.length, 5); + + // Check decoded values + uint8 decodedU8 = abi.decode(decodedParams[0], (uint8)); + assertEq(decodedU8, 42); + + uint16 decodedU16 = abi.decode(decodedParams[1], (uint16)); + assertEq(decodedU16, 1234); + + uint32 decodedU32 = abi.decode(decodedParams[2], (uint32)); + assertEq(decodedU32, 0x12345678); + + uint64 decodedU64 = abi.decode(decodedParams[3], (uint64)); + assertEq(decodedU64, 0x123456789abcdef0); + + uint128 decodedU128 = abi.decode(decodedParams[4], (uint128)); + assertEq(decodedU128, 0x123456789abcdef0fedcba9876543210); + } + + function testDecodeGenericSchemaVectors() public pure { + GenericSchema memory schema; + schema.valuesTypeNames = new string[](1); + schema.valuesTypeNames[0] = "Vec"; + + // Prepare test data + uint8[] memory u8Values = new uint8[](3); + u8Values[0] = 1; + u8Values[1] = 2; + u8Values[2] = 3; + + // Encode test data + bytes memory encodedData = BorshEncoder.encodeUint8Vec(u8Values); + + bytes[] memory decodedParams = BorshDecoder.decodeGenericSchema(schema, encodedData); + + assertEq(decodedParams.length, 1); + + // Check decoded u8 vector + uint8[] memory decodedU8Vec = abi.decode(decodedParams[0], (uint8[])); + assertEq(decodedU8Vec.length, 3); + assertEq(decodedU8Vec[0], 1); + assertEq(decodedU8Vec[1], 2); + assertEq(decodedU8Vec[2], 3); + } + + function testDecodeGenericSchemaArrays() public pure { + GenericSchema memory schema; + schema.valuesTypeNames = new string[](2); + schema.valuesTypeNames[0] = "[u8; 3]"; + schema.valuesTypeNames[1] = "[u16; 2]"; + + // Prepare test data + uint8[] memory u8Values = new uint8[](3); + u8Values[0] = 1; + u8Values[1] = 2; + u8Values[2] = 3; + + uint16[] memory u16Values = new uint16[](2); + u16Values[0] = 1000; + u16Values[1] = 2000; + + // console.log("u8Values"); + // console.logBytes(BorshEncoder.encodeUint8Array(u8Values)); + + // console.log("u16Values"); + // console.logBytes(BorshEncoder.encodeUint16Array(u16Values)); + + // Encode test data + bytes memory encodedData = abi.encodePacked( + BorshEncoder.encodeUint8Array(u8Values), + BorshEncoder.encodeUint16Array(u16Values) + ); + + // console.log("encodedData"); + // console.logBytes(encodedData); + + // console.log("decode data"); + + bytes[] memory decodedParams = BorshDecoder.decodeGenericSchema(schema, encodedData); + + assertEq(decodedParams.length, 2); + + // console.log("decodedParams[0]"); + // console.logBytes(decodedParams[0]); + + // Check decoded u8 array + uint8[] memory decodedU8Array = abi.decode(decodedParams[0], (uint8[])); + assertEq(decodedU8Array.length, 3); + assertEq(decodedU8Array[0], 1); + assertEq(decodedU8Array[1], 2); + assertEq(decodedU8Array[2], 3); + + // console.log("decodedParams[1]"); + // console.logBytes(decodedParams[1]); + + // Check decoded u16 array + uint16[] memory decodedU16Array = abi.decode(decodedParams[1], (uint16[])); + assertEq(decodedU16Array.length, 2); + assertEq(decodedU16Array[0], 1000); + assertEq(decodedU16Array[1], 2000); + } + + function testDecodeGenericSchemaComplex() public pure { + GenericSchema memory schema; + schema.valuesTypeNames = new string[](6); + schema.valuesTypeNames[0] = "u8"; + schema.valuesTypeNames[1] = "u32"; + schema.valuesTypeNames[2] = "u64"; + schema.valuesTypeNames[3] = "u64"; + schema.valuesTypeNames[4] = "[u8; 4]"; + schema.valuesTypeNames[5] = "[u32; 10]"; + + // Prepare test data + uint8 u8Value = 42; + uint32 u32Value = 0x12345678; + uint64 u64Value1 = 0x123456789abcdef0; + uint64 u64Value2 = 0xfedcba9876543210; + + uint8[] memory u8Array = new uint8[](4); + u8Array[0] = 10; + u8Array[1] = 20; + u8Array[2] = 30; + u8Array[3] = 40; + + uint32[] memory u32Array = new uint32[](10); + for (uint256 i = 0; i < 10; i++) { + u32Array[i] = uint32(1000 + i * 100); // 1000, 1100, 1200, ..., 1900 + } + + // Encode test data + bytes memory encodedData = abi.encodePacked( + BorshEncoder.encodeU8(u8Value), + BorshEncoder.encodeU32(u32Value), + BorshEncoder.encodeU64(u64Value1), + BorshEncoder.encodeU64(u64Value2), + BorshEncoder.encodeUint8Array(u8Array), + BorshEncoder.encodeUint32Array(u32Array) + ); + + // Decode using GenericSchema + bytes[] memory decodedParams = BorshDecoder.decodeGenericSchema(schema, encodedData); + + assertEq(decodedParams.length, 6); + + // Check decoded u8 + uint8 decodedU8 = abi.decode(decodedParams[0], (uint8)); + assertEq(decodedU8, u8Value); + + // Check decoded u32 + uint32 decodedU32 = abi.decode(decodedParams[1], (uint32)); + assertEq(decodedU32, u32Value); + + // Check decoded u64 (first) + uint64 decodedU64_1 = abi.decode(decodedParams[2], (uint64)); + assertEq(decodedU64_1, u64Value1); + + // Check decoded u64 (second) + uint64 decodedU64_2 = abi.decode(decodedParams[3], (uint64)); + assertEq(decodedU64_2, u64Value2); + + // Check decoded u8 array [u8; 4] + uint8[] memory decodedU8Array = abi.decode(decodedParams[4], (uint8[])); + assertEq(decodedU8Array.length, 4); + assertEq(decodedU8Array[0], 10); + assertEq(decodedU8Array[1], 20); + assertEq(decodedU8Array[2], 30); + assertEq(decodedU8Array[3], 40); + + // Check decoded u32 array [u32; 10] + uint32[] memory decodedU32Array = abi.decode(decodedParams[5], (uint32[])); + assertEq(decodedU32Array.length, 10); + for (uint256 i = 0; i < 10; i++) { + assertEq(decodedU32Array[i], uint32(1000 + i * 100)); + } + } + + function testDecodeGenericSchemaWithStrings() public pure { + GenericSchema memory schema; + schema.valuesTypeNames = new string[](4); + schema.valuesTypeNames[0] = "String"; + schema.valuesTypeNames[1] = "u32"; + schema.valuesTypeNames[2] = "Vec"; + schema.valuesTypeNames[3] = "[String; 2]"; + + // Prepare test data + string memory singleString = "hello world"; + uint32 numberValue = 42; + + string[] memory stringVec = new string[](2); + stringVec[0] = "vec1"; + stringVec[1] = "vec2"; + + string[] memory stringArray = new string[](2); + stringArray[0] = "array1"; + stringArray[1] = "array2"; + + // Encode test data + bytes memory encodedData = abi.encodePacked( + BorshEncoder.encodeString(singleString), + BorshEncoder.encodeU32(numberValue), + BorshEncoder.encodeStringVec(stringVec), + BorshEncoder.encodeStringArray(stringArray) + ); + + // Decode using GenericSchema + bytes[] memory decodedParams = BorshDecoder.decodeGenericSchema(schema, encodedData); + + assertEq(decodedParams.length, 4); + + // Check decoded string + string memory decodedString = abi.decode(decodedParams[0], (string)); + assertEq(decodedString, singleString); + + // Check decoded u32 + uint32 decodedU32 = abi.decode(decodedParams[1], (uint32)); + assertEq(decodedU32, numberValue); + + // Check decoded string vector + string[] memory decodedStringVec = abi.decode(decodedParams[2], (string[])); + assertEq(decodedStringVec.length, 2); + assertEq(decodedStringVec[0], "vec1"); + assertEq(decodedStringVec[1], "vec2"); + + // Check decoded string array + string[] memory decodedStringArray = abi.decode(decodedParams[3], (string[])); + assertEq(decodedStringArray.length, 2); + assertEq(decodedStringArray[0], "array1"); + assertEq(decodedStringArray[1], "array2"); + } + + /** Real-life Solana accounts decoding **/ + + function testDecodeSolanaSocketConfigAccount() public pure { + GenericSchema memory schema; + schema.valuesTypeNames = new string[](5); + schema.valuesTypeNames[0] = "[u8;8]"; // account discriminator + schema.valuesTypeNames[1] = "[u8;32]"; + schema.valuesTypeNames[2] = "u32"; + schema.valuesTypeNames[3] = "String"; + schema.valuesTypeNames[4] = "u8"; + + bytes8 discriminator = 0x9b0caae01efacc82; + bytes32 owner = 0x0c1a5886fe1093df9fc438c296f9f7275b7718b6bc0e156d8d336c58f083996d; + uint32 chain_slug = 10000002; + string memory version = "0.1.0"; + uint8 bump = 255; + + bytes memory solanaEncodedData = hex"9b0caae01efacc820c1a5886fe1093df9fc438c296f9f7275b7718b6bc0e156d8d336c58f083996d8296980005000000302e312e30ff0000000000"; + + bytes[] memory decodedParams = BorshDecoder.decodeGenericSchema(schema, solanaEncodedData); + + assertEq(decodedParams.length, 5); + + console.log("decoded discriminator"); + // console.logBytes(decodedParams[0]); + uint8[] memory decodedDiscriminator = abi.decode(decodedParams[0], (uint8[])); + bytes memory packedUint8Array = BorshEncoder.packUint8Array(decodedDiscriminator); + assertEq(packedUint8Array, abi.encodePacked(discriminator)); + + console.log("decoded owner"); + uint8[] memory decodedOwner = abi.decode(decodedParams[1], (uint8[])); + packedUint8Array = BorshEncoder.packUint8Array(decodedOwner); + assertEq(packedUint8Array, abi.encodePacked(owner)); + + console.log("decoded chain_slug"); + uint32 decodedChainSlug = abi.decode(decodedParams[2], (uint32)); + assertEq(decodedChainSlug, chain_slug); + + console.log("decoded version"); + string memory decodedVersion = abi.decode(decodedParams[3], (string)); + console.log("decodedVersion"); + console.log(decodedVersion); + assertEq(decodedVersion, version); + + console.log("decoded bump"); + uint8 decodedBump = abi.decode(decodedParams[4], (uint8)); + assertEq(decodedBump, bump); + } + + function testDecodeSuperTokenConfigGenericSchema() public pure { + GenericSchema memory schema; + schema.valuesTypeNames = new string[](5); + schema.valuesTypeNames[0] = "[u8;8]"; // account discriminator + schema.valuesTypeNames[1] = "[u8;32]"; + schema.valuesTypeNames[2] = "[u8;32]"; + schema.valuesTypeNames[3] = "[u8;32]"; + schema.valuesTypeNames[4] = "u8"; + + bytes8 discriminator = 0x9b0caae01efacc82; + bytes32 owner = 0x0c1a5886fe1093df9fc438c296f9f7275b7718b6bc0e156d8d336c58f083996d; + bytes32 socket = 0x0000000000000000000000000000000000000000000000000000000000000000; + bytes32 mint = 0x9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a; + uint8 bump = 255; + + bytes memory solanaEncodedData = hex"9b0caae01efacc820c1a5886fe1093df9fc438c296f9f7275b7718b6bc0e156d8d336c58f083996d00000000000000000000000000000000000000000000000000000000000000009ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522aff"; + + bytes[] memory decodedParams = BorshDecoder.decodeGenericSchema(schema, solanaEncodedData); + + assertEq(decodedParams.length, 5); + + console.log("decoded discriminator"); + // console.logBytes(decodedParams[0]); + uint8[] memory decodedDiscriminator = abi.decode(decodedParams[0], (uint8[])); + console.log("decodedDiscriminator"); + console.log(decodedDiscriminator.length); + bytes memory packedUint8Array = BorshEncoder.packUint8Array(decodedDiscriminator); + console.logBytes(packedUint8Array); + assertEq(packedUint8Array, abi.encodePacked(discriminator)); + + console.log("decodedOwner"); + // console.logBytes(decodedParams[1]); + uint8[] memory decodedOwner = abi.decode(decodedParams[1], (uint8[])); + packedUint8Array = BorshEncoder.packUint8Array(decodedOwner); + console.logBytes(packedUint8Array); + assertEq(packedUint8Array, abi.encodePacked(owner)); + console.log("decodedSocket"); + // console.logBytes(decodedParams[2]); + uint8[] memory decodedSocket = abi.decode(decodedParams[2], (uint8[])); + packedUint8Array = BorshEncoder.packUint8Array(decodedSocket); + console.logBytes(packedUint8Array); + assertEq(packedUint8Array, abi.encodePacked(socket)); + console.log("decodedMint"); + // console.logBytes(decodedParams[3]); + uint8[] memory decodedMint = abi.decode(decodedParams[3], (uint8[])); + packedUint8Array = BorshEncoder.packUint8Array(decodedMint); + console.logBytes(packedUint8Array); + assertEq(packedUint8Array, abi.encodePacked(mint)); + console.log("decodedBump"); + // console.logBytes(decodedParams[4]); + uint8 decodedBump = abi.decode(decodedParams[4], (uint8)); + console.log("decodedBump: ", decodedBump); + assertEq(decodedBump, bump); + } + + /** Test edge cases **/ + + function testDecodeInsufficientData() public { + bytes memory shortData = hex"01"; + + // Should revert when trying to decode u16 from 1-byte data + BorshDecoder.Data memory data = BorshDecoder.from(shortData); + vm.expectRevert("Parse error: unexpected EOI"); + data.decodeU16(); + } + + function testDecodeOutOfBounds() public { + bytes memory data = hex"0102"; + + // Should revert when trying to decode u32 from 2-byte data + BorshDecoder.Data memory decoderData = BorshDecoder.from(data); + vm.expectRevert("Parse error: unexpected EOI"); + decoderData.decodeU32(); + } + + function testDecodeVecInsufficientLength() public { + // Length says 10 but only 5 bytes follow + bytes memory invalidVecData = hex"0a000000010203"; + + BorshDecoder.Data memory data = BorshDecoder.from(invalidVecData); + vm.expectRevert("Parse error: unexpected EOI"); + data.decodeUint8Vec(); + } + + /** Test complex scenarios **/ + + function testDecodeMultipleConsecutive() public pure { + // Encode multiple values consecutively + bytes memory data = abi.encodePacked( + BorshEncoder.encodeU8(42), + BorshEncoder.encodeU16(1234), + BorshEncoder.encodeUint8Vec(_createU8Array()) + ); + + // Decode them one by one + BorshDecoder.Data memory decoderData = BorshDecoder.from(data); + + uint8 u8Val = decoderData.decodeU8(); + assertEq(u8Val, 42); + + uint16 u16Val = decoderData.decodeU16(); + assertEq(u16Val, 1234); + + (uint32 length, uint8[] memory vecVal) = decoderData.decodeUint8Vec(); + assertEq(length, 2); + assertEq(vecVal.length, 2); + assertEq(vecVal[0], 1); + assertEq(vecVal[1], 2); + + // Verify all data consumed + decoderData.done(); + } + + function _createU8Array() private pure returns (uint8[] memory) { + uint8[] memory arr = new uint8[](2); + arr[0] = 1; + arr[1] = 2; + return arr; + } +} \ No newline at end of file diff --git a/test/BorshEncoderTest.t.sol b/test/BorshEncoderTest.t.sol new file mode 100644 index 00000000..57b19be8 --- /dev/null +++ b/test/BorshEncoderTest.t.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import {BorshEncoder} from "../contracts/evmx/watcher/borsh-serde/BorshEncoder.sol"; + +contract BorshEncoderTest is Test { + /** Encode primitive types **/ + + function testEncodeU8() public pure { + uint8 value = 1; + bytes1 encoded = BorshEncoder.encodeU8(value); + console.logBytes1(encoded); + console.logBytes1(bytes1(value)); + assertEq(encoded, bytes1(value)); + } + + function testEncodeU16() public pure { + uint16 value = 0x0102; + bytes2 encoded = BorshEncoder.encodeU16(value); + assertEq(encoded, bytes2(0x0201)); + } + + function testEncodeU32() public pure { + uint32 value = 0x01020304; + bytes4 encoded = BorshEncoder.encodeU32(value); + // console.logBytes4(encoded); + // console.logBytes4(bytes4(value)); + assertEq(encoded, bytes4(0x04030201)); + } + + function testEncodeU64() public pure { + uint64 value = 0x0102030405060708; + bytes8 encoded = BorshEncoder.encodeU64(value); + assertEq(encoded, bytes8(0x0807060504030201)); + } + + function testEncodeU128() public pure { + uint128 value = 0x0102030405060708090a0b0c0d0e0f10; + bytes16 encoded = BorshEncoder.encodeU128(value); + assertEq(encoded, bytes16(0x100f0e0d0c0b0a090807060504030201)); + } + + function testEncodeString() public pure { + string memory value = "hello"; + bytes memory encoded = BorshEncoder.encodeString(value); + console.logBytes(encoded); + console.logBytes(bytes("hello")); + console.logBytes(hex"0500000068656c6c6f"); + // first 4 bytes are the length of the string which is 5 in LE : 0x05000000 + // rest is the string as bytes with no changes + assertEq(encoded, hex"0500000068656c6c6f"); + } + + /** Encode array types with fixed length - no length prefix, just the bytes **/ + + function testEncodeUint8Array() public pure { + bytes memory expectedEncoded = hex"010203"; + + uint8[] memory value = new uint8[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint8Array(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeUint16Array() public pure { + bytes memory expectedEncoded = hex"010002000300"; + + uint16[] memory value = new uint16[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint16Array(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeUint32Array() public pure { + bytes memory expectedEncoded = hex"010000000200000003000000"; + + uint32[] memory value = new uint32[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint32Array(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeUint64Array() public pure { + bytes memory expectedEncoded = hex"010000000000000002000000000000000300000000000000"; + + uint64[] memory value = new uint64[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint64Array(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeUint128Array() public pure { + bytes + memory expectedEncoded = hex"010000000000000000000000000000000200000000000000000000000000000003000000000000000000000000000000"; + + uint128[] memory value = new uint128[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint128Array(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeStringArray() public pure { + // "hello" (5 bytes): 0x0500000068656c6c6f + // "world" (5 bytes): 0x05000000776f726c64 + bytes memory expectedEncoded = hex"0500000068656c6c6f05000000776f726c64"; + + string[] memory value = new string[](2); + value[0] = "hello"; + value[1] = "world"; + bytes memory encoded = BorshEncoder.encodeStringArray(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeStringArrayEmpty() public pure { + // Empty string (0 bytes): 0x00000000 + bytes memory expectedEncoded = hex"0000000000000000"; + + string[] memory value = new string[](2); + value[0] = ""; + value[1] = ""; + bytes memory encoded = BorshEncoder.encodeStringArray(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeStringArraySingleChar() public pure { + bytes memory expectedEncoded = hex"0500000068656c6c6f05000000776f726c64"; + + string[] memory value = new string[](2); + value[0] = "hello"; + value[1] = "world"; + bytes memory encoded = BorshEncoder.encodeStringArray(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + /** Encode Vector types with that can have variable length - length prefix is added **/ + + function testEncodeUint8Vec() public pure { + // Length: 3 as u32 (0x03000000) + elements (0x010203) + bytes memory expectedEncoded = hex"03000000010203"; + + uint8[] memory value = new uint8[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint8Vec(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeUint16Vec() public pure { + // Length: 3 as u32 (0x03000000) + elements (0x010002000300) + bytes memory expectedEncoded = hex"03000000010002000300"; + + uint16[] memory value = new uint16[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint16Vec(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeUint32Vec() public pure { + // Length: 3 as u32 (0x03000000) + elements (0x010000000200000003000000) + bytes memory expectedEncoded = hex"03000000010000000200000003000000"; + + uint32[] memory value = new uint32[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint32Vec(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeUint64Vec() public pure { + // Length: 3 as u32 (0x03000000) + elements (0x010000000000000002000000000000000300000000000000) + bytes + memory expectedEncoded = hex"03000000010000000000000002000000000000000300000000000000"; + + uint64[] memory value = new uint64[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint64Vec(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeUint128Vec() public pure { + // Length: 3 as u32 (0x03000000) + elements + bytes + memory expectedEncoded = hex"03000000010000000000000000000000000000000200000000000000000000000000000003000000000000000000000000000000"; + + uint128[] memory value = new uint128[](3); + value[0] = 1; + value[1] = 2; + value[2] = 3; + bytes memory encoded = BorshEncoder.encodeUint128Vec(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } + + function testEncodeStringVec() public pure { + // Length: 2 as u32 (0x02000000) + string elements + bytes memory expectedEncoded = hex"020000000500000068656c6c6f05000000776f726c64"; + + string[] memory value = new string[](2); + value[0] = "hello"; + value[1] = "world"; + bytes memory encoded = BorshEncoder.encodeStringVec(value); + + console.logBytes(encoded); + assertEq(encoded, expectedEncoded); + } +} diff --git a/test/DigestTest.t.sol b/test/DigestTest.t.sol new file mode 100644 index 00000000..34fe1e1b --- /dev/null +++ b/test/DigestTest.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import {DigestParams} from "../contracts/utils/common/Structs.sol"; +import "../contracts/utils/common/Constants.sol"; +import "forge-std/console.sol"; +import {toBytes32Format} from "../contracts/utils/common/Converters.sol"; + +contract DigestTest is Test { + function testCallType() public pure { + console.log("READ"); + console.logBytes4(READ); + console.log("WRITE"); + console.logBytes4(WRITE); + console.log("SCHEDULE"); + console.logBytes4(SCHEDULE); + + bytes32 superTokenEvm = keccak256(abi.encode("superTokenEvm")); + console.log("superTokenEvm"); + console.logBytes32(superTokenEvm); + } + + function testFunctionSelectorEncoding() public pure { + bytes4 selector = bytes4(keccak256("increase(uint256,uint32,uint8[],uint32[],string)")); + uint8[] memory array_one = new uint8[](3); + array_one[0] = 1; + array_one[1] = 2; + array_one[2] = 3; + uint32[] memory array_two = new uint32[](3); + array_two[0] = 666; + array_two[1] = 777; + array_two[2] = 888; + + bytes memory callData = abi.encodeWithSelector( + selector, + 123456, + 666, + array_one, + array_two, + "hello world" + ); + + console.log("Full calldata"); + console.logBytes(callData); + } + + function testDigest() public pure { + bytes32 expectedDigest = 0xd72bce33c4eb6d4615f2878fd0d0711ff78e080f0ac4a2240d224e859f7f8dc1; + + DigestParams memory inputDigestParams = DigestParams({ + socket: 0x84815e8ca2f6dad7e12902c39a51bc72e13c48139b4fb10025d94e7abea2969c, + transmitter: toBytes32Format(0x138e9840861C983DC0BB9b3e941FB7C0e9Ade320), + payloadId: 0x965c0b8c6c5c8dc6f433b34b72ecddcec35b2f36f700f50aed20a40366efa88a, + deadline: 1750681840, + callType: WRITE, + gasLimit: 10000000, + value: 0, + payload: hex"0914e65e59622aeeefb7f007aef36df62d4c380895553b0643fcc4383c7c24480af77affb0a5db632e9bafb98525232515d440861c9942e447c20eefd8883d349ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a93339e12fb69289a640420f0000000000", + // TODO: fix with correct super-token program id + target: 0x0914e65e59622aeeefb7f007aef36df62d4c380895553b0643fcc4383c7c2448, + appGatewayId: 0x0000000000000000000000004530a440dcc32206f901325143132da1edb8d2e9, + // prevDigestsHash: 0x4cfb2ef587acc8ad0cdb441f5b5e0624f7fef9c2fa084f5e93075cdc54d99d8f + prevBatchDigestHash: 0x0000000000000000000000000000000000000000000000000000000000000000, + extraData: bytes("") + }); + + bytes memory packedParams = abi.encodePacked( + inputDigestParams.socket, + inputDigestParams.transmitter, + inputDigestParams.payloadId, + inputDigestParams.deadline, + inputDigestParams.callType, + inputDigestParams.gasLimit, + inputDigestParams.value, + inputDigestParams.payload, + inputDigestParams.target, + inputDigestParams.appGatewayId, + inputDigestParams.prevBatchDigestHash, + inputDigestParams.extraData + ); + console.log("packedParams"); + console.logBytes(packedParams); + + bytes32 actualDigest = getDigest(inputDigestParams); + assertEq(actualDigest, expectedDigest); + } + + // taken from WatcherPrecompileCore.getDigest() + function getDigest(DigestParams memory params_) public pure returns (bytes32 digest) { + digest = keccak256( + abi.encodePacked( + params_.socket, + params_.transmitter, + params_.payloadId, + params_.deadline, + params_.callType, + params_.gasLimit, + params_.value, + params_.payload, + params_.target, + params_.appGatewayId, + params_.prevBatchDigestHash, + params_.extraData + ) + ); + } +} diff --git a/test/ReturnValueSolanaTest.t.sol b/test/ReturnValueSolanaTest.t.sol new file mode 100644 index 00000000..5c2ed803 --- /dev/null +++ b/test/ReturnValueSolanaTest.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import {GenericSchema} from "../contracts/utils/common/Structs.sol"; +import "../contracts/utils/common/Constants.sol"; +import {BorshDecoder} from "../contracts/evmx/watcher/borsh-serde/BorshDecoder.sol"; +import {BorshEncoder} from "../contracts/evmx/watcher/borsh-serde/BorshEncoder.sol"; +import "forge-std/console.sol"; + +contract ReturnValueSolanaTest is Test { + function testDecoding() public { + string[] memory returnDataValuesTypeNames = new string[](2); + returnDataValuesTypeNames[0] = "[u8; 32]"; + returnDataValuesTypeNames[1] = "Vec"; + + GenericSchema memory returnDataSchema = GenericSchema({ + valuesTypeNames: returnDataValuesTypeNames + }); + + bytes memory returnData = hex"0c1a5886fe1093df9fc438c296f9f7275b7718b6bc0e156d8d336c58f083996d0400000001020304"; + + // GenericSchema memory genericSchema = abi.decode(data, (GenericSchema)); + bytes[] memory parsedData = BorshDecoder.decodeGenericSchema(returnDataSchema, returnData); + + uint8[] memory transmitterSolanaArray = abi.decode(parsedData[0], (uint8[])); + bytes memory transmitterSolana = BorshEncoder.packUint8Array(transmitterSolanaArray); + + uint8[] memory returnValueArray = abi.decode(parsedData[1], (uint8[])); + bytes memory returnValue = BorshEncoder.packUint8Array(returnValueArray); + + console.logBytes(transmitterSolana); + console.logBytes(returnValue); + + // bytes32 hexadecimal representation of solana transmitter address + assertEq(transmitterSolana, hex"0c1a5886fe1093df9fc438c296f9f7275b7718b6bc0e156d8d336c58f083996d"); + // 4 bytes with values: 1, 2, 3, 4 + assertEq(returnValue, hex"01020304"); + } +} \ No newline at end of file diff --git a/test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol b/test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol new file mode 100644 index 00000000..b295ea78 --- /dev/null +++ b/test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "solady/auth/Ownable.sol"; +import "../../../../contracts/evmx/base/AppGatewayBase.sol"; +import "./ISuperToken.sol"; +import "./SuperToken.sol"; +import {SolanaInstruction, SolanaInstructionData, SolanaInstructionDataDescription} from "../../../../contracts/utils/common/Structs.sol"; +import {ForwarderSolana} from "../../../../contracts/evmx/helpers/ForwarderSolana.sol"; +import {BorshDecoder} from "../../../../contracts/evmx/watcher/borsh-serde/BorshDecoder.sol"; +import {BorshEncoder} from "../../../../contracts/evmx/watcher/borsh-serde/BorshEncoder.sol"; + +contract EvmSolanaAppGateway is AppGatewayBase, Ownable { + + event Transferred(uint40 requestCount); + + struct SuperTokenEvmConstructorParams { + string name_; + string symbol_; + uint8 decimals_; + address initialSupplyHolder_; + uint256 initialSupply_; + } + + /** Write input structs **/ + + struct TransferOrderEvmToSolana { + address srcEvmToken; + bytes32 dstSolanaToken; + address userEvm; + bytes32 destUserTokenAddress; + uint256 srcAmount; + uint256 deadline; + } + + /** Read output structs **/ + + struct SolanaTokenBalance { + uint64 amount; + uint64 decimals; + } + + struct SuperTokenConfigAccount { + bytes8 accountDiscriminator; + bytes32 owner; + bytes32 socket; + bytes32 mint; + uint8 bump; + } + + /** Events **/ + + event SuperTokenConfigAccountRead(SuperTokenConfigAccount superTokenConfigAccount); + event TokenAccountRead(bytes32 tokenAccountAddress, uint64 amount, uint64 decimals); + event TriggerIncrease(uint256 amountU64, uint32 amountU32, uint8[] vecU8, uint32[] vecU32, string myString, uint256 triggerCounter); + event MintReturnData(bytes data); + + /** Contract data **/ + + bytes32 public superTokenEvm = _createContractId("superTokenEvm"); + // solana program address + bytes32 public solanaProgramId; + ForwarderSolana public forwarderSolana; + + mapping(bytes32 => SolanaTokenBalance) solanaTokenBalances; + SuperTokenConfigAccount superTokenConfigAccount; + + uint256 triggerCounter; + + constructor( + address owner_, + uint256 fees_, + SuperTokenEvmConstructorParams memory params_, + bytes32 solanaProgramId_, + address forwarderSolanaAddress_, + address addressResolver_ + ) { + // for evm we use standard mode with contract deployment using EVMx + creationCodeWithArgs[superTokenEvm] = abi.encodePacked( + type(SuperToken).creationCode, + abi.encode( + params_.name_, + params_.symbol_, + params_.decimals_, + params_.initialSupplyHolder_, + params_.initialSupply_ + ) + ); + // for Solana we just pass the programId(program address) + solanaProgramId = solanaProgramId_; + forwarderSolana = ForwarderSolana(forwarderSolanaAddress_); + + // sets the fees data like max fees, chain and token for all transfers + // they can be updated for each transfer as well + _setMaxFees(fees_); + _initializeOwner(owner_); + _initializeAppGateway(addressResolver_); + } + + function deployEvmContract(uint32 chainSlug_) external async { + bytes memory initData = abi.encodeWithSelector(SuperToken.setOwner.selector, owner()); + _deploy(superTokenEvm, chainSlug_, IsPlug.YES, initData); + } + + // no need to call this directly, will be called automatically after all contracts are deployed. + // check AppGatewayBase._deploy and AppGatewayBase.onRequestComplete + function initializeOnChain(uint32) public pure override { + return; + } + + function getForwarderSolanaAddressResolver() external view returns (address) { + return address(forwarderSolana.addressResolver__()); + } + + // we have to do it like that as onchain contract is not deployed with AG + // more info in : AppGatewayBase.sol -> _setValidPlug() and getOnChainAddress() + function setIsValidPlugForSolana(bool isValid, uint32 chainSlug_, bytes32 plugAddress) public { + watcher__().setIsValidPlug(isValid, chainSlug_, plugAddress); + } + + function transfer( + bytes memory order_, + SolanaInstruction memory solanaInstruction + ) external async { + TransferOrderEvmToSolana memory order = abi.decode(order_, (TransferOrderEvmToSolana)); + ISuperToken(order.srcEvmToken).burn(order.userEvm, order.srcAmount); + + // we are directly calling the ForwarderSolana + forwarderSolana.callSolana(abi.encode(solanaInstruction), solanaInstruction.data.programId); + + emit Transferred(_getCurrentRequestCount()); + } + + function mintSuperTokenEvm(bytes memory order_) external async { + TransferOrderEvmToSolana memory order = abi.decode(order_, (TransferOrderEvmToSolana)); + ISuperToken(order.srcEvmToken).mint(order.userEvm, order.srcAmount); + + emit Transferred(_getCurrentRequestCount()); + } + + function mintSuperTokenSolana( + SolanaInstruction memory solanaInstruction, + GenericSchema memory returnDataSchema + ) external async { + // we are directly calling the ForwarderSolana + forwarderSolana.callSolana(abi.encode(solanaInstruction), solanaInstruction.data.programId); + then(this.storeAndDecodeMintReturnData.selector, abi.encode(returnDataSchema)); + + emit Transferred(_getCurrentRequestCount()); + } + + function triggerTestSuperTokenSolana(SolanaInstruction memory solanaInstruction) external async { + // we are directly calling the ForwarderSolana + forwarderSolana.callSolana(abi.encode(solanaInstruction), solanaInstruction.data.programId); + + emit Transferred(_getCurrentRequestCount()); + } + + // this is only for debugging purposes to mint tokens on Solana + function transferForDebug(SolanaInstruction memory solanaInstruction) external async { + forwarderSolana.callSolana(abi.encode(solanaInstruction), solanaInstruction.data.programId); + + emit Transferred(_getCurrentRequestCount()); + } + + function readSuperTokenConfigAccount( + SolanaReadRequest memory solanaReadRequest, + GenericSchema memory genericSchema + ) external async { + _setOverrides(Read.ON); + forwarderSolana.callSolana(abi.encode(solanaReadRequest), solanaReadRequest.accountToRead); + then(this.storeAndDecodeSuperTokenConfigAccount.selector, abi.encode(genericSchema)); + } + + function storeAndDecodeSuperTokenConfigAccount(bytes memory data, bytes memory returnData) external async { + GenericSchema memory genericSchema = abi.decode(data, (GenericSchema)); + bytes[] memory parsedData = BorshDecoder.decodeGenericSchema(genericSchema, returnData); + + uint8[] memory decodedDiscriminatorArray = abi.decode(parsedData[0], (uint8[])); + bytes8 decodedDiscriminator = bytes8(BorshEncoder.packUint8Array(decodedDiscriminatorArray)); + uint8[] memory decodedOwnerArray = abi.decode(parsedData[1], (uint8[])); + bytes32 decodedOwner = bytes32(BorshEncoder.packUint8Array(decodedOwnerArray)); + uint8[] memory decodedSocketArray = abi.decode(parsedData[2], (uint8[])); + bytes32 decodedSocket = bytes32(BorshEncoder.packUint8Array(decodedSocketArray)); + uint8[] memory decodedMintArray = abi.decode(parsedData[3], (uint8[])); + bytes32 decodedMint = bytes32(BorshEncoder.packUint8Array(decodedMintArray)); + uint8 decodedBump = abi.decode(parsedData[4], (uint8)); + + SuperTokenConfigAccount memory decodedSuperTokenConfigAccount = SuperTokenConfigAccount({ + accountDiscriminator: decodedDiscriminator, + owner: decodedOwner, + socket: decodedSocket, + mint: decodedMint, + bump: decodedBump + }); + + superTokenConfigAccount = decodedSuperTokenConfigAccount; + + emit SuperTokenConfigAccountRead(decodedSuperTokenConfigAccount); + } + + function storeAndDecodeMintReturnData(bytes memory data, bytes memory returnData) external async { + GenericSchema memory genericSchema = abi.decode(data, (GenericSchema)); + bytes[] memory parsedData = BorshDecoder.decodeGenericSchema(genericSchema, returnData); + + uint8[] memory decodedReturnDataArray = abi.decode(parsedData[0], (uint8[])); + bytes memory decodedReturnData = BorshEncoder.packUint8Array(decodedReturnDataArray); + + emit MintReturnData(decodedReturnData); + } + + function readTokenAccount(SolanaReadRequest memory solanaReadRequest) external async { + _setOverrides(Read.ON); + + forwarderSolana.callSolana(abi.encode(solanaReadRequest), solanaReadRequest.accountToRead); + then(this.storeTokenAccountData.selector, abi.encode(solanaReadRequest.accountToRead)); + } + + function storeTokenAccountData(bytes memory data, bytes memory returnData) external async { + bytes32 tokenAccountAddress = abi.decode(data, (bytes32)); + (uint64 amount, uint64 decimals) = abi.decode(returnData, (uint64, uint64)); + solanaTokenBalances[tokenAccountAddress] = SolanaTokenBalance({ + amount: amount, + decimals: decimals + }); + + emit TokenAccountRead(tokenAccountAddress, amount, decimals); + } + + function increase(uint256 amountU64, uint32 amountU32, uint8[] memory vecU8, uint32[] memory vecU32, string memory myString) public { + triggerCounter++; + emit TriggerIncrease(amountU64, amountU32, vecU8, vecU32, myString, triggerCounter); + } +}