Address
0x718f97ca86b292f49172f72ef751c4e2d216ae66Current 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();
}
}