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

fix(contracts-rfq): TokenZapV1 native gas token behaviour [SLT-389] #3418

Merged
merged 12 commits into from
Nov 28, 2024
Merged
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 {}

ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
/// @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
Loading