Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/PositionDescriptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {SafeCurrencyMetadata} from "./libraries/SafeCurrencyMetadata.sol";
contract PositionDescriptor is IPositionDescriptor {
using StateLibrary for IPoolManager;

/// @notice Thrown when a zero address is provided for a critical parameter
error ZeroAddress(string parameter);

// mainnet addresses
address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address private constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
Expand All @@ -31,6 +34,10 @@ contract PositionDescriptor is IPositionDescriptor {
IPoolManager public immutable poolManager;

constructor(IPoolManager _poolManager, address _wrappedNative, bytes32 _nativeCurrencyLabelBytes) {
// Zero address validation for critical parameters
if (address(_poolManager) == address(0)) revert ZeroAddress("poolManager");
if (_wrappedNative == address(0)) revert ZeroAddress("wrappedNative");

poolManager = _poolManager;
wrappedNative = _wrappedNative;
nativeCurrencyLabelBytes = _nativeCurrencyLabelBytes;
Expand Down
69 changes: 66 additions & 3 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {PositionInfo, PositionInfoLibrary} from "./libraries/PositionInfoLibrary
import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol";
import {NativeWrapper} from "./base/NativeWrapper.sol";
import {IWETH9} from "./interfaces/external/IWETH9.sol";
import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol";

// 444444444
// 444444444444 444444
Expand Down Expand Up @@ -60,7 +61,7 @@ import {IWETH9} from "./interfaces/external/IWETH9.sol";
// 4444 44 44444 44444444444 444444444444444444444 44444444
// 44444 4444 4444444444 444444444444444444444444 44444
// 44444 44444 444 444444 4444444444444444444444444 44444
// 4444 44 44 4 44444444444444444444444444 444 44444
// 4444 44 44 4 44444444444444444444444444 444 444444
// 44444444 444 44 4 4 444444 4 44444444444444444444444444444 4444444
// 444444 44 44444444444 44444444444444 444444444444444 444444
// 444444 44 4444 44444 44 44444444444444444444444 4444444 44444
Expand Down Expand Up @@ -115,6 +116,28 @@ contract PositionManager is
using SafeCast for int256;
using CalldataDecoder for bytes;
using SlippageCheck for BalanceDelta;
using CustomRevert for bytes4;

/// @notice Thrown when a zero address is provided for a critical parameter
error ZeroAddress(string parameter);

/// @notice Thrown when token ID would overflow
error TokenIdOverflow();

/// @notice Thrown when the from address is incorrect
error WrongFrom();

/// @notice Thrown when recipient is zero address
error InvalidRecipient();

/// @notice Thrown when caller is not authorized for token transfer
error NotAuthorized();

/// @notice Thrown when balance would underflow
error InsufficientBalance();

/// @notice Thrown when balance would overflow
error BalanceOverflow();

/// @inheritdoc IPositionManager
/// @dev The ID of the next token that will be minted. Skips 0
Expand All @@ -138,6 +161,12 @@ contract PositionManager is
Notifier(_unsubscribeGasLimit)
NativeWrapper(_weth9)
{
// Comprehensive zero address validation for critical parameters
if (address(_poolManager) == address(0)) revert ZeroAddress("poolManager");
if (address(_permit2) == address(0)) revert ZeroAddress("permit2");
if (address(_tokenDescriptor) == address(0)) revert ZeroAddress("tokenDescriptor");
if (address(_weth9) == address(0)) revert ZeroAddress("weth9");

tokenDescriptor = _tokenDescriptor;
}

Expand Down Expand Up @@ -357,6 +386,9 @@ contract PositionManager is
) internal {
// mint receipt token
uint256 tokenId;
// Add overflow protection for nextTokenId
if (nextTokenId >= type(uint256).max) revert TokenIdOverflow();

// tokenId is assigned to current nextTokenId before incrementing it
unchecked {
tokenId = nextTokenId++;
Expand Down Expand Up @@ -532,9 +564,40 @@ contract PositionManager is
}

/// @dev overrides solmate transferFrom in case a notification to subscribers is needed
/// @dev will revert if pool manager is locked
/// @dev will revert if pool manager is unlocked
/// @dev requires proper authorization: caller must be owner, approved for all, or approved for specific token
function transferFrom(address from, address to, uint256 id) public virtual override onlyIfPoolManagerLocked {
super.transferFrom(from, to, id);
// Get the correct caller context (locker when pool is locked, msg.sender otherwise)
address caller = msgSender();

// Perform the same authorization checks as the parent ERC721 contract
// but using the correct caller context for the locked pool manager scenario
if (from != _ownerOf[id]) WrongFrom.selector.revertWith();
if (to == address(0)) InvalidRecipient.selector.revertWith();

// Critical authorization check: caller must be authorized to transfer this token
if (!(caller == from || isApprovedForAll[from][caller] || caller == getApproved[id])) {
NotAuthorized.selector.revertWith();
}

// Replicate the transfer logic from solmate ERC721 to avoid parent authorization conflicts
// Add validation to prevent underflow/overflow in balance updates
if (_balanceOf[from] == 0) InsufficientBalance.selector.revertWith();
if (_balanceOf[to] >= type(uint256).max) BalanceOverflow.selector.revertWith();

// Underflow of the sender's balance is impossible because we check for
// ownership above and the recipient's balance can't realistically overflow.
unchecked {
_balanceOf[from]--;
_balanceOf[to]++;
}

_ownerOf[id] = to;
delete getApproved[id];

emit Transfer(from, to, id);

// Handle subscriber notifications after successful transfer
if (positionInfo[id].hasSubscriber()) _unsubscribe(id);
}

Expand Down
44 changes: 38 additions & 6 deletions src/UniswapV4DeployerCompetition.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ pragma solidity 0.8.26;
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {VanityAddressLib} from "./libraries/VanityAddressLib.sol";
import {IUniswapV4DeployerCompetition} from "./interfaces/IUniswapV4DeployerCompetition.sol";
import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol";

/// @title UniswapV4DeployerCompetition
/// @notice A contract to crowdsource a salt for the best Uniswap V4 address
contract UniswapV4DeployerCompetition is IUniswapV4DeployerCompetition {
using VanityAddressLib for address;
using CustomRevert for bytes4;

/// @dev The salt for the best address found so far
bytes32 public bestAddressSalt;
Expand All @@ -26,6 +28,17 @@ contract UniswapV4DeployerCompetition is IUniswapV4DeployerCompetition {
/// @dev The deadline for exclusive deployment by deployer after deadline
uint256 public immutable exclusiveDeployDeadline;

/// @dev Rate limiting: track last submission time per address
mapping(address => uint256) public lastSubmissionTime;
/// @dev Minimum time between submissions per address (prevents spam)
uint256 public constant SUBMISSION_COOLDOWN = 60; // 1 minute

/// @notice Thrown when trying to submit too frequently
error SubmissionTooFrequent(address sender, uint256 lastTime, uint256 currentTime);

/// @notice Thrown when zero address salt bypass is attempted
error InvalidZeroAddressSalt();

constructor(
bytes32 _initCodeHash,
uint256 _competitionDeadline,
Expand All @@ -41,16 +54,35 @@ contract UniswapV4DeployerCompetition is IUniswapV4DeployerCompetition {
/// @inheritdoc IUniswapV4DeployerCompetition
function updateBestAddress(bytes32 salt) external {
if (block.timestamp > competitionDeadline) {
revert CompetitionOver(block.timestamp, competitionDeadline);
CompetitionOver.selector.revertWith(block.timestamp, competitionDeadline);
}

// Enhanced rate limiting to prevent spam attacks
uint256 lastTime = lastSubmissionTime[msg.sender];
if (lastTime != 0 && block.timestamp - lastTime < SUBMISSION_COOLDOWN) {
SubmissionTooFrequent.selector.revertWith(msg.sender, lastTime, block.timestamp);
}
lastSubmissionTime[msg.sender] = block.timestamp;

// Enhanced salt validation - fix critical access control bypass
address saltSubAddress = address(bytes20(salt));
if (saltSubAddress != msg.sender && saltSubAddress != address(0)) revert InvalidSender(salt, msg.sender);

// CRITICAL FIX: Prevent zero address bypass vulnerability
// The original logic allowed anyone to submit if saltSubAddress == address(0)
// This was a critical access control bypass
if (saltSubAddress == address(0)) {
InvalidZeroAddressSalt.selector.revertWith();
}

// Now properly validate that the sender matches the salt sub-address
if (saltSubAddress != msg.sender) {
InvalidSender.selector.revertWith(salt, msg.sender);
}

address newAddress = Create2.computeAddress(salt, initCodeHash);
address _bestAddress = bestAddress();
if (!newAddress.betterThan(_bestAddress)) {
revert WorseAddress(newAddress, _bestAddress, newAddress.score(), _bestAddress.score());
WorseAddress.selector.revertWith(newAddress, _bestAddress, newAddress.score(), _bestAddress.score());
}

bestAddressSalt = salt;
Expand All @@ -62,16 +94,16 @@ contract UniswapV4DeployerCompetition is IUniswapV4DeployerCompetition {
/// @inheritdoc IUniswapV4DeployerCompetition
function deploy(bytes memory bytecode) external {
if (keccak256(bytecode) != initCodeHash) {
revert InvalidBytecode();
InvalidBytecode.selector.revertWith();
}

if (block.timestamp <= competitionDeadline) {
revert CompetitionNotOver(block.timestamp, competitionDeadline);
CompetitionNotOver.selector.revertWith(block.timestamp, competitionDeadline);
}

if (msg.sender != deployer && block.timestamp <= exclusiveDeployDeadline) {
// anyone can deploy after the deadline
revert NotAllowedToDeploy(msg.sender, deployer);
NotAllowedToDeploy.selector.revertWith(msg.sender, deployer);
}

// the owner of the contract must be encoded in the bytecode
Expand Down
68 changes: 43 additions & 25 deletions src/V4Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol";

import {PathKey} from "./libraries/PathKey.sol";
import {CalldataDecoder} from "./libraries/CalldataDecoder.sol";
Expand All @@ -26,6 +27,13 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
using SafeCast for *;
using CalldataDecoder for bytes;
using BipsLibrary for uint256;
using CustomRevert for bytes4;

/// @notice Thrown when swap path is empty
error EmptyPath();

/// @notice Thrown when swap path exceeds maximum allowed length
error PathTooLong();

constructor(IPoolManager _poolManager) BaseActionsRouter(_poolManager) {}

Expand Down Expand Up @@ -76,7 +84,7 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
return;
}
}
revert UnsupportedAction(action);
UnsupportedAction.selector.revertWith(action);
}

function _swapExactInputSingle(IV4Router.ExactInputSingleParams calldata params) private {
Expand All @@ -87,19 +95,24 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
}
uint128 amountOut =
_swap(params.poolKey, params.zeroForOne, -int256(uint256(amountIn)), params.hookData).toUint128();
if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(params.amountOutMinimum, amountOut);
if (amountOut < params.amountOutMinimum) V4TooLittleReceived.selector.revertWith(params.amountOutMinimum, amountOut);
}

function _swapExactInput(IV4Router.ExactInputParams calldata params) private {
unchecked {
// Caching for gas savings
uint256 pathLength = params.path.length;
uint128 amountOut;
Currency currencyIn = params.currencyIn;
uint128 amountIn = params.amountIn;
if (amountIn == ActionConstants.OPEN_DELTA) amountIn = _getFullCredit(currencyIn).toUint128();
PathKey calldata pathKey;
// Caching for gas savings
uint256 pathLength = params.path.length;

// Add protection against excessive path lengths that could cause issues
if (pathLength == 0) revert EmptyPath();
if (pathLength > 256) revert PathTooLong(); // Reasonable upper bound

uint128 amountOut;
Currency currencyIn = params.currencyIn;
uint128 amountIn = params.amountIn;
if (amountIn == ActionConstants.OPEN_DELTA) amountIn = _getFullCredit(currencyIn).toUint128();
PathKey calldata pathKey;

unchecked {
for (uint256 i = 0; i < pathLength; i++) {
pathKey = params.path[i];
(PoolKey memory poolKey, bool zeroForOne) = pathKey.getPoolAndSwapDirection(currencyIn);
Expand All @@ -109,9 +122,9 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
amountIn = amountOut;
currencyIn = pathKey.intermediateCurrency;
}

if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(params.amountOutMinimum, amountOut);
}

if (amountOut < params.amountOutMinimum) V4TooLittleReceived.selector.revertWith(params.amountOutMinimum, amountOut);
}

function _swapExactOutputSingle(IV4Router.ExactOutputSingleParams calldata params) private {
Expand All @@ -123,22 +136,27 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
uint128 amountIn = (
uint256(-int256(_swap(params.poolKey, params.zeroForOne, int256(uint256(amountOut)), params.hookData)))
).toUint128();
if (amountIn > params.amountInMaximum) revert V4TooMuchRequested(params.amountInMaximum, amountIn);
if (amountIn > params.amountInMaximum) V4TooMuchRequested.selector.revertWith(params.amountInMaximum, amountIn);
}

function _swapExactOutput(IV4Router.ExactOutputParams calldata params) private {
unchecked {
// Caching for gas savings
uint256 pathLength = params.path.length;
uint128 amountIn;
uint128 amountOut = params.amountOut;
Currency currencyOut = params.currencyOut;
PathKey calldata pathKey;

if (amountOut == ActionConstants.OPEN_DELTA) {
amountOut = _getFullDebt(currencyOut).toUint128();
}
// Caching for gas savings
uint256 pathLength = params.path.length;

// Add protection against excessive path lengths that could cause issues
if (pathLength == 0) revert EmptyPath();
if (pathLength > 256) revert PathTooLong(); // Reasonable upper bound

uint128 amountIn;
uint128 amountOut = params.amountOut;
Currency currencyOut = params.currencyOut;
PathKey calldata pathKey;

if (amountOut == ActionConstants.OPEN_DELTA) {
amountOut = _getFullDebt(currencyOut).toUint128();
}

unchecked {
for (uint256 i = pathLength; i > 0; i--) {
pathKey = params.path[i - 1];
(PoolKey memory poolKey, bool oneForZero) = pathKey.getPoolAndSwapDirection(currencyOut);
Expand All @@ -149,8 +167,8 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
amountOut = amountIn;
currencyOut = pathKey.intermediateCurrency;
}
if (amountIn > params.amountInMaximum) revert V4TooMuchRequested(params.amountInMaximum, amountIn);
}
if (amountIn > params.amountInMaximum) V4TooMuchRequested.selector.revertWith(params.amountInMaximum, amountIn);
}

function _swap(PoolKey memory poolKey, bool zeroForOne, int256 amountSpecified, bytes calldata hookData)
Expand Down
4 changes: 4 additions & 0 deletions src/base/ImmutableState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ contract ImmutableState is IImmutableState {

/// @notice Thrown when the caller is not PoolManager
error NotPoolManager();

/// @notice Thrown when a zero address is provided for a critical parameter
error ZeroAddress(string parameter);

/// @notice Only allow calls from the PoolManager contract
modifier onlyPoolManager() {
Expand All @@ -20,6 +23,7 @@ contract ImmutableState is IImmutableState {
}

constructor(IPoolManager _poolManager) {
if (address(_poolManager) == address(0)) revert ZeroAddress("poolManager");
poolManager = _poolManager;
}
}
Loading