LpLockerv2

0xbb9fee1ceef819fe12e304f058b5e76ef8b2f17b

Verification
Verified
v0.8.35+commit.47b9dedd
Type
Contract
8,119 bytes
ABI entries
43
11 read · 13 write
License
none

Contract information

Address
0xbb9fee1ceef819fe12e304f058b5e76ef8b2f17b
Chain
Robinhood Chain (4663)
Compiler
v0.8.35+commit.47b9dedd
Optimization
Enabled
Creation tx
0x1b8103ba12…fdae3bbcb3

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (11)

MAX_CREATOR_REWARD()uint256
TEAM_REWARD()uint256
creatorTokenIds(address, uint256)uint256
factory()address
getLpTokenIdsForCreator(address)uint256[]
owner()address
positionManager()address
teamOverrideRewardRecipientForToken(uint256)address
teamRecipient()address
tokenRewards(uint256)uint256, uint256, tuple, tuple
version()string

Events (10)

ClaimedRewardsCreatorRewardRecipientAdminUpdatedCreatorRewardRecipientUpdatedInterfaceRewardRecipientAdminUpdatedInterfaceRewardRecipientUpdatedOwnershipTransferredReceivedTeamOverrideRewardRecipientUpdatedTeamRecipientUpdatedTokenRewardAdded

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenFactory_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "teamRecipient_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "AlreadyKnownTokenId",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "creatorReward",
        "type": "uint256"
      }
    ],
    "name": "InvalidCreatorReward",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidMaxCreatorReward",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTeamRecipient",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxCreatorRewardNotSet",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "NotAllowed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creatorRecipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "interfaceRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "teamRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "token0",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "token1",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorReward0",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorReward1",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "interfaceReward0",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "interfaceReward1",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "teamReward0",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "teamReward1",
        "type": "uint256"
      }
    ],
    "name": "ClaimedRewards",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldAdmin",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newAdmin",
        "type": "address"
      }
    ],
    "name": "CreatorRewardRecipientAdminUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "CreatorRewardRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldAdmin",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newAdmin",
        "type": "address"
      }
    ],
    "name": "InterfaceRewardRecipientAdminUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "InterfaceRewardRecipientUpdated",
    "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": "from",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Received",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "TeamOverrideRewardRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "TeamRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorReward",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "interfacer",
        "type": "address"
      }
    ],
    "name": "TokenRewardAdded",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_CREATOR_REWARD",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TEAM_REWARD",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "lpTokenId",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "creatorReward",
            "type": "uint256"
          },
          {
            "components": [
              {
                "internalType": "address",
                "name": "admin",
                "type": "address"
              },
              {
                "internalType": "address",
                "name": "recipient",
                "type": "address"
              }
            ],
            "internalType": "struct ILpLockerv2.RewardRecipient",
            "name": "creator",
            "type": "tuple"
          },
          {
            "components": [
              {
                "internalType": "address",
                "name": "admin",
                "type": "address"
              },
              {
                "internalType": "address",
                "name": "recipient",
                "type": "address"
              }
            ],
            "internalType": "struct ILpLockerv2.RewardRecipient",
            "name": "interfacer",
            "type": "tuple"
          }
        ],
        "internalType": "struct ILpLockerv2.TokenRewardInfo",
        "name": "tokenRewardInfo",
        "type": "tuple"
      }
    ],
    "name": "addTokenReward",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "collectRewards",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "creatorTokenIds",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "factory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getLpTokenIdsForCreator",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "name": "onERC721Received",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "newTeamRecipient",
        "type": "address"
      }
    ],
    "name": "setOverrideTeamRewardRecipientForToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "teamOverrideRewardRecipientForToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "teamRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "tokenRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "creatorReward",
        "type": "uint256"
      },
      {
        "components": [
          {
            "internalType": "address",
            "name": "admin",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "recipient",
            "type": "address"
          }
        ],
        "internalType": "struct ILpLockerv2.RewardRecipient",
        "name": "creator",
        "type": "tuple"
      },
      {
        "components": [
          {
            "internalType": "address",
            "name": "admin",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "recipient",
            "type": "address"
          }
        ],
        "internalType": "struct ILpLockerv2.RewardRecipient",
        "name": "interfacer",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "newAdmin",
        "type": "address"
      }
    ],
    "name": "updateCreatorRewardAdmin",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "updateCreatorRewardRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "newAdmin",
        "type": "address"
      }
    ],
    "name": "updateInterfaceRewardAdmin",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "updateInterfaceRewardRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "updateTeamRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "version",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "withdrawERC20",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "withdrawETH",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IClanker} from "./interfaces/IClanker.sol";
import {ILpLockerv2} from "./interfaces/ILpLockerv2.sol";
import {INonfungiblePositionManager} from "./interfaces/uniswapv3.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

contract LpLockerv2 is Ownable, ILpLockerv2 {
    string public constant version = "0.0.2";

    address public positionManager;
    address public factory;

    uint256 public constant TEAM_REWARD = 20;
    uint256 public immutable MAX_CREATOR_REWARD;
    address public teamRecipient;

    mapping(uint256 => TokenRewardInfo) public tokenRewards;
    mapping(uint256 => address) public teamOverrideRewardRecipientForToken;

    mapping(address => uint256[]) public creatorTokenIds;

    constructor(
        address owner_, // owner of the contract
        address tokenFactory_, // Address of the clanker factory
        address positionManager_, // Address of the position manager
        address teamRecipient_ // address to receive team portion of the rewards
    ) Ownable(owner_) {
        factory = tokenFactory_;
        positionManager = positionManager_;
        teamRecipient = teamRecipient_;

        // match the factory's max creator reward
        MAX_CREATOR_REWARD = IClanker(factory).MAX_CREATOR_REWARD();
        if (MAX_CREATOR_REWARD == 0) {
            revert MaxCreatorRewardNotSet();
        }
        // ensure the max creator reward and team reward add up to 100
        if (MAX_CREATOR_REWARD + TEAM_REWARD != 100) {
            revert InvalidMaxCreatorReward();
        }

        // ensure the team recipient is not the zero address
        if (teamRecipient == address(0)) {
            revert InvalidTeamRecipient();
        }
    }

    modifier onlyFactory() {
        if (msg.sender != factory) {
            revert NotAllowed(msg.sender);
        }
        _;
    }

    // Set the override team reward recipient for a token
    function setOverrideTeamRewardRecipientForToken(uint256 tokenId, address newTeamRecipient)
        external
    {
        if (msg.sender != owner() && msg.sender != factory) {
            revert NotAllowed(msg.sender);
        }

        address oldTeamRecipient = teamOverrideRewardRecipientForToken[tokenId];
        teamOverrideRewardRecipientForToken[tokenId] = newTeamRecipient;

        emit TeamOverrideRewardRecipientUpdated(tokenId, oldTeamRecipient, newTeamRecipient);
    }

    // Update the default team recipient
    function updateTeamRecipient(address newRecipient) external onlyOwner {
        address oldTeamRecipient = teamRecipient;
        teamRecipient = newRecipient;

        // ensure the team recipient is not the zero address
        if (teamRecipient == address(0)) {
            revert InvalidTeamRecipient();
        }
        emit TeamRecipientUpdated(oldTeamRecipient, newRecipient);
    }

    // Add a token reward, this is called by the factory when a token is created
    function addTokenReward(TokenRewardInfo memory tokenRewardInfo) external onlyFactory {
        // check that the token id is not already known
        if (tokenRewards[tokenRewardInfo.lpTokenId].lpTokenId != 0) {
            revert AlreadyKnownTokenId(tokenRewardInfo.lpTokenId);
        }

        // check that the creator reward is not greater than the max creator reward or zero
        if (
            tokenRewardInfo.creatorReward > MAX_CREATOR_REWARD || tokenRewardInfo.creatorReward == 0
        ) {
            revert InvalidCreatorReward(tokenRewardInfo.creatorReward);
        }

        tokenRewards[tokenRewardInfo.lpTokenId] = tokenRewardInfo;
        creatorTokenIds[tokenRewardInfo.creator.admin].push(tokenRewardInfo.lpTokenId);

        emit TokenRewardAdded(
            tokenRewardInfo.lpTokenId,
            tokenRewardInfo.creatorReward,
            tokenRewardInfo.creator.admin,
            tokenRewardInfo.interfacer.admin
        );
    }

    // Collect rewards for a token
    function collectRewards(uint256 tokenId) public {
        address teamRecipient_ = teamOverrideRewardRecipientForToken[tokenId];
        if (teamRecipient_ == address(0)) {
            teamRecipient_ = teamRecipient;
        }

        address creatorRecipient_ = tokenRewards[tokenId].creator.recipient;
        if (creatorRecipient_ == address(0)) {
            creatorRecipient_ = teamRecipient_;
        }

        address interfaceRecipient_ = tokenRewards[tokenId].interfacer.recipient;
        if (interfaceRecipient_ == address(0)) {
            interfaceRecipient_ = teamRecipient_;
        }

        // collect the rewards
        INonfungiblePositionManager nonfungiblePositionManager =
            INonfungiblePositionManager(positionManager);

        (uint256 amount0, uint256 amount1) = nonfungiblePositionManager.collect(
            INonfungiblePositionManager.CollectParams({
                recipient: address(this),
                amount0Max: type(uint128).max,
                amount1Max: type(uint128).max,
                tokenId: tokenId
            })
        );

        (,, address token0, address token1,,,,,,,,) = nonfungiblePositionManager.positions(tokenId);

        IERC20 rewardToken0 = IERC20(token0);
        IERC20 rewardToken1 = IERC20(token1);
        uint256 creatorReward = tokenRewards[tokenId].creatorReward;

        // figure out the rewards distribution
        uint256 teamReward0 = (amount0 * TEAM_REWARD) / 100;
        uint256 teamReward1 = (amount1 * TEAM_REWARD) / 100;
        rewardToken0.transfer(teamRecipient_, teamReward0);
        rewardToken1.transfer(teamRecipient_, teamReward1);

        uint256 interfaceReward0;
        uint256 interfaceReward1;
        uint256 creatorReward0;
        uint256 creatorReward1;

        if (creatorReward == MAX_CREATOR_REWARD) {
            // if the creator reward is max, then there is no interface reward
            creatorReward0 = amount0 - teamReward0;
            creatorReward1 = amount1 - teamReward1;

            rewardToken0.transfer(creatorRecipient_, creatorReward0);
            rewardToken1.transfer(creatorRecipient_, creatorReward1);
        } else {
            // we have both a creator and interface reward
            creatorReward0 = (amount0 * creatorReward) / 100;
            creatorReward1 = (amount1 * creatorReward) / 100;

            interfaceReward0 = amount0 - teamReward0 - creatorReward0;
            interfaceReward1 = amount1 - teamReward1 - creatorReward1;

            rewardToken0.transfer(interfaceRecipient_, interfaceReward0);
            rewardToken1.transfer(interfaceRecipient_, interfaceReward1);

            rewardToken0.transfer(creatorRecipient_, creatorReward0);
            rewardToken1.transfer(creatorRecipient_, creatorReward1);
        }

        emit ClaimedRewards({
            lpTokenId: tokenId,
            creatorRecipient: creatorRecipient_,
            interfaceRecipient: interfaceRecipient_,
            teamRecipient: teamRecipient_,
            token0: token0,
            token1: token1,
            teamReward0: teamReward0,
            teamReward1: teamReward1,
            interfaceReward0: interfaceReward0,
            interfaceReward1: interfaceReward1,
            creatorReward0: creatorReward0,
            creatorReward1: creatorReward1
        });
    }

    // Get the token ids for a creator (using the admin address)
    function getLpTokenIdsForCreator(address user) public view returns (uint256[] memory) {
        return creatorTokenIds[user];
    }

    // Replace the creator reward recipient
    function updateCreatorRewardRecipient(uint256 tokenId, address newRecipient) external {
        TokenRewardInfo memory tokenRewardInfo = tokenRewards[tokenId];

        // Only admin can replace the reward recipient
        if (msg.sender != tokenRewardInfo.creator.admin) {
            revert NotAllowed(msg.sender);
        }

        // Add the new recipient
        address oldRecipient = tokenRewards[tokenId].creator.recipient;
        tokenRewards[tokenId].creator.recipient = newRecipient;

        emit CreatorRewardRecipientUpdated(tokenId, oldRecipient, newRecipient);
    }

    // Replace the interface reward recipient
    function updateInterfaceRewardRecipient(uint256 tokenId, address newRecipient) external {
        TokenRewardInfo memory tokenRewardInfo = tokenRewards[tokenId];

        // Only admin can replace the reward recipient
        if (msg.sender != tokenRewardInfo.interfacer.admin) {
            revert NotAllowed(msg.sender);
        }

        // Add the new recipient
        address oldRecipient = tokenRewards[tokenId].interfacer.recipient;
        tokenRewards[tokenId].interfacer.recipient = newRecipient;

        emit InterfaceRewardRecipientUpdated(tokenId, oldRecipient, newRecipient);
    }

    // Replace the interface reward admin
    function updateInterfaceRewardAdmin(uint256 tokenId, address newAdmin) external {
        TokenRewardInfo memory tokenRewardInfo = tokenRewards[tokenId];

        // Only admin can replace the reward admin
        if (msg.sender != tokenRewardInfo.interfacer.admin) {
            revert NotAllowed(msg.sender);
        }

        // Add the new admin
        address oldAdmin = tokenRewards[tokenId].interfacer.admin;
        tokenRewards[tokenId].interfacer.admin = newAdmin;

        emit InterfaceRewardRecipientAdminUpdated(tokenId, oldAdmin, newAdmin);
    }

    // Replace the creator reward admin
    function updateCreatorRewardAdmin(uint256 tokenId, address newAdmin) external {
        TokenRewardInfo memory tokenRewardInfo = tokenRewards[tokenId];

        // Only admin can replace the admin
        if (msg.sender != tokenRewardInfo.creator.admin) {
            revert NotAllowed(msg.sender);
        }

        // Remove the tokenId from _creatorTokenIds
        uint256 length = creatorTokenIds[tokenRewardInfo.creator.admin].length;
        for (uint256 i = 0; i < length; i++) {
            if (creatorTokenIds[tokenRewardInfo.creator.admin][i] == tokenRewardInfo.lpTokenId) {
                // Swap with the last element
                creatorTokenIds[tokenRewardInfo.creator.admin][i] =
                    creatorTokenIds[tokenRewardInfo.creator.admin][length - 1];
                // Pop the last element
                creatorTokenIds[tokenRewardInfo.creator.admin].pop();
                break;
            }
        }

        // push the token id to the new admin
        creatorTokenIds[newAdmin].push(tokenRewardInfo.lpTokenId);

        address oldAdmin = tokenRewards[tokenId].creator.admin;
        tokenRewards[tokenId].creator.admin = newAdmin;

        emit CreatorRewardRecipientAdminUpdated(tokenId, oldAdmin, newAdmin);
    }

    // Enable contract to receive LP Tokens
    function onERC721Received(address, address from, uint256 id, bytes calldata)
        external
        override
        returns (bytes4)
    {
        // Only Clanker Factory can send NFTs here
        if (from != factory) {
            revert NotAllowed(from);
        }

        emit Received(from, id);
        return IERC721Receiver.onERC721Received.selector;
    }

    // Withdraw ETH from the contract
    function withdrawETH(address recipient) public onlyOwner {
        payable(recipient).transfer(address(this).balance);
    }

    // Withdraw ERC20 tokens from the contract
    function withdrawERC20(address token, address recipient) public onlyOwner {
        IERC20 token_ = IERC20(token);
        token_.transfer(recipient, token_.balanceOf(address(this)));
    }
}
Chain explorer1041msChain node102ms