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
58 changes: 56 additions & 2 deletions contracts/dex-periphery/base/PoolInitializer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,35 @@ import '../interfaces/IPoolInitializer.sol';
import './PeripheryImmutableState.sol';

/// @title Creates and initializes V3 Pools
/// @notice Provides input-validated pool creation and initialization for DEX-223.
/// @dev All token addresses and the initial price are validated before any
/// external call is made to the factory or pool contracts.
abstract contract PoolInitializer is IPoolInitializer, PeripheryImmutableState {

/// @notice Emitted when a pool is created and/or initialized through this contract
/// @param pool The address of the pool
/// @param token0_20 The ERC-20 address of token0
/// @param token1_20 The ERC-20 address of token1
/// @param fee The pool fee tier
/// @param sqrtPriceX96 The initial sqrt price (only meaningful when the pool was initialized)
/// @param created True if the pool was newly created, false if it already existed
/// @param initialized True if the pool was initialized in this call
event PoolCreatedAndInitialized(
address indexed pool,
address indexed token0_20,
address indexed token1_20,
uint24 fee,
uint160 sqrtPriceX96,
bool created,
bool initialized
);

/// @inheritdoc IPoolInitializer
/// @dev Validates all inputs before making external calls:
/// - Token addresses must not be address(0).
/// - ERC-20 token0 must sort before token1 (standard Uniswap ordering).
/// - ERC-223 addresses must not be address(0) (prevents silent misconfiguration).
/// - sqrtPriceX96 must be non-zero (a zero value is an invalid Q64.96 price).
function createAndInitializePoolIfNecessary(
address token0_20,
address token1_20,
Expand All @@ -18,17 +45,44 @@ abstract contract PoolInitializer is IPoolInitializer, PeripheryImmutableState {
uint24 fee,
uint160 sqrtPriceX96
) external payable override returns (address pool) {
require(token0_20 < token1_20);
// --- Input validation ------------------------------------------------

// V1: Zero-address checks for all token parameters
require(token0_20 != address(0), 'PI: ZERO_TOKEN0_20');
require(token1_20 != address(0), 'PI: ZERO_TOKEN1_20');
require(token0_223 != address(0), 'PI: ZERO_TOKEN0_223');
require(token1_223 != address(0), 'PI: ZERO_TOKEN1_223');

// V2: Canonical ordering – token0 must sort before token1
require(token0_20 < token1_20, 'PI: TOKEN_ORDER');

// V3: Initial price must be valid (zero is not a legal Q64.96 price)
require(sqrtPriceX96 > 0, 'PI: ZERO_PRICE');

// --- Pool lookup / creation ------------------------------------------

pool = IDex223Factory(factory).getPool(token0_20, token1_20, fee);

bool created;
bool initialized;

if (pool == address(0)) {
pool = IDex223Factory(factory).createPool(token0_20, token1_20, token0_223, token1_223, fee);
pool = IDex223Factory(factory).createPool(
token0_20, token1_20, token0_223, token1_223, fee
);
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
created = true;
initialized = true;
} else {
(uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0();
if (sqrtPriceX96Existing == 0) {
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
initialized = true;
}
}

emit PoolCreatedAndInitialized(
pool, token0_20, token1_20, fee, sqrtPriceX96, created, initialized
);
}
}
89 changes: 68 additions & 21 deletions contracts/dex-periphery/base/PoolTicksCounter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ pragma solidity >=0.6.0;

import '../../interfaces/IUniswapV3Pool.sol';

/// @title PoolTicksCounter
/// @notice Counts the number of initialized ticks crossed during a swap
/// @dev Security-hardened version addressing underflow, negative-modulo,
/// truncation, and gas-efficiency issues present in the original.
library PoolTicksCounter {
/// @dev This function counts the number of initialized ticks that would incur a gas cost between tickBefore and tickAfter.
/// When tickBefore and/or tickAfter themselves are initialized, the logic over whether we should count them depends on the
/// direction of the swap. If we are swapping upwards (tickAfter > tickBefore) we don't want to count tickBefore but we do
/// want to count tickAfter. The opposite is true if we are swapping downwards.
/// @dev This function counts the number of initialized ticks that would incur a gas cost between tickBefore
/// and tickAfter. When tickBefore and/or tickAfter themselves are initialized, the logic over whether
/// we should count them depends on the direction of the swap. If we are swapping upwards
/// (tickAfter > tickBefore) we don't want to count tickBefore but we do want to count tickAfter.
/// The opposite is true if we are swapping downwards.
function countInitializedTicksCrossed(
IUniswapV3Pool self,
int24 tickBefore,
Expand All @@ -21,27 +26,36 @@ library PoolTicksCounter {
bool tickAfterInitialized;

{
// Get the key and offset in the tick bitmap of the active tick before and after the swap.
int16 wordPos = int16((tickBefore / self.tickSpacing()) >> 8);
uint8 bitPos = uint8((tickBefore / self.tickSpacing()) % 256);
// [FIX V-06] Cache tickSpacing to avoid redundant external STATICCALL calls
int24 spacing = self.tickSpacing();

int16 wordPosAfter = int16((tickAfter / self.tickSpacing()) >> 8);
uint8 bitPosAfter = uint8((tickAfter / self.tickSpacing()) % 256);
// [FIX V-02] Use a helper that floors the division for negative values,
// then compute wordPos and bitPos safely. In Solidity <0.8, signed modulo
// preserves the sign of the dividend (e.g., (-5) % 256 == -5), which produces
// incorrect bitPos when cast to uint8. We use _compress() to get a
// floor-divided compressed tick and _position() to safely split it.
int24 compressedBefore = _compress(tickBefore, spacing);
int24 compressedAfter = _compress(tickAfter, spacing);

// In the case where tickAfter is initialized, we only want to count it if we are swapping downwards.
// If the initializable tick after the swap is initialized, our original tickAfter is a
// multiple of tick spacing, and we are swapping downwards we know that tickAfter is initialized
// and we shouldn't count it.
// [FIX V-03] Safe wordPos/bitPos computation that avoids int16 truncation
(int16 wordPos, uint8 bitPos) = _position(compressedBefore);
(int16 wordPosAfter, uint8 bitPosAfter) = _position(compressedAfter);

// In the case where tickAfter is initialized, we only want to count it if we are
// swapping downwards. If the initializable tick after the swap is initialized, our
// original tickAfter is a multiple of tick spacing, and we are swapping downwards we
// know that tickAfter is initialized and we shouldn't count it.
tickAfterInitialized =
((self.tickBitmap(wordPosAfter) & (1 << bitPosAfter)) > 0) &&
((tickAfter % self.tickSpacing()) == 0) &&
((tickAfter % spacing) == 0) &&
(tickBefore > tickAfter);

// In the case where tickBefore is initialized, we only want to count it if we are swapping upwards.
// Use the same logic as above to decide whether we should count tickBefore or not.
// In the case where tickBefore is initialized, we only want to count it if we are
// swapping upwards. Use the same logic as above to decide whether we should count
// tickBefore or not.
tickBeforeInitialized =
((self.tickBitmap(wordPos) & (1 << bitPos)) > 0) &&
((tickBefore % self.tickSpacing()) == 0) &&
((tickBefore % spacing) == 0) &&
(tickBefore < tickAfter);

if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) {
Expand All @@ -61,30 +75,63 @@ library PoolTicksCounter {
// Our first mask should include the lower tick and everything to its left.
uint256 mask = type(uint256).max << bitPosLower;
while (wordPosLower <= wordPosHigher) {
// If we're on the final tick bitmap page, ensure we only count up to our
// ending tick.
// If we're on the final tick bitmap page, ensure we only count up to our ending tick.
if (wordPosLower == wordPosHigher) {
mask = mask & (type(uint256).max >> (255 - bitPosHigher));
}

uint256 masked = self.tickBitmap(wordPosLower) & mask;
initializedTicksCrossed += countOneBits(masked);

// [FIX V-04] Prevent int16 overflow on increment: break before incrementing past
// wordPosHigher or reaching int16 max, which would wrap to -32768 causing an
// infinite loop in Solidity <0.8 (no overflow checks).
if (wordPosLower == wordPosHigher) break;
wordPosLower++;

// Reset our mask so we consider all bits on the next iteration.
mask = type(uint256).max;
}

if (tickAfterInitialized) {
// [FIX V-01] Guard against uint32 underflow. In Solidity <0.8, unsigned subtraction
// silently wraps: 0 - 1 == type(uint32).max. This can happen if the bitmap iteration
// counted no initialized ticks but the boundary tick checks set the initialized flags.
if (tickAfterInitialized && initializedTicksCrossed > 0) {
initializedTicksCrossed -= 1;
}

if (tickBeforeInitialized) {
if (tickBeforeInitialized && initializedTicksCrossed > 0) {
initializedTicksCrossed -= 1;
}

return initializedTicksCrossed;
}

/// @dev Compresses a tick by the spacing using floor division.
/// Standard Solidity signed division truncates towards zero, but for bitmap
/// addressing we need floor division (towards negative infinity).
/// Example: tick = -5, spacing = 10 => Solidity gives 0, but floor gives -1.
function _compress(int24 tick, int24 spacing) private pure returns (int24) {
int24 compressed = tick / spacing;
// Round towards negative infinity if tick is negative and not evenly divisible
if (tick < 0 && tick % spacing != 0) {
compressed--;
}
return compressed;
}

/// @dev Safely splits a compressed tick into its bitmap word position and bit position.
/// Uses floor-division-aware arithmetic so that negative compressed ticks produce
/// correct (non-negative) bitPos values.
function _position(int24 compressed) private pure returns (int16 wordPos, uint8 bitPos) {
// Arithmetic right-shift by 8 is equivalent to floor division by 256 for signed ints
wordPos = int16(compressed >> 8);
// For bitPos: compressed & 0xFF always yields 0..255 regardless of sign,
// because bitwise-AND on two's complement extracts the low 8 bits correctly.
bitPos = uint8(uint24(compressed) & 0xFF);
}

/// @dev Counts set bits using the Kernighan algorithm.
function countOneBits(uint256 x) private pure returns (uint16) {
uint16 bits = 0;
while (x != 0) {
Expand Down
18 changes: 18 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,24 @@ const config: HardhatUserConfig = {
}
}
},
"contracts/dex-periphery/Revenue_old.sol": {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 5000,
}
}
},
"contracts/dex-periphery/RevenueV1.sol": {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 5000,
}
}
},
}
},

Expand Down