-
Notifications
You must be signed in to change notification settings - Fork 532
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
5 changed files
with
573 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.