Skip to content

Import Solana related libs and changes#215

Merged
ameeshaagrawal merged 13 commits intophase-1from
solana-phase1
Nov 12, 2025
Merged

Import Solana related libs and changes#215
ameeshaagrawal merged 13 commits intophase-1from
solana-phase1

Conversation

@ameeshaagrawal
Copy link
Collaborator

@ameeshaagrawal ameeshaagrawal commented Nov 7, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Solana blockchain integration with support for Solana-specific transactions and withdrawals.
    • Extended fee management to support Solana program configuration and multi-chain settlement.
    • Introduced multi-chain payload resolution with enhanced deadline validation.
  • Bug Fixes

    • Added guard against duplicate payload submission.
    • Improved deadline enforcement in payment processing.
  • Refactor

    • Updated token balance tracking for multi-chain compatibility.
    • Simplified callback handling for asynchronous operations.

@coderabbitai
Copy link

coderabbitai bot commented Nov 7, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (9)
  • main
  • master
  • dev
  • stage
  • prod
  • staging
  • development
  • production
  • release

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR adds Solana mainnet integration to the socket protocol. Key additions include a ForwarderSolana contract for Solana cross-chain calls, cryptographic utilities for Ed25519 signature verification and Solana PDA derivation, Borsh codec libraries for serialization, and updates to the fees management system to support Solana-native withdrawals. The onCompleteData storage variable is removed from AppGatewayBase.

Changes

Cohort / File(s) Summary
Solana Infrastructure
contracts/evmx/helpers/ForwarderSolana.sol, contracts/evmx/helpers/solana-utils/SolanaSignature.sol
New Solana forwarder contract for external-facing cross-chain calls with async payload queueing; Ed25519 signature verification wrapper delegating to cryptographic library.
Cryptographic Utilities
contracts/evmx/helpers/solana-utils/Ed25519.sol, contracts/evmx/helpers/solana-utils/Ed25519_pow.sol, contracts/evmx/helpers/solana-utils/Sha512.sol
Ed25519 signature verification with SHA-512 hashing; modular exponentiation helper for curve operations; complete SHA-512 hash pipeline with 80-round compression.
Solana PDA & Utilities
contracts/evmx/helpers/solana-utils/SolanaPda.sol, contracts/evmx/helpers/solana-utils/program-pda/FeesPlugPdas.sol
PDA derivation, validation, and canonicality checks; Ed25519 curve validation; fees program PDA helpers for config, vault, tokens, and temp storage.
Borsh Codec
contracts/evmx/watcher/borsh-serde/BorshDecoder.sol, contracts/evmx/watcher/borsh-serde/BorshEncoder.sol, contracts/evmx/watcher/borsh-serde/BorshUtils.sol
Borsh serialization/deserialization for primitives, vectors, fixed arrays, and strings; endian swaps; memory utilities; array length extraction.
Fees Management
contracts/evmx/fees/Credit.sol, contracts/evmx/fees/FeesManager.sol
New Solana withdrawal flow with instruction construction; tokenOnChainBalances mapping key changed from address to bytes32; ForwarderSolana initialization; Solana program ID setters.
Core Protocol Updates
contracts/evmx/base/AppGatewayBase.sol, contracts/evmx/watcher/Watcher.sol, contracts/evmx/watcher/precompiles/WritePrecompile.sol, contracts/evmx/watcher/precompiles/SchedulePrecompile.sol, contracts/evmx/watcher/precompiles/ReadPrecompile.sol
Removed onCompleteData storage; added payload-already-set guard; multi-chain digest dispatch (Solana vs. EVM branches); deadline validation in schedule/read precompiles.
Type & Config Updates
contracts/utils/common/Structs.sol, contracts/utils/common/Constants.sol, contracts/protocol/switchboard/SwitchboardBase.sol, foundry.toml
New CCTP and Solana read payload structs; removed unused constants (FORWARD_CALL, DISTRIBUTE_FEE, DEPLOY, CALLBACK); minor comment; optimizer tuning.
Tests & Integration
test/SetupTest.t.sol, test/apps/Counter.t.sol, test/apps/counter/CounterAppGateway.sol
ForwarderSolana instantiation and wiring; test assertion updates; async callback refactoring from direct storage to scheduled callback.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CounterAppGateway
    participant Watcher
    participant WritePrecompile
    participant Forwarder as Forwarder(EVM/Solana)

    User->>CounterAppGateway: withdrawCredits(chainSlug, token, credits, ...)
    alt Is Solana Chain
        CounterAppGateway->>WritePrecompile: _createSolanaDigestParams()
        WritePrecompile->>WritePrecompile: decode SolanaInstruction
        WritePrecompile->>BorshEncoder: encodeFunctionArgs()
        BorshEncoder-->>WritePrecompile: packed function args
        WritePrecompile->>Watcher: queue payload (Solana variant)
    else Is EVM Chain
        CounterAppGateway->>WritePrecompile: _createEvmDigestParams()
        WritePrecompile->>Watcher: queue payload (EVM variant)
    end
    
    Watcher->>Forwarder: resolve via precompile
    alt Solana
        Forwarder->>ForwarderSolana: callSolana(payload, target)
        ForwarderSolana->>Watcher: addPayloadData(rawPayload)
    else EVM
        Forwarder->>EVMForwarder: call target
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Ed25519 cryptographic verification (Ed25519.sol): Dense multi-step elliptic-curve arithmetic with bit manipulation; high risk of implementation errors affecting security. Requires line-by-line verification against RFC 8032 spec.
  • Borsh codec implementations (BorshDecoder.sol, BorshEncoder.sol): Repetitive pattern across many type variants, but each variant must handle endianness, length-prefixing, and boundary checks correctly. Risk: off-by-one errors or incomplete type support.
  • Storage key migration (Credit.sol): Changing tokenOnChainBalances from mapping(uint32 => mapping(address => uint256)) to bytes32 requires auditing all access paths and ensures toBytes32Format() is applied consistently. Risk: missed conversions breaking accounting.
  • Multi-chain digest dispatch (WritePrecompile.sol): Branching logic for Solana vs. EVM with different payload construction. Risk: missing edge cases or incorrect chain slug detection.
  • PDA validation logic (SolanaPda.sol): Canonicality checks and off-curve validation are critical for security; iteration bounds must be verified.

Possibly related PRs

Poem

🌉 From Ethereum chains to Solana's shore,
Ed25519 signs what cryptography swore,
Borsh encodes, PDAs derive with grace,
Multi-chain callbacks find their place,
Storage keys transform—now bytes32 soar! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Import Solana related libs and changes' is vague and does not clearly summarize the main changes. It uses generic phrasing that could apply to many different types of modifications. Provide a more specific title that highlights the primary feature being added, such as 'Add Solana fee withdrawal support and signature verification' or 'Integrate Solana forwarder and PDA derivation utilities'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Comment @coderabbitai help to get the list of available commands and usage tips.

@arthcp
Copy link
Contributor

arthcp commented Nov 8, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 8, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
contracts/evmx/helpers/solana-utils/SolanaSignature.sol (1)

7-16: Simplify by removing unnecessary intermediate variable.

The valid variable is assigned but immediately returned without any additional logic. This can be simplified.

Apply this diff:

 function verifyMessage(
     bytes32 public_key,
     bytes32 signature_r,
     bytes32 signature_s,
     bytes memory message
 ) public pure returns (bool) {
-    bool valid = Ed25519.verify(public_key, signature_r, signature_s, message);
-
-    return valid;
+    return Ed25519.verify(public_key, signature_r, signature_s, message);
 }
contracts/utils/common/Structs.sol (1)

223-229: Consider struct field ordering for gas optimization.

The struct has two bool fields that could benefit from packing. Consider ordering fields to group smaller types together for better storage efficiency in future iterations.

For example:

struct PayloadFees {
    uint256 nativeFees;
    address refundAddress;
    address plug;
    bool isRefundEligible;
    bool isRefunded;
}

This groups the two bools at the end, which may enable better packing with adjacent storage if this struct is used in arrays or mappings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 053fd3c and ff6dfaf.

📒 Files selected for processing (24)
  • contracts/evmx/base/AppGatewayBase.sol (0 hunks)
  • contracts/evmx/fees/Credit.sol (8 hunks)
  • contracts/evmx/fees/FeesManager.sol (3 hunks)
  • contracts/evmx/helpers/ForwarderSolana.sol (1 hunks)
  • contracts/evmx/helpers/solana-utils/Ed25519.sol (1 hunks)
  • contracts/evmx/helpers/solana-utils/Ed25519_pow.sol (1 hunks)
  • contracts/evmx/helpers/solana-utils/Sha512.sol (1 hunks)
  • contracts/evmx/helpers/solana-utils/SolanaPda.sol (1 hunks)
  • contracts/evmx/helpers/solana-utils/SolanaSignature.sol (1 hunks)
  • contracts/evmx/helpers/solana-utils/program-pda/FeesPlugPdas.sol (1 hunks)
  • contracts/evmx/watcher/Watcher.sol (5 hunks)
  • contracts/evmx/watcher/borsh-serde/BorshDecoder.sol (1 hunks)
  • contracts/evmx/watcher/borsh-serde/BorshEncoder.sol (1 hunks)
  • contracts/evmx/watcher/borsh-serde/BorshUtils.sol (1 hunks)
  • contracts/evmx/watcher/precompiles/ReadPrecompile.sol (1 hunks)
  • contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (2 hunks)
  • contracts/evmx/watcher/precompiles/WritePrecompile.sol (5 hunks)
  • contracts/protocol/switchboard/SwitchboardBase.sol (1 hunks)
  • contracts/utils/common/Constants.sol (0 hunks)
  • contracts/utils/common/Structs.sol (2 hunks)
  • foundry.toml (1 hunks)
  • test/SetupTest.t.sol (3 hunks)
  • test/apps/Counter.t.sol (1 hunks)
  • test/apps/counter/CounterAppGateway.sol (1 hunks)
💤 Files with no reviewable changes (2)
  • contracts/evmx/base/AppGatewayBase.sol
  • contracts/utils/common/Constants.sol
🔇 Additional comments (15)
contracts/protocol/switchboard/SwitchboardBase.sol (1)

62-63: Clarify the TODO and verify it doesn't break signature verification.

The TODO comment is vague ("api encode packed" is ambiguous—presumably abi.encodePacked?). Switching encoding schemes changes the hash output and breaks signature verification, since signatures were signed over the current abi.encode() output. This is a breaking change that requires coordination with off-chain signers.

Clarify what needs to change, why, and what the migration path is. This should be tracked in an issue with context rather than an inline comment. Also, this seems orthogonal to the Solana integration scope of this PR—should it be in a separate change?

contracts/evmx/watcher/precompiles/SchedulePrecompile.sol (2)

30-30: LGTM!

The error declaration is clear and follows the existing error pattern in the codebase.


145-150: LGTM!

The deadline enforcement is correctly implemented. The function now properly validates that execution occurs within the valid window: after the scheduled time but before the deadline expires. This aligns with the deadline checks added to ReadPrecompile.

contracts/evmx/watcher/precompiles/ReadPrecompile.sol (2)

71-74: LGTM!

The event emission was added to the setFees function. This is proper practice for tracking fee configuration changes.


67-69: LGTM

The deadline validation is correctly implemented and consistent with SchedulePrecompile.sol. The DeadlinePassed() error is properly defined in common/Errors.sol and will resolve correctly through the import on line 8.

contracts/utils/common/Structs.sol (2)

202-220: LGTM!

The Solana read types are well-designed for flexible account reading with both predefined and generic schema support. The use of keccak256 for schema name hashing is appropriate for efficient comparisons.


232-248: LGTM!

The structs are well-designed for their purposes. SponsoredPayloadFees is simple and clear. MessageOverrides contains all necessary fields for message configuration with appropriate types.

test/apps/Counter.t.sol (1)

87-88: LGTM!

The assertion correctly reflects the updated callback behavior. With the new async callback flow using then(), the onIncrementComplete callback is properly executed, setting incremented to true. This test now validates the correct behavior.

test/apps/counter/CounterAppGateway.sol (1)

29-33: LGTM!

The migration from abi.encodeWithSelector to then() correctly implements the new async callback architecture. Using bytes("") as the callback data is appropriate since onIncrementComplete doesn't require any parameters. This is cleaner than the previous storage-based completion approach.

contracts/evmx/watcher/Watcher.sol (5)

134-137: LGTM!

The cancellation guard correctly prevents processing of cancelled payloads with an early return. This is the appropriate behavior since attempting to resolve a cancelled payload should be a no-op rather than an error.


141-147: LGTM!

The change correctly captures the success result from _markResolved and short-circuits if resolution fails. This prevents incorrect fee settlement and event emission when promise resolution fails, which is the correct behavior.


149-164: LGTM!

Removing the deadline check from _markResolved is the correct refactoring. Deadline validation is now properly handled by the individual precompiles (ReadPrecompile and SchedulePrecompile) during their resolvePayload calls. This improves separation of concerns.


119-127: LGTM!

The multi-line formatting of abi.decode calls improves readability without any semantic changes. This is consistent with Solidity formatting best practices.

Also applies to: 166-174


59-72: The guard is sufficient—this check prevents double-setting effectively.

The payloadData.asyncPromise != address(0) sentinel works because:

  1. payloadData has no reset/clear mechanism anywhere in the codebase
  2. The guard checks the current state before overwriting, blocking subsequent calls
  3. Once asyncPromise is set (non-zero), it persists, ensuring the guard always triggers on retry

The design uses struct field membership as the sentinel rather than an explicit state flag. While an explicit boolean flag would be marginally clearer, the current approach is functionally sound and prevents all double-setting scenarios.

foundry.toml (1)

8-8: Confirm this optimizer configuration is appropriate for alpha development and establish production settings before mainnet deployment.

The alpha-stage protocol status and ffi = true setting indicate this is a development configuration. Low optimizer_runs = 1 with minimal optimizer steps (optimizerSteps = "u") is acceptable for faster iteration during active development, but this configuration will significantly increase gas costs for end users if deployed to production without adjustment.

Before mainnet deployment, establish a production profile in foundry.toml with optimizer_runs >= 200 and comprehensive optimizer steps. Document why development uses minimal optimization.

Comment on lines +456 to +460
// convert EVM uint256 18 decimals to Solana uint64 6 decimals
function convertToSolanaUint64(uint256 amount) pure returns (uint64) {
uint256 scaledAmount = amount / 10 ** 12;
require(scaledAmount <= type(uint64).max, "Amount exceeds uint64 max");
return uint64(scaledAmount);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent loss when converting to Solana decimals

convertToSolanaUint64 floors values that are not multiples of 1e12, so withdrawing an amount like 1 wei burns the full credit but forwards 0 to Solana. This should revert on unsupported decimal precision so users never lose residual value. Add a remainder check before dividing.

 function convertToSolanaUint64(uint256 amount) pure returns (uint64) {
-    uint256 scaledAmount = amount / 10 ** 12;
+    if (amount % 10 ** 12 != 0) revert InvalidAmount();
+    uint256 scaledAmount = amount / 10 ** 12;
     require(scaledAmount <= type(uint64).max, "Amount exceeds uint64 max");
     return uint64(scaledAmount);
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In contracts/evmx/fees/Credit.sol around lines 456 to 460, the
convertToSolanaUint64 function currently floors values by dividing by 10**12
which can silently drop residual wei; add a remainder check before scaling so
the function reverts on unsupported precision. Specifically, require(amount %
(10**12) == 0, "Unsupported decimal precision") (use uint256 literal for the
modulus), then perform the division and keep the existing uint64 overflow
require; this prevents accidental loss of value by reverting when the input has
non-zero lower 12 decimals.

Comment on lines +15 to +106
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<u8>"))) {
uint32 length;
uint8[] memory value;
(length, value) = decodeUint8Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u16>"))) {
uint32 length;
uint16[] memory value;
(length, value) = decodeUint16Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u32>"))) {
uint32 length;
uint32[] memory value;
(length, value) = decodeUint32Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u64>"))) {
uint32 length;
uint64[] memory value;
(length, value) = decodeUint64Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u128>"))) {
uint32 length;
uint128[] memory value;
(length, value) = decodeUint128Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<String>"))) {
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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject trailing bytes before returning.
This should call data.done() before returning so that extra bytes trigger a parse failure instead of being silently ignored. Right now a caller can append junk data and still pass validation, which breaks schema integrity.

         for (uint256 i = 0; i < schema.valuesTypeNames.length; i++) {
             string memory typeName = schema.valuesTypeNames[i];
@@
-        return decodedParams;
+        data.done();
+        return decodedParams;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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<u8>"))) {
uint32 length;
uint8[] memory value;
(length, value) = decodeUint8Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u16>"))) {
uint32 length;
uint16[] memory value;
(length, value) = decodeUint16Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u32>"))) {
uint32 length;
uint32[] memory value;
(length, value) = decodeUint32Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u64>"))) {
uint32 length;
uint64[] memory value;
(length, value) = decodeUint64Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u128>"))) {
uint32 length;
uint128[] memory value;
(length, value) = decodeUint128Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<String>"))) {
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;
}
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<u8>"))) {
uint32 length;
uint8[] memory value;
(length, value) = decodeUint8Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u16>"))) {
uint32 length;
uint16[] memory value;
(length, value) = decodeUint16Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u32>"))) {
uint32 length;
uint32[] memory value;
(length, value) = decodeUint32Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u64>"))) {
uint32 length;
uint64[] memory value;
(length, value) = decodeUint64Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<u128>"))) {
uint32 length;
uint128[] memory value;
(length, value) = decodeUint128Vec(data);
decodedParams[i] = abi.encode(value);
} else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec<String>"))) {
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");
}
}
data.done();
return decodedParams;
}
🤖 Prompt for AI Agents
In contracts/evmx/watcher/borsh-serde/BorshDecoder.sol around lines 15 to 106,
the decoder returns decodedParams without verifying there are no leftover bytes;
call data.done() just before returning to enforce that all input was consumed
and cause a revert if trailing bytes remain, ensuring parse failures on
extra/junk data.

Comment on lines +127 to +147
} 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 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce fixed-length Borsh array lengths

For every fixed-length array type except [u8;N] we never confirm that the decoded array length actually matches N. That means a caller can pass an undersized or oversized payload, we’ll happily pack whatever is provided, and the downstream Solana program will deserialize garbage (or fail) because Borsh expects a fixed-length sequence. This should revert immediately when the ABI input length does not match the declared schema. Add the same length check you already have for [u8;N] to the [u16;N]/[u32;N]/[u64;N]/[u128;N]/[String;N] branches so malformed data can’t slip through.

-            } else if (BorshUtils.startsWith(typeName, "[u16;")) {
-                uint16[] memory abiDecodedArg = abi.decode(data, (uint16[]));
+            } else if (BorshUtils.startsWith(typeName, "[u16;")) {
+                uint256 expectedLength = BorshUtils.extractArrayLength(typeName);
+                uint16[] memory abiDecodedArg = abi.decode(data, (uint16[]));
+                require(abiDecodedArg.length == expectedLength, "[u16;N] length mismatch");
                 bytes memory borshEncodedArg = encodeUint16Array(abiDecodedArg);
                 functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg);
             } else if (BorshUtils.startsWith(typeName, "[u32;")) {
-                uint32[] memory abiDecodedArg = abi.decode(data, (uint32[]));
+                uint256 expectedLength = BorshUtils.extractArrayLength(typeName);
+                uint32[] memory abiDecodedArg = abi.decode(data, (uint32[]));
+                require(abiDecodedArg.length == expectedLength, "[u32;N] length mismatch");
                 bytes memory borshEncodedArg = encodeUint32Array(abiDecodedArg);
                 functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg);
             } else if (BorshUtils.startsWith(typeName, "[u64;")) {
-                uint64[] memory abiDecodedArg = abi.decode(data, (uint64[]));
+                uint256 expectedLength = BorshUtils.extractArrayLength(typeName);
+                uint64[] memory abiDecodedArg = abi.decode(data, (uint64[]));
+                require(abiDecodedArg.length == expectedLength, "[u64;N] length mismatch");
                 bytes memory borshEncodedArg = encodeUint64Array(abiDecodedArg);
                 functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg);
             } else if (BorshUtils.startsWith(typeName, "[u128;")) {
-                uint128[] memory abiDecodedArg = abi.decode(data, (uint128[]));
+                uint256 expectedLength = BorshUtils.extractArrayLength(typeName);
+                uint128[] memory abiDecodedArg = abi.decode(data, (uint128[]));
+                require(abiDecodedArg.length == expectedLength, "[u128;N] length mismatch");
                 bytes memory borshEncodedArg = encodeUint128Array(abiDecodedArg);
                 functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg);
             } else if (BorshUtils.startsWith(typeName, "[String;")) {
-                string[] memory abiDecodedArg = abi.decode(data, (string[]));
+                uint256 expectedLength = BorshUtils.extractArrayLength(typeName);
+                string[] memory abiDecodedArg = abi.decode(data, (string[]));
+                require(abiDecodedArg.length == expectedLength, "[String;N] length mismatch");
                 bytes memory borshEncodedArg = encodeStringArray(abiDecodedArg);
                 functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg);
             }
🤖 Prompt for AI Agents
contracts/evmx/watcher/borsh-serde/BorshEncoder.sol around lines 127 to 147: the
fixed-length array branches for [u16;N], [u32;N], [u64;N], [u128;N], and
[String;N] do not validate that abi.decode produced exactly N elements; add the
same length validation you have for [u8;N]. Parse the expected N from typeName
(the digits between the semicolon and closing bracket), then
require(abiDecodedArg.length == N, "BorshEncoder: invalid fixed-length array
length") (cast lengths to uint256 as needed) before calling the encode*Array
function and packing, so malformed undersized/oversized inputs revert
immediately.

Comment on lines +274 to +295
// TODO: this is temporary, must be injected from function arguments
// bytes32 of Solana Socket address : 9vFEQ5e3xf4eo17WttfqmXmnqN3gUicrhFGppmmNwyqV
bytes32 hardcodedSocket = 0x84815e8ca2f6dad7e12902c39a51bc72e13c48139b4fb10025d94e7abea2969c;
// bytes32 of Solana transmitter address : pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ
bytes32 transmitterSolana = 0x0c1a5886fe1093df9fc438c296f9f7275b7718b6bc0e156d8d336c58f083996d;
return
DigestParams(
// watcherPrecompileConfig__.sockets(params_.payloadHeader.getChainSlug()), // TODO: this does not work, for some reason it returns 0x000.... address
hardcodedSocket,
// toBytes32Format(transmitter_),
transmitterSolana,
payloadId_,
deadline_,
rawPayload_.overrideParams.callType,
gasLimit_,
rawPayload_.overrideParams.value,
payloadPacked,
rawPayload_.transaction.target,
abi.encode(toBytes32Format(appGateway_)),
bytes32(0),
bytes("")
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Stop hardcoding Solana socket/transmitter in digests

_createSolanaDigestParams bakes in fixed bytes32 values for the Solana socket and transmitter. Any environment whose watcher config differs from these literals (read: every non-dev deployment) will compute a different digest off-chain, so proofs can never verify and executions brick. This should read the Solana socket/transmitter from watcher configuration or from the payload parameters, the same way the EVM path already does. Until the sender-provided data populates these fields (or watcher__ exposes dedicated Solana getters), we can’t release this path.

@ameeshaagrawal ameeshaagrawal merged commit 475ec42 into phase-1 Nov 12, 2025
1 check passed
@ameeshaagrawal ameeshaagrawal deleted the solana-phase1 branch November 12, 2025 10:10
This was referenced Nov 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants