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 12 commits into
base: main
Choose a base branch
from
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));
marktoda marked this conversation as resolved.
Show resolved Hide resolved
_take(underlyingCurrency, address(this), inputAmount);
uint256 wrappedAmount = deposit(inputAmount);
marktoda marked this conversation as resolved.
Show resolved Hide resolved
_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 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