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

Add weth wrapping hook #436

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions src/base/hooks/BaseTokenWrapperHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
pragma solidity ^0.8.0;

import {
toBeforeSwapDelta, BeforeSwapDelta, BeforeSwapDeltaLibrary
} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {BaseHook} from "./BaseHook.sol";

/// @title Base Token Wrapper Hook
/// @notice Abstract base contract for implementing token wrapper hooks in Uniswap V4
/// @dev This contract provides the base functionality for wrapping/unwrapping tokens through V4 pools
/// @dev All liquidity operations are blocked as liquidity is managed through the underlying token wrapper
/// @dev Implementing contracts must provide deposit() and withdraw() functions
abstract contract BaseTokenWrapperHook is BaseHook {
Copy link
Contributor

Choose a reason for hiding this comment

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

imo we should be removing the inheritance of SafeCallback that’s happening into BaseHook. I don’t think that should be in there - not all hooks are going to take out locks on the PM (eg this one) so it’s an unnecessary addition.
Could be a separate PR just putting this thought here.

using CurrencyLibrary for Currency;

/// @notice Thrown when attempting to add or remove liquidity
/// @dev Liquidity operations are blocked since all liquidity is managed by the token wrapper
error LiquidityNotAllowed();

/// @notice Thrown when initializing a pool with invalid tokens
/// @dev Pool must contain exactly one wrapper token and its underlying token
error InvalidPoolToken();

/// @notice Thrown when initializing a pool with non-zero fee
/// @dev Fee must be 0 as wrapper pools don't charge fees
error InvalidPoolFee();

/// @notice The wrapped token currency (e.g., WETH)
Currency public immutable wrapperCurrency;

/// @notice The underlying token currency (e.g., ETH)
Currency public immutable underlyingCurrency;

/// @notice Creates a new token wrapper hook
/// @param _manager The Uniswap V4 pool manager
/// @param _wrapper The wrapped token currency (e.g., WETH)
/// @param _underlying The underlying token currency (e.g., ETH)
constructor(IPoolManager _manager, Currency _wrapper, Currency _underlying) BaseHook(_manager) {
wrapperCurrency = _wrapper;
underlyingCurrency = _underlying;
}

/// @notice Returns a struct of permissions to signal which hook functions are to be implemented
/// @dev Used at deployment to validate the address correctly represents the expected permissions
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true,
beforeAddLiquidity: true,
beforeRemoveLiquidity: true,
beforeSwap: true,
beforeSwapReturnDelta: true,
afterSwap: false,
afterInitialize: false,
afterAddLiquidity: false,
afterRemoveLiquidity: false,
beforeDonate: false,
afterDonate: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}

/// @inheritdoc IHooks
function beforeInitialize(address, PoolKey calldata poolKey, uint160) external view override returns (bytes4) {
// ensure pool tokens are the wrapper currency and underlying currency
bool isValidPair = (poolKey.currency0 == wrapperCurrency && poolKey.currency1 == underlyingCurrency)
marktoda marked this conversation as resolved.
Show resolved Hide resolved
|| (poolKey.currency0 == underlyingCurrency && poolKey.currency1 == wrapperCurrency);

if (!isValidPair) revert InvalidPoolToken();
if (poolKey.fee != 0) revert InvalidPoolFee();

return IHooks.beforeInitialize.selector;
}

/// @inheritdoc IHooks
function beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata)
external
pure
override
returns (bytes4)
{
revert LiquidityNotAllowed();
}

/// @inheritdoc IHooks
function beforeRemoveLiquidity(
marktoda marked this conversation as resolved.
Show resolved Hide resolved
address,
PoolKey calldata,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external pure override returns (bytes4) {
revert LiquidityNotAllowed();
}

/// @inheritdoc IHooks
/// @notice Handles the wrapping/unwrapping of tokens during a swap
/// @dev Takes input tokens from sender, performs wrap/unwrap, and settles output tokens
/// @dev No fees are charged on these operations
function beforeSwap(address, PoolKey calldata poolKey, IPoolManager.SwapParams calldata params, bytes calldata)
external
override
returns (bytes4 selector, BeforeSwapDelta swapDelta, uint24 lpFeeOverride)
{
bool isWrapping = _isWrapping(poolKey, params.zeroForOne);
bool isExactInput = params.amountSpecified < 0;

if (isWrapping) {
uint256 inputAmount =
isExactInput ? uint256(-params.amountSpecified) : _getWrapInputRequired(uint256(params.amountSpecified));
Copy link
Contributor

Choose a reason for hiding this comment

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

technically its not safe to invert a negative number without safecast/checking whether the value is the minimum integer. Although I cant think of an attack vector. We do have a large SafeCast.sol in core you can use for most of these things

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not totally sure what you mean here - what is the problem if it's the minimum integer? and how does safecast help?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also theres no int256 => uint256 function in that library

poolManager.take(underlyingCurrency, address(this), inputAmount);
uint256 wrappedAmount = deposit(inputAmount);
Copy link
Contributor

Choose a reason for hiding this comment

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

is it worth deposit just handling a uint128 or smaller, given you have to cast up to uint256 before this, and then back down later

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm ok did this but not sure its really any cleaner

Copy link
Contributor Author

Choose a reason for hiding this comment

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

just pushes the weird casting down into the inheriting contracts

_settle(wrapperCurrency, wrappedAmount);
int128 amountUnspecified = isExactInput ? -int128(int256(wrappedAmount)) : int128(int256(inputAmount));
swapDelta = toBeforeSwapDelta(-int128(params.amountSpecified), amountUnspecified);
} else {
uint256 inputAmount = isExactInput
? uint256(-params.amountSpecified)
: _getUnwrapInputRequired(uint256(params.amountSpecified));
poolManager.take(wrapperCurrency, address(this), inputAmount);
uint256 unwrappedAmount = withdraw(inputAmount);
_settle(underlyingCurrency, unwrappedAmount);
int128 amountUnspecified = isExactInput ? -int128(int256(unwrappedAmount)) : int128(int256(inputAmount));
swapDelta = toBeforeSwapDelta(-int128(params.amountSpecified), amountUnspecified);
}

return (IHooks.beforeSwap.selector, swapDelta, 0);
}

/// @notice Deposits underlying tokens to receive wrapper tokens
/// @param underlyingAmount The amount of underlying tokens to deposit
/// @return wrappedAmount The amount of wrapper tokens received
/// @dev Implementing contracts should handle the wrapping operation
/// The base contract will handle settling tokens with the pool manager
function deposit(uint256 underlyingAmount) internal virtual returns (uint256 wrappedAmount);

/// @notice Withdraws wrapper tokens to receive underlying tokens
/// @param wrappedAmount The amount of wrapper tokens to withdraw
/// @return underlyingAmount The amount of underlying tokens received
/// @dev Implementing contracts should handle the unwrapping operation
/// The base contract will handle settling tokens with the pool manager
function withdraw(uint256 wrappedAmount) internal virtual returns (uint256 underlyingAmount);

/// @notice Calculates underlying tokens needed to receive desired wrapper tokens
/// @param wrappedAmount The desired amount of wrapper tokens
/// @return The required amount of underlying tokens
/// @dev Default implementation assumes 1:1 ratio
/// @dev Override for wrappers with different exchange rates
function _getWrapInputRequired(uint256 wrappedAmount) internal view virtual returns (uint256) {
return wrappedAmount;
}

/// @notice Calculates wrapper tokens needed to receive desired underlying tokens
/// @param underlyingAmount The desired amount of underlying tokens
/// @return The required amount of wrapper tokens
/// @dev Default implementation assumes 1:1 ratio
/// @dev Override for wrappers with different exchange rates
function _getUnwrapInputRequired(uint256 underlyingAmount) internal view virtual returns (uint256) {
return underlyingAmount;
}

/// @notice Helper function to determine if the swap is wrapping underlying to wrapper tokens
/// @param poolKey The pool being used for the swap
/// @param zeroForOne The direction of the swap
/// @return True if swapping underlying to wrapper, false if unwrapping
function _isWrapping(PoolKey calldata poolKey, bool zeroForOne) internal view returns (bool) {
Currency inputCurrency = zeroForOne ? poolKey.currency0 : poolKey.currency1;
return inputCurrency == underlyingCurrency;
}

/// @notice Settles tokens with the pool manager after a wrap/unwrap operation
/// @param currency The currency being settled (wrapper or underlying)
/// @param amount The amount of tokens to settle
/// @dev Handles both native currency (ETH) and ERC20 tokens:
/// - For native currency: Uses settle with value
/// - For ERC20: Syncs pool state, transfers tokens, then settles
function _settle(Currency currency, uint256 amount) internal {
if (currency.isAddressZero()) {
poolManager.settle{value: amount}();
marktoda marked this conversation as resolved.
Show resolved Hide resolved
} else {
poolManager.sync(currency);
currency.transfer(address(poolManager), amount);
poolManager.settle();
}
}
}
45 changes: 45 additions & 0 deletions src/hooks/WETHHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {WETH} from "solmate/src/tokens/WETH.sol";
import {BaseTokenWrapperHook} from "../base/hooks/BaseTokenWrapperHook.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";

/// @title Wrapped Ether Hook
/// @notice Hook for wrapping/unwrapping ETH in Uniswap V4 pools
/// @dev Implements 1:1 wrapping/unwrapping of ETH to WETH
contract WETHHook is BaseTokenWrapperHook {
/// @notice The WETH9 contract
WETH public immutable weth;

error WithdrawFailed();

/// @notice Creates a new WETH wrapper hook
/// @param _manager The Uniswap V4 pool manager
/// @param _weth The WETH9 contract address
constructor(IPoolManager _manager, address payable _weth)
BaseTokenWrapperHook(
_manager,
Currency.wrap(_weth), // wrapper token is WETH
CurrencyLibrary.ADDRESS_ZERO // underlying token is ETH (address(0))
)
{
weth = WETH(payable(_weth));
}

/// @inheritdoc BaseTokenWrapperHook
function deposit(uint256 underlyingAmount) internal override returns (uint256 wrapperAmount) {
weth.deposit{value: underlyingAmount}();
return underlyingAmount; // 1:1 ratio
}

/// @inheritdoc BaseTokenWrapperHook
function withdraw(uint256 wrapperAmount) internal override returns (uint256 underlyingAmount) {
weth.withdraw(wrapperAmount);
return wrapperAmount; // 1:1 ratio
}

/// @notice Required to receive ETH
receive() external payable {}
}
2 changes: 1 addition & 1 deletion test/V4Quoter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ contract QuoterTest is Test, Deployers {

vm.snapshotGasLastCall("Quoter_quoteExactInput_oneHop_startingInitialized");

assertGt(gasEstimate, 50000);
assertGt(gasEstimate, 40000);
assertLt(gasEstimate, 400000);
assertEq(amountOut, 198);
}
Expand Down
Loading