RainFeeLocker
0x57086be6d8009c23d817a387bfeb91ecf02a66d3
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
5,417 bytes
ABI entries
49
15 read · 9 write
License
none
Contract information
- Address
- 0x57086be6d8009c23d817a387bfeb91ecf02a66d3
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x2ABcA1C7aD…6d70230e60
- Creation tx
- 0xa06a801e76…72ec2d92c3
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (15)
PROTOCOL_FEE_SHARE() → uint256
deployerTokenCount(address) → uint256
deployerTokens(address, uint256) → address
factory() → address
feeRecipientTokenCount(address) → uint256
feeRecipientTokens(address, uint256) → address
feeRedirects(address) → address
getLaunchedToken(address) → tuple
onERC721Received(address, address, uint256, bytes) → bytes4
owner() → address
pendingEth(address) → uint256
pendingOwner() → address
protocolFeeRecipient() → address
tokenProtocolFeeShares(address) → uint256
totalPendingEth() → uint256
Events (9)
FactoryUpdatedFeePaymentDeferredFeeRedirectUpdatedFeesClaimedOwnershipTransferStartedOwnershipTransferredPendingFeesWithdrawnPositionLockedProtocolFeeRecipientUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
},
{
"internalType": "address",
"name": "initialProtocolFeeRecipient",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyInitialized",
"type": "error"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "NoFeesToCollect",
"type": "error"
},
{
"inputs": [],
"name": "NotAuthorized",
"type": "error"
},
{
"inputs": [],
"name": "NotDeployer",
"type": "error"
},
{
"inputs": [],
"name": "NotFactory",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "PositionAlreadyLocked",
"type": "error"
},
{
"inputs": [],
"name": "PositionNotHeld",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "TokenNotFound",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "factory",
"type": "address"
}
],
"name": "FactoryUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "FeePaymentDeferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newFeeWallet",
"type": "address"
}
],
"name": "FeeRedirectUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "caller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethTotal",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensBurned",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "protocolEth",
"type": "uint256"
}
],
"name": "FeesClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"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": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "PendingFeesWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "deployer",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "dexId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "pairToken",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "positionManager",
"type": "address"
}
],
"name": "PositionLocked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "ProtocolFeeRecipientUpdated",
"type": "event"
},
{
"inputs": [],
"name": "PROTOCOL_FEE_SHARE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "collectFees",
"outputs": [
{
"internalType": "uint256",
"name": "ethTotal",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokensBurned",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "deployer_",
"type": "address"
}
],
"name": "deployerTokenCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "deployer",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "deployerTokens",
"outputs": [
{
"internalType": "address",
"name": "tokens",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient_",
"type": "address"
}
],
"name": "feeRecipientTokenCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "feeRecipientTokens",
"outputs": [
{
"internalType": "address",
"name": "tokens",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "feeRedirects",
"outputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "getLaunchedToken",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "deployer",
"type": "address"
},
{
"internalType": "address",
"name": "pairedToken",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "dexId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "launchConfigId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "restrictionsEndBlock",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "supply",
"type": "uint256"
},
{
"internalType": "bool",
"name": "isToken0",
"type": "bool"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "bool",
"name": "exists",
"type": "bool"
},
{
"internalType": "uint256",
"name": "initialBuyAmount",
"type": "uint256"
}
],
"internalType": "struct IRainLaunchFactory.LaunchedToken",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "factory_",
"type": "address"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "lockPosition",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "pendingEth",
"outputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "protocolFeeRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newFeeWallet",
"type": "address"
}
],
"name": "setFeeRedirect",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "setProtocolFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "tokenProtocolFeeShares",
"outputs": [
{
"internalType": "uint256",
"name": "share",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalPendingEth",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "withdrawPendingFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {INonfungiblePositionManager, IWETH9} from "./interfaces/IExternal.sol";
import {IRainLaunchFactory} from "./interfaces/IRainLaunch.sol";
contract RainFeeLocker is Ownable2Step, ReentrancyGuard, IERC721Receiver {
using SafeERC20 for IERC20;
uint256 public constant PROTOCOL_FEE_SHARE = 50;
address private constant DEAD = 0x000000000000000000000000000000000000dEaD;
error NotFactory();
error NotAuthorized();
error EthTransferFailed();
error NotDeployer();
error TokenNotFound();
error PositionNotHeld();
error PositionAlreadyLocked();
error NoFeesToCollect();
error AlreadyInitialized();
error ZeroAddress();
event FactoryUpdated(address indexed factory);
event PositionLocked(
address indexed token,
address indexed deployer,
uint256 indexed dexId,
address pairToken,
uint256 positionId,
address positionManager
);
event FeesClaimed(
address indexed token,
address indexed caller,
uint256 ethTotal,
uint256 tokensBurned,
uint256 creatorEth,
uint256 protocolEth
);
event FeePaymentDeferred(address indexed recipient, uint256 amount);
event PendingFeesWithdrawn(address indexed recipient, uint256 amount);
event FeeRedirectUpdated(address indexed token, address indexed newFeeWallet);
event ProtocolFeeRecipientUpdated(address recipient);
address public factory;
address public protocolFeeRecipient;
/// ETH owed to recipients whose direct payout failed (reverting/gas-heavy receiver).
/// A hostile fee wallet can therefore never block collectFees for anyone — its share is
/// parked here and withdrawn separately. totalPendingEth is excluded from every future
/// split so parked ETH is never redistributed.
uint256 public totalPendingEth;
mapping(address recipient => uint256 amount) public pendingEth;
mapping(address token => address recipient) public feeRedirects;
mapping(address token => uint256 share) public tokenProtocolFeeShares;
mapping(address deployer => address[] tokens) public deployerTokens;
mapping(address recipient => address[] tokens) public feeRecipientTokens;
mapping(address token => uint256 indexPlusOne) private _feeRecipientTokenIndexes;
mapping(address token => bool locked) private _lockedTokens;
constructor(address initialOwner, address initialProtocolFeeRecipient) Ownable(initialOwner) {
if (initialProtocolFeeRecipient == address(0) || initialProtocolFeeRecipient == address(this)) {
revert ZeroAddress();
}
protocolFeeRecipient = initialProtocolFeeRecipient;
}
modifier onlyFactory() {
if (msg.sender != factory) revert NotFactory();
_;
}
receive() external payable {}
function initialize(address factory_) external onlyOwner {
if (factory != address(0)) revert AlreadyInitialized();
if (factory_ == address(0)) revert ZeroAddress();
factory = factory_;
emit FactoryUpdated(factory_);
}
function onERC721Received(address operator, address from, uint256, bytes calldata)
external
view
returns (bytes4)
{
if (operator != factory || from != factory) revert NotFactory();
return IERC721Receiver.onERC721Received.selector;
}
function lockPosition(address token) external onlyFactory {
if (_lockedTokens[token]) revert PositionAlreadyLocked();
IRainLaunchFactory.LaunchedToken memory launched = IRainLaunchFactory(factory).getLaunchedToken(token);
if (!launched.exists || launched.token != token) revert TokenNotFound();
address nftOwner = INonfungiblePositionManager(launched.positionManager).ownerOf(launched.positionId);
if (nftOwner != address(this)) revert PositionNotHeld();
_lockedTokens[token] = true;
tokenProtocolFeeShares[token] = PROTOCOL_FEE_SHARE;
deployerTokens[launched.deployer].push(token);
emit PositionLocked(
token,
launched.deployer,
launched.dexId,
launched.pairedToken,
launched.positionId,
launched.positionManager
);
}
function collectFees(address token) external nonReentrant returns (uint256 ethTotal, uint256 tokensBurned) {
IRainLaunchFactory.LaunchedToken memory launched = getLaunchedToken(token);
if (!launched.exists || !_lockedTokens[token]) revert TokenNotFound();
address recipient = feeRedirects[token];
if (recipient == address(0)) recipient = launched.deployer;
if (msg.sender != launched.deployer && msg.sender != recipient && msg.sender != protocolFeeRecipient) {
revert NotAuthorized();
}
(uint256 amount0, uint256 amount1) = INonfungiblePositionManager(launched.positionManager).collect(
INonfungiblePositionManager.CollectParams({
tokenId: launched.positionId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
if (amount0 == 0 && amount1 == 0) revert NoFeesToCollect();
uint256 wethAmount;
(tokensBurned, wethAmount) = launched.isToken0 ? (amount0, amount1) : (amount1, amount0);
if (tokensBurned > 0) IERC20(token).safeTransfer(DEAD, tokensBurned);
// ethTotal is EXACTLY the freshly-unwrapped WETH, never the raw balance. Deriving it from
// balance would sweep unrelated ETH — parked pull-claims, forced selfdestruct donations, a
// self-paying recipient — into this token's split. withdraw() credits precisely wethAmount.
if (wethAmount > 0) IWETH9(launched.pairedToken).withdraw(wethAmount);
ethTotal = wethAmount;
if (ethTotal > 0) {
uint256 protocolEth = (ethTotal * tokenProtocolFeeShares[token]) / 100;
uint256 creatorEth = ethTotal - protocolEth;
if (protocolEth > 0) _payOrDefer(protocolFeeRecipient, protocolEth);
if (creatorEth > 0) _payOrDefer(recipient, creatorEth);
emit FeesClaimed(token, msg.sender, ethTotal, tokensBurned, creatorEth, protocolEth);
return (ethTotal, tokensBurned);
}
emit FeesClaimed(token, msg.sender, 0, tokensBurned, 0, 0);
}
/// Pays out directly when the recipient accepts; otherwise parks the amount as a pull-claim.
/// The gas cap keeps a hostile receiver from burning the collector's gas — any receiver that
/// needs more than 100k gas simply gets deferred and withdraws itself.
function _payOrDefer(address to, uint256 amount) private {
(bool ok,) = to.call{value: amount, gas: 100_000}("");
if (!ok) {
pendingEth[to] += amount;
totalPendingEth += amount;
emit FeePaymentDeferred(to, amount);
}
}
/// Withdraws ETH parked by a failed direct payout in collectFees.
function withdrawPendingFees() external nonReentrant {
uint256 amount = pendingEth[msg.sender];
if (amount == 0) revert NoFeesToCollect();
pendingEth[msg.sender] = 0;
totalPendingEth -= amount;
(bool ok,) = msg.sender.call{value: amount}("");
if (!ok) revert EthTransferFailed();
emit PendingFeesWithdrawn(msg.sender, amount);
}
function getLaunchedToken(address token) public view returns (IRainLaunchFactory.LaunchedToken memory) {
if (factory == address(0)) revert TokenNotFound();
return IRainLaunchFactory(factory).getLaunchedToken(token);
}
function setFeeRedirect(address token, address newFeeWallet) external {
IRainLaunchFactory.LaunchedToken memory launched = getLaunchedToken(token);
if (!launched.exists) revert TokenNotFound();
if (msg.sender != launched.deployer && msg.sender != factory) revert NotDeployer();
_setFeeRedirect(token, newFeeWallet);
}
function setProtocolFeeRecipient(address recipient) external onlyOwner {
if (recipient == address(0) || recipient == address(this)) revert ZeroAddress();
protocolFeeRecipient = recipient;
emit ProtocolFeeRecipientUpdated(recipient);
}
function deployerTokenCount(address deployer_) external view returns (uint256) {
return deployerTokens[deployer_].length;
}
function feeRecipientTokenCount(address recipient_) external view returns (uint256) {
return feeRecipientTokens[recipient_].length;
}
function _setFeeRedirect(address token, address newFeeWallet) private {
if (newFeeWallet == address(this)) revert ZeroAddress();
address previousFeeWallet = feeRedirects[token];
if (previousFeeWallet == newFeeWallet) return;
uint256 previousIndexPlusOne = _feeRecipientTokenIndexes[token];
if (previousFeeWallet != address(0) && previousIndexPlusOne != 0) {
address[] storage previousTokens = feeRecipientTokens[previousFeeWallet];
uint256 previousIndex = previousIndexPlusOne - 1;
uint256 lastIndex = previousTokens.length - 1;
if (previousIndex != lastIndex) {
address movedToken = previousTokens[lastIndex];
previousTokens[previousIndex] = movedToken;
_feeRecipientTokenIndexes[movedToken] = previousIndex + 1;
}
previousTokens.pop();
delete _feeRecipientTokenIndexes[token];
}
feeRedirects[token] = newFeeWallet;
if (newFeeWallet != address(0)) {
feeRecipientTokens[newFeeWallet].push(token);
_feeRecipientTokenIndexes[token] = feeRecipientTokens[newFeeWallet].length;
}
emit FeeRedirectUpdated(token, newFeeWallet);
}
}
Chain explorer950msChain node84ms