Skip to content

Commit

Permalink
Add weth wrapping hook (#436)
Browse files Browse the repository at this point in the history
* feat: add WETH wrapping hook

* feat: add exact output support

* fix: remove old comment

* fix: exactInput is amountSpecified < 0

* fix: remove beforeRemoveLiquidity hook

* feat: use deltaresolver

* feat: use safecast where it makes sense

* fix: re add snaps

* fix: merge conflicts

* fix: ternary instead of or

* fix: flip

* fix: underscore internals

* fix: deploycodeto

* fix: remove named params from wethhook

* fix: internal reverts
  • Loading branch information
marktoda authored Feb 14, 2025
1 parent de15ed6 commit fdb5d3e
Show file tree
Hide file tree
Showing 5 changed files with 573 additions and 1 deletion.
185 changes: 185 additions & 0 deletions src/base/hooks/BaseTokenWrapperHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
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 {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.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 "../../utils/BaseHook.sol";
import {DeltaResolver} from "../DeltaResolver.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, DeltaResolver {
using CurrencyLibrary for Currency;
using SafeCast for int256;
using SafeCast for uint256;

/// @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 Indicates whether wrapping occurs when swapping from token0 to token1
/// @dev This is determined by the relative ordering of the wrapper and underlying tokens
/// @dev If true: token0 is underlying (e.g. ETH) and token1 is wrapper (e.g. WETH)
/// @dev If false: token0 is wrapper (e.g. WETH) and token1 is underlying (e.g. ETH)
/// @dev This is set in the constructor based on the token addresses to ensure consistent behavior
bool public immutable wrapZeroForOne;

/// @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;
wrapZeroForOne = _underlying < _wrapper;
}

/// @inheritdoc BaseHook
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true,
beforeAddLiquidity: true,
beforeSwap: true,
beforeSwapReturnDelta: true,
afterSwap: false,
afterInitialize: false,
beforeRemoveLiquidity: false,
afterAddLiquidity: false,
afterRemoveLiquidity: false,
beforeDonate: false,
afterDonate: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}

/// @notice Validates pool initialization parameters
/// @dev Ensures pool contains wrapper and underlying tokens with zero fee
/// @param poolKey The pool configuration including tokens and fee
/// @return The function selector if validation passes
function _beforeInitialize(address, PoolKey calldata poolKey, uint160) internal view override returns (bytes4) {
// ensure pool tokens are the wrapper currency and underlying currency
bool isValidPair = wrapZeroForOne
? (poolKey.currency0 == underlyingCurrency && poolKey.currency1 == wrapperCurrency)
: (poolKey.currency0 == wrapperCurrency && poolKey.currency1 == underlyingCurrency);

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

return IHooks.beforeInitialize.selector;
}

/// @notice Prevents liquidity operations on wrapper pools
/// @dev Always reverts as liquidity is managed through the token wrapper
function _beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata)
internal
pure
override
returns (bytes4)
{
revert LiquidityNotAllowed();
}

/// @notice Handles token wrapping and unwrapping during swaps
/// @dev Processes both exact input (amountSpecified < 0) and exact output (amountSpecified > 0) swaps
/// @param params The swap parameters including direction and amount
/// @return selector The function selector
/// @return swapDelta The input/output token amounts for pool accounting
/// @return lpFeeOverride The fee override (always 0 for wrapper pools)
function _beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4 selector, BeforeSwapDelta swapDelta, uint24 lpFeeOverride)
{
bool isExactInput = params.amountSpecified < 0;

if (wrapZeroForOne == params.zeroForOne) {
// we are wrapping
uint256 inputAmount =
isExactInput ? uint256(-params.amountSpecified) : _getWrapInputRequired(uint256(params.amountSpecified));
_take(underlyingCurrency, address(this), inputAmount);
uint256 wrappedAmount = _deposit(inputAmount);
_settle(wrapperCurrency, address(this), wrappedAmount);
int128 amountUnspecified =
isExactInput ? -wrappedAmount.toInt256().toInt128() : inputAmount.toInt256().toInt128();
swapDelta = toBeforeSwapDelta(-params.amountSpecified.toInt128(), amountUnspecified);
} else {
// we are unwrapping
uint256 inputAmount = isExactInput
? uint256(-params.amountSpecified)
: _getUnwrapInputRequired(uint256(params.amountSpecified));
_take(wrapperCurrency, address(this), inputAmount);
uint256 unwrappedAmount = _withdraw(inputAmount);
_settle(underlyingCurrency, address(this), unwrappedAmount);
int128 amountUnspecified =
isExactInput ? -unwrappedAmount.toInt256().toInt128() : inputAmount.toInt256().toInt128();
swapDelta = toBeforeSwapDelta(-params.amountSpecified.toInt128(), amountUnspecified);
}

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

/// @notice Transfers tokens to the pool manager
/// @param token The token to transfer
/// @param amount The amount to transfer
/// @inheritdoc DeltaResolver
function _pay(Currency token, address, uint256 amount) internal override {
token.transfer(address(poolManager), amount);
}

/// @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;
}
}
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) {
weth.deposit{value: underlyingAmount}();
return underlyingAmount; // 1:1 ratio
}

/// @inheritdoc BaseTokenWrapperHook
function _withdraw(uint256 wrapperAmount) internal override returns (uint256) {
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

0 comments on commit fdb5d3e

Please sign in to comment.