Skip to content

Feat/celer rfq#10

Open
reddyismav wants to merge 8 commits into
mainfrom
feat/celer-rfq
Open

Feat/celer rfq#10
reddyismav wants to merge 8 commits into
mainfrom
feat/celer-rfq

Conversation

@reddyismav

@reddyismav reddyismav commented Jun 17, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

Release Notes

  • New Features

    • Added CelerExecutor for bridge forwarding plus owner-admin refund handling.
    • Added RFQVaultExecutor supporting solver-signed deposits, fulfilment, optional swap-and-fulfil, batched actions, and refunds.
  • Deployment & Configuration

    • Added deterministic CREATE3 deployment scripts for both executors, with per-network address persistence.
    • Added deploy:rfq-vault-executor npm script.
    • Updated .env.example to clarify SOLVER_SIGNER_ADDRESS applies to both related deployments (still defaults to deployer).
  • Tests

    • Added extensive unit tests covering deposit, fulfil/refund, replay protection, signer validation, swaps, and access control.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Two new executor contracts are introduced: CelerExecutor proxies Celer bridge send/refund operations using a new Pb protobuf decoding library and ICelerBridge interface, while RFQVaultExecutor implements a solver-signer-gated vault with signed fulfil, swap, batch action, and refund flows. Both are deployed deterministically via CREATE3 with new deployment scripts, address registries, and salt constants. A full Foundry test suite validates RFQVaultExecutor behavior.

Changes

RFQVaultExecutor Contract, Deployment, and Tests

Layer / File(s) Summary
RFQVaultExecutor contract scaffold and types
src/executors/RFQVaultExecutor.sol
Defines Action struct, custom errors, discriminator constants, immutable OPEN_ROUTER, mutable SOLVER_SIGNER, and quoteIdUsed/nonceUsed replay-tracking mappings, wired in the constructor.
External execution surface and owner controls
src/executors/RFQVaultExecutor.sol
Implements receiveNative (ETH deposit), receiveERC20 (ERC20 pull), fulfil (direct signed transfer), swapAndFulfil (signed swap with optional approval, action execution, balance-delta output, minOutput enforcement), performActions (signed nonce-gated batch), refund, markForRefund, setSolverSigner, rescueFunds, and payable receive fallback.
Internal hashing, auth, and replay prevention
src/executors/RFQVaultExecutor.sol
_hashFulfilOrRefund and _hashSwapAndFulfil use fixed-size assembly layouts over chainid, contract address, discriminator, and call parameters; _verifyAndMarkQuoteId/_verifyAndMarkNonce authenticate signatures via AuthenticationLib and prevent replay via keccak-derived storage slots with inline assembly; _performAction issues raw external calls and returns success boolean.
Deployment infrastructure and configuration
scripts/deploy/rfqVaultExecutorAddresses.ts, scripts/deploy/deployRFQVaultExecutor.ts, package.json, .env.example
Address read/write helpers for per-network JSON persistence, full deployment script (parameter resolution with hex validation, CREATE3 address derivation, deploy-or-verify branching, on-chain field assertions for owner/solverSigner, persistence, optional Hardhat verification), npm deploy:rfq-vault-executor script entry, and updated env comment for SOLVER_SIGNER_ADDRESS.
Unit tests and mocks
test/unit/RFQVaultExecutor.t.sol
Foundry tests covering receiveNative, fulfil/refund (ERC20 and native, quote-id single-use, invalid signer, cross-function replay), swapAndFulfil (success, insufficient output, zero-approval-token path), performActions, setSolverSigner, rescueFunds; includes assembly-based hash helpers and mock contracts (MockERC20, MockSwap, MockActionTarget, CallerStub).

CelerExecutor Contract and Deployment

Layer / File(s) Summary
Pb protobuf library and ICelerBridge interface
src/common/lib/Pb.sol, src/interfaces/ICelerBridge.sol
Pb implements runtime protobuf decoding: varint decoding with bounded loop (hard revert on overflow), length-delimited byte reading with bounds checking, cursor skip for Varint/LengthDelim values, and assembly-based type converters to uint256, address, and bytes32. ICelerBridge exposes send, sendNative, withdraws view, and withdraw external methods.
CelerExecutor contract
src/executors/CelerExecutor.sol
execute (OPEN_ROUTER-gated) routes native via sendNative or pulls ERC-20 and calls ICelerBridge.send; refundCelerUserAdmin (owner-only) decodes withdrawal message via Pb, snapshots balances, conditionally calls _withdrawIfNeeded, then _payoutRefund to transfer exact refund amount as native ETH or ERC-20; rescueFunds delegates to RescueFundsLib; payable receive/fallback included.
Deployment infrastructure
scripts/deploy/create3.ts, scripts/deploy/celerExecutorAddresses.ts, scripts/deploy/deployCelerExecutor.ts
CREATE3 salt constants for both CelerExecutor and RFQVaultExecutor, address read/write helpers for per-network JSON persistence, full deployment script (CELER_EXECUTOR_CHAIN_CONFIG map, owner/openRouter resolution with hex validation and fallbacks, initcode generation, CREATE3 expected-address derivation, deploy-or-verify branching on existing bytecode, on-chain field assertion for owner/OPEN_ROUTER/CELER_BRIDGE, address persistence, backend config printing, and Hardhat verification).

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 100, 200, 0.5)
    note right of Solver: RFQVaultExecutor flow
    Caller->>RFQVaultExecutor: receiveNative(quoteId) + ETH
    Solver->>RFQVaultExecutor: swapAndFulfil(quoteId, nonce, ..., action, sig)
    RFQVaultExecutor->>RFQVaultExecutor: _verifyAndMarkQuoteId (sig check + replay guard)
    RFQVaultExecutor->>SwapTarget: _performAction (raw low-level call)
    RFQVaultExecutor->>RFQVaultExecutor: balance delta >= minOutput check
    RFQVaultExecutor->>Receiver: transfer outputToken delta
  end
  rect rgba(100, 200, 100, 0.5)
    note right of Owner: CelerExecutor refund flow
    Owner->>CelerExecutor: refundCelerUserAdmin(_request, _sigs, ...)
    CelerExecutor->>CelerExecutor: decWithdrawMsg via Pb protobuf decoder
    CelerExecutor->>ICelerBridge: withdraws(transferId) view check
    CelerExecutor->>ICelerBridge: withdraw(_wdmsg, _sigs, _signers, _powers)
    CelerExecutor->>CelerExecutor: balance delta == request.amount check
    CelerExecutor->>Receiver: transfer ETH or ERC-20
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

Two new executors join the chain's ballet,
One vaults RFQ quotes with a solver's cryptic key,
The other bridges Celer, decoding byte by protobuf byte,
CREATE3 salts seal each address in deterministic light—
Assembly hashes guard against replay's spite,
Signatures and storage slots keep it all locked tight. 🔐🌉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feat/celer rfq' is vague and generic, using abbreviations without context. It doesn't clearly indicate what the PR accomplishes or why these changes matter. Rename to something specific like 'Add CelerExecutor and RFQVaultExecutor deployment infrastructure' or 'Implement Celer bridge and RFQ vault executor contracts'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/celer-rfq

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Line 9: The SOLVER_SIGNER_ADDRESS configuration line has a comment appended
after the equals sign on the same line, which can cause dotenv parsing and
linting issues. Separate the comment from the variable assignment by moving the
comment to a new line above the SOLVER_SIGNER_ADDRESS definition. Remove any
trailing spaces after the equals sign so the line ends with just the equals
sign, keeping the configuration format consistent with dotenv best practices.

In `@scripts/deploy/celerExecutorAddresses.ts`:
- Around line 67-73: The address parameter is assigned directly to
deployments.CelerExecutor without any validation, allowing invalid values to be
persisted to the deployment JSON file. Before the line
`deployments.CelerExecutor = address;`, add validation logic to verify that the
address parameter is in the correct format (such as a valid Ethereum address).
If the address is invalid, throw an error or reject the operation before the
writeFile call executes. This ensures only valid deployment addresses are
persisted to the file.

In `@scripts/deploy/deployCelerExecutor.ts`:
- Around line 228-231: Replace the deployCreate3.staticCall() invocation with
computeCreate3Address() to compute the expected address before checking for
existing bytecode. Use computeCreate3Address() with CELER_EXECUTOR_CREATE3_SALT
and initcode to get the expectedAddress, then proceed with the bytecode
existence check at line 235, and only call deployCreate3() directly if no code
exists at that address. This implements the CreateX-recommended idempotent
deployment pattern that prevents reverts on re-runs when the contract is already
deployed.

In `@scripts/deploy/deployRFQVaultExecutor.ts`:
- Around line 37-59: The functions resolveOwnerAddress and
resolveSolverSignerAddress silently fall back to deployerAddress when
environment variables are present but fail validation against ADDR_HEX_RE, which
can lead to unintended deployments. Modify both functions to throw an error
immediately when process.env.OWNER_ADDRESS or process.env.SOLVER_SIGNER_ADDRESS
are set (non-empty) but do not pass the ADDR_HEX_RE validation, rather than
falling back to deployerAddress. This ensures malformed addresses are caught
early and fail fast during deployment.

In `@src/common/lib/Pb.sol`:
- Around line 35-55: The decVarint function reads up to 10 bytes unconditionally
from memory without validating that those bytes are actually available in the
buffer. To fix this, add a bounds check before the assembly read to ensure the
remaining buffer size is sufficient, and ensure that if the loop completes all
10 iterations without finding a terminating byte (where b & 0x80 == 0), the
function reverts instead of returning a potentially invalid truncated varint.
Specifically, calculate the available bytes as buf.b.length minus buf.idx and
verify that the read operation respects this boundary.

In `@src/executors/CelerExecutor.sol`:
- Around line 34-38: The constructor for CelerExecutor does not validate that
the critical addresses _openRouter and _celerBridgeRouter are non-zero. Add
validation checks in the constructor to ensure both addresses are not zero
before assigning them to OPEN_ROUTER and CELER_BRIDGE respectively. Use require
statements to revert the transaction if either address is zero, preventing
deployment of a misconfigured contract that could route funds incorrectly or
become unusable.
- Around line 61-67: The execute function does not validate msg.value against
the amount parameter, which can lead to spending pre-existing ETH or trapping
excess ETH in the contract. Add a require statement enforcing msg.value ==
amount in the isNative branch before calling CELER_BRIDGE.sendNative, and add a
require statement enforcing msg.value == 0 before the
SafeTransferLib.safeTransferFrom call in the ERC-20 flow to ensure proper
invariants.

In `@src/executors/RFQVaultExecutor.sol`:
- Around line 42-45: Add validation checks in the constructor of
RFQVaultExecutor to reject zero addresses for both _openRouter and _solverSigner
parameters. Use require statements to ensure _openRouter is not address(0) and
_solverSigner is not address(0), with descriptive error messages. Apply the same
validation to the setSolverSigner method (around lines 152-154) to prevent these
critical addresses from being set to zero after deployment, which would break
signed transaction flows and make receiveNative inaccessible.
- Around line 98-100: After the _performAction method is executed within the
approval block (where approvalToken, approvalSpender, and approvalAmount are
used), add a call to revoke the temporary allowance by setting it back to zero
using SafeTransferLib.safeApproveWithRetry with an approvalAmount of 0. This
ensures the spender's allowance is cleared immediately after the swap execution,
preventing any residual allowance that could be exploited for unauthorized token
pulls.

In `@test/unit/RFQVaultExecutor.t.sol`:
- Around line 483-484: Replace the generic vm.expectRevert() calls with specific
error selectors from the Ownable contract. For the vault.setSolverSigner()
calls, determine the exact custom error that Ownable throws (likely
Ownable.Unauthorized() or equivalent) and pass that error selector to
vm.expectRevert() instead of calling it without parameters. This ensures the
test validates the correct revert reason rather than accepting any revert. Apply
this change to both occurrences of the vault.setSolverSigner() test blocks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba006e95-0cb2-49e9-8295-07838ce117f9

📥 Commits

Reviewing files that changed from the base of the PR and between 26e3446 and 7a93122.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • .env.example
  • package.json
  • scripts/deploy/celerExecutorAddresses.ts
  • scripts/deploy/create3.ts
  • scripts/deploy/deployCelerExecutor.ts
  • scripts/deploy/deployRFQVaultExecutor.ts
  • scripts/deploy/rfqVaultExecutorAddresses.ts
  • src/common/lib/Pb.sol
  • src/executors/CelerExecutor.sol
  • src/executors/RFQVaultExecutor.sol
  • src/interfaces/ICelerBridge.sol
  • test/unit/RFQVaultExecutor.t.sol

Comment thread .env.example
# Constructor arguments / deployment checks
OWNER_ADDRESS=
SOLVER_SIGNER_ADDRESS= # BungeeReceiver deploy only; defaults to deployer
SOLVER_SIGNER_ADDRESS= # BungeeReceiver / RFQVaultExecutor deploy; defaults to deployer

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix SOLVER_SIGNER_ADDRESS formatting to avoid dotenv parse/lint ambiguity.

This line should avoid trailing spaces/comment after =. Keep the comment on its own line.

Suggested fix
-SOLVER_SIGNER_ADDRESS=         # BungeeReceiver / RFQVaultExecutor deploy; defaults to deployer
+# BungeeReceiver / RFQVaultExecutor deploy; defaults to deployer
+SOLVER_SIGNER_ADDRESS=
📝 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
SOLVER_SIGNER_ADDRESS= # BungeeReceiver / RFQVaultExecutor deploy; defaults to deployer
# BungeeReceiver / RFQVaultExecutor deploy; defaults to deployer
SOLVER_SIGNER_ADDRESS=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 9-9: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 9-9: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example at line 9, The SOLVER_SIGNER_ADDRESS configuration line has a
comment appended after the equals sign on the same line, which can cause dotenv
parsing and linting issues. Separate the comment from the variable assignment by
moving the comment to a new line above the SOLVER_SIGNER_ADDRESS definition.
Remove any trailing spaces after the equals sign so the line ends with just the
equals sign, keeping the configuration format consistent with dotenv best
practices.

Source: Linters/SAST tools

Comment on lines +67 to +73
deployments.CelerExecutor = address;

await mkdir(dirname(filePath), { recursive: true });
await writeFile(
filePath,
`${JSON.stringify(deployments, null, 2)}\n`,
'utf8',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate address format before persisting deployment JSON.

This write path accepts any string and persists it as CelerExecutor. Invalid values then look like valid deployment state to downstream scripts.

Proposed fix
 import { mkdir, readFile, writeFile } from 'fs/promises';
 import { dirname, resolve } from 'path';
+
+const ADDR_HEX_RE = /^0x[a-fA-F0-9]{40}$/;
@@
 export async function writeCelerExecutorAddress(
   network: string,
   address: string,
   stage = 'prod',
 ): Promise<string> {
+  if (!ADDR_HEX_RE.test(address.trim())) {
+    throw new Error(`Invalid CelerExecutor address: ${address}`);
+  }
   const filePath = celerExecutorAddressesPath(network, stage);
@@
-  deployments.CelerExecutor = address;
+  deployments.CelerExecutor = address.trim();
As per coding guidelines, "Check for resource leaks, race conditions, and unhandled edge cases."
📝 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
deployments.CelerExecutor = address;
await mkdir(dirname(filePath), { recursive: true });
await writeFile(
filePath,
`${JSON.stringify(deployments, null, 2)}\n`,
'utf8',
if (!ADDR_HEX_RE.test(address.trim())) {
throw new Error(`Invalid CelerExecutor address: ${address}`);
}
deployments.CelerExecutor = address.trim();
await mkdir(dirname(filePath), { recursive: true });
await writeFile(
filePath,
`${JSON.stringify(deployments, null, 2)}\n`,
'utf8',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deploy/celerExecutorAddresses.ts` around lines 67 - 73, The address
parameter is assigned directly to deployments.CelerExecutor without any
validation, allowing invalid values to be persisted to the deployment JSON file.
Before the line `deployments.CelerExecutor = address;`, add validation logic to
verify that the address parameter is in the correct format (such as a valid
Ethereum address). If the address is invalid, throw an error or reject the
operation before the writeFile call executes. This ensures only valid deployment
addresses are persisted to the file.

Source: Coding guidelines

Comment on lines +228 to +231
const expectedAddress = (await create3Factory.deployCreate3.staticCall(
CELER_EXECUTOR_CREATE3_SALT,
initcode,
)) as string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For CreateX (https://createx.rocks/), does calling deployCreate3(bytes32,bytes) via eth_call/static simulation revert when the same CREATE3 salt has already been deployed? Please include docs/source references and expected idempotent deployment pattern.

💡 Result:

Yes, calling deployCreate3(bytes32,bytes) via eth_call (or any static simulation) will revert if the address derived from the provided salt has already been deployed [1]. Underlying Mechanism: CreateX uses the CREATE3 pattern, which relies on a two-step deployment: 1. It uses CREATE2 to deploy a proxy contract at an address determined by the CREATE3 salt [1]. 2. It then performs a call to that proxy to deploy the target contract [1]. If the salt has already been used to deploy a contract, the CREATE2 deployment of the proxy (or the subsequent contract initialization call) will fail [1]. The CreateX contract explicitly checks the success of the deployment and reverts if it fails [1]: if (proxy == address(0)) { revert FailedContractCreation({emitter: _SELF}); }... (bool success,) = proxy.call{value: msg.value}(initCode); _requireSuccessfulContractCreation({success: success, newContract: newContract}); When performing an eth_call (static simulation), any operation that triggers a revert will cause the entire call to fail, correctly signaling that the deployment cannot proceed at that address [1]. Idempotent Deployment Pattern: To implement idempotent deployments with CreateX, you should check if the contract already exists at the computed address before attempting to deploy [2]. You can use the computeCreate3Address function provided by the CreateX factory to determine the expected address for a given salt [2][3]: // 1. Compute the expected address address expectedAddress = createX.computeCreate3Address(salt); // 2. Check if code exists at that address uint256 codeSize; assembly { codeSize:= extcodesize(expectedAddress) } if (codeSize == 0) { // 3. Address is empty; safe to deploy createX.deployCreate3{value: msgValue}(salt, initCode); } else { // Contract already exists; handle accordingly } Note: CreateX hashes the provided salt within its _guard function to implement security features like permissioned deployment protection [4]. Therefore, you must use the factory's computeCreate3Address function rather than calculating the address manually to ensure the salt and address match the factory's internal logic [2][3].

Citations:


🏁 Script executed:

cat -n scripts/deploy/deployCelerExecutor.ts | sed -n '220,240p'

Repository: SocketDotTech/openrouter

Length of output: 800


🏁 Script executed:

find . -name "Create3ABI*" -o -name "*Create3*" -type f | grep -i abi | head -20

Repository: SocketDotTech/openrouter

Length of output: 50


🏁 Script executed:

head -50 scripts/deploy/deployCelerExecutor.ts | grep -A 5 -B 5 "Create3ABI"

Repository: SocketDotTech/openrouter

Length of output: 398


🏁 Script executed:

find . -name "create3.ts" -o -name "create3.js" | head -5

Repository: SocketDotTech/openrouter

Length of output: 96


🏁 Script executed:

cat -n ./scripts/deploy/create3.ts | head -100

Repository: SocketDotTech/openrouter

Length of output: 4438


🏁 Script executed:

rg "Create3ABI" scripts/deploy/create3.ts -A 50 | head -80

Repository: SocketDotTech/openrouter

Length of output: 1699


Fix idempotent deployment pattern: compute address before staticCall to prevent revert on re-runs.

Calling deployCreate3.staticCall() before checking if code exists at the address will cause the script to crash if the contract is already deployed. CreateX deployCreate3 reverts when the salt has been used; the call never succeeds to reach the bytecode check at line 235.

Use computeCreate3Address() first to get the expected address, then check if bytecode exists, and only call deployCreate3() if the address is empty. This matches the CreateX-recommended idempotent pattern and is already available in the Create3ABI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deploy/deployCelerExecutor.ts` around lines 228 - 231, Replace the
deployCreate3.staticCall() invocation with computeCreate3Address() to compute
the expected address before checking for existing bytecode. Use
computeCreate3Address() with CELER_EXECUTOR_CREATE3_SALT and initcode to get the
expectedAddress, then proceed with the bytecode existence check at line 235, and
only call deployCreate3() directly if no code exists at that address. This
implements the CreateX-recommended idempotent deployment pattern that prevents
reverts on re-runs when the contract is already deployed.

Comment thread scripts/deploy/deployRFQVaultExecutor.ts
Comment thread src/common/lib/Pb.sol
Comment on lines +35 to +55
function decVarint(Buffer memory buf) internal pure returns (uint256 v) {
bytes10 tmp;
bytes memory bb = buf.b;
v = buf.idx;
assembly ("memory-safe") {
tmp := mload(add(add(bb, 32), v))
}
uint256 b;
v = 0;
for (uint256 i = 0; i < 10; i++) {
assembly ("memory-safe") {
b := byte(i, tmp)
}
v |= (b & 0x7F) << (i * 7);
if (b & 0x80 == 0) {
buf.idx += i + 1;
return v;
}
}
revert();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound decVarint reads to remaining bytes.

Line 39/46 reads up to 10 bytes unconditionally from memory. Truncated varints can decode as valid values (with zero-filled tail) and move buf.idx past buf.b.length instead of reverting. This should reject incomplete varints.

Proposed fix
 function decVarint(Buffer memory buf) internal pure returns (uint256 v) {
-    bytes10 tmp;
     bytes memory bb = buf.b;
-    v = buf.idx;
-    assembly ("memory-safe") {
-        tmp := mload(add(add(bb, 32), v))
-    }
-    uint256 b;
-    v = 0;
+    uint256 start = buf.idx;
     for (uint256 i = 0; i < 10; i++) {
-        assembly ("memory-safe") {
-            b := byte(i, tmp)
-        }
+        if (start + i >= bb.length) revert();
+        uint256 b = uint8(bb[start + i]);
         v |= (b & 0x7F) << (i * 7);
         if (b & 0x80 == 0) {
-            buf.idx += i + 1;
+            buf.idx = start + i + 1;
             return v;
         }
     }
     revert();
 }
📝 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
function decVarint(Buffer memory buf) internal pure returns (uint256 v) {
bytes10 tmp;
bytes memory bb = buf.b;
v = buf.idx;
assembly ("memory-safe") {
tmp := mload(add(add(bb, 32), v))
}
uint256 b;
v = 0;
for (uint256 i = 0; i < 10; i++) {
assembly ("memory-safe") {
b := byte(i, tmp)
}
v |= (b & 0x7F) << (i * 7);
if (b & 0x80 == 0) {
buf.idx += i + 1;
return v;
}
}
revert();
}
function decVarint(Buffer memory buf) internal pure returns (uint256 v) {
bytes memory bb = buf.b;
uint256 start = buf.idx;
for (uint256 i = 0; i < 10; i++) {
if (start + i >= bb.length) revert();
uint256 b = uint8(bb[start + i]);
v |= (b & 0x7F) << (i * 7);
if (b & 0x80 == 0) {
buf.idx = start + i + 1;
return v;
}
}
revert();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/lib/Pb.sol` around lines 35 - 55, The decVarint function reads up
to 10 bytes unconditionally from memory without validating that those bytes are
actually available in the buffer. To fix this, add a bounds check before the
assembly read to ensure the remaining buffer size is sufficient, and ensure that
if the loop completes all 10 iterations without finding a terminating byte
(where b & 0x80 == 0), the function reverts instead of returning a potentially
invalid truncated varint. Specifically, calculate the available bytes as
buf.b.length minus buf.idx and verify that the read operation respects this
boundary.

Comment on lines +34 to +38
constructor(address _owner, address _openRouter, address _celerBridgeRouter) Ownable(_owner) {
CELER_BRIDGE = ICelerBridge(_celerBridgeRouter);
OPEN_ROUTER = _openRouter;
CHAIN_ID = uint64(block.chainid);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject zero critical addresses in the constructor.

If _openRouter or _celerBridgeRouter is zero, the executor is misconfigured at deploy time and can route funds incorrectly or become unusable. This should fail fast in constructor.

Proposed fix
 contract CelerExecutor is Ownable {
     using Pb for Pb.Buffer;

+    error ZeroAddress();
     error CallerNotOpenRouter();
     error InvalidCelerRefund();
     error CelerRefundNotReady();
@@
     constructor(address _owner, address _openRouter, address _celerBridgeRouter) Ownable(_owner) {
+        if (_openRouter == address(0) || _celerBridgeRouter == address(0)) {
+            revert ZeroAddress();
+        }
         CELER_BRIDGE = ICelerBridge(_celerBridgeRouter);
         OPEN_ROUTER = _openRouter;
         CHAIN_ID = uint64(block.chainid);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/executors/CelerExecutor.sol` around lines 34 - 38, The constructor for
CelerExecutor does not validate that the critical addresses _openRouter and
_celerBridgeRouter are non-zero. Add validation checks in the constructor to
ensure both addresses are not zero before assigning them to OPEN_ROUTER and
CELER_BRIDGE respectively. Use require statements to revert the transaction if
either address is zero, preventing deployment of a misconfigured contract that
could route funds incorrectly or become unusable.

Comment on lines +61 to +67
if (isNative) {
CELER_BRIDGE.sendNative{value: amount}(receiver, amount, dstChainId, nonce, maxSlippage);
return;
}

SafeTransferLib.safeTransferFrom(token, OPEN_ROUTER, address(this), amount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce msg.value invariants in execute.

Native flow should require msg.value == amount, and ERC-20 flow should require msg.value == 0. Current logic can spend pre-existing ETH balance or trap extra ETH in this contract.

Proposed fix
     error CallerNotOpenRouter();
     error InvalidCelerRefund();
     error CelerRefundNotReady();
+    error InvalidMsgValue();
@@
         if (isNative) {
+            if (msg.value != amount) revert InvalidMsgValue();
             CELER_BRIDGE.sendNative{value: amount}(receiver, amount, dstChainId, nonce, maxSlippage);
             return;
         }
+        if (msg.value != 0) revert InvalidMsgValue();

         SafeTransferLib.safeTransferFrom(token, OPEN_ROUTER, address(this), amount);
📝 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
if (isNative) {
CELER_BRIDGE.sendNative{value: amount}(receiver, amount, dstChainId, nonce, maxSlippage);
return;
}
SafeTransferLib.safeTransferFrom(token, OPEN_ROUTER, address(this), amount);
if (isNative) {
if (msg.value != amount) revert InvalidMsgValue();
CELER_BRIDGE.sendNative{value: amount}(receiver, amount, dstChainId, nonce, maxSlippage);
return;
}
if (msg.value != 0) revert InvalidMsgValue();
SafeTransferLib.safeTransferFrom(token, OPEN_ROUTER, address(this), amount);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/executors/CelerExecutor.sol` around lines 61 - 67, The execute function
does not validate msg.value against the amount parameter, which can lead to
spending pre-existing ETH or trapping excess ETH in the contract. Add a require
statement enforcing msg.value == amount in the isNative branch before calling
CELER_BRIDGE.sendNative, and add a require statement enforcing msg.value == 0
before the SafeTransferLib.safeTransferFrom call in the ERC-20 flow to ensure
proper invariants.

Comment thread src/executors/RFQVaultExecutor.sol Outdated
Comment on lines +42 to +45
constructor(address _owner, address _openRouter, address _solverSigner) Ownable(_owner) {
OPEN_ROUTER = _openRouter;
SOLVER_SIGNER = _solverSigner;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject zero addresses for signer/router configuration.

This should enforce non-zero addresses in constructor and setSolverSigner. A zero signer bricks signed flows, and a zero OPEN_ROUTER makes receiveNative unreachable.

Suggested fix
 contract RFQVaultExecutor is Ownable {
+    error ZeroAddress();
@@
     constructor(address _owner, address _openRouter, address _solverSigner) Ownable(_owner) {
+        if (_openRouter == address(0) || _solverSigner == address(0)) {
+            revert ZeroAddress();
+        }
         OPEN_ROUTER = _openRouter;
         SOLVER_SIGNER = _solverSigner;
     }
@@
     function setSolverSigner(address _solverSigner) external onlyOwner {
+        if (_solverSigner == address(0)) {
+            revert ZeroAddress();
+        }
         SOLVER_SIGNER = _solverSigner;
     }

Also applies to: 152-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/executors/RFQVaultExecutor.sol` around lines 42 - 45, Add validation
checks in the constructor of RFQVaultExecutor to reject zero addresses for both
_openRouter and _solverSigner parameters. Use require statements to ensure
_openRouter is not address(0) and _solverSigner is not address(0), with
descriptive error messages. Apply the same validation to the setSolverSigner
method (around lines 152-154) to prevent these critical addresses from being set
to zero after deployment, which would break signed transaction flows and make
receiveNative inaccessible.

Comment on lines +98 to +100
if (approvalToken != address(0)) {
SafeTransferLib.safeApproveWithRetry(approvalToken, approvalSpender, approvalAmount);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear temporary allowance after swap execution.

This should revoke the spender allowance after _performAction. Leaving residual allowance allows later token pulls without a new signed intent.

Suggested fix
         if (approvalToken != address(0)) {
             SafeTransferLib.safeApproveWithRetry(approvalToken, approvalSpender, approvalAmount);
         }
@@
         if (!_performAction(swapAction)) {
             revert ActionFailed(0);
         }
+        if (approvalToken != address(0)) {
+            SafeTransferLib.safeApproveWithRetry(approvalToken, approvalSpender, 0);
+        }

Also applies to: 102-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/executors/RFQVaultExecutor.sol` around lines 98 - 100, After the
_performAction method is executed within the approval block (where
approvalToken, approvalSpender, and approvalAmount are used), add a call to
revoke the temporary allowance by setting it back to zero using
SafeTransferLib.safeApproveWithRetry with an approvalAmount of 0. This ensures
the spender's allowance is cleared immediately after the swap execution,
preventing any residual allowance that could be exploited for unauthorized token
pulls.

Comment on lines +483 to +484
vm.expectRevert();
vault.setSolverSigner(newSigner);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use specific revert selectors in owner-only tests.

These checks should assert the exact custom error selector from Ownable; generic vm.expectRevert() can pass on the wrong revert path.

As per coding guidelines, "Assertions are specific - use assertEq, assertGt, etc. instead of generic assert."

Also applies to: 500-501

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/RFQVaultExecutor.t.sol` around lines 483 - 484, Replace the generic
vm.expectRevert() calls with specific error selectors from the Ownable contract.
For the vault.setSolverSigner() calls, determine the exact custom error that
Ownable throws (likely Ownable.Unauthorized() or equivalent) and pass that error
selector to vm.expectRevert() instead of calling it without parameters. This
ensures the test validates the correct revert reason rather than accepting any
revert. Apply this change to both occurrences of the vault.setSolverSigner()
test blocks.

Source: Coding guidelines

}

/// @notice Accept native deposits from OpenRouter and emit a correlating event.
function receiveNative(bytes32 quoteId) external payable {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think you should add a receiveERC20 that we can use if we want to

Comment thread src/executors/RFQVaultExecutor.sol Outdated
Comment on lines +50 to +52
if (msg.sender != OPEN_ROUTER) {
revert CallerNotOpenRouter();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

curious: why is this check needed if you are listening to the event on OR to know if quoteID is a tx and then doing "If native tokens we check the native token emission"

Comment thread src/executors/RFQVaultExecutor.sol Outdated
Comment on lines +146 to +147
bytes32 messageHash = _hashFulfilOrRefund(REFUND_DISCRIMINATOR, quoteId, nonce, token, amount, receiver);
_verifyAndMarkQuoteId(messageHash, signature, quoteId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

im confused, why is this needed?

  • cant we just do quoteId, authData{hash, sig), amountToFill
  • the data being used to validate is coming from backend, there is no source of truth onchain, so not sure why we are computing messageHash onchain


mapping(bytes32 quoteId => uint256 isUsed) public quoteIdUsed;
mapping(uint256 nonce => uint256 isUsed) public nonceUsed;
mapping(bytes32 quoteId => uint256 isMarked) public isMarkedForRefund;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cant this just be a bool?

uint256 nonce,
address token,
uint256 amount,
address receiver,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

expiry missing?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/unit/RFQVaultExecutor.t.sol (2)

652-675: 💤 Low value

Dead code: _hashSwapAndFulfil is never called.

This function is defined but never used. _signSwapAndFulfilWithKey calls _hashSwapAndFulfilAt directly (line 590-601). Either remove this wrapper or use it in the signing function for consistency.

Option 1: Remove dead code
-    function _hashSwapAndFulfil(
-        bytes32 quoteId,
-        uint256 nonce,
-        address approvalToken,
-        address approvalSpender,
-        uint256 approvalAmount,
-        address outputToken_,
-        uint256 minOutput,
-        RFQVaultExecutor.Action memory swapAction,
-        address receiver
-    ) internal view returns (bytes32 hash) {
-        return _hashSwapAndFulfilAt(
-            address(vault),
-            quoteId,
-            nonce,
-            approvalToken,
-            approvalSpender,
-            approvalAmount,
-            outputToken_,
-            minOutput,
-            swapAction,
-            receiver
-        );
-    }
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/RFQVaultExecutor.t.sol` around lines 652 - 675, The function
_hashSwapAndFulfil is never invoked and serves as dead code. Either remove the
_hashSwapAndFulfil function entirely, or refactor _signSwapAndFulfilWithKey
(which currently calls _hashSwapAndFulfilAt directly at lines 590-601) to use
the _hashSwapAndFulfil wrapper function instead for consistency and to eliminate
the unused code.

73-86: ⚖️ Poor tradeoff

Add fuzz tests for amount and boundary validation.

Per coding guidelines, fuzz tests should be used for input validation. Key candidates:

  • fulfil/refund with fuzzed amounts (including zero and type(uint256).max)
  • swapAndFulfil with fuzzed minOutput values
  • Boundary testing around approval amounts

This strengthens confidence that the signature verification and transfer logic handle arbitrary inputs correctly.

Example fuzz test structure
function testFuzz_fulfil_erc20(uint256 amount) public {
    vm.assume(amount > 0 && amount < type(uint128).max);
    token.mint(address(vault), amount);
    
    bytes32 quoteId = keccak256(abi.encode("fuzz", amount));
    bytes memory signature = _signFulfil(quoteId, NONCE, address(token), amount, RECEIVER);
    
    vault.fulfil(quoteId, NONCE, address(token), amount, RECEIVER, signature);
    
    assertEq(token.balanceOf(RECEIVER), amount);
}

As per coding guidelines, "Fuzz tests used appropriately for input validation."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/RFQVaultExecutor.t.sol` around lines 73 - 86, The current
test_fulfil_erc20 function is a deterministic test with hardcoded values.
Convert this and related tests (fulfil, refund, swapAndFulfil) into fuzz tests
by adding fuzzed uint256 parameters (e.g., amount, minOutput) to the function
signatures. Use vm.assume() to set reasonable bounds on the fuzzed values (such
as amount being greater than 0 and less than type(uint128).max) to avoid memory
issues while still testing a wide range of inputs. Add additional fuzz tests to
cover boundary cases like zero amounts, maximum values, and approval edge cases
to ensure signature verification and transfer logic handle arbitrary inputs
correctly.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/unit/RFQVaultExecutor.t.sol`:
- Around line 652-675: The function _hashSwapAndFulfil is never invoked and
serves as dead code. Either remove the _hashSwapAndFulfil function entirely, or
refactor _signSwapAndFulfilWithKey (which currently calls _hashSwapAndFulfilAt
directly at lines 590-601) to use the _hashSwapAndFulfil wrapper function
instead for consistency and to eliminate the unused code.
- Around line 73-86: The current test_fulfil_erc20 function is a deterministic
test with hardcoded values. Convert this and related tests (fulfil, refund,
swapAndFulfil) into fuzz tests by adding fuzzed uint256 parameters (e.g.,
amount, minOutput) to the function signatures. Use vm.assume() to set reasonable
bounds on the fuzzed values (such as amount being greater than 0 and less than
type(uint128).max) to avoid memory issues while still testing a wide range of
inputs. Add additional fuzz tests to cover boundary cases like zero amounts,
maximum values, and approval edge cases to ensure signature verification and
transfer logic handle arbitrary inputs correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da19b866-e6c4-4bb1-ad82-f0cc997a08df

📥 Commits

Reviewing files that changed from the base of the PR and between acf917b and 0df0d44.

📒 Files selected for processing (3)
  • scripts/deploy/deployRFQVaultExecutor.ts
  • src/executors/RFQVaultExecutor.sol
  • test/unit/RFQVaultExecutor.t.sol

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.

3 participants