Completed clean root directory structure: - Root now contains only: .git, .env, docs/, orig/ - Moved all remaining files and directories to orig/: - Config files (.claude, .dockerignore, .drone.yml, etc.) - All .env variants (except active .env) - Git config (.gitconfig, .github, .gitignore, etc.) - Tool configs (.golangci.yml, .revive.toml, etc.) - Documentation (*.md files, @prompts) - Build files (Dockerfiles, Makefile, go.mod, go.sum) - Docker compose files - All source directories (scripts, tests, tools, etc.) - Runtime directories (logs, monitoring, reports) - Dependency files (node_modules, lib, cache) - Special files (--delete) - Removed empty runtime directories (bin/, data/) V2 structure is now clean: - docs/planning/ - V2 planning documents - orig/ - Complete V1 codebase preserved - .env - Active environment config (not in git) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
259 lines
7.9 KiB
Solidity
259 lines
7.9 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
/**
|
|
* @title PoolDetector
|
|
* @notice Efficient onchain pool detection for multiple DEX types
|
|
* @dev Gas-optimized detection using assembly and batched calls
|
|
*/
|
|
contract PoolDetector {
|
|
enum PoolType {
|
|
Unknown,
|
|
UniswapV2,
|
|
UniswapV3,
|
|
AlgebraV19,
|
|
AlgebraIntegral,
|
|
Balancer,
|
|
Curve,
|
|
Camelot,
|
|
SushiswapV2,
|
|
PancakeV3,
|
|
KyberSwap
|
|
}
|
|
|
|
struct PoolInfo {
|
|
address pool;
|
|
PoolType poolType;
|
|
address token0;
|
|
address token1;
|
|
uint256 fee;
|
|
bool isValid;
|
|
uint8 confidence;
|
|
}
|
|
|
|
/**
|
|
* @notice Detect pool type for a single address
|
|
* @param pool The pool address to identify
|
|
* @return info The pool information
|
|
*/
|
|
function detectPool(address pool) external view returns (PoolInfo memory info) {
|
|
info.pool = pool;
|
|
|
|
// Check if contract exists
|
|
uint256 codeSize;
|
|
assembly {
|
|
codeSize := extcodesize(pool)
|
|
}
|
|
|
|
if (codeSize == 0) {
|
|
info.isValid = false;
|
|
return info;
|
|
}
|
|
|
|
// Try to get token0 and token1 (common to all pools)
|
|
(bool hasToken0, address token0) = _tryToken0(pool);
|
|
(bool hasToken1, address token1) = _tryToken1(pool);
|
|
|
|
if (!hasToken0 || !hasToken1) {
|
|
info.isValid = false;
|
|
return info;
|
|
}
|
|
|
|
info.token0 = token0;
|
|
info.token1 = token1;
|
|
info.isValid = true;
|
|
|
|
// Try UniswapV3 specific methods
|
|
if (_isUniswapV3(pool)) {
|
|
info.poolType = PoolType.UniswapV3;
|
|
info.confidence = 95;
|
|
(bool hasFee, uint256 fee) = _tryFeeV3(pool);
|
|
if (hasFee) info.fee = fee;
|
|
return info;
|
|
}
|
|
|
|
// Try Algebra/Camelot (has globalState instead of slot0)
|
|
if (_isAlgebra(pool)) {
|
|
// Check for directional fees (Integral)
|
|
if (_hasDirectionalFees(pool)) {
|
|
info.poolType = PoolType.AlgebraIntegral;
|
|
info.confidence = 90;
|
|
} else {
|
|
info.poolType = PoolType.AlgebraV19;
|
|
info.confidence = 85;
|
|
}
|
|
return info;
|
|
}
|
|
|
|
// Try UniswapV2/Sushiswap
|
|
if (_isUniswapV2(pool)) {
|
|
// Distinguish by factory
|
|
address factory = _getFactory(pool);
|
|
if (factory == 0xc35DADB65012eC5796536bD9864eD8773aBc74C4) {
|
|
info.poolType = PoolType.SushiswapV2;
|
|
} else {
|
|
info.poolType = PoolType.UniswapV2;
|
|
}
|
|
info.confidence = 80;
|
|
return info;
|
|
}
|
|
|
|
// Unknown but valid pool
|
|
info.poolType = PoolType.Unknown;
|
|
info.confidence = 30;
|
|
}
|
|
|
|
/**
|
|
* @notice Batch detect multiple pools
|
|
* @param pools Array of pool addresses
|
|
* @return infos Array of pool information
|
|
*/
|
|
function detectPools(address[] calldata pools)
|
|
external
|
|
view
|
|
returns (PoolInfo[] memory infos)
|
|
{
|
|
infos = new PoolInfo[](pools.length);
|
|
for (uint256 i = 0; i < pools.length; i++) {
|
|
infos[i] = this.detectPool(pools[i]);
|
|
}
|
|
}
|
|
|
|
// Internal detection functions
|
|
|
|
function _isUniswapV3(address pool) private view returns (bool) {
|
|
// Check for slot0, fee, tickSpacing, maxLiquidityPerTick
|
|
(bool hasSlot0,) = _trySlot0(pool);
|
|
(bool hasFee,) = _tryFeeV3(pool);
|
|
(bool hasTickSpacing,) = _tryTickSpacing(pool);
|
|
(bool hasMaxLiquidity,) = _tryMaxLiquidityPerTick(pool);
|
|
|
|
return hasSlot0 && hasFee && hasTickSpacing && hasMaxLiquidity;
|
|
}
|
|
|
|
function _isAlgebra(address pool) private view returns (bool) {
|
|
// Has globalState instead of slot0
|
|
(bool hasGlobalState,) = _tryGlobalState(pool);
|
|
(bool hasSlot0,) = _trySlot0(pool);
|
|
|
|
return hasGlobalState && !hasSlot0;
|
|
}
|
|
|
|
function _isUniswapV2(address pool) private view returns (bool) {
|
|
// Has getReserves, no slot0 or globalState
|
|
(bool hasReserves,) = _tryGetReserves(pool);
|
|
(bool hasSlot0,) = _trySlot0(pool);
|
|
(bool hasGlobalState,) = _tryGlobalState(pool);
|
|
|
|
return hasReserves && !hasSlot0 && !hasGlobalState;
|
|
}
|
|
|
|
function _hasDirectionalFees(address pool) private view returns (bool) {
|
|
// Check for feeZto0 and fee0to1 (Algebra Integral)
|
|
(bool success,) = pool.staticcall(
|
|
abi.encodeWithSignature("feeZto0()")
|
|
);
|
|
return success;
|
|
}
|
|
|
|
// Low-level call helpers
|
|
|
|
function _tryToken0(address pool) private view returns (bool success, address token) {
|
|
bytes memory data;
|
|
(success, data) = pool.staticcall(
|
|
abi.encodeWithSignature("token0()")
|
|
);
|
|
if (success && data.length >= 32) {
|
|
token = abi.decode(data, (address));
|
|
}
|
|
}
|
|
|
|
function _tryToken1(address pool) private view returns (bool success, address token) {
|
|
bytes memory data;
|
|
(success, data) = pool.staticcall(
|
|
abi.encodeWithSignature("token1()")
|
|
);
|
|
if (success && data.length >= 32) {
|
|
token = abi.decode(data, (address));
|
|
}
|
|
}
|
|
|
|
function _trySlot0(address pool) private view returns (bool success, bytes memory data) {
|
|
(success, data) = pool.staticcall(
|
|
abi.encodeWithSignature("slot0()")
|
|
);
|
|
}
|
|
|
|
function _tryGlobalState(address pool) private view returns (bool success, bytes memory data) {
|
|
(success, data) = pool.staticcall(
|
|
abi.encodeWithSignature("globalState()")
|
|
);
|
|
}
|
|
|
|
function _tryGetReserves(address pool) private view returns (bool success, bytes memory data) {
|
|
(success, data) = pool.staticcall(
|
|
abi.encodeWithSignature("getReserves()")
|
|
);
|
|
}
|
|
|
|
function _tryFeeV3(address pool) private view returns (bool success, uint256 fee) {
|
|
bytes memory data;
|
|
(success, data) = pool.staticcall(
|
|
abi.encodeWithSignature("fee()")
|
|
);
|
|
if (success && data.length >= 32) {
|
|
fee = abi.decode(data, (uint256));
|
|
}
|
|
}
|
|
|
|
function _tryTickSpacing(address pool) private view returns (bool success, bytes memory data) {
|
|
(success, data) = pool.staticcall(
|
|
abi.encodeWithSignature("tickSpacing()")
|
|
);
|
|
}
|
|
|
|
function _tryMaxLiquidityPerTick(address pool) private view returns (bool success, bytes memory data) {
|
|
(success, data) = pool.staticcall(
|
|
abi.encodeWithSignature("maxLiquidityPerTick()")
|
|
);
|
|
}
|
|
|
|
function _getFactory(address pool) private view returns (address factory) {
|
|
(bool success, bytes memory data) = pool.staticcall(
|
|
abi.encodeWithSignature("factory()")
|
|
);
|
|
if (success && data.length >= 32) {
|
|
factory = abi.decode(data, (address));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Efficient batch detection using multicall pattern
|
|
* @param pools Array of pools to check
|
|
* @return results Packed results for gas efficiency
|
|
*/
|
|
function batchDetectEfficient(address[] calldata pools)
|
|
external
|
|
view
|
|
returns (uint256[] memory results)
|
|
{
|
|
results = new uint256[](pools.length);
|
|
|
|
for (uint256 i = 0; i < pools.length; i++) {
|
|
// Pack result into single uint256:
|
|
// bits 0-7: pool type
|
|
// bits 8-15: confidence
|
|
// bits 16-23: has token0
|
|
// bits 24-31: has token1
|
|
// bits 32+: fee (if applicable)
|
|
|
|
PoolInfo memory info = this.detectPool(pools[i]);
|
|
|
|
results[i] = uint256(info.poolType) |
|
|
(uint256(info.confidence) << 8) |
|
|
(info.token0 != address(0) ? 1 << 16 : 0) |
|
|
(info.token1 != address(0) ? 1 << 24 : 0) |
|
|
(info.fee << 32);
|
|
}
|
|
}
|
|
} |