TokenLocker

0xfe52aa897703dca37b76b6fc51b12dc6f9f442c0

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
2,816 bytes
ABI entries
15
7 read · 3 write
License
none

Contract information

Address
0xfe52aa897703dca37b76b6fc51b12dc6f9f442c0
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x53080b843e…7e31cbbf80

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (7)

lockCount()uint256
lockedBalance(address, address)uint256
locks(uint256)address, address, uint256, uint64, bool
locksByToken(address, uint256)uint256
locksByTokenCount(address)uint256
locksOf(address, uint256)uint256
locksOfCount(address)uint256

Events (3)

ExtendedLockedWithdrawn

ABI

[
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "unlockAt",
        "type": "uint64"
      }
    ],
    "name": "Extended",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "unlockAt",
        "type": "uint64"
      }
    ],
    "name": "Locked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Withdrawn",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "newUnlockAt",
        "type": "uint64"
      }
    ],
    "name": "extend",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "unlockAt",
        "type": "uint64"
      },
      {
        "internalType": "address",
        "name": "beneficiary",
        "type": "address"
      }
    ],
    "name": "lock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lockCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "name": "lockedBalance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "sum",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "locks",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "unlockAt",
        "type": "uint64"
      },
      {
        "internalType": "bool",
        "name": "withdrawn",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "locksByToken",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "locksByTokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "locksOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "name": "locksOfCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      }
    ],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

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

/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    FLOW — TokenLocker (contract #5a)  ⚠️ WIP, NOT AUDITED

    A generic, public, EXTEND-ONLY token time-lock. Anyone can lock any ERC-20 for a beneficiary until an
    unlock timestamp that can only ever be pushed LATER (never shortened), then the beneficiary withdraws.

    Primary use: the dev first-buy lock — a creator buys their own token at launch and locks it here, a
    visible, trustless "skin in the game / can't rug the buy" signal (the frontend bundles buy → lock).
    Fee-on-transfer safe: the lock records the amount actually RECEIVED, so nothing is over-credited.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract TokenLocker is ReentrancyGuard {
    using SafeERC20 for IERC20;

    struct Lock {
        address token;
        address owner;      // the beneficiary who may extend + withdraw
        uint256 amount;     // amount actually received (fee-on-transfer safe)
        uint64 unlockAt;
        bool withdrawn;
    }

    Lock[] public locks;
    mapping(address => uint256[]) public locksOf;      // beneficiary → lock ids
    mapping(address => uint256[]) public locksByToken; // token → lock ids

    event Locked(uint256 indexed id, address indexed token, address indexed owner, uint256 amount, uint64 unlockAt);
    event Extended(uint256 indexed id, uint64 unlockAt);
    event Withdrawn(uint256 indexed id, uint256 amount);

    /// Lock `amount` of `token` for `beneficiary` until `unlockAt`. Pulls tokens from msg.sender.
    function lock(address token, uint256 amount, uint64 unlockAt, address beneficiary)
        external nonReentrant returns (uint256 id)
    {
        require(token != address(0) && beneficiary != address(0), "zero");
        require(amount > 0, "amount");
        require(unlockAt > block.timestamp, "past");

        IERC20 t = IERC20(token);
        uint256 before = t.balanceOf(address(this));
        t.safeTransferFrom(msg.sender, address(this), amount);
        uint256 received = t.balanceOf(address(this)) - before;
        require(received > 0, "no receipt");

        id = locks.length;
        locks.push(Lock(token, beneficiary, received, unlockAt, false));
        locksOf[beneficiary].push(id);
        locksByToken[token].push(id);
        emit Locked(id, token, beneficiary, received, unlockAt);
    }

    /// Push the unlock time later. Extend-only — can never shorten a lock.
    function extend(uint256 id, uint64 newUnlockAt) external {
        Lock storage l = locks[id];
        require(msg.sender == l.owner, "not owner");
        require(!l.withdrawn, "withdrawn");
        require(newUnlockAt > l.unlockAt, "extend only");
        l.unlockAt = newUnlockAt;
        emit Extended(id, newUnlockAt);
    }

    /// Withdraw a matured lock to its beneficiary.
    function withdraw(uint256 id) external nonReentrant {
        Lock storage l = locks[id];
        require(msg.sender == l.owner, "not owner");
        require(!l.withdrawn, "withdrawn");
        require(block.timestamp >= l.unlockAt, "locked");
        l.withdrawn = true;
        IERC20(l.token).safeTransfer(l.owner, l.amount);
        emit Withdrawn(id, l.amount);
    }

    // ─────────────────────────── views ───────────────────────────
    function lockCount() external view returns (uint256) { return locks.length; }
    function locksOfCount(address owner_) external view returns (uint256) { return locksOf[owner_].length; }
    function locksByTokenCount(address token) external view returns (uint256) { return locksByToken[token].length; }

    /// Sum of still-locked (not yet withdrawn) `token` held for `owner_` — for a "dev locked N" UI badge.
    function lockedBalance(address token, address owner_) external view returns (uint256 sum) {
        uint256[] storage ids = locksOf[owner_];
        for (uint256 i = 0; i < ids.length; i++) {
            Lock storage l = locks[ids[i]];
            if (l.token == token && !l.withdrawn) sum += l.amount;
        }
    }
}
Chain explorer1643msChain node92ms