Skip to content

Commit

Permalink
fix(contracts-rfq): TokenZapV1 native gas token behaviour [SLT-389] (
Browse files Browse the repository at this point in the history
…#3418)

* test: add multi-hop tests for `TokenZapV1`

* fix: `TokenZap` should be able to receive ETH

* test: existing ETH balance, using lower `msg.value`

* fix: `TokenZap` should be able to use previosuly acquired ETH

* test: `TokenZap` should be able to do native transfers

* fix: native ETH transfers to EOAs

* docs: better wording for `msg.value` usage

* docs: minor fixes

* test: more revert cases around empty target / payload

* fix: check for empty target

* fix: separate the pure native gas token transfer case

* chore: silence solhint warning in the test mock
  • Loading branch information
ChiTimesChi authored Nov 28, 2024
1 parent 91dee92 commit ee3705a
Show file tree
Hide file tree
Showing 3 changed files with 347 additions and 22 deletions.
51 changes: 36 additions & 15 deletions packages/contracts-rfq/contracts/zaps/TokenZapV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,51 @@ import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeE
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

/// @title TokenZapV1
/// @notice Facilitates atomic token operations known as "Zaps," allowing to execute predefined actions
/// on behalf of users like deposits or swaps. Supports ERC20 tokens and native gas tokens (e.g., ETH).
/// @dev Tokens must be pre-transferred to the contract for execution, with native tokens sent as msg.value.
/// @notice Facilitates atomic token operations known as "Zaps", allowing the execution of predefined actions
/// on behalf of users, such as deposits or swaps. Supports ERC20 tokens and native gas tokens (e.g., ETH).
/// @dev Tokens must be transferred to the contract before execution, native tokens could be provided as `msg.value`.
/// This contract is stateless and does not hold assets between Zaps; leftover tokens can be claimed by anyone.
/// Ensure Zaps fully utilize tokens or revert to prevent fund loss.
/// Ensure that Zaps fully utilize tokens or revert to prevent the loss of funds.
contract TokenZapV1 is IZapRecipient {
using SafeERC20 for IERC20;
using ZapDataV1 for bytes;

address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

error TokenZapV1__AmountIncorrect();
error TokenZapV1__PayloadLengthAboveMax();
error TokenZapV1__TargetZeroAddress();

/// @notice Performs a Zap action using the specified token and amount. This amount must be previously
/// transferred to this contract (or supplied as msg.value if the token is native gas token).
/// @notice Allows the contract to receive ETH.
/// @dev Leftover ETH can be claimed by anyone. Ensure the full balance is spent during Zaps.
receive() external payable {}

/// @notice Performs a Zap action using the specified token and amount. This amount must have previously been
/// transferred to this contract (could also be supplied as msg.value if the token is a native gas token).
/// Zap action will be performed forwarding full `msg.value` for ERC20s or `amount` for native gas tokens.
/// Note: all funds remaining after the Zap action is performed can be claimed by anyone.
/// Make sure to spend the full balance during the Zaps and avoid sending extra funds if a single Zap is performed.
/// @dev The provided ZapData contains the target address and calldata for the Zap action, and must be
/// encoded using the encodeZapData function.
/// encoded using the encodeZapData function. Native gas token transfers could be done by using empty `payload`,
/// this is the only case where target could be an EOA.
/// @param token Address of the token to be used for the Zap action.
/// @param amount Amount of the token to be used for the Zap action.
/// Must match msg.value if the token is a native gas token.
/// @param zapData Encoded Zap Data containing the target address and calldata for the Zap action.
/// @return selector Selector of this function to signal the caller about the success of the Zap action.
function zap(address token, uint256 amount, bytes calldata zapData) external payable returns (bytes4) {
// Validate the ZapData format and extract the target address.
zapData.validateV1();
address target = zapData.target();
if (target == address(0)) revert TokenZapV1__TargetZeroAddress();
// Note: we don't check the amount that was transferred to TokenZapV1 (or msg.value for native gas tokens).
// Transferring more than `amount` will lead to remaining funds in TokenZapV1, which can be claimed by anyone.
// Ensure that you send the exact amount for a single Zap or spend the full balance for multiple `zap()` calls.
uint256 msgValue = msg.value;
if (token == NATIVE_GAS_TOKEN) {
// For native gas token (e.g., ETH), verify msg.value matches the expected amount.
// No approval needed since native token doesn't use allowances.
if (msg.value != amount) revert TokenZapV1__AmountIncorrect();
// For native gas tokens, we forward the requested amount to the target contract during the Zap action.
// Similar to ERC20s, we allow using pre-transferred native tokens for the Zap.
msgValue = amount;
// No approval is needed since native tokens don't use allowances.
// Note: balance check is performed within `Address.sendValue` or `Address.functionCallWithValue` below.
} else {
// For ERC20 tokens, grant unlimited approval to the target if the current allowance is insufficient.
// This is safe since the contract doesn't custody tokens between zaps.
Expand All @@ -57,9 +71,16 @@ contract TokenZapV1 is IZapRecipient {
// 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});
if (payload.length == 0 && token == NATIVE_GAS_TOKEN) {
// Zap Action in a form of native gas token transfer to the target is requested.
// Note: we avoid using `functionCallWithValue` because the target might be an EOA. This will
// revert with a generic custom error should the target contract revert on incoming transfer.
Address.sendValue({recipient: payable(target), amount: msgValue});
} else {
// Perform the Zap action, forwarding the requested native value to the target contract.
// Note: this will bubble up any revert from the target contract, and revert if target is EOA.
Address.functionCallWithValue({target: target, data: payload, value: msgValue});
}
// Return function selector to indicate successful execution
return this.zap.selector;
}
Expand Down
27 changes: 27 additions & 0 deletions packages/contracts-rfq/test/mocks/WETHMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

// solhint-disable no-empty-blocks
/// @notice WETH mock for testing purposes. DO NOT USE IN PRODUCTION.
contract WETHMock is ERC20 {
constructor() ERC20("Mock Wrapped Ether", "Mock WETH") {}

receive() external payable {
deposit();
}

/// @notice We include an empty "test" function so that this contract does not appear in the coverage report.
function testWETHMock() external {}

function withdraw(uint256 amount) external {
_burn(msg.sender, amount);
Address.sendValue(payable(msg.sender), amount);
}

function deposit() public payable {
_mint(msg.sender, msg.value);
}
}
Loading

0 comments on commit ee3705a

Please sign in to comment.