TitanLockerManagerV2

0x26b0654a0756dcd036d4e7215324f3d2be34d79e

Verification
Verified
v0.8.30+commit.73712a01
Type
Contract
19,299 bytes
ABI entries
43
11 read · 11 write
License
gnu_gpl_v3

Contract information

Address
0x26b0654a0756dcd036d4e7215324f3d2be34d79e
Chain
Robinhood Chain (4663)
Compiler
v0.8.30+commit.73712a01
Optimization
Enabled
Creation tx
0xe09a40e30f…fd37e0fecd

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (11)

creationEnabled()bool
ethFee()uint256
feeReceiver()address
getTokenLockAddress(uint40)address
getTokenLockData(uint40)uint8, uint40, address, address, address, uint256, address, uint40, uint40, uint256
getTokenLockersForAddress(address)uint40[]
isExemptFromFees(address)bool
owner()address
positionManagerKind(address)uint8, bool
tokenFeeBps()uint16
tokenLockerCount()uint40

Events (7)

FeeCollectedFeeConfigUpdatedOwnershipTransferredPositionLockerCreatedPositionManagerSetTokenLockerCreatedVestingLockerCreated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address payable",
        "name": "feeReceiver_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "uint40",
        "name": "id",
        "type": "uint40"
      },
      {
        "internalType": "address",
        "name": "caller",
        "type": "address"
      }
    ],
    "name": "CallerIsNotLocker",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "caller",
        "type": "address"
      }
    ],
    "name": "CallerNotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CreationDisabled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExemptAccountsPayNoEth",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "expected",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "received",
        "type": "uint256"
      }
    ],
    "name": "IncorrectEthFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPositionManagerKind",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidVestingSchedule",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "positionManager",
        "type": "address"
      }
    ],
    "name": "PositionManagerNotAllowed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenFeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint40",
        "name": "id",
        "type": "uint40"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "paidInEth",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeeCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "tokenFeeBps",
        "type": "uint16"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "feeReceiver",
        "type": "address"
      }
    ],
    "name": "FeeConfigUpdated",
    "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": false,
        "internalType": "uint40",
        "name": "id",
        "type": "uint40"
      },
      {
        "indexed": false,
        "internalType": "enum ITitanLockerManagerV2.LockKind",
        "name": "kind",
        "type": "uint8"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "positionManager",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token0",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token1",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "createdBy",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint40",
        "name": "unlockTime",
        "type": "uint40"
      }
    ],
    "name": "PositionLockerCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "positionManager",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "enum ITitanLockerManagerV2.LockKind",
        "name": "kind",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "PositionManagerSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint40",
        "name": "id",
        "type": "uint40"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token0",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token1",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "createdBy",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint40",
        "name": "unlockTime",
        "type": "uint40"
      }
    ],
    "name": "TokenLockerCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint40",
        "name": "id",
        "type": "uint40"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token0",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token1",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "createdBy",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint40",
        "name": "start",
        "type": "uint40"
      },
      {
        "indexed": false,
        "internalType": "uint40",
        "name": "cliff",
        "type": "uint40"
      },
      {
        "indexed": false,
        "internalType": "uint40",
        "name": "end",
        "type": "uint40"
      }
    ],
    "name": "VestingLockerCreated",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId_",
        "type": "uint256"
      },
      {
        "internalType": "uint40",
        "name": "unlockTime_",
        "type": "uint40"
      }
    ],
    "name": "createPositionLock",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenAddress_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount_",
        "type": "uint256"
      },
      {
        "internalType": "uint40",
        "name": "unlockTime_",
        "type": "uint40"
      }
    ],
    "name": "createTokenLock",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenAddress_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount_",
        "type": "uint256"
      },
      {
        "internalType": "uint40",
        "name": "start_",
        "type": "uint40"
      },
      {
        "internalType": "uint40",
        "name": "cliff_",
        "type": "uint40"
      },
      {
        "internalType": "uint40",
        "name": "end_",
        "type": "uint40"
      }
    ],
    "name": "createVestingLock",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creationEnabled",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ethFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeReceiver",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint40",
        "name": "id_",
        "type": "uint40"
      }
    ],
    "name": "getTokenLockAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint40",
        "name": "id_",
        "type": "uint40"
      }
    ],
    "name": "getTokenLockData",
    "outputs": [
      {
        "internalType": "enum ITitanLockerManagerV2.LockKind",
        "name": "kind",
        "type": "uint8"
      },
      {
        "internalType": "uint40",
        "name": "id",
        "type": "uint40"
      },
      {
        "internalType": "address",
        "name": "contractAddress",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "lockOwner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "asset",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "createdBy",
        "type": "address"
      },
      {
        "internalType": "uint40",
        "name": "createdAt",
        "type": "uint40"
      },
      {
        "internalType": "uint40",
        "name": "unlockTime",
        "type": "uint40"
      },
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "address_",
        "type": "address"
      }
    ],
    "name": "getTokenLockersForAddress",
    "outputs": [
      {
        "internalType": "uint40[]",
        "name": "",
        "type": "uint40[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account_",
        "type": "address"
      }
    ],
    "name": "isExemptFromFees",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint40",
        "name": "id_",
        "type": "uint40"
      },
      {
        "internalType": "address",
        "name": "newOwner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "previousOwner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "createdBy_",
        "type": "address"
      }
    ],
    "name": "notifyLockerOwnerChange",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      }
    ],
    "name": "positionManagerKind",
    "outputs": [
      {
        "internalType": "enum ITitanLockerManagerV2.LockKind",
        "name": "kind",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "value_",
        "type": "bool"
      }
    ],
    "name": "setCreationEnabled",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "value_",
        "type": "uint256"
      }
    ],
    "name": "setEthFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account_",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "exempt_",
        "type": "bool"
      }
    ],
    "name": "setFeeExempt",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address payable",
        "name": "value_",
        "type": "address"
      }
    ],
    "name": "setFeeReceiver",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "enum ITitanLockerManagerV2.LockKind",
        "name": "kind_",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "allowed_",
        "type": "bool"
      }
    ],
    "name": "setPositionManager",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "value_",
        "type": "uint16"
      }
    ],
    "name": "setTokenFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenFeeBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenLockerCount",
    "outputs": [
      {
        "internalType": "uint40",
        "name": "",
        "type": "uint40"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner_",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: GPL-3.0-or-later

/**
  ___________________    _   __   __    ____  ________ __ __________
 /_  __/  _/_  __/   |  / | / /  / /   / __ \/ ____/ //_// ____/ __ \
  / /  / /  / / / /| | /  |/ /  / /   / / / / /   / ,<  / __/ / /_/ /
 / / _/ /  / / / ___ |/ /|  /  / /___/ /_/ / /___/ /| |/ /___/ _, _/
/_/ /___/ /_/ /_/  |_/_/ |_/  /_____/\____/\____/_/ |_/_____/_/ |_|

  Locker manager / factory (V2 - tokens + V2/V3/V4 LP)
*/
pragma solidity 0.8.30;

import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { ITitanLockerManagerV2 } from "./ITitanLockerManagerV2.sol";
import { Ownable } from "./Ownable.sol";
import { TitanLockerV2 } from "./TitanLockerV2.sol";

/// @notice Deploys and indexes `TitanLockerV2` instances for three lock kinds -
/// plain/LP ERC20 tokens, Uniswap V3 positions, and Uniswap V4 positions - all
/// through one manager. Each lock is its own child contract, so custody is
/// isolated per lock. V3/V4 positions can only be locked against a
/// position manager the owner has allowlisted, which is both the multichain
/// mechanism and the kill-switch for un-validated integrations.
contract TitanLockerManagerV2 is ITitanLockerManagerV2, Ownable, ReentrancyGuard {
  using SafeERC20 for IERC20;

  error CreationDisabled();
  error IncorrectEthFee(uint256 expected, uint256 received);
  error ExemptAccountsPayNoEth();
  error TokenFeeTooHigh();
  error CallerIsNotLocker(uint40 id, address caller);
  error EthTransferFailed();
  error PositionManagerNotAllowed(address positionManager);
  error InvalidPositionManagerKind();
  error InvalidVestingSchedule();

  uint16 private constant BPS_DENOMINATOR = 10000;

  bool private _creationEnabled = true;
  uint40 private _tokenLockerCount;

  mapping(uint40 => TitanLockerV2) private _tokenLockers;
  /// @dev Lets the frontend search lockers by creator, locked token/position
  /// manager, locker address, or either underlying paired token.
  mapping(address => uint40[]) private _tokenLockersForAddress;

  /// @dev Allowlist of Uniswap position managers. `_pmKind` records whether a
  /// manager is V3- or V4-style; `_pmAllowed` gates whether new locks may use
  /// it. A manager with `_pmAllowed == false` is rejected regardless of kind.
  mapping(address => LockKind) private _pmKind;
  mapping(address => bool) private _pmAllowed;

  uint256 private _ethFee;
  uint16 private _tokenFeeBps;
  address payable private _feeReceiver;
  mapping(address => bool) private _feeExempt;

  constructor(address payable feeReceiver_) Ownable(_msgSender()) {
    if (feeReceiver_ == address(0)) revert ZeroAddress();
    _feeReceiver = feeReceiver_;
    // same starting fee schedule as V1; both adjustable via the setters below.
    _ethFee = 0.2 ether;
    _tokenFeeBps = 500; // 5.00%
  }

  modifier allowCreation() {
    if (!_creationEnabled) revert CreationDisabled();
    _;
  }

  // --- config ---

  function tokenLockerCount() external view override returns (uint40) {
    return _tokenLockerCount;
  }

  function creationEnabled() external view override returns (bool) {
    return _creationEnabled;
  }

  /// @dev Turns off new locks so users can be migrated to a future version.
  /// Existing locks keep working (extend/deposit/withdraw/collect) as normal.
  function setCreationEnabled(bool value_) external override onlyOwner {
    _creationEnabled = value_;
  }

  // --- position-manager allowlist ---

  function positionManagerKind(address positionManager_) external view override returns (LockKind kind, bool allowed) {
    return (_pmKind[positionManager_], _pmAllowed[positionManager_]);
  }

  function setPositionManager(address positionManager_, LockKind kind_, bool allowed_) external override onlyOwner {
    if (positionManager_ == address(0)) revert ZeroAddress();
    if (kind_ != LockKind.UNIV3 && kind_ != LockKind.UNIV4) revert InvalidPositionManagerKind();
    _pmKind[positionManager_] = kind_;
    _pmAllowed[positionManager_] = allowed_;
    emit PositionManagerSet(positionManager_, kind_, allowed_);
  }

  // --- ERC20 lock creation ---

  function createTokenLock(
    address tokenAddress_,
    uint256 amount_,
    uint40 unlockTime_
  ) external payable override allowCreation nonReentrant {
    uint40 id = _tokenLockerCount++;
    uint256 amountToLock = _collectFee(id, tokenAddress_, amount_);

    TitanLockerV2 locker = new TitanLockerV2(
      address(this), id, _msgSender(), LockKind.ERC20, tokenAddress_, 0, unlockTime_, 0, 0, 0, 0
    );
    _tokenLockers[id] = locker;

    IERC20(tokenAddress_).safeTransferFrom(_msgSender(), address(locker), amountToLock);

    _indexNewLocker(id, tokenAddress_, locker);

    (, address token0, address token1) = locker.getUnderlyingTokens();
    _indexUnderlying(id, token0, token1);

    emit TokenLockerCreated(
      id,
      tokenAddress_,
      token0,
      token1,
      _msgSender(),
      IERC20(tokenAddress_).balanceOf(address(locker)),
      unlockTime_
    );
  }

  /// @dev Either takes the flat `_ethFee` (100% of `amount_` gets locked), or -
  /// when no ETH is sent - deducts `_tokenFeeBps` from `amount_` in the token
  /// itself. Exempt accounts pay nothing and must not send ETH.
  function _collectFee(uint40 id_, address tokenAddress_, uint256 amount_) private returns (uint256 amountToLock) {
    bool paidInEth = msg.value != 0;

    if (_feeExempt[_msgSender()]) {
      if (paidInEth) revert ExemptAccountsPayNoEth();
      return amount_;
    }

    if (paidInEth) {
      if (msg.value != _ethFee) revert IncorrectEthFee(_ethFee, msg.value);
      (bool success, ) = _feeReceiver.call{value: msg.value}("");
      if (!success) revert EthTransferFailed();
      emit FeeCollected(id_, true, msg.value);
      return amount_;
    }

    uint256 tokenFeeAmount = (amount_ * _tokenFeeBps) / BPS_DENOMINATOR;
    amountToLock = amount_ - tokenFeeAmount;

    if (tokenFeeAmount != 0) {
      IERC20(tokenAddress_).safeTransferFrom(_msgSender(), _feeReceiver, tokenFeeAmount);
      emit FeeCollected(id_, false, tokenFeeAmount);
    }
  }

  // --- vesting lock creation ---

  /// @dev Locks a fungible token and releases it linearly (with an optional
  /// cliff) to the lock owner between `start_` and `end_`. Same creation-fee
  /// options as `createTokenLock`; the vesting grant is the post-fee amount and
  /// is irrevocable.
  function createVestingLock(
    address tokenAddress_,
    uint256 amount_,
    uint40 start_,
    uint40 cliff_,
    uint40 end_
  ) external payable override allowCreation nonReentrant {
    if (start_ >= end_ || cliff_ < start_ || cliff_ > end_ || end_ <= uint40(block.timestamp)) {
      revert InvalidVestingSchedule();
    }

    uint40 id = _tokenLockerCount++;
    uint256 amountToLock = _collectFee(id, tokenAddress_, amount_);

    // Grant is the nominal post-fee amount; `release()` clamps payouts to the
    // lock's live balance, so fee-on-transfer shortfalls can never over-release.
    TitanLockerV2 locker = new TitanLockerV2(
      address(this), id, _msgSender(), LockKind.ERC20_VESTING, tokenAddress_, 0, end_, start_, cliff_, end_, amountToLock
    );
    _tokenLockers[id] = locker;

    IERC20(tokenAddress_).safeTransferFrom(_msgSender(), address(locker), amountToLock);

    _indexNewLocker(id, tokenAddress_, locker);

    (, address token0, address token1) = locker.getUnderlyingTokens();
    _indexUnderlying(id, token0, token1);

    emit VestingLockerCreated(id, tokenAddress_, token0, token1, _msgSender(), amountToLock, start_, cliff_, end_);
  }

  // --- V3/V4 position lock creation ---

  function createPositionLock(
    address positionManager_,
    uint256 tokenId_,
    uint40 unlockTime_
  ) external payable override allowCreation nonReentrant {
    if (!_pmAllowed[positionManager_]) revert PositionManagerNotAllowed(positionManager_);
    LockKind kind = _pmKind[positionManager_];

    uint40 id = _tokenLockerCount++;
    _collectEthFee(id);

    TitanLockerV2 locker = new TitanLockerV2(
      address(this), id, _msgSender(), kind, positionManager_, tokenId_, unlockTime_, 0, 0, 0, 0
    );
    _tokenLockers[id] = locker;

    // Pull the position NFT from the depositor into the lock. Requires the
    // depositor to have approved this manager as an operator for it.
    IERC721(positionManager_).safeTransferFrom(_msgSender(), address(locker), tokenId_);

    _indexNewLocker(id, positionManager_, locker);

    (, address token0, address token1) = locker.getUnderlyingTokens();
    _indexUnderlying(id, token0, token1);

    emit PositionLockerCreated(id, kind, positionManager_, tokenId_, token0, token1, _msgSender(), unlockTime_);
  }

  /// @dev Flat-ETH-fee path used by position locks (no in-kind option exists
  /// for an NFT). Exempt accounts pay nothing and must not send ETH.
  function _collectEthFee(uint40 id_) private {
    if (_feeExempt[_msgSender()]) {
      if (msg.value != 0) revert ExemptAccountsPayNoEth();
      return;
    }

    if (msg.value != _ethFee) revert IncorrectEthFee(_ethFee, msg.value);

    if (msg.value != 0) {
      (bool success, ) = _feeReceiver.call{value: msg.value}("");
      if (!success) revert EthTransferFailed();
      emit FeeCollected(id_, true, msg.value);
    }
  }

  // --- indexing ---

  /// @dev Records a new locker in the creator / asset / locker-address buckets.
  function _indexNewLocker(uint40 id_, address asset_, TitanLockerV2 locker_) private {
    _tokenLockersForAddress[_msgSender()].push(id_);
    _tokenLockersForAddress[asset_].push(id_);
    _tokenLockersForAddress[address(locker_)].push(id_);
  }

  /// @dev Records the lock under each underlying paired token, when present.
  function _indexUnderlying(uint40 id_, address token0_, address token1_) private {
    if (token0_ != address(0)) _tokenLockersForAddress[token0_].push(id_);
    if (token1_ != address(0)) _tokenLockersForAddress[token1_].push(id_);
  }

  // --- reads ---

  function getTokenLockAddress(uint40 id_) external view override returns (address) {
    return address(_tokenLockers[id_]);
  }

  function getTokenLockData(uint40 id_) external view override returns (
    LockKind kind,
    uint40 id,
    address contractAddress,
    address lockOwner,
    address asset,
    uint256 tokenId,
    address createdBy,
    uint40 createdAt,
    uint40 unlockTime,
    uint256 balance
  ) {
    return _tokenLockers[id_].getLockData();
  }

  function getTokenLockersForAddress(address address_) external view override returns (uint40[] memory) {
    return _tokenLockersForAddress[address_];
  }

  /// @dev Called by a `TitanLockerV2` whenever its ownership changes, so the
  /// search index above stays accurate.
  function notifyLockerOwnerChange(
    uint40 id_,
    address newOwner_,
    address previousOwner_,
    address createdBy_
  ) external override {
    if (_msgSender() != address(_tokenLockers[id_])) revert CallerIsNotLocker(id_, _msgSender());

    if (previousOwner_ != createdBy_) {
      uint40[] storage previousOwnerLocks = _tokenLockersForAddress[previousOwner_];
      for (uint256 i = 0; i < previousOwnerLocks.length; i++) {
        if (previousOwnerLocks[i] != id_) continue;
        previousOwnerLocks[i] = previousOwnerLocks[previousOwnerLocks.length - 1];
        previousOwnerLocks.pop();
        break;
      }
    }

    uint40[] storage newOwnerLocks = _tokenLockersForAddress[newOwner_];
    for (uint256 i = 0; i < newOwnerLocks.length; i++) {
      if (newOwnerLocks[i] == id_) return;
    }
    newOwnerLocks.push(id_);
  }

  // --- fee configuration ---

  function ethFee() external view override returns (uint256) {
    return _ethFee;
  }

  function tokenFeeBps() external view override returns (uint16) {
    return _tokenFeeBps;
  }

  function feeReceiver() external view override returns (address) {
    return _feeReceiver;
  }

  function isExemptFromFees(address account_) external view override returns (bool) {
    return _feeExempt[account_];
  }

  function setEthFee(uint256 value_) external override onlyOwner {
    _ethFee = value_;
    emit FeeConfigUpdated(_ethFee, _tokenFeeBps, _feeReceiver);
  }

  function setTokenFeeBps(uint16 value_) external override onlyOwner {
    if (value_ > BPS_DENOMINATOR) revert TokenFeeTooHigh();
    _tokenFeeBps = value_;
    emit FeeConfigUpdated(_ethFee, _tokenFeeBps, _feeReceiver);
  }

  function setFeeReceiver(address payable value_) external override onlyOwner {
    if (value_ == address(0)) revert ZeroAddress();
    _feeReceiver = value_;
    emit FeeConfigUpdated(_ethFee, _tokenFeeBps, _feeReceiver);
  }

  function setFeeExempt(address account_, bool exempt_) external override onlyOwner {
    _feeExempt[account_] = exempt_;
  }
}
Chain explorer1257msChain node90ms