diff --git a/Errors.md b/Errors.md index 029af1fb..f27ddc0a 100644 --- a/Errors.md +++ b/Errors.md @@ -45,6 +45,7 @@ | Error | Signature | |-------|-----------| | `InvalidSwitchboardTest(bytes32,bytes32)` | `0x702f36a1` | +| `InvalidGatewayTest(bytes32,bytes32,bytes32)` | `0xc6ad0fcf` | ## evmx/watcher/RequestHandler.sol diff --git a/FunctionSignatures.md b/FunctionSignatures.md index def1a84b..f0820224 100644 --- a/FunctionSignatures.md +++ b/FunctionSignatures.md @@ -303,7 +303,7 @@ | -------- | --------- | | `addressResolver__` | `0x6a750469` | | `asyncDeployer__` | `0x2a39e801` | -| `callSolana` | `0x8af147d3` | +| `callSolana` | `0x4ef7957b` | | `chainSlug` | `0xb349ba65` | | `deployForwarder__` | `0xd4e3b034` | | `feesManager__` | `0x70568b58` | @@ -553,7 +553,6 @@ | `completeOwnershipHandover` | `0xf04e283e` | | `contractFactoryPlugs` | `0x35426631` | | `digestHashes` | `0xd1a862bf` | -| `encodeU64Borsh` | `0xacc1b559` | | `expiryTime` | `0x99bc0aea` | | `getDigest` | `0x91b6288b` | | `getPrecompileFees` | `0xb7a3d04c` | diff --git a/contracts/evmx/helpers/ForwarderSolana.sol b/contracts/evmx/helpers/ForwarderSolana.sol index fbdd83c5..989e9903 100644 --- a/contracts/evmx/helpers/ForwarderSolana.sol +++ b/contracts/evmx/helpers/ForwarderSolana.sol @@ -30,7 +30,7 @@ contract ForwarderSolana is ForwarderStorage, Initializable, AddressResolverUtil /// @param addressResolver_ address resolver contract function initialize( uint32 chainSlug_, - bytes32 onChainAddress_, + 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) { @@ -76,7 +76,7 @@ contract ForwarderSolana is ForwarderStorage, Initializable, AddressResolverUtil /// @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(SolanaInstruction memory solanaInstruction) external { + function callSolana(bytes memory solanaPayload, bytes32 target) external { if (address(addressResolver__) == address(0)) { revert AddressResolverNotSet(); } @@ -97,37 +97,17 @@ contract ForwarderSolana is ForwarderStorage, Initializable, AddressResolverUtil // get the switchboard address from the watcher precompile config // address switchboard = watcherPrecompileConfig().switchboards(chainSlug, sbType); - bytes memory solanaPayload = abi.encode(solanaInstruction); - // Queue the call in the middleware. QueueParams memory queueParams; queueParams.overrideParams = overrideParams; queueParams.transaction = Transaction({ chainSlug: chainSlug, - target: onChainAddress, + // 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); - - // Queue the call in the middleware. - // deliveryHelper__().queue( - // QueuePayloadParams({ - // chainSlug: chainSlug, - // callType: isReadCall == Read.ON ? CallType.READ : CallType.WRITE, - // isParallel: isParallelCall, - // isPlug: IsPlug.NO, - // writeFinality: writeFinality, - // asyncPromise: latestAsyncPromise, - // switchboard: switchboardSolana, - // target: onChainAddress, - // appGateway: msg.sender, - // gasLimit: gasLimit, - // value: value, - // readAt: readAt, - // payload: solanaPayload, - // initCallData: bytes("") - // }) - // ); } } diff --git a/contracts/evmx/watcher/Configurations.sol b/contracts/evmx/watcher/Configurations.sol index d5fdf34d..a6ff7d14 100644 --- a/contracts/evmx/watcher/Configurations.sol +++ b/contracts/evmx/watcher/Configurations.sol @@ -15,6 +15,7 @@ abstract contract ConfigurationsStorage is IConfigurations { uint256[50] _gap_before; error InvalidSwitchboardTest(bytes32 sb, bytes32 sbExpected); + error InvalidGatewayTest(bytes32 appGatewayId, bytes32 appGatewayIdExpected, bytes32 switchboard); // slot 50 /// @notice Maps network and plug to their configuration @@ -163,13 +164,14 @@ contract Configurations is ConfigurationsStorage, Initializable, Ownable, Watche bytes32 target_, address appGateway_, bytes32 switchboardType_ - // ) external { ) external view { (bytes32 appGatewayId, bytes32 switchboard) = getPlugConfigs(chainSlug_, target_); - if (appGatewayId != toBytes32Format(appGateway_)) revert InvalidGateway(); + // if (appGatewayId != toBytes32Format(appGateway_)) revert InvalidGateway(); + if (appGatewayId != toBytes32Format(appGateway_)) revert InvalidGatewayTest(appGatewayId, toBytes32Format(appGateway_), switchboard); // emit VerifyConnectionsSB(switchboard, switchboards[chainSlug_][switchboardType_]); // if (switchboard != switchboards[chainSlug_][switchboardType_]) revert InvalidSwitchboard(); - if (switchboard != switchboards[chainSlug_][switchboardType_]) revert InvalidSwitchboardTest(switchboard, switchboards[chainSlug_][switchboardType_]); + if (switchboard != switchboards[chainSlug_][switchboardType_]) + revert InvalidSwitchboardTest(switchboard, switchboards[chainSlug_][switchboardType_]); } /** 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..b498059e --- /dev/null +++ b/contracts/evmx/watcher/borsh-serde/BorshEncoder.sol @@ -0,0 +1,287 @@ +// 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) { + 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) { + bytes memory packed = packUint8Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint16Vec(uint16[] memory arr) internal pure returns (bytes memory) { + bytes memory packed = packUint16Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint32Vec(uint32[] memory arr) internal pure returns (bytes memory) { + bytes memory packed = packUint32Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint64Vec(uint64[] memory arr) internal pure returns (bytes memory) { + bytes memory packed = packUint64Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint128Vec(uint128[] memory arr) internal pure returns (bytes memory) { + bytes memory packed = packUint128Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeStringVec(string[] memory arr) internal pure returns (bytes memory) { + 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 ef7028a9..25a13c2e 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 @@ -48,6 +49,8 @@ abstract contract WritePrecompileStorage is IPrecompile { /// @title WritePrecompile /// @notice Handles write precompile logic contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, WatcherBase { + // using BorshEncoder for borsh; + /// @notice Emitted when fees are set event FeesSet(uint256 writeFees); event ChainMaxMsgValueLimitsUpdated(uint32 chainSlug, uint256 maxMsgValueLimit); @@ -159,12 +162,13 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc { ( address appGateway, - Transaction memory transaction, - , // _writeFinality + Transaction memory transaction, // _writeFinality + , uint256 gasLimit, uint256 value, - // bytes32 switchboard - ) = abi.decode( + + ) = // bytes32 switchboard + abi.decode( payloadParams.precompileData, (address, Transaction, WriteFinality, uint256, uint256, bytes32) ); @@ -275,22 +279,6 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc uint256 gasLimit_, uint256 value_ ) internal view returns (DigestParams memory) { - // create digest - // DigestParams memory digestParams_ = DigestParams( - // configurations__().sockets(transaction.chainSlug), - // transmitter_, - // payloadParams.payloadId, - // deadline, - // payloadParams.callType, - // gasLimit, - // value, - // transaction.payload, - // transaction.target, - // toBytes32Format(appGateway), - // prevBatchDigestHash, - // bytes("") - // ); - return DigestParams( configurations__().sockets(transaction_.chainSlug), @@ -322,16 +310,15 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc transaction_.payload, (SolanaInstruction) ); - // TODO: this is a problem, function arguments must be packed in a way that is not later touched and that can be used on Solana side in raw Instruction call - // like a call data, so it should be Borsh encoded already here - bytes memory functionArgsPacked; - for (uint256 i = 0; i < instruction.data.functionArguments.length; i++) { - uint256 abiDecodedArg = abi.decode(instruction.data.functionArguments[i], (uint256)); - // silent assumption that all arguments are uint64 to simplify the encoding - uint64 arg = uint64(abiDecodedArg); - bytes8 borshEncodedArg = encodeU64Borsh(arg); - functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); - } + bytes memory functionArgsPacked = BorshEncoder.encodeFunctionArgs(instruction); + + // for (uint256 i = 0; i < instruction.data.functionArguments.length; i++) { + // uint256 abiDecodedArg = abi.decode(instruction.data.functionArguments[i], (uint256)); + // // silent assumption that all arguments are uint64 to simplify the encoding + // uint64 arg = uint64(abiDecodedArg); + // bytes8 borshEncodedArg = BorshEncoder.encodeU64(arg); + // functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + // } bytes memory payloadPacked = abi.encodePacked( instruction.data.programId, @@ -420,14 +407,14 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); } - // Borsh helper functions - function encodeU64Borsh(uint64 v) public pure returns (bytes8) { - return bytes8(swapBytes8(v)); - } + // // Borsh helper functions + // function encodeU64Borsh(uint64 v) public pure returns (bytes8) { + // return bytes8(swapBytes8(v)); + // } - 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 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); + // } } diff --git a/contracts/utils/common/Constants.sol b/contracts/utils/common/Constants.sol index 62b19797..6634a270 100644 --- a/contracts/utils/common/Constants.sol +++ b/contracts/utils/common/Constants.sol @@ -19,3 +19,8 @@ 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"); diff --git a/contracts/utils/common/Structs.sol b/contracts/utils/common/Structs.sol index 3592a67f..8aa9796a 100644 --- a/contracts/utils/common/Structs.sol +++ b/contracts/utils/common/Structs.sol @@ -210,6 +210,10 @@ struct RequestParams { bytes onCompleteData; } +/********* Solana payloads *********/ + +/** Solana write payload - SolanaInstruction **/ + struct SolanaInstruction { SolanaInstructionData data; SolanaInstructionDataDescription description; @@ -219,8 +223,6 @@ struct SolanaInstructionData { bytes32 programId; bytes32[] accounts; bytes8 instructionDiscriminator; - // TODO:GW: in one of functionArguments is an array it might need a special handling and encoding - // for now we assume the all functionArguments are simple types (uint256, address, bool, etc.) not complex types (struct, array, etc.) bytes[] functionArguments; } @@ -231,3 +233,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/foundry.toml b/foundry.toml index fc7fae52..16688c3e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,29 +10,29 @@ evm_version = 'paris' via_ir = false [labels] -0xEBBCa7Aa182fE5Fa6f891a776582Ce945E4ff43a = "AddressResolver" -0xa4FF2b386291A6DbBb6186A1fff37C235E9AF698 = "AddressResolverImpl" -0xD4E83aaDF6893e1a1C41646c068e5795d8708429 = "AsyncDeployer" -0x4b731545444500093957B3521E1A29b1e9BB989d = "AsyncDeployerImpl" -0xf5Ec4693bC9E3df9C248299C1bF5E97cbfaDb413 = "AuctionManager" -0xdDc5460808D19bce6cA05b1DaC425ee80C0A0086 = "AuctionManagerImpl" -0x48b3A2a434b9fE975E4B79a3C28c37Bc06a28f8b = "Configurations" -0x3A0123540353594D8beB826371D18d1F9EA2f01d = "ConfigurationsImpl" -0x7597271576B01bA949b9162870878192811e9268 = "DeployForwarder" -0xE7F208422F817cd703C83A859A2068bf1fF047B3 = "DeployForwarderImpl" -0xE7e73a8ffcF155BCe71BaB0a3b7c8BB68cfb6Bf0 = "ERC1967Factory" -0x357529E7D3F2fC1448560D095337D1bEaaA51Db4 = "FeesManager" -0x6944dC2Ae8ca915c4eE15b8b437E09aD8eBF2ED9 = "FeesManagerImpl" +0x10E7a47D76931D6383532C5924ae91De2F540f25 = "AddressResolver" +0x0442aDE746E464387EAEC5f44411aB5E866EC8d2 = "AddressResolverImpl" +0x78b5A6BfC55fC818bd532907c566A48863705C94 = "AsyncDeployer" +0xe3F72951f14d09AA7c44a5b0275472C8572D0Bbe = "AsyncDeployerImpl" +0x39C854C26A339CD567e41902068C829eb9BD93f6 = "AuctionManager" +0x6D820620F087d4B687bb41424B7078CA649a9b26 = "AuctionManagerImpl" +0xF0FA3c9a0d71F3e2BAADF323f6b4F0303C60d69E = "Configurations" +0x3390533482A5f94Ebc4e97CFE4Dc678569857629 = "ConfigurationsImpl" +0xa8cdDce9B5F6C4B1FDefa9e1317C0b3d27ee54b3 = "DeployForwarder" +0x116bcEace2F8aa251bf09cbADF5d186bA5376353 = "DeployForwarderImpl" +0x0d2772e0E5A0F9544e9093E2F57a33B5990c5E49 = "ERC1967Factory" +0xedf1aCca2162532BD03F514B62E1aCA9A22f3387 = "FeesManager" +0xaD1D9175BdcbF91e080364FE385dC8601BBBEA72 = "FeesManagerImpl" 0x9De353dD1131aB4e502590D3a1832652FA316268 = "FeesPool" -0xe116CcF80015162584C77D1e9D1cbE1109443f91 = "ForwarderSolana" -0x0F471B6023CeE741Ec0287EE178b3A5cA90585b2 = "ForwarderSolanaImpl" -0xB1365F70cF2c9d5858F12c8DfB5ECBb66543538C = "PromiseResolver" -0x055C05c3f7cC24f216d42B30c4B94b343eF62f4e = "ReadPrecompile" -0xD243A5761C30Caf3ECC9305F778Ca111698E1182 = "RequestHandler" -0x8A59c8Ec8279366778f7fC4d8f7Ca111D4CfBD3D = "RequestHandlerImpl" -0x36b13Ae0b6d533d8B98EC6d7C7086Eec11361F4A = "SchedulePrecompile" -0xDF6726Cc8867e6AEd6C20f4696a8B98a46036467 = "Watcher" -0x86a2659B57e393aC5Ea4e8bf97e4b1e595a2f9C2 = "WatcherImpl" -0x4276fDBc8383De3CB6F527a40D2e5E4725Ec8BBb = "WritePrecompile" -0xDd13A39F991A7Dd702a0BEBcB31EB2e54d1625E6 = "WritePrecompileImpl" -0x4530a440dcc32206f901325143132da1eDB8d2E9 = "APP_GATEWAY" +0xb0bC05F65EaEC98A837686D77AcC50240d1468F7 = "ForwarderSolana" +0x4Ba121BED687db40f4f17A8E25a22e878543E423 = "ForwarderSolanaImpl" +0xE7C9211D6939d76243B3E88c55bfB125967ec36d = "PromiseResolver" +0x4cdCB7a3cd0D90E0187701600e63DC32CFA8b143 = "ReadPrecompile" +0xd922fd7D4C073594d344DD365585c552D97Fd6A9 = "RequestHandler" +0x738c91b43D6e9c34E4ccDBf661fd926C225Ca222 = "RequestHandlerImpl" +0xAc95d5d8dEf53218792e720E769c3307CB0D5556 = "SchedulePrecompile" +0x98ff886e4EAB2087Bd10589b7E84B57cC2083932 = "Watcher" +0xd56Ca3ffdD70509cfA78E33D699Ab7dA1bE07f53 = "WatcherImpl" +0x038F81B90D925d4100317D0BBEbF5ea32d70b5aC = "WritePrecompile" +0x3FcD9B95d6Ed738562e79C56669F8BEbDB9CE626 = "WritePrecompileImpl" +0x5b0a2656b79212f7Fa6FD77F9583290386860EC4 = "APP_GATEWAY" diff --git a/hardhat-scripts/deploy/1.deploy.ts b/hardhat-scripts/deploy/1.deploy.ts index 566bf22e..bc279dc6 100644 --- a/hardhat-scripts/deploy/1.deploy.ts +++ b/hardhat-scripts/deploy/1.deploy.ts @@ -269,7 +269,8 @@ const deployEVMxContracts = async () => { proxyFactory, deployUtils ); - const forwarderSolanaAddress = deployUtils.addresses[Contracts.ForwarderSolana]; + const forwarderSolanaAddress = + deployUtils.addresses[Contracts.ForwarderSolana]; console.log("ForwarderSolana Proxy:", forwarderSolanaAddress); } catch (error) { console.log("Error deploying ForwarderSolana:", error); diff --git a/hardhat-scripts/deploy/3.configureChains.ts b/hardhat-scripts/deploy/3.configureChains.ts index 0c6deb80..b99d158d 100644 --- a/hardhat-scripts/deploy/3.configureChains.ts +++ b/hardhat-scripts/deploy/3.configureChains.ts @@ -108,7 +108,10 @@ async function setOnchainContracts( 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")); + console.log( + "solanaSwitchboard as bytes32 reversed: ", + Buffer.from(toBytes32Format(solanaSwitchboard)).toString("hex") + ); await updateContractSettings( EVMX_CHAIN_ID, Contracts.Configurations, @@ -116,10 +119,14 @@ async function setOnchainContracts( [ChainSlug.SOLANA_DEVNET, FAST_SWITCHBOARD_TYPE], solanaSwitchboard, "setSwitchboard", - [ChainSlug.SOLANA_DEVNET, FAST_SWITCHBOARD_TYPE, toBytes32Format(solanaSwitchboard)], + [ + ChainSlug.SOLANA_DEVNET, + FAST_SWITCHBOARD_TYPE, + toBytes32Format(solanaSwitchboard), + ], signer ); - + await updateContractSettings( EVMX_CHAIN_ID, Contracts.Configurations, diff --git a/script/super-token-solana/DeployEVMSolanaApps.s.sol b/script/super-token-solana/DeployEVMSolanaApps.s.sol index 7b55ecec..584af6f0 100644 --- a/script/super-token-solana/DeployEVMSolanaApps.s.sol +++ b/script/super-token-solana/DeployEVMSolanaApps.s.sol @@ -23,7 +23,7 @@ contract DeployEVMSolanaApps is Script { // fill with correct values after deployment bytes32 solanaProgramId = vm.envBytes32("SOLANA_TARGET_PROGRAM"); - address forwarderSolanaAddress = 0xe116CcF80015162584C77D1e9D1cbE1109443f91; + address forwarderSolanaAddress = 0xb0bC05F65EaEC98A837686D77AcC50240d1468F7; // Setting fee payment on Arbitrum Sepolia uint256 fees = 10 ether; @@ -43,9 +43,6 @@ contract DeployEVMSolanaApps is Script { addressResolver ); - // TODO: deploy super token on evm - // TODO: callSolana() on gateway - console.log("Contracts deployed:"); console.log("EvmSolanaAppGateway:", address(gateway)); console.log("solanaProgramId:"); diff --git a/script/super-token-solana/EvmSolanaOnchainCalls.s.sol b/script/super-token-solana/EvmSolanaOnchainCalls.s.sol index 141d1792..491ec4da 100644 --- a/script/super-token-solana/EvmSolanaOnchainCalls.s.sol +++ b/script/super-token-solana/EvmSolanaOnchainCalls.s.sol @@ -3,9 +3,21 @@ pragma solidity ^0.8.21; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; -import {ETH_ADDRESS} from "../../contracts/utils/common/Constants.sol"; +import { + ETH_ADDRESS, + TOKEN_ACCOUNT, + MINT_ACCOUNT +} from "../../contracts/utils/common/Constants.sol"; import {EvmSolanaAppGateway} from "../../test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol"; -import {SolanaInstruction, SolanaInstructionData, SolanaInstructionDataDescription} from "../../contracts/utils/common/Structs.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 { @@ -26,44 +38,12 @@ contract EvmSolanaOnchainCalls is Script { console.logBytes32(switchboardSolana); console.log("User address: ", userEvmAddress); - // console.log("Deploying SuperToken on Optimism Sepolia..."); - // appGateway.deployEvmContract(11155420); - - // appGateway.transfer( - // abi.encode( - // EvmSolanaAppGateway.TransferOrderEvmToSolana({ - // srcEvmToken: 0x4200000000000000000000000000000000000006, - // dstSolanaToken: 0x66619ffe200970bf084fa4713da27d7dff551179adac93fc552787c7555f3482, - // userEvm: 0x4200000000000000000000000000000000000005, - // destUserTokenAddress: 0x44419ffe200970bf084fa4713da27d7dff551179adac93fc552787c7555f3482, - // srcAmount: 1000000000000000000, - // deadline: 1715702400 - // }) - // ), - // switchboardSolana - // ); - uint256 srcAmount = 1000000; // mintOnEvm(srcAmount, userEvmAddress, appGateway); // mintOnSolana(srcAmount, userEvmAddress, appGateway); - transferEvmToSolana(srcAmount, userEvmAddress, appGateway); - - // This works: - // appGateway.transferForDebug( - // buildSolanaInstruction( - // EvmSolanaAppGateway.TransferOrderEvmToSolana({ - // srcEvmToken: 0x4200000000000000000000000000000000000006, - // // mint on local-testnet: BdUzPsaAicEWinR7b14YLtvavwM8zYn8BaHKqGQ8by2q - // dstSolanaToken: 0x9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a, - // userEvm: 0x4200000000000000000000000000000000000005, - // // alice super token ata: LVuCmGaoHjAGu54dFppzujS1Ti61CBac57taeQbokUr - // destUserTokenAddress: 0x04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb, - // srcAmount: 1000000, - // deadline: 1715702400 - // }) - // ), - // switchboardSolana - // ); + // transferEvmToSolana(srcAmount, userEvmAddress, appGateway); + // readSolanaSuperTokenConfigAccount(appGateway); + readSolanaTokenAccount(appGateway); } function transferEvmToSolana( @@ -75,7 +55,7 @@ contract EvmSolanaOnchainCalls is Script { EvmSolanaAppGateway.TransferOrderEvmToSolana memory order = EvmSolanaAppGateway .TransferOrderEvmToSolana({ - srcEvmToken: 0x817fe2ED9c6EE7507C30D1feea417d728546efA1, // Forwarder(!!) for Super-token contract on given chain + srcEvmToken: 0x2A159f24E2562E5874550BE4702CAC3eAe288411, // Forwarder(!!) for Super-token contract on given chain // mint on local-testnet: BdUzPsaAicEWinR7b14YLtvavwM8zYn8BaHKqGQ8by2q dstSolanaToken: 0x9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a, userEvm: userEvmAddress, @@ -101,7 +81,7 @@ contract EvmSolanaOnchainCalls is Script { bytes memory order = abi.encode( EvmSolanaAppGateway.TransferOrderEvmToSolana({ - srcEvmToken: 0x817fe2ED9c6EE7507C30D1feea417d728546efA1, // Forwarder(!!) for Super-token contract on given chain + srcEvmToken: 0x2A159f24E2562E5874550BE4702CAC3eAe288411, // Forwarder(!!) for Super-token contract on given chain dstSolanaToken: 0x9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a, // irrelevant for EVM minting userEvm: userEvmAddress, destUserTokenAddress: 0x04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb, // irrelevant for EVM minting @@ -144,6 +124,44 @@ contract EvmSolanaOnchainCalls is Script { appGateway.mintSuperTokenSolana(solanaInstruction); } + 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 : jox6eY2gcjaKneNv96TKpjN7f3Rjcpn9dN9ZLNt3Krs + bytes32 accountToRead = 0x0af77affb0a5db632e9bafb98525232515d440861c9942e447c20eefd8883d34; + + // 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); + } + + /*************** builder functions ***************/ + function buildSolanaInstruction( EvmSolanaAppGateway.TransferOrderEvmToSolana memory order ) internal view returns (SolanaInstruction memory) { @@ -199,6 +217,26 @@ contract EvmSolanaOnchainCalls is Script { }); } + 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 @@ -230,11 +268,12 @@ contract EvmSolanaOnchainCalls is Script { }); functionArguments[2] = abi.encode(complexTestStruct); - string[] memory functionArgumentTypeNames = new string[](1); functionArgumentTypeNames[0] = "u64"; functionArgumentTypeNames[1] = "[u64;100]"; - functionArgumentTypeNames[2] = "{\"ComplexTestStruct\": {\"name\": \"string\",\"addr\": \"[u8;32]\",\"isActive\": \"boolean\",\"value\": \"u64\"}}"; + functionArgumentTypeNames[ + 2 + ] = '{"ComplexTestStruct": {"name": "string","addr": "[u8;32]","isActive": "boolean","value": "u64"}}'; bytes1[] memory accountFlags = new bytes1[](5); // superTokenConfigPda is not writable @@ -251,8 +290,6 @@ contract EvmSolanaOnchainCalls is Script { // mint instruction discriminator bytes8 instructionDiscriminator = 0x3339e12fb69289a6; - - return SolanaInstruction({ data: SolanaInstructionData({ @@ -275,5 +312,3 @@ contract EvmSolanaOnchainCalls is Script { uint256 value; } } - - diff --git a/setupInfraContracts.sh b/setupInfraContracts.sh index c08fed42..57b29e44 100644 --- a/setupInfraContracts.sh +++ b/setupInfraContracts.sh @@ -16,4 +16,4 @@ time npx hardhat run hardhat-scripts/misc-scripts/eventTopics.ts --no-compile time npx hardhat run hardhat-scripts/misc-scripts/functionSigs.ts --no-compile time npx ts-node hardhat-scripts/misc-scripts/createLabels.ts time npx hardhat run hardhat-scripts/verify/verify.ts --no-compile -yarn lint \ No newline at end of file +# yarn lint \ No newline at end of file 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 index 62e85fbc..b7ea0ffc 100644 --- a/test/DigestTest.t.sol +++ b/test/DigestTest.t.sol @@ -7,25 +7,21 @@ import "../contracts/utils/common/Constants.sol"; import "forge-std/console.sol"; contract DigestTest is Test { - - function testCallType() pure public { - bytes4 READ = bytes4(keccak256("READ")); - bytes4 WRITE = bytes4(keccak256("WRITE")); - bytes4 SCHEDULE = bytes4(keccak256("SCHEDULE")); - + 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); } - /** - emit DigestWithSourceParams(digest: 0xd64549c2e9bc8c443a5e8a5e375c72258a7131088b6fcd0c3297b40a686195b3, digestParams: DigestParams({ socket: 0x84815e8ca2f6dad7e12902c39a51bc72e13c48139b4fb10025d94e7abea2969c, transmitter: 0x138e9840861C983DC0BB9b3e941FB7C0e9Ade320, payloadId: 0x8c60d67962292aec8829ece076feee3bc37b486f9e6939cf56fa4f6bf25553bd, deadline: 1749307926 [1.749e9], callType: 1, gasLimit: 10000000 [1e7], value: 0, payload: 0x0914e65e59622aeeefb7f007aef36df62d4c380895553b0643fcc4383c7c24480af77affb0a5db632e9bafb98525232515d440861c9942e447c20eefd8883d349ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a93339e12fb69289a640420f0000000000, target: 0x55d893e742d43eafc1e6509eefca9ceb635a39bd3394041d334203ed35720922, appGatewayId: 0x000000000000000000000000751085ca028d2bcfc58cee2514def1ed72c843cd, prevDigestsHash: 0x4cfb2ef587acc8ad0cdb441f5b5e0624f7fef9c2fa084f5e93075cdc54d99d8f })) - */ - function testDigest3() public { - bytes32 expectedDigest = 0xd64549c2e9bc8c443a5e8a5e375c72258a7131088b6fcd0c3297b40a686195b3; + function testDigest() public pure { + bytes32 expectedDigest = 0xc26b01718c6f97b51ad73743bb5b1ac2abb53966d15a2948f65db43b30cce1a1; DigestParams memory inputDigestParams = DigestParams({ socket: 0x84815e8ca2f6dad7e12902c39a51bc72e13c48139b4fb10025d94e7abea2969c, @@ -40,24 +36,23 @@ contract DigestTest is Test { target: 0x0914e65e59622aeeefb7f007aef36df62d4c380895553b0643fcc4383c7c2448, appGatewayId: 0x0000000000000000000000004530a440dcc32206f901325143132da1edb8d2e9, // prevDigestsHash: 0x4cfb2ef587acc8ad0cdb441f5b5e0624f7fef9c2fa084f5e93075cdc54d99d8f - prevDigestsHash: 0x0000000000000000000000000000000000000000000000000000000000000000 + prevBatchDigestHash: 0x0000000000000000000000000000000000000000000000000000000000000000, + extraData: bytes("") }); - assertEq(uint256(inputDigestParams.callType), uint256(1)); - 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.prevDigestsHash, - bytes("") + 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); @@ -80,8 +75,8 @@ contract DigestTest is Test { params_.payload, params_.target, params_.appGatewayId, - params_.prevDigestsHash, - bytes("") + params_.prevBatchDigestHash, + params_.extraData ) ); } diff --git a/test/apps/Counter.t.sol b/test/apps/Counter.t.sol index 4d0b5010..3472fe0b 100644 --- a/test/apps/Counter.t.sol +++ b/test/apps/Counter.t.sol @@ -90,7 +90,7 @@ contract CounterTest is AppGatewayBaseSetup { counterGateway ); address optCounter = fromBytes32Format(optCounterBytes32); - + uint256 arbCounterBefore = Counter(arbCounter).counter(); uint256 optCounterBefore = Counter(optCounter).counter(); diff --git a/test/apps/ParallelCounter.t.sol b/test/apps/ParallelCounter.t.sol index 3acd413c..51763bb3 100644 --- a/test/apps/ParallelCounter.t.sol +++ b/test/apps/ParallelCounter.t.sol @@ -137,7 +137,7 @@ contract ParallelCounterTest is AppGatewayBaseSetup { parallelCounterGateway ); address arbCounter = fromBytes32Format(arbCounterBytes32); - + (bytes32 optCounterBytes32, address optCounterForwarder) = getOnChainAndForwarderAddresses( optChainSlug, counterId1, diff --git a/test/apps/SuperToken.t.sol b/test/apps/SuperToken.t.sol index 3b35db2d..18a9ea24 100644 --- a/test/apps/SuperToken.t.sol +++ b/test/apps/SuperToken.t.sol @@ -151,7 +151,7 @@ contract SuperTokenTest is AppGatewayBaseSetup { IAppGateway(appContracts.superTokenApp) ); address onChainOpt = fromBytes32Format(onChainOptBytes32); - + uint256 arbBalanceBefore = SuperToken(onChainArb).balanceOf(owner); uint256 optBalanceBefore = SuperToken(onChainOpt).balanceOf(owner); diff --git a/test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol b/test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol index 3e679626..f5a5cd84 100644 --- a/test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol +++ b/test/apps/app-gateways/super-token/EvmSolanaAppGateway.sol @@ -7,12 +7,10 @@ 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 { - bytes32 public superTokenEvm = _createContractId("superTokenEvm"); - // solana program address - bytes32 public solanaProgramId; - ForwarderSolana public forwarderSolana; event Transferred(uint40 requestCount); @@ -24,6 +22,8 @@ contract EvmSolanaAppGateway is AppGatewayBase, Ownable { uint256 initialSupply_; } + /** Write input structs **/ + struct TransferOrderEvmToSolana { address srcEvmToken; bytes32 dstSolanaToken; @@ -33,6 +33,34 @@ contract EvmSolanaAppGateway is AppGatewayBase, Ownable { uint256 deadline; } + /** Read output structs **/ + + struct SolanaTokenBalance { + uint64 amount; + uint64 decimals; + } + + struct SuperTokenConfigAccount { + bytes8 accountDiscriminator; + bytes32 owner; + bytes32 socket; + bytes32 mint; + uint8 bump; + } + + event SuperTokenConfigAccountRead(SuperTokenConfigAccount superTokenConfigAccount); + event TokenAccountRead(bytes32 tokenAccountAddress, uint64 amount, uint64 decimals); + + /** Contract data **/ + + bytes32 public superTokenEvm = _createContractId("superTokenEvm"); + // solana program address + bytes32 public solanaProgramId; + ForwarderSolana public forwarderSolana; + + mapping(bytes32 => SolanaTokenBalance) solanaTokenBalances; + SuperTokenConfigAccount superTokenConfigAccount; + constructor( address owner_, uint256 fees_, @@ -85,10 +113,8 @@ contract EvmSolanaAppGateway is AppGatewayBase, Ownable { TransferOrderEvmToSolana memory order = abi.decode(order_, (TransferOrderEvmToSolana)); ISuperToken(order.srcEvmToken).burn(order.userEvm, order.srcAmount); - // SolanaInstruction memory solanaInstruction = buildSolanaInstruction(order); - - /// we are directly calling the ForwarderSolana - forwarderSolana.callSolana(solanaInstruction); + // we are directly calling the ForwarderSolana + forwarderSolana.callSolana(abi.encode(solanaInstruction), solanaInstruction.data.programId); emit Transferred(_getCurrentRequestCount()); } @@ -102,20 +128,72 @@ contract EvmSolanaAppGateway is AppGatewayBase, Ownable { function mintSuperTokenSolana(SolanaInstruction memory solanaInstruction) external async { // we are directly calling the ForwarderSolana - forwarderSolana.callSolana(solanaInstruction); + 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 { - // ISuperToken(order.srcEvmToken).burn(order.userEvm, order.srcAmount); - - // we are directly calling the ForwarderSolana - forwarderSolana.callSolana(solanaInstruction); + 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 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 buildSolanaInstruction( TransferOrderEvmToSolana memory order