Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"contracts/swaps/uniswap/UniswapV4Swapper.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Uniswap / OZ interfaces and constants
import {IUniversalRouter} from "@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
import {ISignatureTransfer} from "@uniswap/permit2/src/interfaces/ISignatureTransfer.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IWETH9} from "./interfaces/IWETH9.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Commands} from "@uniswap/universal-router/contracts/libraries/Commands.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
/**
* @title UniswapV4Swapper
* @author antonis@typesystem.xyz
* @notice Single-pool exact-in / exact-out swaps on Uniswap v3/v4 via Universal Router.
* @dev Automatically detects V3 pools (hooks == address(0)) and uses appropriate swap command.
*/
contract UniswapV4Swapper is ReentrancyGuard {
using SafeERC20 for IERC20;
// --- Immutable addresses ---
IUniversalRouter public immutable router;
IPoolManager public immutable poolManager;
IPermit2 public immutable permit2;
IWETH9 public immutable weth;
// --- Ownership ---
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
// --- Errors ---
error InsufficientInputBalance();
error InsufficientOutputAmount();
error InsufficientBalanceForMaxInput();
error Permit2AllowanceTooLow();
error InsufficientAllowance();
// --- Events ---
event SwapExactIn(address indexed input, address indexed output, uint256 amountIn, uint256 amountOut, address indexed to);
event SwapExactOut(address indexed input, address indexed output, uint256 amountIn, uint256 amountOut, address indexed to, uint256 refund);
function _emitSwapExactOut(
address input,
address output,
uint256 amountIn,
uint256 amountOut,
address to,
uint256 refund
) internal {
emit SwapExactOut(input, output, amountIn, amountOut, to, refund);
}
constructor(address _router, address _poolManager, address _permit2, address _weth) {
router = IUniversalRouter(payable(_router));
poolManager = IPoolManager(_poolManager);
permit2 = IPermit2(_permit2);
weth = IWETH9(_weth);
owner = msg.sender;
}
// --- Known pools allowlist (canonical pool metadata) ---
// key: keccak256(abi.encodePacked(token0, token1, fee)) with token0<token1 ordering
struct CanonicalPoolMeta { int24 tickSpacing; IHooks hooks; bool exists; }
mapping(bytes32 => CanonicalPoolMeta) private _knownPools;
/// @notice Owner registers or updates a canonical v4 pool’s metadata (tickSpacing, hooks).
function setKnownPool(
address tokenA,
address tokenB,
uint24 fee,
int24 tickSpacing,
address hooks
) external onlyOwner {
require(tokenA != tokenB && tokenA != address(0) && tokenB != address(0), "bad tokens");
(address t0, address t1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
bytes32 k = keccak256(abi.encodePacked(t0, t1, fee));
_knownPools[k] = CanonicalPoolMeta({ tickSpacing: tickSpacing, hooks: IHooks(hooks), exists: true });
}
/// @notice Owner can remove a known pool entry.
function clearKnownPool(address tokenA, address tokenB, uint24 fee) external onlyOwner {
(address t0, address t1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
bytes32 k = keccak256(abi.encodePacked(t0, t1, fee));
delete _knownPools[k];
}
/// @notice View helper: safely build the PoolKey and direction using the known allowlist.
/// Reverts if the pair+fee is not registered. This avoids callers supplying wrong tickSpacing/hooks.
function safePoolKey(
address tokenIn,
address tokenOut,
uint24 fee
) public view returns (PoolKey memory key, bool zeroForOne) {
require(tokenIn != address(0) && tokenOut != address(0) && tokenIn != tokenOut, "bad tokens");
(address t0, address t1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn);
bytes32 k = keccak256(abi.encodePacked(t0, t1, fee));
CanonicalPoolMeta memory meta = _knownPools[k];
require(meta.exists, "pool not allowlisted");
zeroForOne = tokenIn == t0;
key = PoolKey({
currency0: Currency.wrap(t0),
currency1: Currency.wrap(t1),
fee: fee,
tickSpacing: meta.tickSpacing,
hooks: meta.hooks
});
}
function getKnownPoolMeta(address tokenA, address tokenB, uint24 fee) external view returns (bool exists, int24 tickSpacing, address hooks) {
(address t0, address t1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
bytes32 k = keccak256(abi.encodePacked(t0, t1, fee));
CanonicalPoolMeta memory meta = _knownPools[k];
return (meta.exists, meta.tickSpacing, address(meta.hooks));
}
/// @dev Ensure Permit2 has ERC20 allowance from this contract and that the router has Permit2 allowance for `needed`.
function _ensurePermit2Approvals(address token, uint256 needed) internal {
IERC20 erc = IERC20(token);
uint256 currentAllowance = erc.allowance(address(this), address(permit2));
if (currentAllowance < needed) {
if (currentAllowance > 0) {
erc.safeDecreaseAllowance(address(permit2), currentAllowance);
}
erc.safeIncreaseAllowance(address(permit2), type(uint256).max);
}
// Provide a bounded Permit2 allowance to the router for this token.
// Use a long expiration to avoid frequent renewals in tests; callers can always call approveTokenWithPermit2 to customize.
permit2.approve(token, address(router), uint160(needed), type(uint48).max);
}
/// @dev Ensure the Universal Router has a direct ERC20 allowance from this contract (for test/mocks or backward compatibility).
function _ensureRouterAllowance(address token, uint256 needed) internal {
IERC20 erc = IERC20(token);
uint256 currentAllowance = erc.allowance(address(this), address(router));
if (currentAllowance < needed) {
if (currentAllowance > 0) {
erc.safeDecreaseAllowance(address(router), currentAllowance);
}
erc.safeIncreaseAllowance(address(router), type(uint256).max);
}
}
// -------------------------------
// Helpers to simplify caller API
// -------------------------------
function _buildKeyAndDirection(
address tokenIn,
address tokenOut,
uint24 fee,
int24 tickSpacing,
address hooks
) internal pure returns (PoolKey memory key, bool zeroForOne) {
require(tokenIn != address(0) && tokenOut != address(0), "zero token");
require(tokenIn != tokenOut, "same token");
// Order tokens for PoolKey
(address t0, address t1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn);
zeroForOne = tokenIn == t0; // if input is token0 then direction is 0->1
key = PoolKey({
currency0: Currency.wrap(t0),
currency1: Currency.wrap(t1),
fee: fee,
tickSpacing: tickSpacing,
hooks: IHooks(hooks)
});
}
/**
* @notice Approve Permit2 to pull `token` from this contract, and Permit2 to allow Universal Router.
* @dev Uses safeDecreaseAllowance and safeIncreaseAllowance to grant max allowance to Permit2.
*/
function approveTokenWithPermit2(address token, uint160 amount, uint48 expiration) external {
IERC20 erc = IERC20(token);
uint256 currentAllowance = erc.allowance(address(this), address(permit2));
// Only update allowance if needed (saves gas)
if (currentAllowance < type(uint256).max) {
// Reset-to-zero then set, for safety with nonstandard tokens
if (currentAllowance > 0) {
erc.safeDecreaseAllowance(address(permit2), currentAllowance);
}
erc.safeIncreaseAllowance(address(permit2), type(uint256).max);
}
// Now let UniversalRouter spend via Permit2 with bounded allowance + expiry
permit2.approve(token, address(router), amount, expiration);
}
/**
* @notice Internal helper for swapExactInputSingle (no nonReentrant).
*/
function _swapExactInputSingle(
PoolKey memory key,
uint128 amountIn,
uint128 minAmountOut,
bool zeroForOne,
bool unwrapOutput,
uint256 deadline,
address recipient
) internal returns (uint256 amountOut) {
address inputToken = zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1);
address outputToken = zeroForOne ? Currency.unwrap(key.currency1) : Currency.unwrap(key.currency0);
if (IERC20(inputToken).balanceOf(address(this)) < amountIn) revert InsufficientInputBalance();
uint256 balanceBefore = IERC20(outputToken).balanceOf(address(this));
// Ensure either Permit2 or Router can pull the exact input; if neither, set up Permit2 automatically
uint256 p2Allow = IERC20(inputToken).allowance(address(this), address(permit2));
if (p2Allow < amountIn) {
uint256 rAllow = IERC20(inputToken).allowance(address(this), address(router));
if (rAllow < amountIn) {
// Set both, to support real router (Permit2 path) and mocks (direct allowance)
_ensurePermit2Approvals(inputToken, amountIn);
_ensureRouterAllowance(inputToken, amountIn);
}
}
{
// Detect if this is a V3 pool (hooks == address(0))
bool isV3 = address(key.hooks) == address(0);
if (isV3) {
// --- V3 Swap via Universal Router ---
bytes memory commands = abi.encodePacked(uint8(Commands.V3_SWAP_EXACT_IN));
bytes[] memory inputs = new bytes[](1);
// V3 path encoding: [tokenIn, fee, tokenOut]
bytes memory path;
if (zeroForOne) {
path = abi.encodePacked(inputToken, key.fee, outputToken);
} else {
path = abi.encodePacked(outputToken, key.fee, inputToken);
}
inputs[0] = abi.encode(address(this), amountIn, minAmountOut, path, false);
router.execute(commands, inputs, deadline);
} else {
// --- V4 Swap via Universal Router ---
bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
IV4Router.ExactInputSingleParams({
poolKey: key,
zeroForOne: zeroForOne,
amountIn: amountIn,
amountOutMinimum: minAmountOut,
hookData: bytes("")
})
);
params[1] = abi.encode(zeroForOne ? key.currency0 : key.currency1, amountIn);
params[2] = abi.encode(zeroForOne ? key.currency1 : key.currency0, minAmountOut);
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(actions, params);
router.execute(commands, inputs, deadline);
}
uint256 balanceAfter = IERC20(outputToken).balanceOf(address(this));
unchecked { amountOut = balanceAfter - balanceBefore; }
}
if (amountOut < minAmountOut) revert InsufficientOutputAmount();
if (unwrapOutput && outputToken == address(weth)) {
// unwrap exactly the amountOut and send native ETH
weth.withdraw(amountOut);
(bool success, ) = recipient.call{value: amountOut}("");
require(success, "ETH transfer failed");
} else {
IERC20(outputToken).safeTransfer(recipient, amountOut);
}
emit SwapExactIn(inputToken, outputToken, amountIn, amountOut, recipient);
}
/**
* @notice Swap exact input for >= min output (single v4 pool).
* @param key PoolKey for the v4 pool.
* @param amountIn Exact input amount.
* @param minAmountOut Minimum acceptable output.
* @param zeroForOne true: token0->token1, false: token1->token0.
* @param deadline Unix timestamp after which the tx reverts.
* @param recipient Address to receive the output tokens.
*/
function swapExactInputSingle(
PoolKey memory key,
uint128 amountIn,
uint128 minAmountOut,
bool zeroForOne,
bool unwrapOutput,
uint256 deadline,
address recipient
) external nonReentrant returns (uint256 amountOut) {
// Top up only the shortfall from caller (supports pre-funded or pull-from-caller)
address inputToken = zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1);
uint256 bal = IERC20(inputToken).balanceOf(address(this));
if (bal < amountIn) {
uint256 needed = amountIn - bal;
if (IERC20(inputToken).allowance(msg.sender, address(this)) < needed) {
revert InsufficientAllowance();
}
IERC20(inputToken).safeTransferFrom(msg.sender, address(this), needed);
}
return _swapExactInputSingle(key, amountIn, minAmountOut, zeroForOne, unwrapOutput, deadline, recipient);
}
/// @notice Exact-in using allowlisted canonical pool metadata (no user-supplied tickSpacing/hooks).
function swapExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint128 amountIn,
uint128 minAmountOut,
bool unwrapOutput,
uint256 deadline,
address recipient
) external nonReentrant returns (uint256 amountOut) {
(PoolKey memory key, bool zeroForOne) = safePoolKey(tokenIn, tokenOut, fee);
// Top up only the shortfall
uint256 bal = IERC20(tokenIn).balanceOf(address(this));
if (bal < amountIn) {
uint256 needed = amountIn - bal;
if (IERC20(tokenIn).allowance(msg.sender, address(this)) < needed) revert InsufficientAllowance();
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), needed);
}
return _swapExactInputSingle(key, amountIn, minAmountOut, zeroForOne, unwrapOutput, deadline, recipient);
}
/**
* @notice Internal helper for swapExactOutputSingle (no nonReentrant).
*/
function _swapExactOutputSingle(
PoolKey memory key,
uint128 amountOut,
uint128 amountInMaximum,
bool zeroForOne,
bool unwrapOutput,
uint256 deadline,
address recipient
) internal returns (uint256 amountIn) {
address inputToken = zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1);
address outputToken = zeroForOne ? Currency.unwrap(key.currency1) : Currency.unwrap(key.currency0);
uint256 balanceBefore = IERC20(inputToken).balanceOf(address(this));
if (balanceBefore < amountInMaximum) revert InsufficientBalanceForMaxInput();
// Ensure either Permit2 or Router can pull up to the max input; if neither, set up Permit2 automatically
uint256 p2Allow = IERC20(inputToken).allowance(address(this), address(permit2));
if (p2Allow < amountInMaximum) {
uint256 rAllow = IERC20(inputToken).allowance(address(this), address(router));
if (rAllow < amountInMaximum) {
_ensurePermit2Approvals(inputToken, amountInMaximum);
_ensureRouterAllowance(inputToken, amountInMaximum);
}
}
uint256 refund;
{
// Detect if this is a V3 pool (hooks == address(0))
bool isV3 = address(key.hooks) == address(0);
if (isV3) {
// --- V3 Swap via Universal Router ---
bytes memory commands = abi.encodePacked(uint8(Commands.V3_SWAP_EXACT_OUT));
bytes[] memory inputs = new bytes[](1);
// V3 path encoding: [tokenOut, fee, tokenIn] (reversed for exact output)
bytes memory path;
if (zeroForOne) {
path = abi.encodePacked(outputToken, key.fee, inputToken);
} else {
path = abi.encodePacked(inputToken, key.fee, outputToken);
}
inputs[0] = abi.encode(address(this), amountOut, amountInMaximum, path, false);
router.execute(commands, inputs, deadline);
} else {
// --- V4 Swap via Universal Router ---
bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_OUT_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
IV4Router.ExactOutputSingleParams({
poolKey: key,
zeroForOne: zeroForOne,
amountOut: amountOut,
amountInMaximum: amountInMaximum,
hookData: bytes("")
})
);
params[1] = abi.encode(zeroForOne ? key.currency0 : key.currency1, amountInMaximum);
params[2] = abi.encode(zeroForOne ? key.currency1 : key.currency0, amountOut);
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(actions, params);
router.execute(commands, inputs, deadline);
}
}
// Post-router balance (router pulled `used` and minted `max-used` back to us)
uint256 midBalance = IERC20(inputToken).balanceOf(address(this));
// refund = (max + mid - start) / 2 (derivation: mid = start - used + (max - used))
refund = (uint256(amountInMaximum) + midBalance - balanceBefore) / 2;
// Transfer the received output and refund the unused input from the *max* (not the whole balance)
if (unwrapOutput && outputToken == address(weth)) {
weth.withdraw(amountOut);
(bool successOut, ) = recipient.call{value: amountOut}("");
require(successOut, "ETH transfer failed");
} else {
IERC20(outputToken).safeTransfer(recipient, amountOut);
}
if (refund != 0) {
IERC20(inputToken).safeTransfer(recipient, refund);
}
// Actual input used equals start minus end (after refund leaves this contract)
uint256 endBalance = IERC20(inputToken).balanceOf(address(this));
unchecked { amountIn = balanceBefore - endBalance; }
_emitSwapExactOut(inputToken, outputToken, amountIn, amountOut, recipient, refund);
return amountIn;
}
/**
* @notice Swap up to `amountInMaximum` to receive exactly `amountOut` (single v4 pool).
* @param key PoolKey for the v4 pool.
* @param amountOut Exact output to receive.
* @param amountInMaximum Max input allowed.
* @param zeroForOne true: token0->token1, false: token1->token0.
* @param deadline Unix timestamp after which the tx reverts.
* @param recipient Address to receive the output tokens and refunds.
*/
function swapExactOutputSingle(
PoolKey memory key,
uint128 amountOut,
uint128 amountInMaximum,
bool zeroForOne,
bool unwrapOutput,
uint256 deadline,
address recipient
) external nonReentrant returns (uint256 amountIn) {
// Top up only the shortfall up to amountInMaximum (supports pre-funded or pull-from-caller)
address inputToken = zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1);
if (amountInMaximum > 0) {
uint256 bal = IERC20(inputToken).balanceOf(address(this));
if (bal < amountInMaximum) {
uint256 needed = amountInMaximum - bal;
if (IERC20(inputToken).allowance(msg.sender, address(this)) < needed) {
revert InsufficientAllowance();
}
IERC20(inputToken).safeTransferFrom(msg.sender, address(this), needed);
}
}
return _swapExactOutputSingle(key, amountOut, amountInMaximum, zeroForOne, unwrapOutput, deadline, recipient);
}
/// @notice Exact-out using allowlisted canonical pool metadata (no user-supplied tickSpacing/hooks).
function swapExactOutputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint128 amountOut,
uint128 amountInMaximum,
bool unwrapOutput,
uint256 deadline,
address recipient
) external nonReentrant returns (uint256 amountIn) {
(PoolKey memory key, bool zeroForOne) = safePoolKey(tokenIn, tokenOut, fee);
// Top up only the shortfall up to max
if (amountInMaximum > 0) {
uint256 bal = IERC20(tokenIn).balanceOf(address(this));
if (bal < amountInMaximum) {
uint256 needed = amountInMaximum - bal;
if (IERC20(tokenIn).allowance(msg.sender, address(this)) < needed) revert InsufficientAllowance();
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), needed);
}
}
return _swapExactOutputSingle(key, amountOut, amountInMaximum, zeroForOne, unwrapOutput, deadline, recipient);
}
/**
* @notice Swap exact input ETH for >= min output (single v4 pool).
* Wraps ETH to WETH and calls swapExactInputSingle internally.
* @param key PoolKey for the v4 pool.
* @param minAmountOut Minimum acceptable output.
* @param zeroForOne true: token0->token1, false: token1->token0.
* @param deadline Unix timestamp after which the tx reverts.
* @param recipient Address to receive the output tokens.
*/
function swapExactInputSingleETH(
PoolKey memory key,
uint128 minAmountOut,
bool zeroForOne,
bool unwrapOutput,
uint256 deadline,
address recipient
) external payable nonReentrant returns (uint256 amountOut) {
uint128 amountIn = uint128(msg.value);
weth.deposit{value: amountIn}();
// Ensure either Permit2 or Router can pull WETH; if neither, set up Permit2 automatically
uint256 p2AllowW = IERC20(address(weth)).allowance(address(this), address(permit2));
if (p2AllowW < amountIn) {
uint256 rAllowW = IERC20(address(weth)).allowance(address(this), address(router));
if (rAllowW < amountIn) {
_ensurePermit2Approvals(address(weth), amountIn);
_ensureRouterAllowance(address(weth), amountIn);
}
}
amountOut = _swapExactInputSingle(key, amountIn, minAmountOut, zeroForOne, unwrapOutput, deadline, recipient);
}
/**
* @notice Swap up to `amountInMaximum` ETH to receive exactly `amountOut` (single v4 pool).
* Wraps ETH to WETH and calls swapExactOutputSingle internally.
* @param key PoolKey for the v4 pool.
* @param amountOut Exact output to receive.
* @param amountInMaximum Max input allowed.
* @param zeroForOne true: token0->token1, false: token1->token0.
* @param deadline Unix timestamp after which the tx reverts.
* @param recipient Address to receive the output tokens and refunds.
* @param unwrap Whether to unwrap WETH output to ETH.
*/
function swapExactOutputSingleETH(
PoolKey memory key,
uint128 amountOut,
uint128 amountInMaximum,
bool zeroForOne,
uint256 deadline,
address recipient,
bool unwrap
) external payable nonReentrant returns (uint256 amountIn) {
require(msg.value >= amountInMaximum, "Insufficient ETH sent");
weth.deposit{value: amountInMaximum}();
// Ensure either Permit2 or Router can pull up to the max WETH; if neither, set up Permit2 automatically
uint256 p2AllowW = IERC20(address(weth)).allowance(address(this), address(permit2));
if (p2AllowW < amountInMaximum) {
uint256 rAllowW = IERC20(address(weth)).allowance(address(this), address(router));
if (rAllowW < amountInMaximum) {
_ensurePermit2Approvals(address(weth), amountInMaximum);
_ensureRouterAllowance(address(weth), amountInMaximum);
}
}
amountIn = _swapExactOutputSingle(key, amountOut, amountInMaximum, zeroForOne, unwrap, deadline, recipient);
// Refund unused WETH as ETH
uint256 wethBalance = weth.balanceOf(address(this));
if (wethBalance > 0) {
weth.withdraw(wethBalance);
(bool success, ) = recipient.call{value: wethBalance}("");
require(success, "ETH refund transfer failed");
}
// Refund any excess ETH sent
uint256 excess = msg.value - amountInMaximum;
if (excess > 0) {
(bool successExcess, ) = msg.sender.call{value: excess}("");
require(successExcess, "Excess ETH refund failed");
}
}
/**
* @notice Swap exact input with Permit2 signature for token transfer.
* @param key PoolKey for the v4 pool.
* @param amountIn Exact input amount.
* @param minAmountOut Minimum acceptable output.
* @param zeroForOne true: token0->token1, false: token1->token0.
* @param deadline Unix timestamp after which the tx reverts.
* @param recipient Address to receive the output tokens.
* @param permit PermitTransferFrom struct for Permit2.
* @param signature Signature for Permit2 permitTransferFrom.
*/
function swapExactInputSingleWithPermit2(
PoolKey memory key,
uint128 amountIn,
uint128 minAmountOut,
bool zeroForOne,
uint256 deadline,
address recipient,
ISignatureTransfer.PermitTransferFrom calldata permit,
bytes calldata signature
) external nonReentrant returns (uint256 amountOut) {
// Transfer tokens from user to this contract
ISignatureTransfer.SignatureTransferDetails memory details = ISignatureTransfer.SignatureTransferDetails({
to: address(this),
requestedAmount: amountIn
});
ISignatureTransfer(address(permit2)).permitTransferFrom(permit, details, msg.sender, signature);
// Ensure Permit2 approvals exist for router to pull during settle
address inputToken = zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1);
uint256 p2Allow = IERC20(inputToken).allowance(address(this), address(permit2));
if (p2Allow < amountIn) {
uint256 rAllow = IERC20(inputToken).allowance(address(this), address(router));
if (rAllow < amountIn) {
_ensurePermit2Approvals(inputToken, amountIn);
_ensureRouterAllowance(inputToken, amountIn);
}
}
amountOut = _swapExactInputSingle(key, amountIn, minAmountOut, zeroForOne, false, deadline, recipient);
}
/**
* @notice Swap exact output with Permit2 signature for token transfer.
* @param key PoolKey for the v4 pool.
* @param amountOut Exact output to receive.
* @param amountInMaximum Max input allowed.
* @param zeroForOne true: token0->token1, false: token1->token0.
* @param deadline Unix timestamp after which the tx reverts.
* @param recipient Address to receive the output tokens and refunds.
* @param permit PermitTransferFrom struct for Permit2.
* @param signature Signature for Permit2 permitTransferFrom.
*/
function swapExactOutputSingleWithPermit2(
PoolKey memory key,
uint128 amountOut,
uint128 amountInMaximum,
bool zeroForOne,
uint256 deadline,
address recipient,
ISignatureTransfer.PermitTransferFrom calldata permit,
bytes calldata signature
) external nonReentrant returns (uint256 amountIn) {
// Transfer tokens from user to this contract
ISignatureTransfer.SignatureTransferDetails memory details = ISignatureTransfer.SignatureTransferDetails({
to: address(this),
requestedAmount: amountInMaximum
});
ISignatureTransfer(address(permit2)).permitTransferFrom(permit, details, msg.sender, signature);
// Ensure Permit2 approvals exist for router to pull during settle
address inputToken = zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1);
uint256 p2Allow = IERC20(inputToken).allowance(address(this), address(permit2));
if (p2Allow < amountInMaximum) {
uint256 rAllow = IERC20(inputToken).allowance(address(this), address(router));
if (rAllow < amountInMaximum) {
_ensurePermit2Approvals(inputToken, amountInMaximum);
_ensureRouterAllowance(inputToken, amountInMaximum);
}
}
amountIn = _swapExactOutputSingle(key, amountOut, amountInMaximum, zeroForOne, false, deadline, recipient);
}
receive() external payable {}
}
"
},
"lib/universal-router/contracts/interfaces/IUniversalRouter.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;
interface IUniversalRouter {
/// @notice Thrown when a required command has failed
error ExecutionFailed(uint256 commandIndex, bytes message);
/// @notice Thrown when attempting to send ETH directly to the contract
error ETHNotAccepted();
/// @notice Thrown when executing commands with an expired deadline
error TransactionDeadlinePassed();
/// @notice Thrown when attempting to execute commands and an incorrect number of inputs are provided
error LengthMismatch();
// @notice Thrown when an address that isn't WETH tries to send ETH to the router without calldata
error InvalidEthSender();
/// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired.
/// @param commands A set of concatenated commands, each 1 byte in length
/// @param inputs An array of byte strings containing abi encoded inputs for each command
/// @param deadline The deadline by which the transaction must be executed
function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable;
}
"
},
"lib/v4-core/src/interfaces/IPoolManager.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Currency} from "../types/Currency.sol";
import {PoolKey} from "../types/PoolKey.sol";
import {IHooks} from "./IHooks.sol";
import {IERC6909Claims} from "./external/IERC6909Claims.sol";
import {IProtocolFees} from "./IProtocolFees.sol";
import {BalanceDelta} from "../types/BalanceDelta.sol";
import {PoolId} from "../types/PoolId.sol";
import {IExtsload} from "./IExtsload.sol";
import {IExttload} from "./IExttload.sol";
import {ModifyLiquidityParams, SwapParams} from "../types/PoolOperation.sol";
/// @notice Interface for the PoolManager
interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload, IExttload {
/// @notice Thrown when a currency is not netted out after the contract is unlocked
error CurrencyNotSettled();
/// @notice Thrown when trying to interact with a non-initialized pool
error PoolNotInitialized();
/// @notice Thrown when unlock is called, but the contract is already unlocked
error AlreadyUnlocked();
/// @notice Thrown when a function is called that requires the contract to be unlocked, but it is not
error ManagerLocked();
/// @notice Pools are limited to type(int16).max tickSpacing in #initialize, to prevent overflow
error TickSpacingTooLarge(int24 tickSpacing);
/// @notice Pools must have a positive non-zero tickSpacing passed to #initialize
error TickSpacingTooSmall(int24 tickSpacing);
/// @notice PoolKey must have currencies where address(currency0) < address(currency1)
error CurrenciesOutOfOrderOrEqual(address currency0, address currency1);
/// @notice Thrown when a call to updateDynamicLPFee is made by an address that is not the hook,
/// or on a pool that does not have a dynamic swap fee.
error UnauthorizedDynamicLPFeeUpdate();
/// @notice Thrown when trying to swap amount of 0
error SwapAmountCannotBeZero();
///@notice Thrown when native currency is passed to a non native settlement
error NonzeroNativeValue();
/// @notice Thrown when `clear` is called with an amount that is not exactly equal to the open currency delta.
error MustClearExactPositiveDelta();
/// @notice Emitted when a new pool is initialized
/// @param id The abi encoded hash of the pool key struct for the new pool
/// @param currency0 The first currency of the pool by address sort order
/// @param currency1 The second currency of the pool by address sort order
/// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
/// @param tickSpacing The minimum number of ticks between initialized ticks
/// @param hooks The hooks contract address for the pool, or address(0) if none
/// @param sqrtPriceX96 The price of the pool on initialization
/// @param tick The initial tick of the pool corresponding to the initialized price
event Initialize(
PoolId indexed id,
Currency indexed currency0,
Currency indexed currency1,
uint24 fee,
int24 tickSpacing,
IHooks hooks,
uint160 sqrtPriceX96,
int24 tick
);
/// @notice Emitted when a liquidity position is modified
/// @param id The abi encoded hash of the pool key struct for the pool that was modified
/// @param sender The address that modified the pool
/// @param tickLower The lower tick of the position
/// @param tickUpper The upper tick of the position
/// @param liquidityDelta The amount of liquidity that was added or removed
/// @param salt The extra data to make positions unique
event ModifyLiquidity(
PoolId indexed id, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta, bytes32 salt
);
/// @notice Emitted for swaps between currency0 and currency1
/// @param id The abi encoded hash of the pool key struct for the pool that was modified
/// @param sender The address that initiated the swap call, and that received the callback
/// @param amount0 The delta of the currency0 balance of the pool
/// @param amount1 The delta of the currency1 balance of the pool
/// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96
/// @param liquidity The liquidity of the pool after the swap
/// @param tick The log base 1.0001 of the price of the pool after the swap
/// @param fee The swap fee in hundredths of a bip
event Swap(
PoolId indexed id,
address indexed sender,
int128 amount0,
int128 amount1,
uint160 sqrtPriceX96,
uint128 liquidity,
int24 tick,
uint24 fee
);
/// @notice Emitted for donations
/// @param id The abi encoded hash of the pool key struct for the pool that was donated to
/// @param sender The address that initiated the donate call
/// @param amount0 The amount donated in currency0
/// @param amount1 The amount donated in currency1
event Donate(PoolId indexed id, address indexed sender, uint256 amount0, uint256 amount1);
/// @notice All interactions on the contract that account deltas require unlocking. A caller that calls `unlock` must implement
/// `IUnlockCallback(msg.sender).unlockCallback(data)`, where they interact with the remaining functions on this contract.
/// @dev The only functions callable without an unlocking are `initialize` and `updateDynamicLPFee`
/// @param data Any data to pass to the callback, via `IUnlockCallback(msg.sender).unlockCallback(data)`
/// @return The data returned by the call to `IUnlockCallback(msg.sender).unlockCallback(data)`
function unlock(bytes calldata data) external returns (bytes memory);
/// @notice Initialize the state for a given pool ID
/// @dev A swap fee totaling MAX_SWAP_FEE (100%) makes exact output swaps impossible since the input is entirely consumed by the fee
/// @param key The pool key for the pool to initialize
/// @param sqrtPriceX96 The initial square root price
/// @return tick The initial tick of the pool
function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick);
/// @notice Modify the liquidity for the given pool
/// @dev Poke by calling with a zero liquidityDelta
/// @param key The pool to modify liquidity in
/// @param params The parameters for modifying the liquidity
/// @param hookData The data to pass through to the add/removeLiquidity hooks
/// @return callerDelta The balance delta of the caller of modifyLiquidity. This is the total of both principal, fee deltas, and hook deltas if applicable
/// @return feesAccrued The balance delta of the fees generated in the liquidity range. Returned for informational purposes
/// @dev Note that feesAccrued can be artificially inflated by a malicious actor and integrators should be careful using the value
/// For pools with a single liquidity position, actors can donate to themselves to inflate feeGrowthGlobal (and consequently feesAccrued)
/// atomically donating and collecting fees in the same unlockCallback may make the inflated value more extreme
function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata hookData)
external
returns (BalanceDelta callerDelta, BalanceDelta feesAccrued);
/// @notice Swap against the given pool
/// @param key The pool to swap in
/// @param params The parameters for swapping
/// @param hookData The data to pass through to the swap hooks
/// @return swapDelta The balance delta of the address swapping
/// @dev Swapping on low liquidity pools may cause unexpected swap amounts when liquidity available is less than amountSpecified.
/// Additionally note that if interacting with hooks that have the BEFORE_SWAP_RETURNS_DELTA_FLAG or AFTER_SWAP_RETURNS_DELTA_FLAG
/// the hook may alter the swap input/output. Integrators should perform checks on the returned swapDelta.
function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData)
external
returns (BalanceDelta swapDelta);
/// @notice Donate the given currency amounts to the in-range liquidity providers of a pool
/// @dev Calls to donate can be frontrun adding just-in-time liquidity, with the aim of receiving a portion donated funds.
/// Donors should keep this in mind when designing donation mechanisms.
/// @dev This function donates to in-range LPs at slot0.tick. In certain edge-cases of the swap algorithm, the `sqrtPrice` of
/// a pool can be at the lower boundary of tick `n`, but the `slot0.tick` of the pool is already `n - 1`. In this case a call to
/// `donate` would donate to tick `n - 1` (slot0.tick) not tick `n` (getTickAtSqrtPrice(slot0.sqrtPriceX96)).
/// Read the comments in `Pool.swap()` for more information about this.
/// @param key The key of the pool to donate to
/// @param amount0 The amount of currency0 to donate
/// @param amount1 The amount of currency1 to donate
/// @param hookData The data to pass through to the donate hooks
/// @return BalanceDelta The delta of the caller after the donate
function donate(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData)
external
returns (BalanceDelta);
/// @notice Writes the current ERC20 balance of the specified currency to transient storage
/// This is used to checkpoint balances for the manager and derive deltas for the caller.
/// @dev This MUST be called before any ERC20 tokens are sent into the contract, but can be skipped
/// for native tokens because the amount to settle is determined by the sent value.
/// However, if an ERC20 token has been synced and not settled, and the caller instead wants to settle
/// native funds, this function can be called with the native currency to then be able to settle the native currency
function sync(Currency currency) external;
/// @notice Called by the user to net out some value owed to the user
/// @dev Will revert if the requested amount is not available, consider using `mint` instead
/// @dev Can also be used as a mechanism for free flash loans
/// @param currency The currency to withdraw from the pool manager
/// @param to The address to withdraw to
/// @param amount The amount of currency to withdraw
function take(Currency currency, address to, uint256 amount) external;
/// @notice Called by the user to pay what is owed
/// @return paid The amount of currency settled
function settle() external payable returns (uint256 paid);
/// @notice Called by the user to pay on behalf of another address
/// @param recipient The address to credit for the payment
/// @return paid The amount of currency settled
function settleFor(address recipient) external payable returns (uint256 paid);
/// @notice WARNING - Any currency that is cleared, will be non-retrievable, and locked in the contract permanently.
/// A call to clear will zero out a positive balance WITHOUT a corresponding transfer.
/// @dev This could be used to clear a balance that is considered dust.
/// Additionally, the amount must be the exact positive balance. This is to enforce that the caller is aware of the amount being cleared.
function clear(Currency currency, uint256 amount) external;
/// @notice Called by the user to move value into ERC6909 balance
/// @param to The address to mint the tokens to
/// @param id The currency address to mint to ERC6909s, as a uint256
/// @param amount The amount of currency to mint
/// @dev The id is converted to a uint160 to correspond to a currency address
/// If the upper 12 bytes are not 0, they will be 0-ed out
function mint(address to, uint256 id, uint256 amount) external;
/// @notice Called by the user to move value from ERC6909 balance
/// @param from The address to burn the tokens from
/// @param id The currency address to burn from ERC6909s, as a uint256
/// @param amount The amount of currency to burn
/// @dev The id is converted to a uint160 to correspond to a currency address
/// If the upper 12 bytes are not 0, they will be 0-ed out
function burn(address from, uint256 id, uint256 amount) external;
/// @notice Updates the pools lp fees for the a pool that has enabled dynamic lp fees.
/// @dev A swap fee totaling MAX_SWAP_FEE (100%) makes exact output swaps impossible since the input is entirely consumed by the fee
/// @param key The key of the pool to update dynamic LP fees for
/// @param newDynamicLPFee The new dynamic pool LP fee
function updateDynamicLPFee(PoolKey memory key, uint24 newDynamicLPFee) external;
}
"
},
"lib/v4-periphery/src/interfaces/IV4Router.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {PathKey} from "../libraries/PathKey.sol";
import {IImmutableState} from "./IImmutableState.sol";
/// @title IV4Router
/// @notice Interface for the V4Router contract
interface IV4Router is IImmutableState {
/// @notice Emitted when an exactInput swap does not receive its minAmountOut
error V4TooLittleReceived(uint256 minAmountOutReceived, uint256 amountReceived);
/// @notice Emitted when an exactOutput is asked for more than its maxAmountIn
error V4TooMuchRequested(uint256 maxAmountInRequested, uint256 amountRequested);
/// @notice Parameters for a single-hop exact-input swap
struct ExactInputSingleParams {
PoolKey poolKey;
bool zeroForOne;
uint128 amountIn;
uint128 amountOutMinimum;
bytes hookData;
}
/// @notice Parameters for a multi-hop exact-input swap
struct ExactInputParams {
Currency currencyIn;
PathKey[] path;
uint128 amountIn;
uint128 amountOutMinimum;
}
/// @notice Parameters for a single-hop exact-output swap
struct ExactOutputSingleParams {
PoolKey poolKey;
bool zeroForOne;
uint128 amountOut;
uint128 amountInMaximum;
bytes hookData;
}
/// @notice Parameters for a multi-hop exact-output swap
struct ExactOutputParams {
Currency currencyOut;
PathKey[] path;
uint128 amountOut;
uint128 amountInMaximum;
}
}
"
},
"lib/permit2/src/interfaces/IPermit2.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {ISignatureTransfer} from "./ISignatureTransfer.sol";
import {IAllowanceTransfer} from "./IAllowanceTransfer.sol";
/// @notice Permit2 handles signature-based transfers in SignatureTransfer and allowance-based transfers in AllowanceTransfer.
/// @dev Users must approve Permit2 before calling any of the transfer functions.
interface IPermit2 is ISignatureTransfer, IAllowanceTransfer {
// IPermit2 unifies the two interfaces so users have maximal flexibility with their approval.
}
"
},
"lib/permit2/src/interfaces/ISignatureTransfer.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IEIP712} from "./IEIP712.sol";
/// @title SignatureTransfer
/// @notice Handles ERC20 token transfers through signature based actions
/// @dev Requires user's token approval on the Permit2 contract
interface ISignatureTransfer is IEIP712 {
/// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount
/// @param maxAmount The maximum amount a spender can request to transfer
error InvalidAmount(uint256 maxAmount);
/// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred
/// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred
error LengthMismatch();
/// @notice Emits an event when the owner successfully invalidates an unordered nonce.
event UnorderedNonceInvalidation(address indexed owner, uint256 word, uint256 mask);
/// @notice The token and amount details for a transfer signed in the permit transfer signature
struct TokenPermissions {
// ERC20 token address
address token;
// the maximum amount that can be spent
uint256 amount;
}
/// @notice The signed permit message for a single token transfer
struct PermitTransferFrom {
TokenPermissions permitted;
// a unique value for every token owner's signature to prevent signature replays
uint256 nonce;
// deadline on the permit signature
uint256 deadline;
}
/// @notice Specifies the recipient address and amount for batched transfers.
/// @dev Recipients and amounts correspond to the index of the signed token permissions array.
/// @dev Reverts if the requested amount is greater than the permitted signed amount.
struct SignatureTransferDetails {
// recipient address
address to;
// spender requested amount
uint256 requestedAmount;
}
/// @notice Used to reconstruct the signed permit message for multiple token transfers
/// @dev Do not need to pass in spender address as it is required that it is msg.sender
/// @dev Note that a user still signs over a spender address
struct PermitBatchTransferFrom {
// the tokens and corresponding amounts permitted for a transfer
TokenPermissions[] permitted;
// a unique value for every token owner's signature to prevent signature replays
uint256 nonce;
// deadline on the permit signature
uint256 deadline;
}
/// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection
/// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order
/// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce
/// @dev It returns a uint256 bitmap
/// @dev The index, or wordPosition is capped at type(uint248).max
function nonceBitmap(address, uint256) external view returns (uint256);
/// @notice Transfers a token using a signed permit message
/// @dev Reverts if the requested amount is greater than the permitted signed amount
/// @param permit The permit data signed over by the owner
/// @param owner The owner of the tokens to transfer
/// @param transferDetails The spender's requested transfer details for the permitted token
/// @param signature The signature to verify
function permitTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes calldata signature
) external;
/// @notice Transfers a token using a signed permit message
/// @notice Includes extra data provided by the caller to verify signature over
/// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition
/// @dev Reverts if the requested amount is greater than the permitted signed amount
/// @param permit The permit data signed over by the owner
/// @param owner The owner of the tokens to transfer
/// @param transferDetails The spender's requested transfer details for the permitted token
/// @param witness Extra data to include when checking the user signature
/// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash
/// @param signature The signature to verify
function permitWitnessTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes32 witness,
string calldata witnessTypeString,
bytes calldata signature
) external;
/// @notice Transfers multiple tokens using a signed permit message
/// @param permit The permit data signed over by the owner
/// @param owner The owner of the tokens to transfer
/// @param transferDetails Specifies the recipient and requested amount for the token transfer
/// @param signature The signature to verify
function permitTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes calldata signature
) external;
/// @notice Transfers multiple tokens using a signed permit message
/// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition
/// @notice Includes extra data provided by the caller to verify signature over
/// @param permit The permit data signed over by the owner
/// @param owner The owner of the tokens to transfer
/// @param transferDetails Specifies the recipient and requested amount for the token transfer
/// @param witness Extra data to include when checking the user signature
/// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash
/// @param signature The signature to verify
function permitWitnessTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes32 witness,
string calldata witnessTypeString,
bytes calldata signature
) external;
/// @notice Invalidates the bits specified in mask for the bitmap at the word position
/// @dev The wordPos is maxed at type(uint248).max
/// @param wordPos A number to index the nonceBitmap at
/// @param mask A bitmap masked against msg.sender's current bitmap at the word position
function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external;
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)
pragma solidity >=0.4.16;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC-20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
/**
* @dev An operation with an ERC-20 token failed.
*/
error SafeERC20FailedOperation(address token);
/**
* @dev Indicates a failed `decreaseAllowance` request.
*/
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
if (!_safeTransfer(token, to, value, true)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
if (!_safeTransferFrom(token, from, to, value, true)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
*/
function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
return _safeTransfer(token, to, value, false);
}
/**
* @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
*/
function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
return _safeTransferFrom(token, from, to, value, false);
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
forceApprove(token, spender, oldAllowance + value);
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
* value, non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
unchecked {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance < requestedDecrease) {
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
forceApprove(token, spender, currentAllowance - requestedDecrease);
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*
* NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
* only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
* set here.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
if (!_safeApprove(token, spender, value, false)) {
if (!_safeApprove(token, spender, 0, true)) revert SafeERC20FailedOperation(address(token));
if (!_safeApprove(token, spender, value, true)) revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that relies on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
safeTransfer(token, to, value);
} else if (!token.transferAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
* has no code. This can be used to implement an {ERC721}-like safe transfer that relies on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferFromAndCallRelaxed(
IERC1363 token,
address from,
address to,
uint256 value,
bytes memory data
) internal {
if (to.code.length == 0) {
safeTransferFrom(token, from, to, value);
} else if (!token.transferFromAndCall(from, to, value, data)) {
revert SafeERC20FailedOperation(address(token));
Submitted on: 2025-10-24 11:15:49
Comments
Log in to comment.
No comments yet.