Skip to main content
PulseScanner.io

Address

0x718f97ca86b292f49172f72ef751c4e2d216ae66
Current Holdings
$0.00
TXs sent
not counted
First Active
2026-09-10
block 27,511,611
Last Active
1 day ago
block 27,511,611
Funded By
not identified

Net worth historyi

No net-worth snapshots recorded yet
exact matchPewswapFeeRewardRoutersolc 0.8.24+commit.e11b9ed9runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * PewswapFeeRewardRouter
 * v2
 * One-transaction swap + fee split + reward buy for PulseChain (369) and
 * Robinhood Chain (4663). Ethereum and Base are intentionally unsupported.
 *
 * Switch is an OFF-CHAIN aggregator: its routes are priced by the quote
 * service and returned as pre-built calldata. This contract therefore does
 * NOT price anything itself. The frontend fetches two Switch quotes and
 * passes their calldata in:
 *   - `swapCalldata`   : inputToken -> outputToken, output recipient = THIS contract.
 *   - `rewardCalldata` : outputToken -> rewardToken, output recipient = the user.
 *
 * Security model:
 *   - Both legs execute ONLY against the owner-configured `switchRouter`. No arbitrary
 *     call target is ever allowed, so a crafted calldata cannot redirect funds
 *     or abuse this contract's token approvals.
 *   - Approvals to the router are set to the exact leg amount and reset to 0
 *     immediately after each leg.
 *   - Fees are computed on-chain from the ACTUAL measured output, never from a
 *     caller-supplied number.
 *   - If the reward leg fails (real output under quote, route moved, etc.) the
 *     reward budget is paid to the user in the output token instead, so the
 *     user always receives their value and the swap never reverts for that.
 */
interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract PewswapFeeRewardRouter {
    uint256 public constant PULSECHAIN_CHAIN_ID = 369;
    uint256 public constant ROBINHOOD_CHAIN_ID = 4663;

    uint256 public constant MAX_TOTAL_FEE_BPS = 100;
    uint256 private constant BPS_DENOMINATOR = 10_000;

    // Switch's native-asset sentinel (PLS on PulseChain, ETH on Robinhood).
    address public constant NATIVE_SENTINEL = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

    address public constant PULSECHAIN_SWITCH_ROUTER = 0x2dFc8B6e13fF7F04e37Ef97006084805D65a6F19;
    address public constant ROBINHOOD_SWITCH_ROUTER = 0x8b2bBdF41C1486b3482bD0e9603d72f012EE8599;
    address public constant DEFAULT_SWITCH_FEE_RECIPIENT = 0xd10Dc5c09F902cEef3e53e1c544de4BdF9888412;
    address public constant DEFAULT_PARTNER_RECIPIENT = 0xd93DDed4134fA7aB65c112fAc4CF1c7BE7A8827F;

    address public constant PULSECHAIN_REWARD = 0xE318a5c4bFEBb23DA12A47ac429B8325B7D45B80;
    address public constant ROBINHOOD_REWARD = 0x1f94F701f22eF86c8eb91CE65A4A0a8dd9C7a287;

    address public owner;
    address public pendingOwner;
    // Not immutable: Switch migrates its router (observed on Robinhood Chain).
    address public switchRouter;
    address public switchFeeRecipient;
    address public partnerRecipient;
    address public rewardToken;
    uint256 public immutable deploymentChainId;
    uint256 public switchFeeBps;
    uint256 public partnerFeeBps;
    uint256 public rewardFeeBps;
    bool public paused;

    uint256 private unlocked = 1;

    uint256 public constant MAX_FALLBACK_HISTORY = 50;

    struct RewardFallback {
        address user;
        address outputToken;
        uint256 amount;
        uint256 blockNumber;
        uint256 timestamp;
    }

    // Ring buffer: only the most recent MAX_FALLBACK_HISTORY records are kept.
    RewardFallback[MAX_FALLBACK_HISTORY] private _rewardFallbacks;
    uint256 private _fallbackNext;
    uint256 private _fallbackTotal;
    mapping(address => mapping(address => uint256)) private _rewardPaidByUser;
    mapping(address => uint256) private _rewardPaidByToken;
    uint256 private _totalRewardPaid;

    event SwapRewarded(
        address indexed user,
        address indexed outputToken,
        uint256 grossOutput,
        uint256 switchFee,
        uint256 partnerFee,
        uint256 rewardBudget,
        uint256 rewardPaid,
        bool rewardInOutputToken
    );
    event FundingWithdrawn(address indexed token, address indexed recipient, uint256 amount);
    event SwitchRouterUpdated(address indexed previousRouter, address indexed newRouter);
    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event FeeConfigurationUpdated(uint256 switchFeeBps, uint256 partnerFeeBps, uint256 rewardFeeBps);
    event FeeRecipientUpdated(address indexed previousRecipient, address indexed newRecipient);
    event PartnerRecipientUpdated(address indexed previousRecipient, address indexed newRecipient);
    event RewardTokenUpdated(address indexed previousToken, address indexed newToken);
    event Paused(address indexed account);
    event Unpaused(address indexed account);
    event RewardPaid(address indexed user, address indexed token, uint256 amount);
    event ERC20Rescued(address indexed token, address indexed recipient, uint256 requested, uint256 sent);
    event NativeRescued(address indexed recipient, uint256 amount);
    event RewardFallbackOccurred(
        address indexed user,
        address indexed outputToken,
        uint256 amount,
        uint256 blockNumber,
        uint256 timestamp
    );

    error Unauthorized();
    error InvalidAddress();
    error InvalidChain();
    error Reentrancy();
    error NativeValueMismatch();
    error SwapFailed();
    error InsufficientOutput();
    error TransferFailed();
    error InvalidFeeConfiguration();
    error InvalidRewardOutput();
    error PausedError();

    constructor() {
        if (block.chainid != PULSECHAIN_CHAIN_ID && block.chainid != ROBINHOOD_CHAIN_ID) {
            revert InvalidChain();
        }

        owner = msg.sender;
        switchRouter = block.chainid == PULSECHAIN_CHAIN_ID
            ? PULSECHAIN_SWITCH_ROUTER
            : ROBINHOOD_SWITCH_ROUTER;
        switchFeeRecipient = DEFAULT_SWITCH_FEE_RECIPIENT;
        partnerRecipient = DEFAULT_PARTNER_RECIPIENT;
        switchFeeBps = 30;
        partnerFeeBps = 0;
        rewardFeeBps = 30;
        rewardToken = block.chainid == PULSECHAIN_CHAIN_ID ? PULSECHAIN_REWARD : ROBINHOOD_REWARD;
        deploymentChainId = block.chainid;
    }

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

    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert InvalidAddress();
        pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner, newOwner);
    }

    function acceptOwnership() external {
        if (msg.sender != pendingOwner) revert Unauthorized();
        address previousOwner = owner;
        owner = msg.sender;
        pendingOwner = address(0);
        emit OwnershipTransferred(previousOwner, msg.sender);
    }

    function setFeeConfiguration(uint256 newSwitchFeeBps, uint256 newPartnerFeeBps, uint256 newRewardFeeBps)
        external
        onlyOwner
    {
        if (newSwitchFeeBps + newPartnerFeeBps + newRewardFeeBps > MAX_TOTAL_FEE_BPS) {
            revert InvalidFeeConfiguration();
        }
        switchFeeBps = newSwitchFeeBps;
        partnerFeeBps = newPartnerFeeBps;
        rewardFeeBps = newRewardFeeBps;
        emit FeeConfigurationUpdated(newSwitchFeeBps, newPartnerFeeBps, newRewardFeeBps);
    }

    function setSwitchFeeRecipient(address newRecipient) external onlyOwner {
        if (newRecipient == address(0)) revert InvalidAddress();
        emit FeeRecipientUpdated(switchFeeRecipient, newRecipient);
        switchFeeRecipient = newRecipient;
    }

    function setPartnerRecipient(address newRecipient) external onlyOwner {
        if (newRecipient == address(0)) revert InvalidAddress();
        emit PartnerRecipientUpdated(partnerRecipient, newRecipient);
        partnerRecipient = newRecipient;
    }

    function setRewardToken(address newRewardToken) external onlyOwner {
        if (newRewardToken == address(0) || newRewardToken == NATIVE_SENTINEL) revert InvalidAddress();
        emit RewardTokenUpdated(rewardToken, newRewardToken);
        rewardToken = newRewardToken;
    }

    function pause() external onlyOwner {
        paused = true;
        emit Paused(msg.sender);
    }

    function unpause() external onlyOwner {
        paused = false;
        emit Unpaused(msg.sender);
    }

    /// @notice Point the router at Switch's current aggregator after a migration.
    function setSwitchRouter(address newRouter) external onlyOwner {
        if (newRouter == address(0)) revert InvalidAddress();
        emit SwitchRouterUpdated(switchRouter, newRouter);
        switchRouter = newRouter;
    }

    modifier nonReentrant() {
        if (unlocked != 1) revert Reentrancy();
        unlocked = 2;
        _;
        unlocked = 1;
    }

    receive() external payable {}

    /**
     * @param inputToken   ERC20 address or NATIVE_SENTINEL for the token being sold.
     * @param inputAmount  Exact input amount (must equal msg.value when native).
     * @param outputToken  ERC20 address of the token being bought (fee/reward token).
     * @param minOutput    Minimum gross output required from the main swap.
     * @param swapCalldata Switch calldata for inputToken -> outputToken, recipient = this contract.
     * @param rewardCalldata Switch calldata for outputToken -> rewardToken, recipient = the user.
     *                       Pass empty bytes to pay the reward budget directly in the output token.
     */
    function swapWithReward(
        address inputToken,
        uint256 inputAmount,
        address outputToken,
        uint256 minOutput,
        uint256 minRewardOut,
        bytes calldata swapCalldata,
        bytes calldata rewardCalldata
    ) external payable nonReentrant {
        if (paused) revert PausedError();
        if (outputToken == address(0) || outputToken == NATIVE_SENTINEL) revert InvalidAddress();

        // 1. Take the input from the user and fund the main swap leg.
        uint256 nativeValueForSwap;
        if (inputToken == NATIVE_SENTINEL) {
            if (msg.value != inputAmount) revert NativeValueMismatch();
            nativeValueForSwap = inputAmount;
        } else {
            if (msg.value != 0) revert NativeValueMismatch();
            _safeTransferFrom(inputToken, msg.sender, address(this), inputAmount);
            _approveRouter(inputToken, inputAmount);
        }

        // 2. Execute the main swap; output is delivered to this contract.
        uint256 beforeOut = IERC20(outputToken).balanceOf(address(this));
        _callRouter(nativeValueForSwap, swapCalldata);
        if (inputToken != NATIVE_SENTINEL) _approveRouter(inputToken, 0);

        uint256 grossOutput = IERC20(outputToken).balanceOf(address(this)) - beforeOut;
        if (grossOutput < minOutput) revert InsufficientOutput();

        _settleFeesAndReward(outputToken, beforeOut, grossOutput, minRewardOut, rewardCalldata);
    }

    function _settleFeesAndReward(
        address outputToken,
        uint256 beforeOut,
        uint256 grossOutput,
        uint256 minRewardOut,
        bytes calldata rewardCalldata
    ) private {
        uint256 switchFee = (grossOutput * switchFeeBps) / BPS_DENOMINATOR;
        uint256 partnerFee = (grossOutput * partnerFeeBps) / BPS_DENOMINATOR;
        uint256 rewardBudget = (grossOutput * rewardFeeBps) / BPS_DENOMINATOR;

        if (switchFee > 0) _safeTransfer(outputToken, switchFeeRecipient, switchFee);
        if (partnerFee > 0) _safeTransfer(outputToken, partnerRecipient, partnerFee);

        (uint256 rewardPaid, bool rewardInOutputToken) =
            _payReward(outputToken, rewardBudget, minRewardOut, rewardCalldata);

        uint256 netOutput = IERC20(outputToken).balanceOf(address(this)) - beforeOut;
        if (netOutput > 0) _safeTransfer(outputToken, msg.sender, netOutput);

        emit SwapRewarded(
            msg.sender,
            outputToken,
            grossOutput,
            switchFee,
            partnerFee,
            rewardBudget,
            rewardPaid,
            rewardInOutputToken
        );
    }

    function _payReward(
        address outputToken,
        uint256 rewardBudget,
        uint256 minRewardOut,
        bytes calldata rewardCalldata
    ) private returns (uint256 rewardPaid, bool rewardInOutputToken) {
        if (rewardBudget == 0) return (0, false);
        if (rewardCalldata.length == 0) {
            if (minRewardOut != 0) revert InvalidRewardOutput();
            _safeTransfer(outputToken, msg.sender, rewardBudget);
            return (rewardBudget, true);
        }

        // Switch delivers the reward-buy output to the calldata's sender (this
        // contract), so measure this contract's balance and forward to the user.
        uint256 rewardBefore = IERC20(rewardToken).balanceOf(address(this));
        uint256 userRewardBefore = IERC20(rewardToken).balanceOf(msg.sender);
        _approveRouter(outputToken, rewardBudget);
        (bool ok, ) = switchRouter.call(rewardCalldata);
        _approveRouter(outputToken, 0);

        if (ok) {
            uint256 contractDelta = IERC20(rewardToken).balanceOf(address(this)) - rewardBefore;
            uint256 directUserDelta = IERC20(rewardToken).balanceOf(msg.sender) - userRewardBefore;
            uint256 delta = contractDelta + directUserDelta;
            if (delta > 0 && delta < minRewardOut) revert InvalidRewardOutput();
            if (delta > 0) {
                if (contractDelta > 0) _safeTransfer(rewardToken, msg.sender, contractDelta);
                _recordReward(msg.sender, delta);
                return (delta, false);
            }
        }

        // Fallback: route failed or delivered nothing; pay budget in output token and log it.
        _recordFallback(msg.sender, outputToken, rewardBudget);
        _safeTransfer(outputToken, msg.sender, rewardBudget);
        return (rewardBudget, true);
    }

    function _recordReward(address user, uint256 amount) private {
        _rewardPaidByUser[user][rewardToken] += amount;
        _rewardPaidByToken[rewardToken] += amount;
        _totalRewardPaid += amount;
        emit RewardPaid(user, rewardToken, amount);
    }

    function rewardPaidByUser(address user, address token) external view returns (uint256) {
        return _rewardPaidByUser[user][token];
    }

    function rewardPaidByToken(address token) external view returns (uint256) {
        return _rewardPaidByToken[token];
    }

    function totalRewardPaid() external view returns (uint256) {
        return _totalRewardPaid;
    }

    function _recordFallback(address user, address outputToken, uint256 amount) private {
        _rewardFallbacks[_fallbackNext] = RewardFallback(user, outputToken, amount, block.number, block.timestamp);
        _fallbackNext = (_fallbackNext + 1) % MAX_FALLBACK_HISTORY;
        unchecked {
            _fallbackTotal++;
        }
        emit RewardFallbackOccurred(user, outputToken, amount, block.number, block.timestamp);
    }

    /// @notice Total fallbacks ever recorded, including ones evicted from the ring buffer.
    function rewardFallbackTotal() external view returns (uint256) {
        return _fallbackTotal;
    }

    /// @notice Number of fallback records currently retained (at most MAX_FALLBACK_HISTORY).
    function rewardFallbackStored() public view returns (uint256) {
        return _fallbackTotal < MAX_FALLBACK_HISTORY ? _fallbackTotal : MAX_FALLBACK_HISTORY;
    }

    /// @notice The retained fallback records, ordered oldest to newest.
    function getRecentRewardFallbacks() external view returns (RewardFallback[] memory list) {
        uint256 stored = rewardFallbackStored();
        list = new RewardFallback[](stored);
        uint256 start = _fallbackTotal <= MAX_FALLBACK_HISTORY ? 0 : _fallbackNext;
        for (uint256 i = 0; i < stored; i++) {
            list[i] = _rewardFallbacks[(start + i) % MAX_FALLBACK_HISTORY];
        }
    }

    /// @notice Recover any ERC20, including fee-on-transfer/tax tokens.
    /// The event reports the actual balance decrease, which can be less than
    /// `amount` when the token charges a transfer tax.
    function rescueERC20(address token, address recipient, uint256 amount)
        external
        onlyOwner
        nonReentrant
    {
        if (token == address(0) || token == NATIVE_SENTINEL || recipient == address(0)) {
            revert InvalidAddress();
        }
        uint256 beforeBalance = IERC20(token).balanceOf(address(this));
        if (amount > beforeBalance) revert TransferFailed();
        _safeTransfer(token, recipient, amount);
        uint256 afterBalance = IERC20(token).balanceOf(address(this));
        emit ERC20Rescued(token, recipient, amount, beforeBalance - afterBalance);
    }

    /// @notice Recover native PLS/ETH held by the router.
    function rescueNative(address payable recipient, uint256 amount)
        external
        onlyOwner
        nonReentrant
    {
        if (recipient == address(0) || amount > address(this).balance) revert InvalidAddress();
        (bool ok, ) = recipient.call{value: amount}("");
        if (!ok) revert TransferFailed();
        emit NativeRescued(recipient, amount);
    }

    /// @notice Backwards-compatible alias for rescuing ERC20/native funds.
    function withdrawFunding(address token, address recipient, uint256 amount)
        external
        onlyOwner
        nonReentrant
    {
        if (recipient == address(0)) revert InvalidAddress();
        if (token == NATIVE_SENTINEL) {
            if (amount > address(this).balance) revert InvalidAddress();
            (bool ok, ) = recipient.call{value: amount}("");
            if (!ok) revert TransferFailed();
        } else {
            if (token == address(0)) revert InvalidAddress();
            uint256 beforeBalance = IERC20(token).balanceOf(address(this));
            if (amount > beforeBalance) revert TransferFailed();
            _safeTransfer(token, recipient, amount);
        }
        emit FundingWithdrawn(token, recipient, amount);
    }

    function _callRouter(uint256 value, bytes calldata data) private {
        (bool ok, ) = switchRouter.call{value: value}(data);
        if (!ok) revert SwapFailed();
    }

    function _approveRouter(address token, uint256 amount) private {
        (bool ok, bytes memory ret) =
            token.call(abi.encodeWithSelector(IERC20.approve.selector, switchRouter, amount));
        if (!ok || (ret.length != 0 && !abi.decode(ret, (bool)))) revert TransferFailed();
    }

    function _safeTransfer(address token, address to, uint256 amount) private {
        (bool ok, bytes memory ret) =
            token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
        if (!ok || (ret.length != 0 && !abi.decode(ret, (bool)))) revert TransferFailed();
    }

    function _safeTransferFrom(address token, address from, address to, uint256 amount) private {
        (bool ok, bytes memory ret) =
            token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
        if (!ok || (ret.length != 0 && !abi.decode(ret, (bool)))) revert TransferFailed();
    }
}