SentrySwapRouterV2
0x8bfdc6cc38db45bdaf2f254415251b109058a97c
Verification
Verified
v0.8.20+commit.a1b79de6
Type
Contract
8,568 bytes
ABI entries
39
8 read · 15 write
License
none
Contract information
- Address
- 0x8bfdc6cc38db45bdaf2f254415251b109058a97c
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.20+commit.a1b79de6
- Optimization
- Enabled
- Creator
- 0xbF551eED83…f18600B7C5
- Creation tx
- 0xf76637fe2c…490c4641a3
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (8)
MAX_FEE_BPS() → uint16
feeBps() → uint16
owner() → address
referralBps() → uint16
swapRouter() → address
treasury() → address
v2Factory() → address
weth9() → address
Events (6)
FeeBpsUpdatedOwnershipTransferredReferralBpsUpdatedReferralPaidSwapExecutedTreasuryUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "swapRouter_",
"type": "address"
},
{
"internalType": "address",
"name": "v2Factory_",
"type": "address"
},
{
"internalType": "address",
"name": "weth9_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "uint16",
"name": "feeBps_",
"type": "uint16"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "DirectEthNotAccepted",
"type": "error"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "FeeTooHigh",
"type": "error"
},
{
"inputs": [],
"name": "NoV2Pair",
"type": "error"
},
{
"inputs": [],
"name": "ReferralExceedsFee",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "received",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minimum",
"type": "uint256"
}
],
"name": "SlippageExceeded",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "previousBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "newBps",
"type": "uint16"
}
],
"name": "FeeBpsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "previousBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "newBps",
"type": "uint16"
}
],
"name": "ReferralBpsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "referrer",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "swapper",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "isBuy",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ReferralPaid",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bool",
"name": "isBuy",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "feeAmount",
"type": "uint256"
}
],
"name": "SwapExecuted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "previousTreasury",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "TreasuryUpdated",
"type": "event"
},
{
"inputs": [],
"name": "MAX_FEE_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOut",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "uint256",
"name": "amountOutMinimum",
"type": "uint256"
}
],
"name": "buyExactEthForTokens",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOut",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "uint256",
"name": "amountOutMinimum",
"type": "uint256"
},
{
"internalType": "address",
"name": "referrer",
"type": "address"
}
],
"name": "buyExactEthForTokens",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOut",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountOutMinimum",
"type": "uint256"
}
],
"name": "buyExactEthForTokensV2",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOut",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountOutMinimum",
"type": "uint256"
},
{
"internalType": "address",
"name": "referrer",
"type": "address"
}
],
"name": "buyExactEthForTokensV2",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "feeBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "referralBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueEth",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenIn",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
}
],
"name": "sellExactTokensForEth",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenIn",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "referrer",
"type": "address"
}
],
"name": "sellExactTokensForEth",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenIn",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
}
],
"name": "sellExactTokensForEthV2",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenIn",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "referrer",
"type": "address"
}
],
"name": "sellExactTokensForEthV2",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "newFeeBps",
"type": "uint16"
}
],
"name": "setFeeBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "newReferralBps",
"type": "uint16"
}
],
"name": "setReferralBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "swapRouter",
"outputs": [
{
"internalType": "contract IV3SwapRouter02",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "v2Factory",
"outputs": [
{
"internalType": "contract IUniswapV2Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth9",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IV3SwapRouter02 {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
returns (uint256 amountOut);
}
interface IUniswapV2Factory {
function getPair(address tokenA, address tokenB) external view returns (address);
}
interface IUniswapV2Pair {
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}
interface IWETH9 {
function deposit() external payable;
function withdraw(uint256 wad) external;
}
/**
* @title SentrySwapRouterV2
* @notice Second-generation fee-taking swap wrapper: routes through the
* canonical Uniswap V3 SwapRouter02 (like its predecessor) AND directly
* against Uniswap V2 pairs, so any WETH-paired token on either venue is
* swappable through one contract.
*
* V2 execution needs no external periphery: the pair address resolves
* from the immutable V2 factory (never caller-supplied, so a malicious
* pair can't be injected), WETH transfers to the pair, and `swap()` is
* called with constant-product math (0.3% pair fee). Sells use the
* canonical fee-on-transfer-safe pattern (pair balance delta) and buys
* enforce the minimum on the recipient's actual balance delta, so taxed
* tokens degrade honestly instead of reverting in the pair.
*
* Fee model unchanged from v1: `feeBps` of the ETH side of every swap
* goes to the treasury. Buys skim the ETH input before the swap; sells
* skim the ETH output after unwrapping. The token side is never touched.
*
* Referrals: the referral variants take a `referrer` that receives
* `referralBps` of the ETH side, carved out of the app fee (the
* swapper's all-in cost never changes). referralBps can never exceed
* feeBps, self-referrals pay the treasury in full, and a referrer that
* cannot receive ETH forfeits its cut to the treasury rather than
* blocking the swap.
*
* The custodial backend routes its V2 swaps through this contract too:
* a raw transfer-then-swap from an EOA would be atomically frontrunnable
* (anyone can call `pair.swap` after the transfer lands), while this
* contract does both in one transaction.
*
* Holds no funds between transactions; `rescue*` covers forced dust.
*/
contract SentrySwapRouterV2 is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
IV3SwapRouter02 public immutable swapRouter;
IUniswapV2Factory public immutable v2Factory;
address public immutable weth9;
address public treasury;
uint16 public feeBps;
/// @notice Referrer's slice of the ETH side, in bps of volume.
/// Always a subset of feeBps; 0 disables referral payouts.
uint16 public referralBps;
/// @notice Hard ceiling on the app fee: 3%.
uint16 public constant MAX_FEE_BPS = 300;
event SwapExecuted(
address indexed sender,
address indexed token,
bool indexed isBuy,
uint256 amountIn,
uint256 amountOut,
uint256 feeAmount
);
event FeeBpsUpdated(uint16 previousBps, uint16 newBps);
event ReferralBpsUpdated(uint16 previousBps, uint16 newBps);
event TreasuryUpdated(address previousTreasury, address newTreasury);
event ReferralPaid(
address indexed referrer,
address indexed swapper,
address indexed token,
bool isBuy,
uint256 amount
);
error ZeroAddress();
error ZeroAmount();
error FeeTooHigh();
error ReferralExceedsFee();
error NoV2Pair();
error SlippageExceeded(uint256 received, uint256 minimum);
error EthTransferFailed();
error DirectEthNotAccepted();
constructor(
address swapRouter_,
address v2Factory_,
address weth9_,
address treasury_,
uint16 feeBps_
) {
if (swapRouter_ == address(0) || weth9_ == address(0) || treasury_ == address(0)) {
revert ZeroAddress();
}
// v2Factory_ may be zero on chains without a V2 deployment; the
// V2 entrypoints then revert NoV2Pair.
if (feeBps_ > MAX_FEE_BPS) revert FeeTooHigh();
swapRouter = IV3SwapRouter02(swapRouter_);
v2Factory = IUniswapV2Factory(v2Factory_);
weth9 = weth9_;
treasury = treasury_;
feeBps = feeBps_;
}
/* ────────────────────────── V3 venue ────────────────────────── */
/**
* @notice Buy `tokenOut` with native ETH through the V3 pool at
* `poolFee`. Identical semantics to the v1 router.
*/
function buyExactEthForTokens(address tokenOut, uint24 poolFee, uint256 amountOutMinimum)
external
payable
nonReentrant
returns (uint256 amountOut)
{
return _buyV3(tokenOut, poolFee, amountOutMinimum, address(0));
}
/// @notice Referral variant: `referrer` receives referralBps of the
/// ETH input, carved out of the app fee. address(0) = no referral.
function buyExactEthForTokens(
address tokenOut,
uint24 poolFee,
uint256 amountOutMinimum,
address referrer
) external payable nonReentrant returns (uint256 amountOut) {
return _buyV3(tokenOut, poolFee, amountOutMinimum, referrer);
}
function _buyV3(address tokenOut, uint24 poolFee, uint256 amountOutMinimum, address referrer)
private
returns (uint256 amountOut)
{
if (msg.value == 0) revert ZeroAmount();
uint256 fee = (msg.value * feeBps) / 10_000;
uint256 swapIn = msg.value - fee;
amountOut = swapRouter.exactInputSingle{value: swapIn}(
IV3SwapRouter02.ExactInputSingleParams({
tokenIn: weth9,
tokenOut: tokenOut,
fee: poolFee,
recipient: msg.sender,
amountIn: swapIn,
amountOutMinimum: amountOutMinimum,
sqrtPriceLimitX96: 0
})
);
_payFee(referrer, tokenOut, true, fee);
emit SwapExecuted(msg.sender, tokenOut, true, msg.value, amountOut, fee);
}
/**
* @notice Sell `amountIn` of `tokenIn` for native ETH through the V3
* pool at `poolFee`. `minEthOut` is POST-fee, identical to v1.
*/
function sellExactTokensForEth(address tokenIn, uint24 poolFee, uint256 amountIn, uint256 minEthOut)
external
nonReentrant
returns (uint256 ethOut)
{
return _sellV3(tokenIn, poolFee, amountIn, minEthOut, address(0));
}
/// @notice Referral variant: `referrer` receives referralBps of the
/// ETH output, carved out of the app fee. address(0) = no referral.
function sellExactTokensForEth(
address tokenIn,
uint24 poolFee,
uint256 amountIn,
uint256 minEthOut,
address referrer
) external nonReentrant returns (uint256 ethOut) {
return _sellV3(tokenIn, poolFee, amountIn, minEthOut, referrer);
}
function _sellV3(address tokenIn, uint24 poolFee, uint256 amountIn, uint256 minEthOut, address referrer)
private
returns (uint256 ethOut)
{
if (amountIn == 0) revert ZeroAmount();
uint256 balanceBefore = IERC20(tokenIn).balanceOf(address(this));
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
uint256 received = IERC20(tokenIn).balanceOf(address(this)) - balanceBefore;
if (received == 0) revert ZeroAmount();
IERC20(tokenIn).forceApprove(address(swapRouter), received);
uint256 wethOut = swapRouter.exactInputSingle(
IV3SwapRouter02.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: weth9,
fee: poolFee,
recipient: address(this),
amountIn: received,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
ethOut = _settleEthSale(tokenIn, received, wethOut, minEthOut, referrer);
}
/* ────────────────────────── V2 venue ────────────────────────── */
/**
* @notice Buy `tokenOut` with native ETH against its WETH V2 pair.
* @param amountOutMinimum Minimum tokens the BUYER must actually
* receive (checked on their balance delta, so fee-on-transfer taxes
* count against the minimum instead of being silently eaten).
*/
function buyExactEthForTokensV2(address tokenOut, uint256 amountOutMinimum)
external
payable
nonReentrant
returns (uint256 amountOut)
{
return _buyV2(tokenOut, amountOutMinimum, address(0));
}
/// @notice Referral variant: `referrer` receives referralBps of the
/// ETH input, carved out of the app fee. address(0) = no referral.
function buyExactEthForTokensV2(address tokenOut, uint256 amountOutMinimum, address referrer)
external
payable
nonReentrant
returns (uint256 amountOut)
{
return _buyV2(tokenOut, amountOutMinimum, referrer);
}
function _buyV2(address tokenOut, uint256 amountOutMinimum, address referrer)
private
returns (uint256 amountOut)
{
if (msg.value == 0) revert ZeroAmount();
address pair = _v2Pair(tokenOut);
uint256 fee = (msg.value * feeBps) / 10_000;
uint256 swapIn = msg.value - fee;
IWETH9(weth9).deposit{value: swapIn}();
(uint256 reserveIn, uint256 reserveOut) = _orderedReserves(pair, weth9, tokenOut);
uint256 expectedOut = _getAmountOut(swapIn, reserveIn, reserveOut);
IERC20(weth9).safeTransfer(pair, swapIn);
uint256 balanceBefore = IERC20(tokenOut).balanceOf(msg.sender);
(uint256 out0, uint256 out1) = weth9 < tokenOut
? (uint256(0), expectedOut)
: (expectedOut, uint256(0));
IUniswapV2Pair(pair).swap(out0, out1, msg.sender, "");
amountOut = IERC20(tokenOut).balanceOf(msg.sender) - balanceBefore;
if (amountOut < amountOutMinimum) revert SlippageExceeded(amountOut, amountOutMinimum);
_payFee(referrer, tokenOut, true, fee);
emit SwapExecuted(msg.sender, tokenOut, true, msg.value, amountOut, fee);
}
/**
* @notice Sell `amountIn` of `tokenIn` for native ETH against its
* WETH V2 pair. Canonical fee-on-transfer-safe pattern: the swap
* uses what actually ARRIVED at the pair. `minEthOut` is POST-fee.
*/
function sellExactTokensForEthV2(address tokenIn, uint256 amountIn, uint256 minEthOut)
external
nonReentrant
returns (uint256 ethOut)
{
return _sellV2(tokenIn, amountIn, minEthOut, address(0));
}
/// @notice Referral variant: `referrer` receives referralBps of the
/// ETH output, carved out of the app fee. address(0) = no referral.
function sellExactTokensForEthV2(address tokenIn, uint256 amountIn, uint256 minEthOut, address referrer)
external
nonReentrant
returns (uint256 ethOut)
{
return _sellV2(tokenIn, amountIn, minEthOut, referrer);
}
function _sellV2(address tokenIn, uint256 amountIn, uint256 minEthOut, address referrer)
private
returns (uint256 ethOut)
{
if (amountIn == 0) revert ZeroAmount();
address pair = _v2Pair(tokenIn);
(uint256 reserveIn, uint256 reserveOut) = _orderedReserves(pair, tokenIn, weth9);
IERC20(tokenIn).safeTransferFrom(msg.sender, pair, amountIn);
uint256 arrived = IERC20(tokenIn).balanceOf(pair) - reserveIn;
if (arrived == 0) revert ZeroAmount();
uint256 wethOut = _getAmountOut(arrived, reserveIn, reserveOut);
(uint256 out0, uint256 out1) = tokenIn < weth9
? (uint256(0), wethOut)
: (wethOut, uint256(0));
IUniswapV2Pair(pair).swap(out0, out1, address(this), "");
ethOut = _settleEthSale(tokenIn, arrived, wethOut, minEthOut, referrer);
}
/* ────────────────────────── Admin ───────────────────────────── */
function setFeeBps(uint16 newFeeBps) external onlyOwner {
if (newFeeBps > MAX_FEE_BPS) revert FeeTooHigh();
if (newFeeBps < referralBps) revert ReferralExceedsFee();
emit FeeBpsUpdated(feeBps, newFeeBps);
feeBps = newFeeBps;
}
/// @notice Set the referrer's slice of the ETH side (bps of volume,
/// at most feeBps). The season dial: 25 -> 20 -> 15 -> 10.
function setReferralBps(uint16 newReferralBps) external onlyOwner {
if (newReferralBps > feeBps) revert ReferralExceedsFee();
emit ReferralBpsUpdated(referralBps, newReferralBps);
referralBps = newReferralBps;
}
function setTreasury(address newTreasury) external onlyOwner {
if (newTreasury == address(0)) revert ZeroAddress();
emit TreasuryUpdated(treasury, newTreasury);
treasury = newTreasury;
}
/// @notice Recover tokens that were sent here outside the swap flow.
function rescueToken(address token, uint256 amount) external onlyOwner {
IERC20(token).safeTransfer(owner(), amount);
}
/// @notice Recover ETH that was forced here outside the swap flow.
function rescueEth(uint256 amount) external onlyOwner {
_sendEth(owner(), amount);
}
/* ────────────────────────── Internals ───────────────────────── */
/// @dev Unwrap the WETH proceeds of a sale, skim the fee, enforce the
/// post-fee minimum, pay the seller. Shared by both venues.
function _settleEthSale(address tokenIn, uint256 amountIn, uint256 wethOut, uint256 minEthOut, address referrer)
private
returns (uint256 ethOut)
{
IWETH9(weth9).withdraw(wethOut);
uint256 fee = (wethOut * feeBps) / 10_000;
ethOut = wethOut - fee;
if (ethOut < minEthOut) revert SlippageExceeded(ethOut, minEthOut);
_payFee(referrer, tokenIn, false, fee);
_sendEth(msg.sender, ethOut);
emit SwapExecuted(msg.sender, tokenIn, false, amountIn, ethOut, fee);
}
/// @dev Split `fee` between the referrer and the treasury. The
/// referrer's cut is referralBps/feeBps of the fee, which equals
/// referralBps of the swap's ETH side. Self-referrals earn nothing,
/// and a referrer whose ETH transfer fails forfeits the cut to the
/// treasury so a hostile receiver can never block a swap.
function _payFee(address referrer, address token, bool isBuy, uint256 fee) private {
uint256 referralCut = 0;
if (fee != 0 && referralBps != 0 && referrer != address(0) && referrer != msg.sender) {
referralCut = (fee * referralBps) / feeBps;
if (referralCut != 0) {
(bool ok,) = referrer.call{value: referralCut}("");
if (ok) {
emit ReferralPaid(referrer, msg.sender, token, isBuy, referralCut);
} else {
referralCut = 0;
}
}
}
_sendEth(treasury, fee - referralCut);
}
/// @dev Pair lookup from the immutable factory only — callers never
/// supply pair addresses.
function _v2Pair(address token) private view returns (address pair) {
if (address(v2Factory) == address(0)) revert NoV2Pair();
pair = v2Factory.getPair(weth9, token);
if (pair == address(0)) revert NoV2Pair();
}
function _orderedReserves(address pair, address tokenIn, address tokenOut)
private
view
returns (uint256 reserveIn, uint256 reserveOut)
{
(uint112 r0, uint112 r1,) = IUniswapV2Pair(pair).getReserves();
(reserveIn, reserveOut) = tokenIn < tokenOut ? (uint256(r0), uint256(r1)) : (uint256(r1), uint256(r0));
}
/// @dev Standard x*y=k output with the 0.3% pair fee.
function _getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
private
pure
returns (uint256)
{
uint256 amountInWithFee = amountIn * 997;
return (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
}
function _sendEth(address to, uint256 amount) private {
if (amount == 0) return;
(bool ok,) = to.call{value: amount}("");
if (!ok) revert EthTransferFailed();
}
/// @dev Only WETH9.withdraw may push ETH here.
receive() external payable {
if (msg.sender != weth9) revert DirectEthNotAccepted();
}
}
Chain explorer2906msChain node78ms