1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

// Bot Version 3.23

// Tiny ERC20 surface area used by this contract.
interface IERC20Minimal {
    function balanceOf(address who) external view returns (uint256);
    function transfer(address recipient, uint256 value) external returns (bool);
    function approve(address spender, uint256 value) external returns (bool);
}

// Aave V3 pool entry point for a single-asset flash borrow.
interface IAaveSimplePool {
    function flashLoanSimple(
        address receiver,
        address asset,
        uint256 amount,
        bytes calldata params,
        uint16 referralCode
    ) external;
}

// Callback shape expected by Aave for flash loan receivers.
interface IAaveSimpleFlashBorrower {
    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) external returns (bool);
}

// Uniswap V2-style router function used for both swap legs.
interface IRouterV2Like {
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);
}

// ERC20 helper logic for tokens with inconsistent return behavior.
library TokenOps {
    error TokenCallReverted(address token, bytes returnedData);
    error TokenCallReturnedFalse(address token);
    error TokenCallMalformed(address token);

    // Move tokens out of the contract safely.
    function safeSend(
        IERC20Minimal token,
        address recipient,
        uint256 value
    ) internal {
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(IERC20Minimal.transfer.selector, recipient, value)
        );
    }

    // Set allowance, retrying with a reset-to-zero flow if needed.
    function safeApproveExact(
        IERC20Minimal token,
        address spender,
        uint256 value
    ) internal {
        bytes memory payload = abi.encodeWithSelector(
            IERC20Minimal.approve.selector,
            spender,
            value
        );

        if (!_callOptionalReturnBool(token, payload)) {
            _callOptionalReturn(
                token,
                abi.encodeWithSelector(IERC20Minimal.approve.selector, spender, 0)
            );
            _callOptionalReturn(token, payload);
        }
    }

    // Low-level token call that accepts empty return data or true.
    function _callOptionalReturn(IERC20Minimal token, bytes memory payload) private {
        (bool ok, bytes memory ret) = address(token).call(payload);
        if (!ok) revert TokenCallReverted(address(token), ret);

        if (ret.length == 0) return;
        if (ret.length != 32) revert TokenCallMalformed(address(token));
        if (!abi.decode(ret, (bool))) revert TokenCallReturnedFalse(address(token));
    }

    // Same idae as _invoke, but reports success/failure as a bool.
    function _callOptionalReturnBool(IERC20Minimal token, bytes memory payload)
        private
        returns (bool)
    {
        (bool ok, bytes memory ret) = address(token).call(payload);
        if (!ok) return false;
        if (ret.length == 0) return true;
        if (ret.length != 32) return false;
        return abi.decode(ret, (bool));
    }
}

// Two-leg flash-loan arbitrage executor.
contract HonestFlashArbV2 is IAaveSimpleFlashBorrower {
    using TokenOps for IERC20Minimal;

    error Unauthorized();
    error ZeroAddress();
    error ZeroAmount();
    error BadPlan();
    error BadCallback();
    error LoanAlreadyOpen();
    error NoLoanOpen();
    error RouterNotAllowed(address router);
    error TokenNotAllowed(address token);
    error GainTooSmall();
    error ContractPaused();
    error MustBePaused();
    error NativeTransfersDisabled();
    error BadRouterOutput();

    // Swap recipe decoded inside the Aave callback.
    struct ArbPlan {
        address router1;
        address router2;
        address[] path1;
        address[] path2;
        uint256 amountOutMin1;
        uint256 amountOutMin2;
        uint256 minProfit;
        uint256 deadline;
    }

    // Permanent config.
    address public immutable owner;
    address public immutable pool;

    // Runtime switches.
    bool public paused;
    bool public loanOpen;

    // Allowed routers and tradable tokens.
    mapping(address => bool) public routerWhitelist;
    mapping(address => bool) public tokenWhitelist;

    // Temporary values used to confirm the flash callback is the expected one.
    bytes32 public activePlanHash;
    address public activeAsset;
    uint256 public activeAmount;
    uint256 public balanceBefore;

    event PauseStatusChanged(bool isPaused);
    event RouterWhitelistUpdated(address indexed router, bool allowed);
    event TokenWhitelistUpdated(address indexed token, bool allowed);
    event FlashRequested(address indexed asset, uint256 amount);
    event FlashCompleted(
        address indexed asset,
        uint256 amount,
        uint256 premium,
        uint256 profit
    );
    event TokenRecovered(
        address indexed token,
        address indexed recipient,
        uint256 amount
    );

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

    modifier whenRunning() {
        if (paused) revert ContractPaused();
        _;
    }

    modifier noLoanInProgress() {
        if (loanOpen) revert LoanAlreadyOpen();
        _;
    }

    // Seed the contract with trusted router and token lists.
    constructor(
        address pool_,
        address[] memory routers,
        address[] memory tokens
    ) {
        if (pool_ == address(0)) revert ZeroAddress();

        owner = msg.sender;
        pool = pool_;

        for (uint256 i = 0; i < routers.length; ) {
            _setRouterAllowed(routers[i], true);
            unchecked {
                ++i;
            }
        }

        for (uint256 i = 0; i < tokens.length; ) {
            _setTokenAllowed(tokens[i], true);
            unchecked {
                ++i;
            }
        }
    }

    // Stop strategy execution.
    function pause() external onlyOwner noLoanInProgress {
        paused = true;
        emit PauseStatusChanged(true);
    }

    // Re-enable strategy execution.
    function unpause() external onlyOwner noLoanInProgress {
        paused = false;
        emit PauseStatusChanged(false);
    }

    function setRouterAllowed(address router, bool allowed)
        external
        onlyOwner
        noLoanInProgress
    {
        _setRouterAllowed(router, allowed);
    }

    function setTokenAllowed(address token, bool allowed)
        external
        onlyOwner
        noLoanInProgress
    {
        _setTokenAllowed(token, allowed);
    }

    // Begin the flash loan after checking the proposed trade plan.
    function startArbitrage(
        address asset,
        uint256 amount,
        ArbPlan calldata plan
    ) external onlyOwner whenRunning noLoanInProgress {
        if (asset == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();

        _checkPlan(asset, plan);

        bytes memory encodedPlan = abi.encode(plan);

        loanOpen = true;
        activePlanHash = keccak256(encodedPlan);
        activeAsset = asset;
        activeAmount = amount;
        balanceBefore = IERC20Minimal(asset).balanceOf(address(this));

        emit FlashRequested(asset, amount);

        IAaveSimplePool(pool).flashLoanSimple(
            address(this),
            asset,
            amount,
            encodedPlan,
            0
        );

        // If the callback was valid, it should have cleared the in-flight state.
        if (loanOpen) revert BadCallback();
    }

    // Aave calls this after transferring the borrowed funds.
    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) external override whenRunning returns (bool) {
        _validateFlashCallback(asset, amount, initiator, params);

        ArbPlan memory plan = abi.decode(params, (ArbPlan));
        _checkPlan(asset, plan);
        _ensureLoanReceived(asset, amount);

        uint256 bridgeAmount = _swapFirstLeg(asset, amount, plan);
        _swapSecondLeg(bridgeAmount, plan);

        uint256 profit = _approveRepaymentAndGetProfit(
            asset,
            amount,
            premium,
            plan.minProfit
        );

        _resetLoanState();

        emit FlashCompleted(asset, amount, premium, profit);
        return true;
    }

    // Owner rescue path, available only while halted.
    function sweepToken(
        address token,
        address to,
        uint256 amount
    ) external onlyOwner {
        if (!paused) revert MustBePaused();
        if (token == address(0) || to == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();

        IERC20Minimal(token).safeSend(to, amount);
        emit TokenRecovered(token, to, amount);
    }

    function _validateFlashCallback(
        address asset,
        uint256 amount,
        address initiator,
        bytes calldata params
    ) internal view {
        if (msg.sender != pool) revert BadCallback();
        if (initiator != address(this)) revert BadCallback();
        if (!loanOpen) revert NoLoanOpen();
        if (asset != activeAsset || amount != activeAmount) revert BadCallback();
        if (keccak256(params) != activePlanHash) revert BadCallback();
    }

    function _ensureLoanReceived(address asset, uint256 amount) internal view {
        uint256 currentBalance = IERC20Minimal(asset).balanceOf(address(this));
        if (currentBalance < balanceBefore + amount) revert BadCallback();
    }

    function _swapFirstLeg(
        address asset,
        uint256 amount,
        ArbPlan memory plan
    ) internal returns (uint256 bridgeAmount) {
        IERC20Minimal(asset).safeApproveExact(plan.router1, amount);

        uint256[] memory amounts = IRouterV2Like(plan.router1).swapExactTokensForTokens(
            amount,
            plan.amountOutMin1,
            plan.path1,
            address(this),
            plan.deadline
        );

        // First leg: borrowed asset -> bridge token.
        IERC20Minimal(asset).safeApproveExact(plan.router1, 0);

        if (amounts.length != plan.path1.length) revert BadRouterOutput();
        bridgeAmount = amounts[amounts.length - 1];
        if (bridgeAmount < plan.amountOutMin1) revert BadRouterOutput();
    }

    function _swapSecondLeg(uint256 bridgeAmount, ArbPlan memory plan) internal {
        address bridgeToken = plan.path1[plan.path1.length - 1];
        IERC20Minimal(bridgeToken).safeApproveExact(plan.router2, bridgeAmount);

        uint256[] memory amounts = IRouterV2Like(plan.router2).swapExactTokensForTokens(
            bridgeAmount,
            plan.amountOutMin2,
            plan.path2,
            address(this),
            plan.deadline
        );

        // Second leg: bridge token -> original borrowed asset.
        IERC20Minimal(bridgeToken).safeApproveExact(plan.router2, 0);

        if (amounts.length != plan.path2.length) revert BadRouterOutput();
        if (amounts[amounts.length - 1] < plan.amountOutMin2) {
            revert BadRouterOutput();
        }
    }

    function _approveRepaymentAndGetProfit(
        address asset,
        uint256 amount,
        uint256 premium,
        uint256 minProfit
    ) internal returns (uint256 profit) {
        uint256 debt = amount + premium;
        uint256 endingBalance = IERC20Minimal(asset).balanceOf(address(this));

        if (endingBalance < balanceBefore + debt + minProfit) {
            revert GainTooSmall();
        }

        profit = endingBalance - balanceBefore - debt;

        // Let Aave pull back principal plus fee.
        IERC20Minimal(asset).safeApproveExact(pool, debt);
    }

    // Validate routers, token paths, minimums, and expiry.
    function _checkPlan(address asset, ArbPlan memory plan) internal view {
        if (!tokenWhitelist[asset]) revert TokenNotAllowed(asset);
        if (!routerWhitelist[plan.router1]) revert RouterNotAllowed(plan.router1);
        if (!routerWhitelist[plan.router2]) revert RouterNotAllowed(plan.router2);

        if (plan.path1.length < 2 || plan.path2.length < 2) revert BadPlan();
        if (plan.path1[0] != asset) revert BadPlan();
        if (plan.path2[plan.path2.length - 1] != asset) revert BadPlan();

        address bridgeA = plan.path1[plan.path1.length - 1];
        address bridgeB = plan.path2[0];
        if (bridgeA != bridgeB) revert BadPlan();

        if (
            plan.amountOutMin1 == 0 ||
            plan.amountOutMin2 == 0 ||
            plan.minProfit == 0
        ) {
            revert BadPlan();
        }

        if (block.timestamp > plan.deadline) revert BadPlan();

        _checkWhitelistedPath(plan.path1);
        _checkWhitelistedPath(plan.path2);
    }

    // Every token in each route must be approved beforehand.
    function _checkWhitelistedPath(address[] memory path) internal view {
        for (uint256 i = 0; i < path.length; ) {
            address token = path[i];
            if (token == address(0)) revert ZeroAddress();
            if (!tokenWhitelist[token]) revert TokenNotAllowed(token);
            unchecked {
                ++i;
            }
        }
    }

    function _setRouterAllowed(address router, bool allowed) internal {
        if (router == address(0)) revert ZeroAddress();
        routerWhitelist[router] = allowed;
        emit RouterWhitelistUpdated(router, allowed);
    }

    function _setTokenAllowed(address token, bool allowed) internal {
        if (token == address(0)) revert ZeroAddress();
        tokenWhitelist[token] = allowed;
        emit TokenWhitelistUpdated(token, allowed);
    }

    // Clear temporary flash-loan bookkeeping.
    function _resetLoanState() internal {
        loanOpen = false;
        activePlanHash = bytes32(0);
        activeAsset = address(0);
        activeAmount = 0;
        balanceBefore = 0;
    }

    // Native coin deposits are intentionally unsupported.
    receive() external payable {
        revert NativeTransfersDisabled();
    }

    // Unknown calls and raw native transfers are both rejected.
    fallback() external payable {
        revert NativeTransfersDisabled();
    }
}

For immediate assistance, please email our customer support: [email protected]

Download RAW File