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
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