Skip to main content
PulseScanner.io

Address

0x4eae949d24ff11be54ddcd95bc4b91200dffae15
Current Holdings
$0.00
TXs sent
not counted
First Active
2026-01-14
block 25,525,120
Last Active
245 days ago
block 25,525,120
Funded By
not identified

Net worth historyi

No net-worth snapshots recorded yet
exact matchPulseXStableSwapAdaptersolc 0.8.28+commit.7893614aruntime exact · creation exact
//SPDX-License-Identifier: MIT

pragma solidity ^0.8.28;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPulseXStableSwapPool} from "../interfaces/IPulseXStableSwapPool.sol";
import {Governable} from "../Governable.sol";
import {DexAdapter} from "./DexAdapter.sol";

/**
 * @title PulseXStableSwapAdapter
 * @notice Adapter for PulseX StableSwap pools
 */
contract PulseXStableSwapAdapter is DexAdapter, Governable {
    using SafeERC20 for IERC20;

    error PulseXStableSwapAdapter__OutOfBound();
    error PulseXStableSwapAdapter__NotRegisteredPool();
    error PulseXStableSwapAdapter__PoolAlreadyRegistered();
    error PulseXStableSwapAdapter__InvalidPool();
    error PulseXStableSwapAdapter__InsufficientOutput();
    error PulseXStableSwapAdapter__InvalidPoolAddress();
    error PulseXStableSwapAdapter__ZeroAmountIn();
    error PulseXStableSwapAdapter__TooManyPools();
    error PulseXStableSwapAdapter__TokenNotFound();

    struct PoolData {
        bool isRegistered;
        mapping(address => uint256) tokenIndex;
        address[] tokens;
    }

    /// @dev Maximum number of pools allowed to prevent gas bloat
    uint256 public constant MAX_POOLS = 50;

    /// @dev Array of PulseX StableSwap Pool addresses
    address[] private pools;
    /// @dev Mapping from pool address to its data structure
    mapping(address => PoolData) private poolData;

    /// @dev Events for pool management
    event PoolAdded(address indexed pool, address[] tokens);
    event PoolRemoved(address indexed pool, uint256 indexed index);

    constructor(address governor, bytes32 _description) DexAdapter(_description) Governable(governor) {}

    /**
     * @dev Registers a new pool in the adapter. This function can only be called by the contract's governor.
     * @param pool The address of the PulseX StableSwap pool to be registered.
     * @notice The function will read token addresses from the pool and store their indices.
     */
    function addPool(address pool) external onlyGovernor {
        if (pool == address(0) || pool.code.length == 0) {
            revert PulseXStableSwapAdapter__InvalidPoolAddress();
        }

        if (pools.length >= MAX_POOLS) {
            revert PulseXStableSwapAdapter__TooManyPools();
        }

        PoolData storage _poolData = poolData[pool];
        if (_poolData.isRegistered) {
            revert PulseXStableSwapAdapter__PoolAlreadyRegistered();
        }

        IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
        uint256 nCoins = poolContract.N_COINS();

        address[] memory tokens = new address[](nCoins);
        for (uint256 i = 0; i < nCoins; i++) {
            tokens[i] = poolContract.coins(i);
            _poolData.tokenIndex[tokens[i]] = i;
        }

        _poolData.tokens = tokens;
        _poolData.isRegistered = true;
        pools.push(pool);

        emit PoolAdded(pool, tokens);
    }

    /**
     * @dev Removes a pool from the list. This function can only be called by the contract's governor.
     * @param poolIndex The index of the pool in the pools array to be removed.
     */
    function removePool(uint256 poolIndex) external onlyGovernor {
        uint256 poolsLen = pools.length;
        if (poolsLen == 0 || poolIndex >= poolsLen) {
            revert PulseXStableSwapAdapter__OutOfBound();
        }

        address poolAddress = pools[poolIndex];
        delete poolData[poolAddress];

        if (poolIndex < poolsLen - 1) {
            pools[poolIndex] = pools[poolsLen - 1];
        }
        pools.pop();

        emit PoolRemoved(poolAddress, poolIndex);
    }

    /**
     * @dev Gets token indices for a swap in a specific pool
     * @param pool The pool address
     * @param tokenIn Input token address
     * @param tokenOut Output token address
     * @return indexIn Index of input token
     * @return indexOut Index of output token
     */
    function _getTokenIndices(address pool, address tokenIn, address tokenOut)
        internal
        view
        returns (uint256 indexIn, uint256 indexOut)
    {
        PoolData storage _poolData = poolData[pool];
        if (!_poolData.isRegistered) {
            revert PulseXStableSwapAdapter__NotRegisteredPool();
        }

        bool tokenInFound = false;
        bool tokenOutFound = false;

        for (uint256 i = 0; i < _poolData.tokens.length; i++) {
            if (_poolData.tokens[i] == tokenIn) {
                indexIn = i;
                tokenInFound = true;
            }
            if (_poolData.tokens[i] == tokenOut) {
                indexOut = i;
                tokenOutFound = true;
            }
            if (tokenInFound && tokenOutFound) break;
        }

        if (!tokenInFound || !tokenOutFound) {
            revert PulseXStableSwapAdapter__TokenNotFound();
        }
    }

    /// @inheritdoc DexAdapter
    function _swapIn(
        address tokenIn,
        address tokenOut,
        address to,
        uint256 amountIn,
        uint256 amountOutMin,
        bytes memory extraArgs
    ) internal override returns (uint256 amountOut) {
        if (amountIn == 0) revert PulseXStableSwapAdapter__ZeroAmountIn();

        address pool = abi.decode(extraArgs, (address));
        PoolData storage _poolData = poolData[pool];
        if (!_poolData.isRegistered) {
            revert PulseXStableSwapAdapter__NotRegisteredPool();
        }

        (uint256 indexIn, uint256 indexOut) = _getTokenIndices(pool, tokenIn, tokenOut);

        IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);

        IERC20(tokenIn).forceApprove(pool, amountIn);

        IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
        amountOut = poolContract.get_dy(indexIn, indexOut, amountIn);

        if (amountOut < amountOutMin) {
            revert PulseXStableSwapAdapter__InsufficientOutput();
        }

        uint256 balanceBefore = IERC20(tokenOut).balanceOf(address(this));
        poolContract.exchange(indexIn, indexOut, amountIn, amountOutMin);
        uint256 balanceAfter = IERC20(tokenOut).balanceOf(address(this));
        amountOut = balanceAfter - balanceBefore;

        if (amountOut < amountOutMin) {
            revert PulseXStableSwapAdapter__InsufficientOutput();
        }

        IERC20(tokenOut).safeTransfer(to, amountOut);
    }

    /// @inheritdoc DexAdapter
    function _swapOut(
        address tokenIn,
        address tokenOut,
        address to,
        uint256 amountOut,
        uint256 amountInMax,
        bytes memory extraArgs
    ) internal override returns (uint256 amountIn) {
        address pool = abi.decode(extraArgs, (address));
        PoolData storage _poolData = poolData[pool];
        if (!_poolData.isRegistered) {
            revert PulseXStableSwapAdapter__NotRegisteredPool();
        }

        (uint256 indexIn, uint256 indexOut) = _getTokenIndices(pool, tokenIn, tokenOut);

        IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
        amountIn = amountInMax;

        if (amountIn == 0) {
            revert PulseXStableSwapAdapter__ZeroAmountIn();
        }

        IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);

        IERC20(tokenIn).forceApprove(pool, amountIn);

        uint256 balanceBefore = IERC20(tokenOut).balanceOf(address(this));
        poolContract.exchange(indexIn, indexOut, amountIn, 0);
        uint256 balanceAfter = IERC20(tokenOut).balanceOf(address(this));
        uint256 actualAmountOut = balanceAfter - balanceBefore;

        if (actualAmountOut < amountOut) {
            revert PulseXStableSwapAdapter__InsufficientOutput();
        }

        IERC20(tokenOut).safeTransfer(to, actualAmountOut);
    }

    /// @inheritdoc DexAdapter
    function _getMaxOutput(address tokenIn, address tokenOut, uint256 amountIn)
        internal
        view
        override
        returns (uint256 maxOutput, bytes memory extraArgs)
    {
        address targetPool;
        uint256 poolsLen = pools.length;

        for (uint256 i; i < poolsLen;) {
            targetPool = pools[i];
            PoolData storage _poolData = poolData[targetPool];

            if (_poolData.isRegistered) {
                bool tokenInFound = false;
                bool tokenOutFound = false;
                for (uint256 j = 0; j < _poolData.tokens.length; j++) {
                    if (_poolData.tokens[j] == tokenIn) tokenInFound = true;
                    if (_poolData.tokens[j] == tokenOut) tokenOutFound = true;
                }

                if (tokenInFound && tokenOutFound) {
                    (uint256 indexIn, uint256 indexOut) = _getTokenIndices(targetPool, tokenIn, tokenOut);
                    IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(targetPool);
                    uint256 dy = poolContract.get_dy(indexIn, indexOut, amountIn);
                    if (dy > maxOutput) {
                        maxOutput = dy;
                        extraArgs = abi.encode(targetPool);
                    }
                }
            }
            unchecked {
                ++i;
            }
        }
    }

    /// @inheritdoc DexAdapter
    function _getMinInput(address tokenIn, address tokenOut, uint256 amountOut)
        internal
        view
        override
        returns (uint256 minInput, bytes memory extraArgs)
    {
        address targetPool;
        uint256 poolsLen = pools.length;

        for (uint256 i; i < poolsLen;) {
            targetPool = pools[i];
            PoolData storage _poolData = poolData[targetPool];

            if (_poolData.isRegistered) {
                bool tokenInFound = false;
                bool tokenOutFound = false;
                for (uint256 j = 0; j < _poolData.tokens.length; j++) {
                    if (_poolData.tokens[j] == tokenIn) tokenInFound = true;
                    if (_poolData.tokens[j] == tokenOut) tokenOutFound = true;
                }

                if (tokenInFound && tokenOutFound) {
                    (uint256 indexIn, uint256 indexOut) = _getTokenIndices(targetPool, tokenIn, tokenOut);
                    IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(targetPool);
                    uint256 dx = _findInputForOutput(poolContract, indexIn, indexOut, amountOut);
                    if (dx != 0 && (minInput == 0 || dx < minInput)) {
                        minInput = dx;
                        extraArgs = abi.encode(targetPool);
                    }
                }
            }
            unchecked {
                ++i;
            }
        }
    }

    /// @inheritdoc DexAdapter
    function _getOutputFromArgs(address tokenIn, address tokenOut, uint256 amountIn, bytes memory extraArgs)
        internal
        view
        override
        returns (uint256 amountOut)
    {
        address pool = abi.decode(extraArgs, (address));
        PoolData storage _poolData = poolData[pool];
        if (!_poolData.isRegistered) return 0;

        bool tokenInFound = false;
        bool tokenOutFound = false;
        for (uint256 i = 0; i < _poolData.tokens.length; i++) {
            if (_poolData.tokens[i] == tokenIn) tokenInFound = true;
            if (_poolData.tokens[i] == tokenOut) tokenOutFound = true;
        }
        if (!tokenInFound || !tokenOutFound) return 0;

        (uint256 indexIn, uint256 indexOut) = _getTokenIndices(pool, tokenIn, tokenOut);
        IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
        return poolContract.get_dy(indexIn, indexOut, amountIn);
    }

    /// @inheritdoc DexAdapter
    function _getInputFromArgs(address tokenIn, address tokenOut, uint256 amountOut, bytes memory extraArgs)
        internal
        view
        override
        returns (uint256 amountIn)
    {
        address pool = abi.decode(extraArgs, (address));
        PoolData storage _poolData = poolData[pool];
        if (!_poolData.isRegistered) return 0;

        bool tokenInFound = false;
        bool tokenOutFound = false;
        for (uint256 i = 0; i < _poolData.tokens.length; i++) {
            if (_poolData.tokens[i] == tokenIn) tokenInFound = true;
            if (_poolData.tokens[i] == tokenOut) tokenOutFound = true;
        }
        if (!tokenInFound || !tokenOutFound) return 0;

        (uint256 indexIn, uint256 indexOut) = _getTokenIndices(pool, tokenIn, tokenOut);
        IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
        return _findInputForOutput(poolContract, indexIn, indexOut, amountOut);
    }

    /// @inheritdoc DexAdapter
    function _getAdapterArgs(address tokenIn, address tokenOut)
        internal
        view
        override
        returns (bytes[] memory extraArgs)
    {
        uint256 poolsLen = pools.length;
        uint256 validPoolsCount;
        address pool;

        for (uint256 i; i < poolsLen;) {
            pool = pools[i];
            PoolData storage _poolData = poolData[pool];

            if (_poolData.isRegistered) {
                bool tokenInFound = false;
                bool tokenOutFound = false;
                for (uint256 k = 0; k < _poolData.tokens.length; k++) {
                    if (_poolData.tokens[k] == tokenIn) tokenInFound = true;
                    if (_poolData.tokens[k] == tokenOut) tokenOutFound = true;
                }

                if (tokenInFound && tokenOutFound) {
                    unchecked {
                        ++validPoolsCount;
                    }
                }
            }
            unchecked {
                ++i;
            }
        }

        extraArgs = new bytes[](validPoolsCount);
        uint256 j;

        for (uint256 i; i < poolsLen;) {
            pool = pools[i];
            PoolData storage _poolData = poolData[pool];

            if (_poolData.isRegistered) {
                bool tokenInFound = false;
                bool tokenOutFound = false;
                for (uint256 k = 0; k < _poolData.tokens.length; k++) {
                    if (_poolData.tokens[k] == tokenIn) tokenInFound = true;
                    if (_poolData.tokens[k] == tokenOut) tokenOutFound = true;
                }

                if (tokenInFound && tokenOutFound) {
                    extraArgs[j] = abi.encode(pool);
                    unchecked {
                        ++j;
                    }
                }
            }
            unchecked {
                ++i;
            }
        }
    }

    /**
     * @dev Retrieves details of a specific pool identified by its index in the 'pools' array.
     * @param poolIndex The index of the pool in the 'pools' array.
     * @return pool The address of the pool at the specified index.
     * @return tokens Array of token addresses in the pool.
     * @notice The function will return the address of the pool and its tokens.
     *         It is important to ensure that the poolIndex is within the bounds of the array to avoid out-of-bounds errors.
     */
    function getPoolAtIndex(uint256 poolIndex) external view returns (address pool, address[] memory tokens) {
        pool = pools[poolIndex];
        PoolData storage _poolData = poolData[pool];
        tokens = _poolData.tokens;
    }

    /// @return poolsLength The number of pools currently registered
    function allPoolsLength() external view returns (uint256) {
        return pools.length;
    }

    /**
     * @dev Finds the required input amount for a desired output using binary search
     * @param poolContract The pool contract interface
     * @param indexIn Index of input token
     * @param indexOut Index of output token
     * @param amountOut Desired output amount
     * @return amountIn Required input amount
     */
    function _findInputForOutput(
        IPulseXStableSwapPool poolContract,
        uint256 indexIn,
        uint256 indexOut,
        uint256 amountOut
    ) internal view returns (uint256 amountIn) {
        uint256 low = amountOut;
        uint256 high = (amountOut * 200) / 100;

        uint256 maxIterations = 100;
        for (uint256 i = 0; i < 20; i++) {
            uint256 testOutput = poolContract.get_dy(indexIn, indexOut, high);
            if (testOutput >= amountOut) {
                break;
            }
            high = (high * 150) / 100;
            if (i == 19) {
                return 0;
            }
        }

        for (uint256 i = 0; i < maxIterations; i++) {
            amountIn = (low + high) / 2;
            if (amountIn == low || amountIn == high) {
                break;
            }

            uint256 testOutput = poolContract.get_dy(indexIn, indexOut, amountIn);

            if (testOutput >= amountOut) {
                high = amountIn;
            } else {
                low = amountIn;
            }
        }

        amountIn = high;

        uint256 finalOutput = poolContract.get_dy(indexIn, indexOut, amountIn);
        if (finalOutput < amountOut) {
            return 0;
        }
    }

    /**
     * @dev Gets the token index for a specific token in a pool
     * @param pool The pool address
     * @param token The token address
     * @return index The index of the token in the pool
     */
    function getTokenIndex(address pool, address token) external view returns (uint256 index) {
        PoolData storage _poolData = poolData[pool];
        if (!_poolData.isRegistered) {
            revert PulseXStableSwapAdapter__NotRegisteredPool();
        }
        index = _poolData.tokenIndex[token];

        bool tokenFound = false;
        for (uint256 i = 0; i < _poolData.tokens.length; i++) {
            if (_poolData.tokens[i] == token) {
                tokenFound = true;
                break;
            }
        }
        if (!tokenFound) {
            revert PulseXStableSwapAdapter__TokenNotFound();
        }
    }
}