Contract detail
X402ProxyFacilitatorV4
X402ProxyFacilitatorV4 is a verified Robinhood Chain smart contract at 0x6556ab0c…465bd43f with 0 indexed transactions.
- Address type
- Smart contract
- Transactions
- 0
- History coverage
- No indexed activity found
- Native balance
- 0 ETH
- Balance observed at
- Block #35599785
- Emitted events
- 1
- Verification
- Verified
- Compiler
- v0.8.27+commit.40a35a09
- Verification match
- partial
- Creation transaction
- 0x50d0dd07…250b5e55
- Created at block
- #850416
- Language
- solidity
- EVM version
- cancun
- License
- none
- Compiler optimization
- Enabled · 200 runs
Verified contract
Contract source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import {UD60x18, ud, ZERO} from "prb-math/UD60x18.sol";
import {exp} from "prb-math/ud60x18/Math.sol";
import {mulDiv} from "prb-math/Common.sol";
import "./libraries/TickMath.sol";
import {IAcrossSpokePool} from "./interfaces/IAcrossSpokePool.sol";
import {IAerodromePool} from "./interfaces/IAerodromePool.sol";
import {IFiatTokenV2} from "./interfaces/IFiatTokenV2.sol";
/// @title X402ProxyFacilitatorV4
/// @notice X402 payment facilitator contract.
/// @dev V4 adds ERC-2612 `transferWithPermit` and token-aware settlement. Cross-chain flows remain USDC-only.
contract X402ProxyFacilitatorV4 is
Initializable,
OwnableUpgradeable,
PausableUpgradeable,
UUPSUpgradeable,
EIP712Upgradeable,
ReentrancyGuardUpgradeable
{
using SafeERC20 for IERC20;
// -------- Constants --------
/// @notice Total MRDN supply allocated for cashback.
uint256 public constant CASHBACK_SUPPLY = 700_000_000 * 1e18; // 700M tokens with 18 decimals
/// @notice Maximum USDC value (6 decimals) eligible for cashback per transaction.
uint256 public constant MAX_CASHBACK_PER_TX = 5 * 1e6; // $5 max cashback per transaction (USDC has 6 decimals)
// Fee parameters
/// @notice Basis points divisor (10,000).
uint256 public constant BASIS_POINTS_DIVISOR = 10000;
/// @notice Maximum fee in basis points allowed by the contract.
uint256 public constant MAX_FEE_BPS = 1000; // 10% max fee
/// @notice Treasury fee in basis points (max bounded by contract).
uint256 public treasuryFeeBps;
/// @notice Recipient balances mapping (legacy; V4 settles instantly).
/// @dev recipientBalances[tokenAddress][walletAddress] = balance
mapping(address => mapping(address => uint256)) public recipientBalances;
/// @notice Accrued platform fee balance for a platform, per token.
/// @dev platformFees[tokenAddress][platformAddress] = balance
mapping(address => mapping(address => uint256)) public platformFees;
/// @notice Address that receives treasury fee withdrawals.
address public treasury;
/// @notice USDC token address used for EIP-3009 and cross-chain flows.
address public usdcContract;
/// @notice MRDN token address used for cashback payments.
address public mrdnToken;
/// @notice Aerodrome pool used for MRDN/USDC TWAP. Base only.
address public aerodromePool;
/// @notice TWAP window (in seconds) used for Aerodrome pricing. Base only.
uint32 public twapWindow;
// Cashback parameters
uint256 public constant CASHBACK_SCALE_FACTOR = 1e18; // For precision
uint256 public constant INITIAL_CASHBACK_RATE = 2e16; // 0.02 = 2%
/// @notice Cashback decay constant (UD60x18-scaled e.g., 6.9e9 for 6.9e-9)
uint256 public decayConstant;
/// @notice Total MRDN distributed as cashback so far.
uint256 public totalMrdnCashbackDistributed;
// Cross chain
uint256 constant BASE_CHAIN_ID = 8453;
/// @notice Remote USDC address on a destination chain (keyed by chain id).
mapping(uint256 => address) public remoteUsdc;
/// @notice Backend signer used to authenticate Across payloads.
address public backendSigner;
/// @notice Whether an Across message nonce has been used (replay protection).
mapping(bytes32 => bool) public usedAcrossNonces;
/// @notice Parameters used for initiating an Across bridge (depositV3).
struct AcrossDepositParams {
uint256 destinationChainId;
uint256 outputAmount;
uint32 quoteTimestamp;
uint32 fillDeadline;
}
/// @notice Across SpokePool used for depositV3 bridging.
address public acrossSpokePool;
/// @notice Accrued treasury fee balance per token.
mapping(address => uint256) public treasuryFees;
/// @notice Whether an address is whitelisted to call payment entrypoints.
mapping(address => bool) public isWhitelistedCaller;
// Across analytics: append delimiter + unique identifier to depositV3 calldata
// Delimiter specified by Across: 0x1dc0de
bytes3 private constant ACROSS_CALLDATA_DELIMITER = 0x1dc0de;
bytes2 private constant ACROSS_UNIQUE_ID = 0x00a3; // Provided by Across for this integrator
// EIP-712 typehash for service-signed Across messages
// keccak256("AcrossMessage(address originalSender,address creditedRecipient,address platform,uint256 expectedAmount,uint256 platformFeeBps,bytes32 nonce,uint256 sourceChainId,uint256 destinationChainId,address token,address recipientContract)")
// = 0x74800e6f1923b7ffd17d20ac98efb76f3f17d2e91699aa88f85d0da2160d8399
bytes32 private constant ACROSS_MESSAGE_TYPEHASH =
0x74800e6f1923b7ffd17d20ac98efb76f3f17d2e91699aa88f85d0da2160d8399;
// -------- Events --------
event TransferWithAuthorizationForwarded(
address indexed from,
address indexed to,
uint256 value,
bytes32 nonce,
address indexed platform,
uint256 platformFeeBps,
uint256 sourceChainId,
uint256 destinationChainId
);
event TransferWithPermitForwarded(
address indexed token,
address indexed from,
address indexed platform,
uint256 value,
uint256 deadline,
uint256 platformFeeBps,
uint256 sourceChainId,
uint256 destinationChainId
);
event PlatformFeeWithdrawn(address indexed platform, address indexed token, uint256 amount);
event TreasuryFeesWithdrawn(address indexed token, uint256 amount, address indexed to);
event InstantSettlement(
address indexed from,
address indexed recipient,
address indexed platform,
uint256 grossAmount,
uint256 platformFee,
uint256 treasuryFee,
uint256 netAmount,
uint256 sourceChainId,
uint256 destinationChainId
);
event FeeUpdated(uint256 newTreasuryFeeBps);
event TWAPWindowUpdated(uint32 newWindow);
event UsdcContractUpdated(address usdcContract);
event RemoteUsdcUpdated(uint256 indexed chainId, address remoteUsdc);
event CashbackPaid(
address indexed recipient,
uint256 usdcAmount,
uint256 mrdnAmount,
uint256 indexed sourceChainId,
uint256 indexed destinationChainId
);
event BackendSignerUpdated(address signer);
event WhitelistedCallerUpdated(address indexed caller, bool whitelisted);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
modifier onlyAcrossSpokePool() {
require(msg.sender == acrossSpokePool, "not across");
_;
}
// -------- Upgrades --------
/// @custom:oz-upgrades-unsafe-allow missing-initializer-call
/// @custom:oz-upgrades-validate-as-initializer
function initializeV4() public reinitializer(4) {}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
/// @notice Returns the facilitator version identifier.
function version() external pure returns (uint256) {
return 4;
}
// -------- Admin / Safety --------
/// @notice Emergency withdrawal of arbitrary tokens held by the facilitator (owner-only).
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
IERC20(token).safeTransfer(owner(), amount);
}
/// @notice Update the treasury fee (in basis points).
function setTreasuryFeeBps(uint256 newTreasuryFeeBps) external onlyOwner {
require(newTreasuryFeeBps <= MAX_FEE_BPS, "Fee too high");
treasuryFeeBps = newTreasuryFeeBps;
emit FeeUpdated(newTreasuryFeeBps);
}
/// @notice Update the Aerodrome TWAP window (in seconds).
function setTWAPWindow(uint32 newWindow) external onlyOwner {
require(newWindow >= 60, "Window too short"); // At least 1 minute
require(newWindow <= 3600, "Window too long"); // Max 1 hour
twapWindow = newWindow;
emit TWAPWindowUpdated(newWindow);
}
/// @notice Update the cashback decay constant (UD60x18-scaled).
function setDecayConstant(uint256 newDecayConstant) external onlyOwner {
decayConstant = newDecayConstant;
}
/// @notice Pause payment entrypoints (owner-only).
function pause() external onlyOwner {
_pause();
}
/// @notice Unpause payment entrypoints (owner-only).
function unpause() external onlyOwner {
_unpause();
}
/// @notice Configure the USDC token address used for EIP-3009 and Across settlement.
function setUsdcContract(address newUsdc) external onlyOwner {
require(newUsdc != address(0), "invalid usdc");
usdcContract = newUsdc;
emit UsdcContractUpdated(newUsdc);
}
/// @notice Configure the remote USDC token address for a given chain id.
function setRemoteUsdc(uint256 chainId, address remoteUsdc_) external onlyOwner {
require(chainId != block.chainid, "invalid chain");
require(remoteUsdc_ != address(0), "invalid usdc");
remoteUsdc[chainId] = remoteUsdc_;
emit RemoteUsdcUpdated(chainId, remoteUsdc_);
}
/// @notice Configure the Across SpokePool address.
function setAcrossSpokePool(address _spoke) external onlyOwner {
require(_spoke != address(0), "invalid spoke");
acrossSpokePool = _spoke;
}
/// @notice Configure the backend signer used for Across message authentication.
function setBackendSigner(address signer) external onlyOwner {
require(signer != address(0), "invalid signer");
backendSigner = signer;
emit BackendSignerUpdated(signer);
}
/// @notice Add/remove an address to/from the whitelist of trusted callers.
function setWhitelistedCaller(address caller, bool whitelisted) external onlyOwner {
isWhitelistedCaller[caller] = whitelisted;
emit WhitelistedCallerUpdated(caller, whitelisted);
}
// -------- Payments --------
/// @notice Same-chain payment using Circle's EIP-3009 authorization (USDC).
/// @dev `to` is ignored and kept for ABI compatibility; pass the facilitator address.
function transferWithAuthorization(
address from,
address,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature,
address recipient,
address platform,
uint256 platformFeeBps
) external whenNotPaused nonReentrant {
require(isWhitelistedCaller[msg.sender], "not whitelisted");
require(recipient != address(0), "invalid recipient");
IFiatTokenV2(usdcContract)
.transferWithAuthorization(from, address(this), value, validAfter, validBefore, nonce, signature);
_handleFeesAndCashback(
usdcContract, from, recipient, platform, value, platformFeeBps, block.chainid, block.chainid
);
emit TransferWithAuthorizationForwarded(
from, address(this), value, nonce, platform, platformFeeBps, block.chainid, block.chainid
);
}
/// @notice Same-chain or cross-chain payment using Circle's EIP-3009 authorization (USDC).
/// @dev If `depositParams.destinationChainId != block.chainid`, this initiates an Across bridge (USDC only).
/// `to` is ignored and kept for ABI compatibility; pass the facilitator address.
function transferWithAuthorization(
address from,
address,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature,
address recipient,
address platform,
uint256 platformFeeBps,
AcrossDepositParams calldata depositParams,
bytes calldata backendMessageSig
) external whenNotPaused nonReentrant {
require(isWhitelistedCaller[msg.sender], "not whitelisted");
require(recipient != address(0), "invalid recipient");
uint256 destChain = depositParams.destinationChainId;
require(destChain != 0, "invalid dest");
IFiatTokenV2(usdcContract)
.transferWithAuthorization(from, address(this), value, validAfter, validBefore, nonce, signature);
if (destChain == block.chainid) {
// Same-chain: no Across bridging. Process locally.
_handleFeesAndCashback(
usdcContract, from, recipient, platform, value, platformFeeBps, block.chainid, block.chainid
);
emit TransferWithAuthorizationForwarded(
from, address(this), value, nonce, platform, platformFeeBps, block.chainid, block.chainid
);
} else {
// Cross-chain: requires backend signature and Across deposit
_bridgeViaAcross(from, recipient, platform, value, platformFeeBps, nonce, depositParams, backendMessageSig);
emit TransferWithAuthorizationForwarded(
from, address(this), value, nonce, platform, platformFeeBps, block.chainid, destChain
);
}
}
/// @notice Same-chain payment using ERC-2612 permit + transferFrom.
/// @dev The `signature` must be 65 bytes formatted as `r || s || v`. `to` is ignored; pass the facilitator address.
function transferWithPermit(
address token,
address from,
address,
uint256 value,
uint256 deadline,
bytes memory signature,
address recipient,
address platform,
uint256 platformFeeBps
) external whenNotPaused nonReentrant {
require(isWhitelistedCaller[msg.sender], "not whitelisted");
require(token != address(0), "invalid token");
require(recipient != address(0), "invalid recipient");
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
_permit(token, from, value, deadline, signature);
IERC20(token).safeTransferFrom(from, address(this), value);
uint256 received = IERC20(token).balanceOf(address(this)) - balanceBefore;
require(received == value, "fee-on-transfer unsupported");
_handleFeesAndCashback(token, from, recipient, platform, value, platformFeeBps, block.chainid, block.chainid);
emit TransferWithPermitForwarded(
token, from, platform, value, deadline, platformFeeBps, block.chainid, block.chainid
);
}
// -------- Across callbacks --------
/// @notice Called by the Across relayer on the destination chain to finalize a bridged payment.
function handleV3AcrossMessage(
address tokenSent,
uint256 amount,
address relayer, // relayer - set to address(0) to allow all relayers
bytes calldata message
)
external
onlyAcrossSpokePool
whenNotPaused
nonReentrant
{
require(tokenSent == usdcContract, "unexpected token");
(
address originalSender,
address creditedRecipient,
address platform,
uint256 expectedAmount,
uint256 platformFeeBps,
bytes32 nonce_,
uint256 sourceChainId,
bytes memory backendSig
) = abi.decode(message, (address, address, address, uint256, uint256, bytes32, uint256, bytes));
require(amount == expectedAmount, "amount mismatch");
require(remoteUsdc[sourceChainId] != address(0), "unconf src");
require(
_verifyAcrossMessageSignature(
originalSender,
creditedRecipient,
platform,
expectedAmount,
platformFeeBps,
nonce_,
sourceChainId,
block.chainid,
usdcContract,
address(this),
backendSig
),
"bad backend sig"
);
// Now that the message is authenticated, mark nonce as used
require(!usedAcrossNonces[nonce_], "nonce used");
usedAcrossNonces[nonce_] = true;
_handleFeesAndCashback(
usdcContract,
originalSender,
creditedRecipient,
platform,
amount,
platformFeeBps,
sourceChainId,
block.chainid
);
emit TransferWithAuthorizationForwarded(
originalSender, address(this), amount, nonce_, platform, platformFeeBps, sourceChainId, block.chainid
);
}
/* ------- Internal payment related functions -------- */
function _bridgeViaAcross(
address from,
address recipient,
address platform,
uint256 amount,
uint256 platformFeeBps,
bytes32 nonce,
AcrossDepositParams memory depositParams,
bytes memory backendMessageSig
) internal {
require(backendMessageSig.length != 0, "signature required");
uint256 destChain = depositParams.destinationChainId;
address remoteUsdcAddress = remoteUsdc[destChain];
require(remoteUsdcAddress != address(0), "bridge config missing");
address spokePool = acrossSpokePool;
require(spokePool != address(0), "across not configured");
uint32 quoteTimestamp = depositParams.quoteTimestamp;
uint32 fillDeadline = depositParams.fillDeadline;
require(uint256(fillDeadline) > block.timestamp, "fill deadline passed");
require(uint256(quoteTimestamp) <= block.timestamp, "future quote");
require(uint256(fillDeadline) >= uint256(quoteTimestamp), "bad window");
// Use SafeERC20 to handle tokens with non-standard approve behavior
IERC20(usdcContract).forceApprove(spokePool, amount);
// Build depositV3 call data, append Across delimiter (0x1dc0de) and our unique identifier to calldata only
// Do NOT include the delimiter/ID inside any depositV3 parameter (including message).
bytes memory messageBytes = abi.encode(
from,
recipient,
platform,
depositParams.outputAmount,
platformFeeBps,
nonce,
block.chainid,
backendMessageSig
);
bytes memory encoded = abi.encodeWithSelector(
IAcrossSpokePool.depositV3.selector,
from,
address(this),
usdcContract,
remoteUsdcAddress,
amount,
depositParams.outputAmount,
destChain,
address(0),
quoteTimestamp,
fillDeadline,
0,
messageBytes
);
// Append delimiter + unique ID to calldata and perform low-level call
bytes memory fullCalldata = abi.encodePacked(encoded, ACROSS_CALLDATA_DELIMITER, ACROSS_UNIQUE_ID);
(bool ok,) = address(spokePool).call(fullCalldata);
require(ok, "across deposit failed");
}
function _handleFeesAndCashback(
address token,
address from,
address recipient,
address platform,
uint256 value,
uint256 platformFeeBps,
uint256 sourceChainId,
uint256 destinationChainId
) internal {
_settleAndDistribute(token, from, recipient, platform, value, platformFeeBps, sourceChainId, destinationChainId);
if (token == usdcContract && platform != treasury) {
_routeCashback(from, value, sourceChainId, destinationChainId);
}
}
function _settleAndDistribute(
address token,
address from,
address recipient,
address platform,
uint256 value,
uint256 platformFeeBps,
uint256 sourceChainId,
uint256 destinationChainId
) internal {
uint256 contractBalance = IERC20(token).balanceOf(address(this));
require(contractBalance >= value, "Insufficient contract balance");
uint256 platformFee = 0;
uint256 recipientPortion = value;
if (platform != address(0) && platformFeeBps > 0) {
require(platformFeeBps <= MAX_FEE_BPS, "Platform fee too high");
platformFee = (value * platformFeeBps) / BASIS_POINTS_DIVISOR;
recipientPortion = value - platformFee;
platformFees[token][platform] += platformFee;
}
// Treasury fee applies after platform fee (maintains V2 economics)
uint256 treasuryFee = 0;
// dont charge treasury fee if platform is treasury
if (platform != treasury) {
treasuryFee = (recipientPortion * treasuryFeeBps) / BASIS_POINTS_DIVISOR;
treasuryFees[token] += treasuryFee;
}
uint256 netAmount = recipientPortion - treasuryFee;
IERC20(token).safeTransfer(recipient, netAmount);
emit InstantSettlement(
from, recipient, platform, value, platformFee, treasuryFee, netAmount, sourceChainId, destinationChainId
);
}
function _routeCashback(address payer, uint256 value, uint256 sourceChainId, uint256 destinationChainId) internal {
// Cashback is only enabled on Base (destination must be Base)
if (_isBaseChain()) {
uint256 cashbackAmount = _calculateAndSendCashback(payer, value);
if (cashbackAmount > 0) {
emit CashbackPaid(payer, value, cashbackAmount, sourceChainId, destinationChainId);
}
}
}
function _permit(address token, address owner, uint256 value, uint256 deadline, bytes memory signature) internal {
require(signature.length == 65, "bad sig");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
if (v < 27) {
v += 27;
}
IERC20Permit(token).permit(owner, address(this), value, deadline, v, r, s);
}
// -------- Withdrawals --------
/// @notice Withdraw accrued platform fees for `token` (to `msg.sender`).
function withdrawPlatformFees(address token) external nonReentrant {
uint256 platformBalance = platformFees[token][msg.sender];
require(platformBalance > 0, "No platform fees to withdraw");
// Check that the contract has sufficient token balance
uint256 contractBalance = IERC20(token).balanceOf(address(this));
require(contractBalance >= platformBalance, "Insufficient contract balance");
// Reset platform balance for this token
platformFees[token][msg.sender] = 0;
IERC20(token).safeTransfer(msg.sender, platformBalance);
emit PlatformFeeWithdrawn(msg.sender, token, platformBalance);
}
/// @notice Withdraw accrued treasury fees for `token` (to `treasury`).
/// @dev Callable by anyone; always transfers to the configured treasury address.
function withdrawTreasuryFees(address token) external {
uint256 amount = treasuryFees[token];
require(amount > 0, "No treasury fees");
treasuryFees[token] = 0;
IERC20(token).safeTransfer(treasury, amount);
emit TreasuryFeesWithdrawn(token, amount, treasury);
}
function _isBaseChain() internal view returns (bool) {
// Treat both Base mainnet (8453) and Base Sepolia (84532) as Base
return block.chainid == BASE_CHAIN_ID || block.chainid == 84532;
}
// -------- Legacy balance views --------
/// @notice Returns the stored balance for a wallet/token (legacy; V4 settles instantly).
function balanceStored(address wallet, address token) external view returns (uint256) {
return recipientBalances[token][wallet];
}
/// @notice Returns the withdrawable balance for a wallet/token (legacy; V4 settles instantly).
function balanceReadyToWithdrawal(address wallet, address token) external view returns (uint256) {
// For now, all stored balance is ready to withdraw
// This can be extended later to handle pending vs ready states
return recipientBalances[token][wallet];
}
// -------- Pricing / Cashback views --------
/// @notice Returns the current MRDN price (in USDC units) derived from Aerodrome TWAP.
function getMRDNPrice() external view returns (uint256) {
return _getMRDNPriceFromTWAP();
}
function _getMRDNPriceFromTWAP() internal view returns (uint256) {
if (aerodromePool == address(0)) return 10_000; // Default to 0.01 USD as safety measure (USDC has 6 decimals)
int24 twapTick = _getTimeWeightedAverageTick(aerodromePool, twapWindow);
return getQuoteAtTick(twapTick, 1e18, mrdnToken, usdcContract);
}
function _getTimeWeightedAverageTick(address pool, uint32 twapInterval) internal view returns (int24) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = twapInterval; // from (twapInterval) seconds ago
secondsAgos[1] = 0; // to now
(int56[] memory tickCumulatives,) = IAerodromePool(pool).observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int56 interval = int56(uint56(twapInterval));
int56 weightedTick = tickCumulativesDelta / interval;
if (tickCumulativesDelta < 0 && (tickCumulativesDelta % interval != 0)) {
// Floor division toward negative infinity to mirror Uniswap conventions
weightedTick -= 1;
}
return int24(weightedTick);
}
/// @notice Uniswap-style quote helper used for TWAP pricing.
function getQuoteAtTick(int24 tick, uint256 baseAmount, address baseToken, address quoteToken)
public
pure
returns (uint256 quoteAmount)
{
uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick);
if (sqrtRatioX96 <= type(uint128).max) {
uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96;
if (baseToken < quoteToken) {
quoteAmount = mulDiv(ratioX192, baseAmount, uint256(1) << 192);
} else {
quoteAmount = mulDiv(uint256(1) << 192, baseAmount, ratioX192);
}
} else {
uint256 ratioX128 = mulDiv(sqrtRatioX96, sqrtRatioX96, uint256(1) << 64);
if (baseToken < quoteToken) {
quoteAmount = mulDiv(ratioX128, baseAmount, uint256(1) << 128);
} else {
quoteAmount = mulDiv(uint256(1) << 128, baseAmount, ratioX128);
}
}
}
/// @notice Current cashback rate in basis points (based on total distributed and decay constant).
function getCurrentCashbackRate() external view returns (uint256) {
if (mrdnToken == address(0)) return 0;
return _calculateCashbackRate(totalMrdnCashbackDistributed);
}
/// @notice Preview the MRDN cashback amount for a given USDC amount.
function calculateCashbackAmount(uint256 usdcAmount) external view returns (uint256) {
if (mrdnToken == address(0)) return 0;
uint256 mrdnBalance = IERC20(mrdnToken).balanceOf(address(this));
if (mrdnBalance == 0) return 0;
uint256 cashbackRate = _calculateCashbackRate(totalMrdnCashbackDistributed);
uint256 mrdnPriceInUsdc = _getMRDNPriceFromTWAP();
if (mrdnPriceInUsdc == 0) return 0;
uint256 cashbackUsdcValue = (usdcAmount * cashbackRate) / BASIS_POINTS_DIVISOR;
if (cashbackUsdcValue > MAX_CASHBACK_PER_TX) {
cashbackUsdcValue = MAX_CASHBACK_PER_TX;
}
uint256 cashbackMrdnAmount = (cashbackUsdcValue * 1e18) / mrdnPriceInUsdc;
if (cashbackMrdnAmount > mrdnBalance) {
cashbackMrdnAmount = mrdnBalance;
}
return cashbackMrdnAmount;
}
function _calculateAndSendCashback(address recipient, uint256 usdcAmount) internal returns (uint256) {
if (mrdnToken == address(0)) return 0;
uint256 mrdnBalance = IERC20(mrdnToken).balanceOf(address(this));
if (mrdnBalance == 0) return 0;
// Calculate cashback percentage: y(x) = 0.02 * exp(-6.9e-9 * x)
// where x is the cumulative MRDN cashback distributed
uint256 cashbackRate = _calculateCashbackRate(totalMrdnCashbackDistributed);
// Get MRDN price in USDC from TWAP
uint256 mrdnPriceInUsdc = _getMRDNPriceFromTWAP();
if (mrdnPriceInUsdc == 0) return 0;
// Calculate cashback in USDC value, then convert to MRDN amount
uint256 cashbackUsdcValue = (usdcAmount * cashbackRate) / BASIS_POINTS_DIVISOR;
if (cashbackUsdcValue > MAX_CASHBACK_PER_TX) {
cashbackUsdcValue = MAX_CASHBACK_PER_TX;
}
// Convert USDC value to MRDN amount
uint256 cashbackMrdnAmount = (cashbackUsdcValue * 1e18) / mrdnPriceInUsdc;
// Cap at available balance
if (cashbackMrdnAmount > mrdnBalance) {
cashbackMrdnAmount = mrdnBalance;
}
// Send MRDN cashback if amount is greater than 0
if (cashbackMrdnAmount > 0) {
totalMrdnCashbackDistributed += cashbackMrdnAmount;
IERC20(mrdnToken).safeTransfer(recipient, cashbackMrdnAmount);
}
return cashbackMrdnAmount;
}
function _calculateCashbackRate(uint256 x) internal view returns (uint256) {
// y(x) = 0.02 * exp(-decayConstant * x)
// where x is the amount of tokens sent as cashback (capped at CASHBACK_SUPPLY)
if (x == 0) return 200; // 2% in basis points
if (x >= CASHBACK_SUPPLY) return 0;
// Calculate -6.9e-9 * x
// 6.9e-9 = 6.9 / 1e9 = 6.9 / 1000000000
// First convert x to UD60x18 (x is already in wei, which is 1e18 units)
UD60x18 xValue = ud(x);
// Use the configurable decay constant (already in UD60x18 format)
// For 6.9e-9: 6.9e-9 * 1e18 = 6.9e9 = 6900000000
UD60x18 coefficient = ud(decayConstant);
// Calculate the exponent: 6.9e-9 * x
// xValue is already in 1e18 scale, coefficient is 6.9e9
// So we need to divide by 1e18 to get the correct scale
UD60x18 exponentValue = xValue.mul(coefficient).div(ud(1e18));
// Calculate exp(-exponent)
// Since we need exp(-x), and UD60x18 doesn't handle negatives directly,
// we use exp(-x) = 1 / exp(x)
UD60x18 expValue;
if (exponentValue.eq(ZERO)) {
expValue = ud(1e18);
} else if (exponentValue.gte(ud(20e18))) {
// For very large exponents, exp(-x) approaches 0
return 0;
} else {
// Calculate exp(exponent) then take reciprocal
UD60x18 expPositive = exp(exponentValue);
expValue = ud(1e18).div(expPositive);
}
// Multiply by 0.02 to get the cashback rate
// 0.02 in UD60x18 = 0.02 * 1e18 = 2e16
UD60x18 baseRate = ud(2e16);
UD60x18 cashbackRate = baseRate.mul(expValue);
// Convert to basis points (multiply by 10000)
// cashback rate is currently in decimal (e.g., 0.02), need to convert to bps
// First convert from UD60x18 to uint256, then scale to basis points
uint256 rateInWei = cashbackRate.unwrap();
uint256 cashbackBps = (rateInWei * 10000) / 1e18;
// Cap at 200 basis points (2%)
if (cashbackBps > 200) {
cashbackBps = 200;
}
return cashbackBps;
}
// -------- Across signature helpers --------
/// @notice Returns the EIP-712 digest the backend is expected to sign for an Across payload.
function generateBackendSigDigest(
address originalSender,
address creditedRecipient,
address platform,
uint256 expectedAmount,
uint256 platformFeeBps,
bytes32 nonce,
uint256 sourceChainId,
uint256 destinationChainId,
address token,
address recipientContract
) external view returns (bytes32) {
bytes32 structHash = keccak256(
abi.encode(
ACROSS_MESSAGE_TYPEHASH,
originalSender,
creditedRecipient,
platform,
expectedAmount,
platformFeeBps,
nonce,
sourceChainId,
destinationChainId,
token,
recipientContract
)
);
return _hashTypedDataV4(structHash);
}
/// @notice Verifies a backend signature for an Across payload.
function verifyAcrossMessageSignature(
address originalSender,
address creditedRecipient,
address platform,
uint256 expectedAmount,
uint256 platformFeeBps,
bytes32 nonce,
uint256 sourceChainId,
uint256 destinationChainId,
address token,
address recipientContract,
bytes memory backendSig
) external view returns (bool) {
return _verifyAcrossMessageSignature(
originalSender,
creditedRecipient,
platform,
expectedAmount,
platformFeeBps,
nonce,
sourceChainId,
destinationChainId,
token,
recipientContract,
backendSig
);
}
function _verifyAcrossMessageSignature(
address originalSender,
address creditedRecipient,
address platform,
uint256 expectedAmount,
uint256 platformFeeBps,
bytes32 nonce,
uint256 sourceChainId,
uint256 destinationChainId,
address token,
address recipientContract,
bytes memory backendSig
) internal view returns (bool) {
require(backendSigner != address(0), "signer not set");
bytes32 structHash = keccak256(
abi.encode(
ACROSS_MESSAGE_TYPEHASH,
originalSender,
creditedRecipient,
platform,
expectedAmount,
platformFeeBps,
nonce,
sourceChainId,
destinationChainId,
token,
recipientContract
)
);
bytes32 digest = _hashTypedDataV4(structHash);
address recovered = ECDSA.recover(digest, backendSig);
return recovered == backendSigner;
}
}