Contract detail
NativeRouter
Live Robinhood Chain account facts for 0xa5ec1f0a…da658ea4 with at least 25 observed transactions. The history total is updating as indexing continues.
- Address type
- Smart contract
- Transactions
- 25+ observed
- First indexed block
- #60424
- Latest indexed block
- #70044978
- History coverage
- Recent activity available; total is catching up
- ERC-20 transfers
- 1
- Emitted events
- 4
- Verification
- Verified
- Compiler
- v0.8.28+commit.7893614a
- Verification match
- full
- Creation transaction
- 0x5c7c8898…87d4a263
- Created at block
- #60424
- Language
- solidity
- EVM version
- cancun
- License
- none
- Compiler optimization
- Enabled · 2000000 runs
Related activity
Transactions
Verified contract
Contract source code
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {EIP712, ECDSA} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {IWETH9} from "./interfaces/IWETH9.sol";
import {INativeRouter} from "./interfaces/INativeRouter.sol";
import "./libraries/ConstantsLib.sol";
import {Multicall} from "./libraries/Multicall.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {TransferHelper} from "./libraries/TransferHelper.sol";
import {ReentrancyGuardTransient} from "./libraries/ReentrancyGuardTransient.sol";
import {CreditVault} from "./CreditVault.sol";
import {NativeRFQPool} from "./NativeRFQPool.sol";
contract NativeRouter is INativeRouter, EIP712, Ownable2Step, Pausable, Multicall, ReentrancyGuardTransient {
using SafeERC20 for IERC20;
using SafeCast for uint256;
/// @dev Address of the CreditVault
address public vault;
/// @dev Address of the Wrapped Ether (WETH9) contract
address public immutable WETH9;
/// @notice Mapping of all trusted Native pools
mapping(address => bool) public isNativePools;
/// @dev Address of signer authorized to sign tradeRFQT signatures
mapping(address => bool) public signers;
/// @notice Mapping of all trusted external swap sources (pools, routers)
mapping(address => bool) public externalSwapSources;
/*//////////////////////////////////////////////////////////////////////////
RECEIVE FUNCTION
//////////////////////////////////////////////////////////////////////////*/
receive() external payable {
require(msg.sender == WETH9, ErrorsLib.OnlyWETH9());
}
/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/
constructor(address _vault, address _WETH9, address _signer) EIP712("Native Router", "1") {
require(_vault != address(0) && _WETH9 != address(0), ErrorsLib.ZeroAddress());
vault = _vault;
WETH9 = _WETH9;
setSigner(_signer, true);
}
/*//////////////////////////////////////////////////////////////////////////
EXTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Execute a Request for Quote (RFQ) trade based on market maker's signed quote
/// @param quote The RFQ quote containing trade details and signatures
/// @param actualSellerAmount The actual amount of tokens to be sold, can be different from quote amount within deviation limit
/// @param actualMinOutputAmount The minimum amount of tokens to be received, overrides quote's amountOutMinimum if provided
function tradeRFQT(
RFQTQuote memory quote,
uint256 actualSellerAmount,
uint256 actualMinOutputAmount
) external payable override nonReentrant whenNotPaused {
require(block.timestamp <= quote.deadlineTimestamp, ErrorsLib.QuoteExpired());
require(quote.confidenceExtractedValueN <= quote.deadlineTimestamp, ErrorsLib.InvalidConfidenceFactorN());
require(quote.confidenceExtractedValueE <= MAX_CONFIDENCE_FACTOR_E, ErrorsLib.InvalidConfidenceFactorE());
require(quote.widgetFee.feeRate <= MAX_WIDGET_FEE_BIPS, ErrorsLib.InvalidWidgetFeeRate());
require(isNativePools[quote.pool], ErrorsLib.InvalidNativePool());
_verifyRFQSignature(quote);
// set effective amount to quote amount first
uint256 effectiveSellerTokenAmount = quote.sellerTokenAmount;
// adjust the effectiveSellerTokenAmount and amountOutMinimum
if (actualSellerAmount > 0) {
if (actualSellerAmount > quote.sellerTokenAmount) {
// Upward deviation: max 10%
uint256 upwardDeviation =
(actualSellerAmount - quote.sellerTokenAmount) * 10_000 / quote.sellerTokenAmount;
require(upwardDeviation < MAX_AMOUNT_UPWARD_DEVIATION_BPS, ErrorsLib.AmountDeviationExceeds());
} else {
// Downward deviation: max 50%
uint256 downwardDeviation =
(quote.sellerTokenAmount - actualSellerAmount) * 10_000 / quote.sellerTokenAmount;
require(downwardDeviation < MAX_AMOUNT_DOWNWARD_DEVIATION_BPS, ErrorsLib.AmountDeviationExceeds());
}
effectiveSellerTokenAmount = actualSellerAmount;
}
if (actualMinOutputAmount > 0) {
quote.amountOutMinimum = actualMinOutputAmount;
} else if (actualSellerAmount > 0 && quote.amountOutMinimum > 0) {
// Proportionally adjust amountOutMinimum when actualSellerAmount is provided but actualMinOutputAmount is not
quote.amountOutMinimum = (quote.amountOutMinimum * effectiveSellerTokenAmount) / quote.sellerTokenAmount;
}
// charge widget fee based on the actual amount
effectiveSellerTokenAmount = _transferSellerToken(
quote.multiHop,
NativeRFQPool(payable(quote.pool)).treasury(),
quote.sellerToken,
effectiveSellerTokenAmount,
quote.widgetFee
);
NativeRFQPool(payable(quote.pool)).tradeRFQT(effectiveSellerTokenAmount, quote);
}
/// @notice Execute an external swap through external liquidity sources
/// @param params The external swap parameters
function externalSwap(ExternalSwap calldata params) external payable override nonReentrant whenNotPaused {
require(block.timestamp <= params.deadlineTimestamp, ErrorsLib.QuoteExpired());
require(externalSwapSources[params.source], ErrorsLib.InvalidExternalSwapSource());
_verifyExternalSwap(params);
bool isBuyerTokenNative = params.buyerToken == address(0);
// Record recipient's initial balance
uint256 initialBuyerTokenBalance =
isBuyerTokenNative ? params.recipient.balance : IERC20(params.buyerToken).balanceOf(params.recipient);
// Handle sellerToken transfer and approval
if (params.sellerToken == address(0)) {
// Native token
require(msg.value == params.sellerTokenAmount, ErrorsLib.InvalidAmount());
} else {
// ERC20 token
require(msg.value == 0, ErrorsLib.UnexpectedMsgValue());
IERC20(params.sellerToken).safeTransferFrom(msg.sender, address(this), params.sellerTokenAmount);
// Approve the spender to use exact amount
IERC20(params.sellerToken).safeApprove(params.spender, params.sellerTokenAmount);
}
// Execute external swap call
(bool success,) = params.source.call{value: msg.value}(params.callData);
require(success, ErrorsLib.ExternalCallFailed());
// Check recipient's final balance
uint256 finalBuyerTokenBalance =
isBuyerTokenNative ? params.recipient.balance : IERC20(params.buyerToken).balanceOf(params.recipient);
uint256 receivedAmount = finalBuyerTokenBalance - initialBuyerTokenBalance;
require(
receivedAmount >= params.amountOutMinimum,
ErrorsLib.NotEnoughAmountOut(receivedAmount, params.amountOutMinimum)
);
emit ExternalSwapped(
params.source,
params.recipient,
params.sellerToken,
params.buyerToken,
params.sellerTokenAmount,
receivedAmount,
params.quoteId
);
}
/// @notice Unwraps WETH9 to ETH by unwrapping all WETH currently in this contract
/// @dev SECURITY CONSIDERATION: This function is permissionless and will unwrap ALL WETH in the contract
/// @dev Must be called immediately after receiving WETH in the same multicall transaction
/// @dev Typical usage flow:
/// 1. Call tradeRFQ to receive WETH (in multicall)
/// 2. Call this function immediately in the same multicall to unwrap received WETH
/// 3. Never leave WETH in this contract between transactions
/// @param recipient The address that will receive the unwrapped ETH
function unwrapWETH9(address recipient) public payable nonReentrant {
require(recipient != address(0), ErrorsLib.ZeroAddress());
uint256 balanceWETH9 = IWETH9(WETH9).balanceOf(address(this));
require(balanceWETH9 > 0, ErrorsLib.InsufficientWETH9());
uint256 beforeBalance = address(this).balance;
IWETH9(WETH9).withdraw(balanceWETH9);
balanceWETH9 = address(this).balance - beforeBalance;
TransferHelper.safeTransferETH(recipient, balanceWETH9);
emit UnwrapWETH9(recipient, balanceWETH9);
}
/*//////////////////////////////////////////////////////////////////////////
ADMIN FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Emergency function to refund stuck ETH from the contract
/// @dev Can only be called by contract owner in emergency situations
/// @param recipient The address to receive the refunded ETH
/// @param amount The amount of ETH to refund
function refundETH(address recipient, uint256 amount) public payable onlyOwner nonReentrant {
amount = Math.min(address(this).balance, amount);
TransferHelper.safeTransferETH(recipient, amount);
emit RefundETH(recipient, amount);
}
/// @notice Emergency function to refund stuck ERC20 tokens from the contract
/// @dev Can only be called by contract owner in emergency situations
/// @param token The address of the ERC20 token to refund
/// @param recipient The address to receive the refunded tokens
/// @param amount The amount of tokens to refund
function refundERC20(address token, address recipient, uint256 amount) public payable onlyOwner nonReentrant {
amount = Math.min(IERC20(token).balanceOf(address(this)), amount);
TransferHelper.safeTransfer(token, recipient, amount);
emit RefundERC20(token, recipient, amount);
}
/// @notice Set the authorized signer for widget fee and auto-sign messages
/// @dev Can only be called by contract owner
/// @param signer The new signer address to be set
/// @param isSigner Whether the signer is authorized to sign RFQ trade and fillOrder signatures
function setSigner(address signer, bool isSigner) public onlyOwner {
require(signer != address(0), ErrorsLib.ZeroAddress());
signers[signer] = isSigner;
emit SignerUpdated(signer, isSigner);
}
/// @notice Updates native pool whitelist status
/// @dev Only callable by owner
/// @param isActive to whitelist, false to remove from whitelist
function setNativePool(address pool, bool isActive) external onlyOwner {
require(pool != address(0), ErrorsLib.ZeroAddress());
isNativePools[pool] = isActive;
emit NativePoolUpdated(pool, isActive);
}
/// @notice Updates external swap source whitelist status
/// @param source The address of the external swap source
/// @param isActive true to whitelist, false to remove from whitelist
function setExternalSwapSource(address source, bool isActive) external onlyOwner {
require(source != address(0), ErrorsLib.ZeroAddress());
externalSwapSources[source] = isActive;
emit ExternalSwapSourceUpdated(source, isActive);
}
/// @notice Pauses all RFQ and fillOrder operations
function pause() external onlyOwner {
_pause();
}
/// @notice Unpauses all RFQ and fillOrder operations
function unpause() external onlyOwner {
_unpause();
}
/*//////////////////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
function _transferSellerToken(
bool multiHop,
address payee,
address sellerToken,
uint256 sellerTokenAmount,
WidgetFee memory widgetFee
) internal returns (uint256 effectiveSellerTokenAmount) {
if (msg.value > 0 && !multiHop) {
require(sellerToken == address(0), ErrorsLib.UnexpectedMsgValue());
require(sellerTokenAmount == msg.value, ErrorsLib.InvalidAmount());
// slither-disable-next-line arbitrary-send-eth
IWETH9(WETH9).deposit{value: sellerTokenAmount}();
effectiveSellerTokenAmount = _chargeWidgetFee(widgetFee, sellerTokenAmount, WETH9, true);
TransferHelper.safeTransfer(WETH9, payee, effectiveSellerTokenAmount);
} else {
effectiveSellerTokenAmount = _chargeWidgetFee(widgetFee, sellerTokenAmount, sellerToken, multiHop);
if (multiHop) {
TransferHelper.safeTransfer(sellerToken, payee, effectiveSellerTokenAmount);
} else {
TransferHelper.safeTransferFrom(sellerToken, msg.sender, payee, effectiveSellerTokenAmount);
}
}
}
function _chargeWidgetFee(
WidgetFee memory widgetFee,
uint256 amountIn,
address sellerToken,
bool hasAlreadyPaid
) internal returns (uint256) {
uint256 fee = widgetFee.feeRate > 0 ? (amountIn * widgetFee.feeRate) / 10_000 : 0;
if (fee > 0) {
if (hasAlreadyPaid) {
IERC20(sellerToken).safeTransfer(widgetFee.feeRecipient, fee);
} else {
TransferHelper.safeTransferFrom(sellerToken, msg.sender, widgetFee.feeRecipient, fee);
}
emit WidgetFeeTransfer(widgetFee.feeRecipient, widgetFee.feeRate, fee, sellerToken);
amountIn -= fee;
}
return amountIn;
}
function _verifyRFQSignature(RFQTQuote memory quote) internal view {
bytes32 quoteHash = keccak256(
abi.encode(
quote.pool,
quote.recipient,
quote.sellerToken,
quote.buyerToken,
quote.sellerTokenAmount,
quote.buyerTokenAmount,
quote.deadlineTimestamp,
quote.confidenceExtractedValueT,
quote.confidenceExtractedValueE,
quote.confidenceExtractedValueM,
quote.multiHop,
quote.signature
)
);
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(
RFQ_QUOTE_WIDGET_SIGNATURE_HASH, quoteHash, quote.widgetFee.feeRecipient, quote.widgetFee.feeRate
)
)
);
address recoveredSigner = ECDSA.recover(digest, quote.widgetFeeSignature);
require(signers[recoveredSigner], ErrorsLib.InvalidSignature());
}
function _verifyExternalSwap(ExternalSwap calldata params) internal view {
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(
EXTERNAL_SWAP_SIGNATURE_HASH,
params.source,
params.spender,
params.recipient,
params.sellerToken,
params.sellerTokenAmount,
params.buyerToken,
params.amountOutMinimum,
params.deadlineTimestamp,
params.quoteId,
keccak256(params.callData)
)
)
);
address recoveredSigner = ECDSA.recover(digest, params.signature);
require(signers[recoveredSigner], ErrorsLib.InvalidSignature());
}
}