Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contracts-rfq): Token Zap [SLT-389] #3352

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open

Conversation

ChiTimesChi
Copy link
Collaborator

@ChiTimesChi ChiTimesChi commented Oct 29, 2024

Description

Minimal implementation of a contract that facilitates the generic Zap action for ERC20 tokens or native gas tokens (ETH/BNB/AVAX/others).

This enables atomic workflows like:

  • Bridge funds through FastBridgeV2.
  • On destination chain deposit the proceeds into some DeFi protocol on behalf of the original user.

The challenge here is that we don't know how much tokens will we receive on destination chain in advance, so we can't form the exact calldata for the destination deposit call during the quoting phase. Therefore a hot-swapping mechanism was used, where a placeholder value is used for initial encoding, which is replaced in place by the actual bridged amount on the destination chain.

A single permisionless TokenZap contract could be used for any DeFi protocol and any FastBridge (V2+) deployment, assuming:

  • The DeFi protocol supports router-like deposits on behalf of the user.
  • The deposit is performed using a single function interaction (plus the token approval if needed).

Full Workflow

Let's suppose Alice wants to deposit USDC into a Vault contract on Arbitrum. Alice has 100 USDC to start with on Optimism. The Arbitrum Vault exposes a following function to make the deposits: depositFunds(address token, uint256 amount, address onBehalfOf). Following steps need to be done (abstracted away from the Alice by the FE):

  1. ABI-encode calldata of the deposit call: payload = abi.encodeCall(vault.depositFunds, (arbUSDC, 0, alice))

    Note: we don't know the destination amount yet, so we use placeholder zero value instead

  2. Determine the position of the encoded amount within payload: amountPosition = 4 + 32 * 1 = 36

    Note: this is usually 4 (function selector), plus extra 32 per every argument before amount in the list of parameters (regardless of the argument type). If the amount is not encoded within payload at all, any value greater or equal than payload length could be used in step 3.

  3. Generate parameters for the Zap:
    • TokenZap exposes an off-chain getter: zapData = tokenZap.encodeZapData(arbVault, payload, amountPosition)
    • zapNative = 0 - this should be changed only when a Zap with ERC20 requires an additional native payment (e.g. token bridged via native bridge)
  4. Request a quote specifying the following parameters:
     origin_chain: 10
     origin_token: "0xOP_USDC"
     origin_amount: 100 * 10**6
     origin_sender: "0xALICE"
     dest_chain: 42141
     dest_token: "0xARB_USDC"
     dest_recipient: "0xARB_TOKEN_ZAP"
     zap_data: zapData
     zapNative: 0
  5. Suppose one of the Relayers responded that they are ready to relay such transaction, giving 99.9 USDC on Arbitrum.
  6. Alices accepts a quote and initiates a transaction on Optimism using the parameters from step (4) and adding dest_amount from step (5).
  7. Relayer completes the fill on Arbitrum:
    • They supply 99.9 USDC, which are transferred to TokenZap.
    • Then a Zap is performed using the Zap Data: TokenZap performs a 99.9 USDC deposit on behalf of Alice

      Note: initially encoded placeholder zero value is replaced by the actual bridged amount by TokenZap automatically.

  8. As a result, Relayer later receives 100 USDC on Optimism, while Aice has 99.9 USDC deposited on Optimism into the Vault.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced ZapDataV1 library for efficient data management and validation.
    • Launched TokenZapV1 contract enabling token "Zap" actions with secure transfers.
    • Added integration tests for FastBridgeV2 functionality related to token bridging.
    • Introduced VaultManyArguments mock contract for enhanced testing scenarios.
  • Bug Fixes

    • Enhanced error handling in TokenZapV1 for incorrect amounts and payload lengths.
  • Tests

    • Comprehensive test suite for TokenZapV1 and ZapDataV1 ensuring functionality and error handling.

packages/contracts-rfq/contracts/libs/ZapDataV1.sol Dismissed Show dismissed Hide dismissed
packages/contracts-rfq/contracts/libs/ZapDataV1.sol Dismissed Show dismissed Hide dismissed
packages/contracts-rfq/contracts/libs/ZapDataV1.sol Dismissed Show dismissed Hide dismissed
Copy link
Contributor

coderabbitai bot commented Oct 29, 2024

Walkthrough

The changes introduce several new Solidity files, including a library (ZapDataV1) for encoding and validating tightly packed data structures, a contract (TokenZapV1) for executing "Zap" actions with ERC20 tokens, and various testing contracts. The tests validate the functionality of these components, ensuring correct encoding, decoding, and error handling. Additionally, mock contracts are created for testing purposes, enhancing the overall test coverage of the new features.

Changes

File Path Change Summary
packages/contracts-rfq/contracts/libs/ZapDataV1.sol Added library ZapDataV1 with functions for validation, encoding, and extracting data from tightly packed structures. Introduced custom errors for improved error handling.
packages/contracts-rfq/contracts/zaps/TokenZapV1.sol Introduced contract TokenZapV1 implementing IZapRecipient, enabling Zap actions with ERC20 tokens. Added methods for zapping, encoding, and decoding Zap data.
packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol Created ZapDataV1Harness for testing the ZapDataV1 library, with public functions to validate and encode zap data.
packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol Added FastBridgeV2TokenZapV1DstTest for integration testing of FastBridge V2 functionality, including event logging and various test functions for bridge transactions.
packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol Introduced FastBridgeV2TokenZapV1SrcTest for integration testing of the FastBridge V2 functionality, focusing on bridge requests and event assertions.
packages/contracts-rfq/test/integration/TokenZapV1.t.sol Added abstract contract TokenZapV1IntegrationTest for testing TokenZapV1, including setup and utility functions for managing bridge parameters and token interactions.
packages/contracts-rfq/test/libs/ZapDataV1.t.sol Created ZapDataV1Test for testing the ZapDataV1 library, validating encoding and decoding processes, and handling various error scenarios.
packages/contracts-rfq/test/mocks/VaultManyArguments.sol Introduced VaultManyArguments mock contract for testing, including deposit functions and error handling.
packages/contracts-rfq/test/mocks/VaultMock.sol Added abstract contract VaultMock for testing, managing user balances and deposit functionalities with error handling.
packages/contracts-rfq/test/zaps/TokenZapV1.t.sol Created TokenZapV1Test contract for comprehensive testing of the TokenZapV1 functionality, covering various scenarios and edge cases for token deposits and zap operations.

Possibly related PRs

Suggested reviewers

  • aureliusbtc
  • trajan0x

Poem

In a world of bytes and zaps,
A rabbit hops with joyful claps.
Encoding data, tight and neat,
With every function, a tasty treat!
So let us cheer, with hops and bounds,
For smart contracts and joyful sounds! 🐇✨


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between cbc4fbd and 2e5f7d1.

📒 Files selected for processing (1)
  • packages/contracts-rfq/test/libs/ZapDataV1.t.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/contracts-rfq/test/libs/ZapDataV1.t.sol

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

codecov bot commented Oct 29, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 34.88365%. Comparing base (e939e4e) to head (2e5f7d1).
Report is 24 commits behind head on master.

Additional details and impacted files
@@                 Coverage Diff                 @@
##              master       #3352         +/-   ##
===================================================
+ Coverage   34.81813%   34.88365%   +0.06552%     
===================================================
  Files            529         531          +2     
  Lines          33787       33821         +34     
  Branches          82          82                 
===================================================
+ Hits           11764       11798         +34     
  Misses         21002       21002                 
  Partials        1021        1021                 
Flag Coverage Δ
packages 90.44834% <ø> (ø)
solidity 98.79518% <100.00000%> (+0.13746%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

cloudflare-workers-and-pages bot commented Oct 30, 2024

Deploying sanguine-fe with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2e5f7d1
Status: ✅  Deploy successful!
Preview URL: https://a74d7765.sanguine-fe.pages.dev
Branch Preview URL: https://feat-token-zap.sanguine-fe.pages.dev

View logs

@ChiTimesChi ChiTimesChi changed the title [WIP] feat(contracts-rfq): Token Zap [SLT-389] feat(contracts-rfq): Token Zap [SLT-389] Oct 30, 2024
@github-actions github-actions bot added size/l and removed size/m labels Oct 30, 2024
Copy link
Contributor

@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: 11

🧹 Outside diff range and nitpick comments (11)
packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol (1)

7-9: Add NatSpec documentation for public functions

The functions lack documentation explaining their purpose, parameters, and expected behavior. This is particularly important for:

  • validateV1: What constitutes valid data?
  • payload: How is the amount integrated into the encoded data?

Example NatSpec for payload:

/// @notice Generates the final payload by incorporating the actual amount into the encoded zap data
/// @param encodedZapData The pre-encoded zap data with amount placeholder
/// @param amount The actual amount to be used in the payload
/// @return The final payload with the actual amount inserted

Also applies to: 31-33

packages/contracts-rfq/test/mocks/VaultManyArguments.sol (2)

8-8: Consider using a more descriptive error name.

The error name VaultManyArguments__SomeError is vague. Consider renaming it to better describe the specific error condition it represents, such as VaultManyArguments__InvalidEncodedData since it's used in the _validateBytes function.

-    error VaultManyArguments__SomeError();
+    error VaultManyArguments__InvalidEncodedData();

30-32: Remove unnecessary payable modifier.

Since this function always reverts, the payable modifier is unnecessary.

-    function depositWithRevert() external payable {
+    function depositWithRevert() external {
         revert VaultManyArguments__SomeError();
     }
packages/contracts-rfq/test/integration/TokenZapV1.t.sol (1)

15-23: Consider adding comments explaining the magic numbers.

While the constants are well-defined, it would be helpful to add comments explaining:

  1. The significance of the chain IDs (1337 and 7331)
  2. The reasoning behind the specific amounts (1 ether and 0.9999 ether)
  3. The meaning of ZAP_NATIVE value (123_456)
packages/contracts-rfq/test/zaps/TokenZapV1.t.sol (2)

11-30: Consider adding more test scenarios in setUp.

While the basic setup is good, consider adding test scenarios with:

  • Tokens with different decimals (e.g., 6, 8, 18)
  • Multiple users for testing access control
  • Edge case amounts (e.g., very small/large values)

101-143: Add gas usage assertions for native token operations.

The native token tests should include gas usage assertions to ensure optimal gas consumption, especially important for bridging operations.

packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol (2)

98-101: Ensure consistent naming convention for test functions

The function test_relay_depositTokenRevertParams_revert contains _revert twice in its name. Consider renaming it for clarity and consistency.

Apply this diff to rename the function:

-function test_relay_depositTokenRevertParams_revert() public {
+function test_relay_depositTokenRevert() public {

115-118: Ensure consistent naming convention for test functions

The function test_relay_depositNativeRevertParams_revert contains _revert twice in its name. Consider renaming it for clarity and consistency.

Apply this diff to rename the function:

-function test_relay_depositNativeRevertParams_revert() public {
+function test_relay_depositNativeRevert() public {
packages/contracts-rfq/contracts/zaps/TokenZapV1.sol (1)

14-14: Document the purpose of NATIVE_GAS_TOKEN constant

While the placeholder address 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE is often used to represent native gas tokens, adding a comment or NatSpec documentation explaining its usage can improve code clarity.

packages/contracts-rfq/test/libs/ZapDataV1.t.sol (2)

51-51: Use EXPECTED_VERSION constant in version assertions

To enhance maintainability and consistency, consider using the EXPECTED_VERSION constant instead of hardcoding the version number in the assertEq statements.

Apply this diff to update the assertions:

- assertEq(harness.version(zapData), 1);
+ assertEq(harness.version(zapData), EXPECTED_VERSION);

Also applies to: 65-65


106-106: Use EXPECTED_VERSION constant in version checks

Replace the hardcoded version number with the EXPECTED_VERSION constant in the vm.assume statements to improve consistency.

Apply this diff to update the version checks:

- vm.assume(version != 1);
+ vm.assume(version != EXPECTED_VERSION);

Also applies to: 125-125

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between a330c8b and ae4fe60.

📒 Files selected for processing (10)
  • packages/contracts-rfq/contracts/libs/ZapDataV1.sol (1 hunks)
  • packages/contracts-rfq/contracts/zaps/TokenZapV1.sol (1 hunks)
  • packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol (1 hunks)
  • packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol (1 hunks)
  • packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol (1 hunks)
  • packages/contracts-rfq/test/integration/TokenZapV1.t.sol (1 hunks)
  • packages/contracts-rfq/test/libs/ZapDataV1.t.sol (1 hunks)
  • packages/contracts-rfq/test/mocks/VaultManyArguments.sol (1 hunks)
  • packages/contracts-rfq/test/mocks/VaultMock.sol (1 hunks)
  • packages/contracts-rfq/test/zaps/TokenZapV1.t.sol (1 hunks)
🧰 Additional context used
📓 Learnings (1)
packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol (1)
Learnt from: ChiTimesChi
PR: synapsecns/sanguine#3284
File: packages/contracts-rfq/test/FastBridgeV2.Src.t.sol:919-993
Timestamp: 2024-10-14T14:48:01.520Z
Learning: In Solidity test files for FastBridgeV2 (e.g., `packages/contracts-rfq/test/FastBridgeV2.Src.t.sol`), code duplication in test functions is acceptable to maintain readability and maintainability, even if it contradicts DRY principles.
🪛 GitHub Check: Slither
packages/contracts-rfq/contracts/zaps/TokenZapV1.sol

[warning] 28-48: Unused return
TokenZapV1.zap(address,uint256,bytes) (contracts/zaps/TokenZapV1.sol#28-48) ignores return value by Address.functionCallWithValue({target:target,data:payload,value:msg.value}) (contracts/zaps/TokenZapV1.sol#46)

🔇 Additional comments (30)
packages/contracts-rfq/test/mocks/VaultMock.sol (3)

1-8: LGTM: Contract setup follows best practices.

The contract correctly:

  • Specifies SPDX license
  • Uses a recent Solidity version
  • Imports SafeERC20 for secure token handling
  • Is marked as abstract as intended for inheritance
  • Uses proper OpenZeppelin imports

10-12: 🛠️ Refactor suggestion

Consider using ImmutableX pattern for NATIVE_GAS_TOKEN.

While the current implementation is functional, consider using the immutable pattern for the NATIVE_GAS_TOKEN address to save gas in test environments:

-    address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
+    address internal immutable NATIVE_GAS_TOKEN;

Then initialize it in the constructor. This would allow for easier testing with different native token addresses if needed.

Likely invalid or redundant comment.


16-23: Verify reentrancy protection in inheriting contracts.

The _deposit function handles external calls (safeTransferFrom) before state changes. While this follows CEI (Checks-Effects-Interactions) pattern internally, inheriting contracts need to ensure they maintain reentrancy protection when implementing deposit logic.

Additionally, consider adding events for deposits to aid in testing and monitoring:

+    event Deposited(address indexed user, address indexed token, uint256 amount);
     function _deposit(address user, address token, uint256 amount) internal {
         if (token == NATIVE_GAS_TOKEN) {
             if (msg.value != amount) revert VaultMock__AmountIncorrect();
         } else {
             IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
         }
         balanceOf[user][token] += amount;
+        emit Deposited(user, token, amount);
     }

Let's verify the inheritance chain to ensure proper reentrancy protection:

packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol (3)

1-4: Verify Solidity version compatibility across deployment chains

The contract uses Solidity 0.8.24, which is a very recent version. While this version includes important security features and optimizations, ensure that all target deployment chains (mentioned in PR objectives: ETH, BNB, AVAX) have full support for this version.


6-6: LGTM! Well-structured test harness

The contract follows best practices for test harnesses:

  • Clear naming convention
  • Single responsibility (testing ZapDataV1 library)
  • Pure functions for deterministic testing

11-21: 🛠️ Refactor suggestion

Consider adding input validation for encodeV1

The encodeV1 function accepts parameters without validation. Consider adding checks for:

  • amountPosition_ bounds relative to payload_ length
  • target_ being a non-zero address
  • payload_ minimum length
packages/contracts-rfq/test/mocks/VaultManyArguments.sol (1)

1-7: LGTM! Clear contract setup with appropriate warnings.

The contract follows best practices with proper license, pragma version, and clear documentation about its testing purpose.

packages/contracts-rfq/test/integration/TokenZapV1.t.sol (4)

1-13: LGTM! Well-organized imports and appropriate pragma version.

The file structure follows best practices with clear organization of imports and uses a stable Solidity version.


25-44: LGTM! Well-organized state variables.

The state variables are logically grouped and use appropriate visibility modifiers.


119-159: LGTM! Well-implemented utility functions.

The utility functions make good use of Solidity's encoding functions and provide clear interfaces for test scenarios.


46-58: Verify the implementation of mintTokens().

The mintTokens() function is declared but not implemented in this abstract contract. Ensure that all concrete implementations provide the necessary token minting logic.

✅ Verification successful

mintTokens() is correctly implemented across test hierarchy

The abstract mintTokens() function in TokenZapV1.t.sol is properly implemented by its concrete test cases:

  • FastBridgeV2.TokenZapV1.Src.t.sol: Implements minting for source chain testing
  • FastBridgeV2.TokenZapV1.Dst.t.sol: Implements minting for destination chain testing

Both implementations provide the necessary token minting logic and state setup required for their respective test scenarios.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for concrete implementations of TokenZapV1IntegrationTest
ast-grep --pattern 'contract $_ is TokenZapV1IntegrationTest {
  $$$
  function mintTokens() $_ {
    $$$
  }
  $$$
}'

Length of output: 120


Script:

#!/bin/bash
# Let's first find the base test contract
fd "TokenZapV1IntegrationTest" --type f

# Then let's look for any implementations of mintTokens
rg "function mintTokens" -A 5

Length of output: 5670

packages/contracts-rfq/test/zaps/TokenZapV1.t.sol (4)

1-9: LGTM: Imports and pragma are appropriate.

The pragma version is up-to-date (0.8.24) and all necessary test dependencies are imported.


145-165: LGTM: Comprehensive encoding/decoding tests.

The roundtrip tests effectively verify the encoding/decoding logic with both standard and edge cases.


62-99: Consider testing with fee-on-transfer tokens.

The ERC20 tests cover standard scenarios but miss some important edge cases:

  • Fee-on-transfer tokens
  • Rebasing tokens
  • Tokens with transfer hooks

1-212: Verify integration with FastBridgeV2.

Given the PR objective of integrating with FastBridgeV2, we should verify the interaction points.

packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol (7)

8-18: Well-defined BridgeRequested event

The BridgeRequested event is properly structured with appropriate indexed parameters, accurately capturing all necessary details for the bridging operation.


20-23: Correct initialization of test environment

The setUp function correctly sets the chain ID using vm.chainId(SRC_CHAIN_ID) before invoking the parent setUp(), ensuring the test environment mirrors the source chain.


25-30: Accurate token minting and approval

The mintTokens function effectively mints the required tokens to the user and sets the necessary approval for the fastBridge contract to handle the tokens.


32-41: Proper simulation of user-initiated bridge transactions

The bridge function appropriately uses vm.prank to simulate a transaction from the user's perspective, correctly handling both token and native asset transfers based on the isToken flag.


43-64: Accurate event expectation setup for testing

The expectEventBridgeRequested function correctly encodes the bridge transaction and sets up the expected event emission, ensuring that the tests can accurately verify event logs.


66-74: Effective balance verification after bridging

The checkBalances function accurately verifies that the user's balance decreases and the fastBridge contract's balance increases appropriately, confirming the correct transfer of assets during the bridge operation.


76-110: Comprehensive test coverage for bridging scenarios

The suite of test functions provides thorough coverage of various bridging scenarios, including token and native asset transfers, handling of deposit parameters, and revert cases. This ensures robust validation of the bridging functionality.

packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol (5)

20-23: Initialization in setUp function is correct

The chain ID is correctly set, and the base setup is invoked appropriately.


25-30: Token minting and approval logic is appropriate in mintTokens

The relayer's balance is correctly set up, and token approvals are properly configured.


32-42: relay function implementation correctly handles transaction relaying

The function encodes the bridge transaction and correctly invokes the fastBridge.relay function with appropriate parameters.


44-64: Event expectation in expectEventBridgeRelayed is properly set up

The function correctly configures the expected event emission with all the required parameters.


66-82: Balance verification in checkBalances function is thorough

The function accurately asserts balances based on whether tokens or native assets are used.

packages/contracts-rfq/contracts/zaps/TokenZapV1.sol (2)

74-81: Verify that amountPosition and payload.length fit within uint16 bounds

When casting amountPosition to uint16, ensure that both amountPosition and payload.length do not exceed the maximum value of uint16 to prevent truncation or overflow errors.


28-48: zap function implementation looks correct

The zap function properly validates the Zap data, handles token approvals, manages native gas tokens, constructs the payload with the correct amount, and forwards the call to the target contract with the appropriate msg.value.

🧰 Tools
🪛 GitHub Check: Slither

[warning] 28-48: Unused return
TokenZapV1.zap(address,uint256,bytes) (contracts/zaps/TokenZapV1.sol#28-48) ignores return value by Address.functionCallWithValue({target:target,data:payload,value:msg.value}) (contracts/zaps/TokenZapV1.sol#46)

packages/contracts-rfq/test/libs/ZapDataV1.t.sol (1)

9-143: Comprehensive test coverage and clean implementation

The test contract provides thorough coverage of various scenarios for the ZapDataV1 library, including both valid and invalid cases. The implementation is clean and follows best practices, ensuring robust validation of encoding and decoding functionalities.

Copy link
Contributor

@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: 10

🧹 Outside diff range and nitpick comments (14)
packages/contracts-rfq/test/mocks/VaultMock.sol (1)

10-12: Consider adding documentation for NATIVE_GAS_TOKEN.

While the sentinel value 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE is commonly used to represent native gas tokens (ETH/BNB/AVAX), it would be helpful to document this convention for clarity.

-    address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
+    /// @dev Sentinel value representing native gas tokens (ETH/BNB/AVAX)
+    /// This is a widely adopted convention in DeFi protocols
+    address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol (2)

6-6: Add NatSpec documentation for the contract.

While the contract structure is good, adding NatSpec documentation would improve clarity about its testing purpose and relationship with the ZapDataV1 library.

+/// @title ZapDataV1Harness
+/// @notice Test harness for exposing internal functions of ZapDataV1 library
+/// @dev Used for unit testing the encoding and validation of zap data
 contract ZapDataV1Harness {

7-9: Add NatSpec documentation for public functions.

While the functions are straightforward wrappers, adding NatSpec documentation would improve testability and maintainability.

+    /// @notice Validates the encoded zap data format
+    /// @param encodedZapData The encoded zap data to validate
     function validateV1(bytes calldata encodedZapData) public pure {
         ZapDataV1.validateV1(encodedZapData);
     }

+    /// @notice Encodes zap data into a bytes array
+    /// @param amountPosition_ Position where amount should be inserted
+    /// @param target_ Target contract address
+    /// @param payload_ Function call payload
+    /// @return encodedZapData The encoded zap data
     function encodeV1(
         uint16 amountPosition_,
         address target_,
         bytes memory payload_
     )

Also applies to: 11-21

packages/contracts-rfq/test/mocks/VaultManyArguments.sol (2)

26-28: Add validation for zero value deposits.

Consider adding a check to ensure msg.value > 0 to prevent unnecessary zero-value transactions.

 function depositNoAmount(address user) external payable {
+    if (msg.value == 0) revert VaultManyArguments__SomeError();
     _deposit(user, NATIVE_GAS_TOKEN, msg.value);
 }

30-32: Remove unnecessary payable modifier.

Since this function always reverts and doesn't use msg.value, the payable modifier is unnecessary.

-    function depositWithRevert() external payable {
+    function depositWithRevert() external {
         revert VaultManyArguments__SomeError();
     }
packages/contracts-rfq/test/integration/TokenZapV1.t.sol (3)

23-23: Consider documenting the ZAP_NATIVE constant.

The magic number 123_456 would benefit from a comment explaining its significance in the test scenarios.

-    uint256 internal constant ZAP_NATIVE = 123_456;
+    // Arbitrary amount of native tokens to be sent along with the zap transaction
+    uint256 internal constant ZAP_NATIVE = 123_456;

46-58: Consider adding deployment validations in setUp.

While the setup is well-structured, consider adding assertions to verify successful contract deployments and role assignments.

 function setUp() public virtual {
     fastBridge = new FastBridgeV2(address(this));
+    require(address(fastBridge) != address(0), "FastBridge deployment failed");
     fastBridge.grantRole(fastBridge.RELAYER_ROLE(), relayer);
+    require(fastBridge.hasRole(fastBridge.RELAYER_ROLE(), relayer), "Role assignment failed");

149-159: Add documentation for deposit payload helper functions.

The helper functions would benefit from NatSpec documentation explaining their purpose and parameters.

+    /// @notice Creates a payload for depositing tokens into the vault
+    /// @param token The token address to deposit (can be native token address)
+    /// @return The encoded deposit function call
     function getDepositPayload(address token) public view returns (bytes memory) {
         return abi.encodeCall(dstVault.deposit, (token, abi.encode(token), DST_AMOUNT, user, abi.encode(user)));
     }

+    /// @notice Creates a payload for deposits that don't require an amount parameter
+    /// @return The encoded deposit function call
     function getDepositNoAmountPayload() public view returns (bytes memory) {
         return abi.encodeCall(dstVault.depositNoAmount, (user));
     }

+    /// @notice Creates a payload that will cause the deposit to revert (for testing)
+    /// @return The encoded deposit function call
     function getDepositRevertPayload() public view returns (bytes memory) {
         return abi.encodeCall(dstVault.depositWithRevert, ());
     }
packages/contracts-rfq/test/zaps/TokenZapV1.t.sol (5)

12-12: Document test constants and magic values.

Consider adding comments to explain:

  • The significance of the AMOUNT value (0.987 ether)
  • The special address 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE used for native gas tokens
-    uint256 internal constant AMOUNT = 0.987 ether;
+    /// @dev Non-round amount to ensure proper decimal handling
+    uint256 internal constant AMOUNT = 0.987 ether;

-    address internal nativeGasToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
+    /// @dev Special address representing native gas tokens (ETH/BNB/AVAX)
+    address internal nativeGasToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

Also applies to: 19-19


44-47: Document the calculation of amount position in payload.

The magic number 4 + 32 * 2 needs explanation. It represents the position of the amount parameter in the encoded function call.

     function getZapData(bytes memory originalPayload) public view returns (bytes memory) {
-        // Amount is the third argument of the deposit function
+        // Position calculation:
+        // 4 bytes: function selector
+        // 32 bytes: first parameter (token address)
+        // 32 bytes: second parameter (encoded token)
+        // Next 32 bytes: amount parameter
         return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32 * 2);
     }

62-99: Add test case for zero amount zap.

The test suite should verify that the contract correctly handles zero amount transactions, as they might be valid in some protocols.

Consider adding:

function test_zap_erc20_zeroAmount() public {
    bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0));
    bytes4 returnValue = tokenZap.zap(address(erc20), 0, zapData);
    assertEq(returnValue, tokenZap.zap.selector);
    assertEq(vault.balanceOf(user, address(erc20)), 0);
}

101-143: Add test for handling unexpected native token receipts.

Consider adding a test case where the contract receives native tokens through selfdestruct to ensure it doesn't affect the zap operation.

Consider adding:

function test_zap_native_selfdestructFunds() public {
    // Setup a contract that can selfdestruct and send ETH
    address payable selfDestructContract = payable(makeAddr("selfDestruct"));
    deal(selfDestructContract, AMOUNT);
    
    // Receive unexpected funds
    vm.prank(selfDestructContract);
    (bool success,) = address(tokenZap).call{value: AMOUNT}("");
    require(success, "Transfer failed");
    
    // Should not affect normal zap operation
    test_zap_native_placeholderZero();
}

145-165: Add specific edge cases to encoding tests.

While the property-based test is good, consider adding specific edge cases like max uint256 values and special addresses.

Consider adding:

function test_encodeZapData_maxValues() public {
    address maxAddress = address(uint160(type(uint160).max));
    uint256 maxAmount = type(uint256).max;
    
    test_encodeZapData_roundtrip(maxAddress, maxAmount, maxAmount);
}
packages/contracts-rfq/contracts/zaps/TokenZapV1.sol (1)

16-17: Enhance error messages for better debugging

The custom errors TokenZapV1__AmountIncorrect and TokenZapV1__PayloadLengthAboveMax could include additional information to aid in debugging, such as the expected versus actual amounts or payload lengths.

Consider modifying the errors to include more context:

-error TokenZapV1__AmountIncorrect();
+error TokenZapV1__AmountIncorrect(uint256 expected, uint256 actual);

-error TokenZapV1__PayloadLengthAboveMax();
+error TokenZapV1__PayloadLengthAboveMax(uint256 maxLength, uint256 actualLength);

And update where the errors are used accordingly.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between a330c8b and ae4fe60.

📒 Files selected for processing (10)
  • packages/contracts-rfq/contracts/libs/ZapDataV1.sol (1 hunks)
  • packages/contracts-rfq/contracts/zaps/TokenZapV1.sol (1 hunks)
  • packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol (1 hunks)
  • packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol (1 hunks)
  • packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol (1 hunks)
  • packages/contracts-rfq/test/integration/TokenZapV1.t.sol (1 hunks)
  • packages/contracts-rfq/test/libs/ZapDataV1.t.sol (1 hunks)
  • packages/contracts-rfq/test/mocks/VaultManyArguments.sol (1 hunks)
  • packages/contracts-rfq/test/mocks/VaultMock.sol (1 hunks)
  • packages/contracts-rfq/test/zaps/TokenZapV1.t.sol (1 hunks)
🧰 Additional context used
📓 Learnings (1)
packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol (1)
Learnt from: ChiTimesChi
PR: synapsecns/sanguine#3284
File: packages/contracts-rfq/test/FastBridgeV2.Src.t.sol:919-993
Timestamp: 2024-10-14T14:48:01.520Z
Learning: In Solidity test files for FastBridgeV2 (e.g., `packages/contracts-rfq/test/FastBridgeV2.Src.t.sol`), code duplication in test functions is acceptable to maintain readability and maintainability, even if it contradicts DRY principles.
🪛 GitHub Check: Slither
packages/contracts-rfq/contracts/zaps/TokenZapV1.sol

[warning] 28-48: Unused return
TokenZapV1.zap(address,uint256,bytes) (contracts/zaps/TokenZapV1.sol#28-48) ignores return value by Address.functionCallWithValue({target:target,data:payload,value:msg.value}) (contracts/zaps/TokenZapV1.sol#46)

🔇 Additional comments (25)
packages/contracts-rfq/test/mocks/VaultMock.sol (1)

1-8: LGTM! Solid contract foundation.

The contract setup follows best practices with:

  • Recent Solidity version
  • Proper use of SafeERC20 for token operations
  • Appropriate abstract modifier for a base contract
packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol (2)

1-4: LGTM! Well-structured file setup.

The file uses appropriate licensing, latest stable Solidity version, and correct import syntax.


31-33: Verify amount parameter bounds.

The payload function accepts an unbounded uint256 amount parameter. Consider adding validation to ensure the amount fits within the expected bounds of the target protocol.

packages/contracts-rfq/test/mocks/VaultManyArguments.sol (2)

1-8: Well-structured contract setup with clear documentation.

The contract is properly documented with a clear warning about its test-only nature. Good practice to explicitly state "DO NOT USE IN PRODUCTION" for mock contracts.


10-24: Add input validation for edge cases.

While the function validates encoded data, consider adding these safety checks:

  1. Zero address validation for token and user
  2. When token is native gas token, validate that amount matches msg.value
  3. When token is ERC20, ensure msg.value is zero

Let's check if VaultMock has these validations:

packages/contracts-rfq/test/integration/TokenZapV1.t.sol (1)

1-14: LGTM! Well-structured imports and setup.

The file uses the latest Solidity version and has a clean organization of imports, properly separating core contracts from test mocks.

packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol (7)

7-18: Well-Defined Event BridgeRequested

The declaration of the BridgeRequested event accurately captures all necessary parameters for logging bridge requests, facilitating effective event handling and debugging.


20-23: Proper Override of setUp Function

The setUp function correctly overrides the parent function, sets the chain ID using vm.chainId(SRC_CHAIN_ID), and calls super.setUp() to ensure proper test environment initialization.


25-30: Correct Token Minting and Approval in mintTokens

The mintTokens function effectively mints tokens to the user and sets up the necessary token approvals for the fastBridge contract. The use of vm.prank(user) correctly simulates the user's context for the approval transaction.


32-41: Accurate Implementation of bridge Function

The bridge function accurately handles the bridging logic by pranking as the user and invoking fastBridge.bridge with the correct parameters and value based on the isToken flag.


43-64: Proper Setup for Event Expectation in expectEventBridgeRequested

The expectEventBridgeRequested function correctly encodes the bridge transaction, computes the transaction ID, and sets up the expected event emission using vm.expectEmit. The event parameters are precisely constructed to match the expected BridgeRequested event.


66-74: Effective Balance Verification in checkBalances

The checkBalances function accurately asserts the user's and fastBridge's balances for both token and native asset scenarios, ensuring the correctness of the bridging operation's impact on balances.


76-110: Comprehensive Test Cases Covering Various Bridging Scenarios

The test functions (test_bridge_depositTokenParams, test_bridge_depositTokenWithZapNativeParams, etc.) thoroughly cover different bridging scenarios, including token deposits, native asset deposits, and cases with various parameters. Each test function correctly sets up expectations, invokes the bridge, and verifies balances.

packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol (6)

8-18: Event BridgeRelayed is correctly defined

The BridgeRelayed event is correctly declared with appropriate indexed parameters for efficient filtering and logging of bridge transactions.


20-23: setUp function correctly initializes test environment

The setUp function properly overrides the parent setup, sets the chain ID to DST_CHAIN_ID, and ensures the test environment is correctly configured.


25-30: mintTokens function effectively prepares token balances

The mintTokens function successfully mints tokens to the relayer and sets up the necessary approvals for the fastBridge contract.


66-82: Balance assertions accurately validate fund transfers

The checkBalances function correctly asserts the balances of involved addresses, ensuring that tokens or native assets are precisely transferred and accounted for in the dstVault.


84-118: Test functions provide comprehensive coverage of relay scenarios

The test functions effectively cover various scenarios for both token and native asset transfers, including:

  • Successful deposits with tokens and native assets.
  • Testing with additional zapNative amounts.
  • Expected reverts when interacting with the VaultManyArguments contract.

This comprehensive testing ensures robust validation of the relay functionality.


32-42: ⚠️ Potential issue

Verify Ether value sent in fastBridge.relay when relaying tokens

In the relay function, when isToken is true, the call to fastBridge.relay sends paramsV2.zapNative as value. This could result in unintended Ether being sent alongside token transfers. Please verify if this behavior is intentional and aligns with the expected functionality.

If unintentional, consider adjusting the value assignment:

 fastBridge.relay{value: isToken ? paramsV2.zapNative : DST_AMOUNT}(encodedBridgeTx);

To:

 fastBridge.relay{value: isToken ? 0 : DST_AMOUNT}(encodedBridgeTx);
packages/contracts-rfq/contracts/libs/ZapDataV1.sol (6)

27-35: Proper validation in validateV1 function

The validateV1 function effectively ensures that the encoded ZapData is valid by checking the length and confirming the version matches the expected constant. This safeguards against improperly formatted data.


37-65: Efficient encoding in encodeV1 function

The encodeV1 function correctly encodes the ZapData struct by tightly packing the fields. It includes appropriate validation to ensure the amountPosition_ is within the bounds of the payload_, preventing potential out-of-bounds errors.


67-73: Accurate version extraction in version function

The inline assembly in the version function correctly extracts the version field from the encoded data. The use of assembly here is justified for efficient data retrieval.


75-81: Correct target address retrieval in target function

The target function uses inline assembly to accurately extract the target address from the encoded ZapData. This implementation is efficient and reliable.


83-108: Robust payload handling in payload function

The payload function properly handles the case where the amount is present or not in the original payload. It safely replaces the placeholder amount with the actual amount when necessary, ensuring the integrity of the data passed to the target contract.


110-117: Safe extraction of amount position in _amountPosition function

The inline assembly in _amountPosition correctly retrieves the amount position from the encoded data. This aids in accurately modifying the payload when replacing the token amount.

Comment on lines +53 to +60
function checkERC20HappyPath(bytes memory zapData, uint256 msgValue) public {
// Transfer tokens to the zap contract first
erc20.transfer(address(tokenZap), AMOUNT);
bytes4 returnValue = tokenZap.zap{value: msgValue}(address(erc20), AMOUNT, zapData);
assertEq(returnValue, tokenZap.zap.selector);
// Check that the vault registered the deposit
assertEq(vault.balanceOf(user, address(erc20)), AMOUNT);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance ERC20 test utility with balance checks.

Consider adding initial balance checks to ensure test preconditions and verify that no tokens remain in the zap contract after the operation.

     function checkERC20HappyPath(bytes memory zapData, uint256 msgValue) public {
+        uint256 initialBalance = erc20.balanceOf(address(tokenZap));
         // Transfer tokens to the zap contract first
         erc20.transfer(address(tokenZap), AMOUNT);
+        assertEq(erc20.balanceOf(address(tokenZap)), initialBalance + AMOUNT, "Pre-zap balance check failed");
         bytes4 returnValue = tokenZap.zap{value: msgValue}(address(erc20), AMOUNT, zapData);
         assertEq(returnValue, tokenZap.zap.selector);
         // Check that the vault registered the deposit
         assertEq(vault.balanceOf(user, address(erc20)), AMOUNT);
+        assertEq(erc20.balanceOf(address(tokenZap)), initialBalance, "Post-zap balance check failed");
     }
📝 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 checkERC20HappyPath(bytes memory zapData, uint256 msgValue) public {
// Transfer tokens to the zap contract first
erc20.transfer(address(tokenZap), AMOUNT);
bytes4 returnValue = tokenZap.zap{value: msgValue}(address(erc20), AMOUNT, zapData);
assertEq(returnValue, tokenZap.zap.selector);
// Check that the vault registered the deposit
assertEq(vault.balanceOf(user, address(erc20)), AMOUNT);
}
function checkERC20HappyPath(bytes memory zapData, uint256 msgValue) public {
uint256 initialBalance = erc20.balanceOf(address(tokenZap));
// Transfer tokens to the zap contract first
erc20.transfer(address(tokenZap), AMOUNT);
assertEq(erc20.balanceOf(address(tokenZap)), initialBalance + AMOUNT, "Pre-zap balance check failed");
bytes4 returnValue = tokenZap.zap{value: msgValue}(address(erc20), AMOUNT, zapData);
assertEq(returnValue, tokenZap.zap.selector);
// Check that the vault registered the deposit
assertEq(vault.balanceOf(user, address(erc20)), AMOUNT);
assertEq(erc20.balanceOf(address(tokenZap)), initialBalance, "Post-zap balance check failed");
}

Comment on lines 38 to 41
// Issue the approval only if the current allowance is less than the required amount
if (IERC20(token).allowance(address(this), target) < amount) {
IERC20(token).forceApprove(target, type(uint256).max);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid unlimited approvals to reduce security risk

Setting an unlimited allowance (type(uint256).max) to the target contract can be risky if the target is malicious or becomes compromised. It's safer to approve only the necessary amount.

Apply this diff to approve only the required amount:

if (IERC20(token).allowance(address(this), target) < amount) {
-    IERC20(token).forceApprove(target, type(uint256).max);
+    IERC20(token).forceApprove(target, 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
// Issue the approval only if the current allowance is less than the required amount
if (IERC20(token).allowance(address(this), target) < amount) {
IERC20(token).forceApprove(target, type(uint256).max);
}
// Issue the approval only if the current allowance is less than the required amount
if (IERC20(token).allowance(address(this), target) < amount) {
IERC20(token).forceApprove(target, amount);
}

Comment on lines 28 to 48
function zap(address token, uint256 amount, bytes calldata zapData) external payable returns (bytes4) {
// Check that the ZapData is valid before decoding it
zapData.validateV1();
address target = zapData.target();
// Approve the target contract to spend the token. TokenZapV1 does not custody any tokens outside of the
// zap action, so we can approve the arbitrary target contract.
if (token == NATIVE_GAS_TOKEN) {
// No approvals are needed for the native gas token, just check that the amount is correct
if (msg.value != amount) revert TokenZapV1__AmountIncorrect();
} else {
// Issue the approval only if the current allowance is less than the required amount
if (IERC20(token).allowance(address(this), target) < amount) {
IERC20(token).forceApprove(target, type(uint256).max);
}
}
// Perform the Zap action, forwarding full msg.value to the target contract
// Note: this will bubble up any revert from the target contract
bytes memory payload = zapData.payload(amount);
Address.functionCallWithValue({target: target, data: payload, value: msg.value});
return this.zap.selector;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Consider adding reentrancy protection to the zap function

The zap function allows external calls to arbitrary contracts via functionCallWithValue. To prevent potential reentrancy attacks, it's advisable to add reentrancy protection using OpenZeppelin's ReentrancyGuard.

Apply this diff to add reentrancy protection:

+import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

-contract TokenZapV1 is IZapRecipient {
+contract TokenZapV1 is IZapRecipient, ReentrancyGuard {

-function zap(address token, uint256 amount, bytes calldata zapData) external payable returns (bytes4) {
+function zap(address token, uint256 amount, bytes calldata zapData) external payable nonReentrant returns (bytes4) {

Committable suggestion was skipped due to low confidence.

🧰 Tools
🪛 GitHub Check: Slither

[warning] 28-48: Unused return
TokenZapV1.zap(address,uint256,bytes) (contracts/zaps/TokenZapV1.sol#28-48) ignores return value by Address.functionCallWithValue({target:target,data:payload,value:msg.value}) (contracts/zaps/TokenZapV1.sol#46)


🛠️ Refactor suggestion

Emit an event after successful zap execution

Emitting an event upon successful execution of the zap function can improve transparency and facilitate off-chain tracking and debugging.

Apply this diff to add an event and emit it:

+event ZapExecuted(address indexed sender, address token, uint256 amount, address target);

 function zap(address token, uint256 amount, bytes calldata zapData) external payable nonReentrant returns (bytes4) {
     // existing code
     Address.functionCallWithValue({target: target, data: payload, value: msg.value});
+    emit ZapExecuted(msg.sender, token, amount, target);
     return this.zap.selector;
 }

Committable suggestion was skipped due to low confidence.

🧰 Tools
🪛 GitHub Check: Slither

[warning] 28-48: Unused return
TokenZapV1.zap(address,uint256,bytes) (contracts/zaps/TokenZapV1.sol#28-48) ignores return value by Address.functionCallWithValue({target:target,data:payload,value:msg.value}) (contracts/zaps/TokenZapV1.sol#46)

Copy link
Contributor

@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: 3

🧹 Outside diff range and nitpick comments (5)
packages/contracts-rfq/test/libs/ZapDataV1.t.sol (2)

18-29: Add NatSpec documentation for the helper function.

While the function is well-implemented, adding NatSpec documentation would improve maintainability by explaining the purpose and parameters of this helper function.

+    /// @notice Helper function to encode ZapData for testing
+    /// @param version The version of the ZapData format
+    /// @param amountPosition Position of the amount in the payload
+    /// @param target The target contract address
+    /// @param payload The encoded function call data
+    /// @return The encoded ZapData bytes
     function encodeZapData(
         uint16 version,
         uint16 amountPosition,
         address target,
         bytes memory payload
     )

31-70: Consider adding edge case tests for payload sizes.

The roundtrip tests are well-structured and comprehensive. Consider adding specific test cases for:

  • Maximum payload size (close to uint16.max)
  • Empty prefix/postfix combinations
  • Boundary conditions for amount positions
packages/contracts-rfq/test/zaps/TokenZapV1.t.sol (3)

14-14: Consider using wei notation for AMOUNT constant.

Using decimal notation for ether values can lead to precision issues. Consider using wei notation for better precision and clarity.

-    uint256 internal constant AMOUNT = 0.987 ether;
+    uint256 internal constant AMOUNT = 987000000000000000; // 0.987 ether in wei

34-53: Add NatSpec documentation to helper functions.

The helper functions lack documentation explaining their purpose and parameters. Consider adding NatSpec comments for better maintainability.

+    /// @notice Generates vault deposit payload with amount parameter
+    /// @param token The token address to deposit
+    /// @param amount The amount to deposit
+    /// @return The encoded payload
     function getVaultPayload(address token, uint256 amount) public view returns (bytes memory) {
         return abi.encodeCall(vault.deposit, (token, abi.encode(token), amount, user, abi.encode(user)));
     }

+    /// @notice Generates vault deposit payload without amount parameter
+    /// @return The encoded payload
     function getVaultPayloadNoAmount() public view returns (bytes memory) {
         return abi.encodeCall(vault.depositNoAmount, (user));
     }

64-101: Add edge case tests for ERC20 operations.

Consider adding tests for the following scenarios:

  • Tokens with non-standard decimals (e.g., 6 or 8 decimals)
  • Tokens with transfer fees
  • Tokens that return false instead of reverting on failure

Would you like me to help implement these additional test cases?

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between ae4fe60 and 1ec3df8.

📒 Files selected for processing (3)
  • packages/contracts-rfq/contracts/libs/ZapDataV1.sol (1 hunks)
  • packages/contracts-rfq/test/libs/ZapDataV1.t.sol (1 hunks)
  • packages/contracts-rfq/test/zaps/TokenZapV1.t.sol (1 hunks)
🔇 Additional comments (4)
packages/contracts-rfq/test/libs/ZapDataV1.t.sol (2)

1-16: LGTM! Clean setup and proper test structure.

The file structure follows testing best practices with clear imports and proper test contract setup.


140-147: Previous comment still applies: Fix invalid slicing of calldata bytes

The issue with direct calldata slicing remains unresolved.

packages/contracts-rfq/test/zaps/TokenZapV1.t.sol (1)

103-145: Add tests for handling unexpected native token receipts.

Consider adding tests for scenarios where the contract receives native tokens through:

  • selfdestruct
  • coinbase transaction
  • direct transfer without function call
packages/contracts-rfq/contracts/libs/ZapDataV1.sol (1)

1-119: Well-structured implementation of ZapDataV1 library.

The library is thoroughly documented, and the use of inline assembly is efficient and appropriately justified for extracting specific data from the encoded payload. Error handling with custom errors enhances clarity and gas efficiency. Overall, the implementation aligns well with best practices for Solidity libraries.

Comment on lines +36 to +60
function zap(address token, uint256 amount, bytes calldata zapData) external payable returns (bytes4) {
// Validate the ZapData format and extract target address
zapData.validateV1();
address target = zapData.target();
if (token == NATIVE_GAS_TOKEN) {
// For native gas token (e.g. ETH), verify msg.value matches expected amount.
// No approval needed since native token doesn't use allowances.
if (msg.value != amount) revert TokenZapV1__AmountIncorrect();
} else {
// For ERC20 tokens, grant unlimited approval to target if current allowance insufficient.
// This is safe since contract doesn't custody tokens between zaps.
if (IERC20(token).allowance(address(this), target) < amount) {
IERC20(token).forceApprove(target, type(uint256).max);
}
// Note: Balance check omitted as target contract will revert if insufficient funds
}
// Construct the payload for the target contract call with the Zap action.
// The payload is modified to replace the placeholder amount with the actual amount.
bytes memory payload = zapData.payload(amount);
// Perform the Zap action, forwarding full msg.value to the target contract.
// Note: this will bubble up any revert from the target contract.
Address.functionCallWithValue({target: target, data: payload, value: msg.value});
// Return function selector to indicate successful execution
return this.zap.selector;
}

Check warning

Code scanning / Slither

Unused return Medium

Copy link
Contributor

@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: 2

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec3df8 and cbc4fbd.

📒 Files selected for processing (1)
  • packages/contracts-rfq/contracts/zaps/TokenZapV1.sol (1 hunks)
🔇 Additional comments (1)
packages/contracts-rfq/contracts/zaps/TokenZapV1.sol (1)

103-114: decodeZapData Function Implementation is Correct

The decodeZapData function effectively decodes the Zap Data and replaces the placeholder amount with the actual amount when necessary. The use of validateV1 ensures that the zapData is correctly formatted, enhancing reliability and security.

Comment on lines +86 to +97
if (payload.length > ZapDataV1.AMOUNT_NOT_PRESENT) {
revert TokenZapV1__PayloadLengthAboveMax();
}
// External integrations do not need to understand the specific `AMOUNT_NOT_PRESENT` semantics.
// Therefore, they can specify any value greater than or equal to `payload.length` to indicate
// that the amount is not present in the payload.
if (amountPosition >= payload.length) {
amountPosition = ZapDataV1.AMOUNT_NOT_PRESENT;
}
// At this point we checked that both `amountPosition` and `payload.length` fit in uint16.
return ZapDataV1.encodeV1(uint16(amountPosition), target, payload);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure Safe Casting to uint16 to Prevent Data Truncation

In the encodeZapData function, both payload.length and amountPosition are cast to uint16 without explicitly verifying that they fit within the uint16 range. If either value exceeds 65,535, the cast will truncate the data, potentially causing incorrect encoding of the Zap Data. To prevent this, add checks to ensure that payload.length and amountPosition are less than or equal to type(uint16).max before casting.

Apply this diff to add the necessary checks:

 function encodeZapData(
     address target,
     bytes memory payload,
     uint256 amountPosition
 )
     external
     pure
     returns (bytes memory)
 {
-    if (payload.length > ZapDataV1.AMOUNT_NOT_PRESENT) {
+    if (payload.length > ZapDataV1.AMOUNT_NOT_PRESENT || payload.length > type(uint16).max) {
         revert TokenZapV1__PayloadLengthAboveMax();
     }
+    if (amountPosition > type(uint16).max) {
+        revert TokenZapV1__PayloadLengthAboveMax();
+    }
     // External integrations do not need to understand the specific `AMOUNT_NOT_PRESENT` semantics.
     // Therefore, they can specify any value greater than or equal to `payload.length` to indicate
     // that the amount is not present in the payload.

Committable suggestion was skipped due to low confidence.

Comment on lines +15 to +17
/// IMPORTANT: This contract is stateless and does not custody assets between Zaps. Any tokens left in the contract
/// can be claimed by anyone. To prevent loss of funds, ensure that Zaps either consume the entire token amount
/// or revert the transaction.
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Implement a Recovery Mechanism for Unused Tokens

While the documentation states that any tokens left in the contract can be claimed by anyone, there is no function provided to facilitate the recovery of these tokens. If tokens are accidentally sent to the contract without being utilized in a Zap, they may become permanently inaccessible, leading to potential loss of funds. Consider adding a public function that allows recovery of any ERC20 or native tokens held by the contract to prevent such losses.

Apply this diff to add a recovery function:

+    /// @notice Allows anyone to recover tokens accidentally sent to the contract.
+    /// @param token Address of the token to recover. Use NATIVE_GAS_TOKEN for native tokens.
+    function recoverTokens(address token) external {
+        if (token == NATIVE_GAS_TOKEN) {
+            uint256 balance = address(this).balance;
+            if (balance > 0) {
+                payable(msg.sender).transfer(balance);
+            }
+        } else {
+            uint256 balance = IERC20(token).balanceOf(address(this));
+            if (balance > 0) {
+                IERC20(token).safeTransfer(msg.sender, balance);
+            }
+        }
+    }

Committable suggestion was skipped due to low confidence.

Copy link
Collaborator

@parodime parodime left a comment

Choose a reason for hiding this comment

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

i added some test assumptions that were failing on my end, otherwise LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants