Skip to main content
PulseScanner.io

Address

0x1e5df975bf9eeec856bbe7aa96aafd9cdd83de6d
Current Holdings
$0.00
TXs sent
not counted
First Active
2026-06-02
block 26,685,608
Last Active
104 days ago
block 26,685,608
Funded By
not identified

Net worth historyi

No net-worth snapshots recorded yet
exact matchPulseArbsolc 0.8.20+commit.a1b79de6runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title PulseArb — Atomic multi-hop DEX arbitrage for PulseChain
///
/// @notice Two execution modes:
///   1. `executeArb`      — funded arb: bot pre-holds the input capital.
///   2. `executeArbFlash` — flash-swap arb: borrow from a UniswapV2 pair,
///                          execute the cycle, repay loan + fee, keep profit.
///                          Zero capital required; TX reverts with no loss if
///                          profit < minProfit.
///
/// @dev  Security model:
///   - `onlyOwner` on both entry points.
///   - `uniswapV2Call` validates `msg.sender == pairs[0]` (the flash provider)
///     and `sender == address(this)` (initiated by us, not an external caller).
///   - Re-entrancy guard via `_flashActive` flag.

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

interface IUniswapV2Pair {
    function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
    function token0() external view returns (address);
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}

contract PulseArb {
    address public immutable owner;

    /// @dev Re-entrancy guard: set to true during a flash-swap callback.
    bool private _flashActive;

    error NotOwner();
    error InvalidPath();
    error NotACycle();
    error InsufficientProfit(uint256 got, uint256 need);
    error TransferFailed();
    error ReentrantCall();
    error InvalidFlashCaller();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    modifier noReentrant() {
        if (_flashActive) revert ReentrantCall();
        _flashActive = true;
        _;
        _flashActive = false;
    }

    constructor() {
        owner = msg.sender;
    }

    // =========================================================================
    // MODE 1 — FUNDED ARBITRAGE
    // =========================================================================

    /// @notice Execute a multi-hop arbitrage cycle using pre-held capital.
    ///
    /// @param pairs     UniswapV2-compatible pair addresses (one per hop).
    /// @param tokens    Token path — length must be pairs.length + 1.
    ///                  tokens[0] == tokens[last] (cycle invariant).
    /// @param feeBps    Fee in basis points per pair (e.g. 30 = 0.3%).
    /// @param amountIn  Amount of tokens[0] to seed the cycle (pre-approved).
    /// @param minProfit Minimum net profit in tokens[0] units — reverts if not met.
    function executeArb(
        address[] calldata pairs,
        address[] calldata tokens,
        uint256[] calldata feeBps,
        uint256 amountIn,
        uint256 minProfit
    ) external onlyOwner {
        uint256 n = pairs.length;
        if (n == 0 || tokens.length != n + 1 || feeBps.length != n) revert InvalidPath();
        if (tokens[0] != tokens[n]) revert NotACycle();

        if (!IERC20(tokens[0]).transferFrom(msg.sender, address(this), amountIn)) revert TransferFailed();

        uint256 amount = _executeHops(pairs, tokens, feeBps, 0, n, amountIn);

        if (amount < amountIn + minProfit) {
            revert InsufficientProfit(amount > amountIn ? amount - amountIn : 0, minProfit);
        }

        if (!IERC20(tokens[0]).transfer(msg.sender, amount)) revert TransferFailed();
    }

    // =========================================================================
    // MODE 2 — FLASH-SWAP ARBITRAGE
    // =========================================================================

    /// @notice Initiate a capital-free arbitrage via UniswapV2 flash swap.
    ///
    /// @dev Flow:
    ///   1. Borrow `flashAmount` of `tokens[0]` from `pairs[0]` (flash provider).
    ///   2. `pairs[0]` calls back `uniswapV2Call` with the encoded route.
    ///   3. Callback executes hops 1..n-1, repays `pairs[0]`, keeps profit.
    ///
    /// @param pairs        Full route including the flash provider as `pairs[0]`.
    ///                     `pairs[0]` must hold a reserve in `tokens[0]`.
    ///                     `pairs[0]` should NOT also appear in hops 1..n-1
    ///                     (otherwise the reserve math will be off).
    /// @param tokens       Token path. tokens[0] == tokens[last] == borrowed token.
    /// @param feeBps       Fee per hop. feeBps[0] = fee of flash provider pair
    ///                     (used to compute repayment amount).
    /// @param flashAmount  Amount of tokens[0] to borrow.
    /// @param minProfit    Minimum profit kept after repayment or revert.
    function executeArbFlash(
        address[] calldata pairs,
        address[] calldata tokens,
        uint256[] calldata feeBps,
        uint256 flashAmount,
        uint256 minProfit
    ) external onlyOwner noReentrant {
        uint256 n = pairs.length;
        if (n < 2 || tokens.length != n + 1 || feeBps.length != n) revert InvalidPath();
        if (tokens[0] != tokens[n]) revert NotACycle();

        bytes memory data = abi.encode(pairs, tokens, feeBps, flashAmount, minProfit);

        // Determine which token slot to request from the flash pair
        address t0 = IUniswapV2Pair(pairs[0]).token0();
        bool tokenIsZero = (tokens[0] == t0);
        uint256 out0 = tokenIsZero ? flashAmount : 0;
        uint256 out1 = tokenIsZero ? 0 : flashAmount;

        // Triggers the flash swap → pairs[0] sends tokens → calls uniswapV2Call
        IUniswapV2Pair(pairs[0]).swap(out0, out1, address(this), data);
    }

    /// @notice UniswapV2 flash-swap callback.
    ///
    /// @dev Called by `pairs[0]` after it sends us `flashAmount` of `tokens[0]`.
    ///      We execute the arb (hops 1..n-1), then repay pairs[0].
    ///
    /// Security:
    ///   - `msg.sender` must equal `pairs[0]` encoded in `data`.
    ///   - `sender` must be `address(this)` (we initiated the flash swap).
    ///   - `_flashActive` re-entrancy guard is active.
    function uniswapV2Call(
        address sender,
        uint256 /*amount0*/,
        uint256 /*amount1*/,
        bytes calldata data
    ) external {
        // Must have been triggered by our own executeArbFlash
        if (sender != address(this)) revert InvalidFlashCaller();
        if (!_flashActive) revert ReentrantCall();

        (
            address[] memory pairs,
            address[] memory tokens,
            uint256[] memory feeBps,
            uint256 amountBorrowed,
            uint256 minProfit
        ) = abi.decode(data, (address[], address[], uint256[], uint256, uint256));

        uint256 n = pairs.length;

        // Caller must be the flash-provider pair encoded in the route
        if (msg.sender != pairs[0]) revert InvalidFlashCaller();

        // Execute hops 1..n-1 with the borrowed tokens
        uint256 amount = _executeHops(pairs, tokens, feeBps, 1, n, amountBorrowed);

        // Repay flash loan: amountBorrowed * 10000 / (10000 - fee) + 1 (rounds up)
        uint256 fee = feeBps[0];
        uint256 repayAmount = (amountBorrowed * 10000) / (10000 - fee) + 1;

        if (amount < repayAmount + minProfit) {
            revert InsufficientProfit(
                amount > repayAmount ? amount - repayAmount : 0,
                minProfit
            );
        }

        // Repay the flash provider (must transfer directly to the pair contract)
        if (!IERC20(tokens[0]).transfer(pairs[0], repayAmount)) revert TransferFailed();

        // Residual profit stays in this contract — owner withdraws via withdraw()
    }

    // =========================================================================
    // TOKEN RESCUE
    // =========================================================================

    /// @notice Withdraw a specific amount of a token to the owner.
    function withdraw(address token, uint256 amount) external onlyOwner {
        if (!IERC20(token).transfer(owner, amount)) revert TransferFailed();
    }

    /// @notice Withdraw the entire balance of a token to the owner.
    function withdrawAll(address token) external onlyOwner {
        uint256 bal = IERC20(token).balanceOf(address(this));
        if (bal > 0) {
            if (!IERC20(token).transfer(owner, bal)) revert TransferFailed();
        }
    }

    // =========================================================================
    // Internal helpers
    // =========================================================================

    /// @dev Execute swap hops from index `start` to `end-1` (exclusive).
    ///      Returns the final output amount.
    function _executeHops(
        address[] memory pairs,
        address[] memory tokens,
        uint256[] memory feeBps,
        uint256 start,
        uint256 end,
        uint256 amountIn
    ) internal returns (uint256 amount) {
        amount = amountIn;
        for (uint256 i = start; i < end; i++) {
            address pair     = pairs[i];
            address tokenIn  = tokens[i];
            address tokenOut = tokens[i + 1];

            (uint112 r0, uint112 r1,) = IUniswapV2Pair(pair).getReserves();
            address token0 = IUniswapV2Pair(pair).token0();

            bool zeroForOne = (tokenIn == token0);
            uint256 reserveIn  = zeroForOne ? uint256(r0) : uint256(r1);
            uint256 reserveOut = zeroForOne ? uint256(r1) : uint256(r0);

            uint256 amountOut = _getAmountOut(amount, reserveIn, reserveOut, feeBps[i]);

            if (!IERC20(tokenIn).transfer(pair, amount)) revert TransferFailed();

            (uint256 out0, uint256 out1) = zeroForOne
                ? (uint256(0), amountOut)
                : (amountOut, uint256(0));
            IUniswapV2Pair(pair).swap(out0, out1, address(this), "");

            amount = amountOut;
        }
    }

    /// @dev UniswapV2 constant-product output formula.
    ///      amountOut = (amountIn * (10000 - fee) * reserveOut)
    ///                / (reserveIn * 10000 + amountIn * (10000 - fee))
    function _getAmountOut(
        uint256 amountIn,
        uint256 reserveIn,
        uint256 reserveOut,
        uint256 fee
    ) internal pure returns (uint256) {
        require(amountIn > 0 && reserveIn > 0 && reserveOut > 0, "Bad reserves");
        uint256 k = 10000 - fee;
        uint256 amountInWithFee = amountIn * k;
        uint256 numerator   = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 10000 + amountInWithFee;
        return numerator / denominator;
    }
}