-
Notifications
You must be signed in to change notification settings - Fork 521
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
d328a34
e756de4
9f5d78a
7592bd3
b8af339
4c970f6
a82e4ad
64bf496
e31062f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it worth There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm ok did this but not sure its really any cleaner There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} | ||
} |
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 {} | ||
} |
There was a problem hiding this comment.
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.