RawbinLocker
0x148fc8d6e1ac421ce79e2b506f78e33efba0aa27
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
4,443 bytes
ABI entries
21
7 read · 3 write
License
none
Contract information
- Address
- 0x148fc8d6e1ac421ce79e2b506f78e33efba0aa27
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x9eE7E27aC6…c905010999
- Creation tx
- 0x2c42c158be…1ff3a7d74d
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (7)
CREATOR_BPS() → uint16
creatorOf(uint256) → address
factory() → address
npm() → address
onERC721Received(address, address, uint256, bytes) → bytes4
pendingEth(address) → uint256
weth() → address
Events (4)
EthAccruedFeesCollectedPendingWithdrawnPositionLocked
ABI
[
{
"inputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "npm_",
"type": "address"
},
{
"internalType": "contract IRawbinFactoryView",
"name": "factory_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "target",
"type": "address"
}
],
"name": "AddressEmptyCode",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "AddressInsufficientBalance",
"type": "error"
},
{
"inputs": [],
"name": "FailedInnerCall",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "EthAccrued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "platform",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethCollected",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensCollected",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint16",
"name": "creatorBps",
"type": "uint16"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "PendingWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "curve",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
}
],
"name": "PositionLocked",
"type": "event"
},
{
"inputs": [],
"name": "CREATOR_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "collect",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "creatorOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "contract IRawbinFactoryView",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
}
],
"name": "lockPosition",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "npm",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pendingEth",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "withdrawPending",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./interfaces/IUniswapV3.sol";
interface IRawbinFactoryView {
function feeRecipient() external view returns (address);
function isCurve(address) external view returns (bool);
}
/// @title RawbinLocker
/// @custom:mission We're building a transparent launchpad. Verified contracts, no
/// hidden owner over tokens, no token tax, liquidity you can never rug. Read the
/// code and verify everything yourself. Every launch is a drop. — rawbin
/// @notice Holds every migrated token's Uniswap V3 position NFT forever. There is
/// NO decreaseLiquidity / withdraw path — the principal liquidity can never
/// be pulled (rug-proof, "locked forever"). Only the trading fees are
/// collectable, and BOTH sides (ETH/WETH and token) are split the same way:
/// 25% to the token creator, 75% to the platform. Anyone can trigger a
/// collect; funds always route correctly.
contract RawbinLocker is ReentrancyGuard, IERC721Receiver {
using SafeERC20 for IERC20;
INonfungiblePositionManager public immutable npm;
IRawbinFactoryView public immutable factory;
address public immutable weth;
/// @notice positionId => token creator (receives the token-side fees).
mapping(uint256 => address) public creatorOf;
/// @notice RB-03: ETH that couldn't be pushed to a recipient during collect()
/// (e.g. a creator contract that reverts on receive). Accrued here so
/// one bad recipient can never brick collect() for everyone else; the
/// owner of the funds pulls them separately.
mapping(address => uint256) public pendingEth;
event EthAccrued(address indexed recipient, uint256 amount);
event PendingWithdrawn(address indexed recipient, uint256 amount);
/// @notice Creator's share of BOTH fee sides (basis points). Platform gets the rest.
uint16 public constant CREATOR_BPS = 2500; // 25%
event PositionLocked(uint256 indexed tokenId, address indexed curve, address indexed creator);
event FeesCollected(
uint256 indexed tokenId,
address indexed platform,
address indexed creator,
uint256 ethCollected,
uint256 tokensCollected,
uint16 creatorBps
);
constructor(INonfungiblePositionManager npm_, IRawbinFactoryView factory_) {
npm = npm_;
factory = factory_;
weth = npm_.WETH9();
}
/// @notice Register a freshly-migrated position. Called by a factory curve in the
/// migration tx; the NFT must already be owned by this locker.
function lockPosition(uint256 tokenId, address creator) external {
require(factory.isCurve(msg.sender), "only curve");
require(creator != address(0), "creator=0");
require(creatorOf[tokenId] == address(0), "already locked");
require(npm.ownerOf(tokenId) == address(this), "not held");
creatorOf[tokenId] = creator;
emit PositionLocked(tokenId, msg.sender, creator);
}
/// @notice Collect accrued trading fees. Permissionless — anyone can call.
/// Both sides of the pair are split: creator gets CREATOR_BPS (25%),
/// platform (live factory.feeRecipient()) gets the rest — on ETH and token.
function collect(uint256 tokenId) external nonReentrant {
address creator = creatorOf[tokenId];
require(creator != address(0), "unknown position");
// Pull all owed fees (both sides) to this contract.
(uint256 amount0, uint256 amount1) = npm.collect(
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
(, , address token0, address token1, , , , , , , , ) = npm.positions(tokenId);
// Sort which side is ETH (WETH) and which is the meme token.
address memeToken;
uint256 wethAmt;
uint256 tokenAmt;
if (token0 == weth) {
wethAmt = amount0;
memeToken = token1;
tokenAmt = amount1;
} else {
wethAmt = amount1;
memeToken = token0;
tokenAmt = amount0;
}
address platform = factory.feeRecipient();
// ETH side: 25% creator, 75% platform (unwrap WETH to native ETH).
// Non-blocking: a recipient that reverts on receive has its share accrued
// to pendingEth instead of reverting the whole collect (RB-03).
if (wethAmt > 0) {
IWETH9(weth).withdraw(wethAmt);
uint256 ethToCreator = (wethAmt * CREATOR_BPS) / 10_000;
_payEthOrAccrue(creator, ethToCreator);
_payEthOrAccrue(platform, wethAmt - ethToCreator);
}
// Token side: 25% creator, 75% platform.
if (tokenAmt > 0) {
uint256 tokToCreator = (tokenAmt * CREATOR_BPS) / 10_000;
IERC20(memeToken).safeTransfer(creator, tokToCreator);
IERC20(memeToken).safeTransfer(platform, tokenAmt - tokToCreator);
}
emit FeesCollected(tokenId, platform, creator, wethAmt, tokenAmt, CREATOR_BPS);
}
/// @dev Push ETH best-effort; on failure accrue it for a later pull. `platform`
/// is the timelocked feeRecipient and `creator` is the token creator —
/// paying either is intended, not an arbitrary send.
///
/// RB-07: the call is given a fixed gas stipend. Without it, a malicious
/// recipient could burn 63/64 of the forwarded gas (EIP-150) so the
/// following `pendingEth` SSTORE runs out of gas and reverts the whole
/// collect() — freezing the other recipient's (e.g. the platform's) share.
/// A recipient that needs more gas than the stipend simply has its share
/// accrued and pulls it later via withdrawPending() with its own gas.
uint256 internal constant PAYOUT_GAS_STIPEND = 50_000;
// slither-disable-next-line arbitrary-send-eth
function _payEthOrAccrue(address to, uint256 amount) internal {
if (amount == 0) return;
(bool ok, ) = to.call{value: amount, gas: PAYOUT_GAS_STIPEND}("");
if (!ok) {
pendingEth[to] += amount;
emit EthAccrued(to, amount);
}
}
/// @notice Withdraw ETH that a previous collect() couldn't deliver to you.
function withdrawPending() external nonReentrant {
uint256 amount = pendingEth[msg.sender];
require(amount > 0, "nothing pending");
pendingEth[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "withdraw failed");
emit PendingWithdrawn(msg.sender, amount);
}
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
// Receives ETH only from unwrapping WETH.
receive() external payable {
require(msg.sender == weth, "eth only from weth");
}
}
Chain explorer3638msChain node74ms