跳到主要内容

Secure Lock

Secure Lock 是 CheesePad 的链上锁仓产品,用于流动性池、代币分配与归属(vesting)计划。
它将透明的公开锁仓浏览器与引导式流程结合在一起,帮助团队正确配置锁仓,同时便于第三方服务可靠地索引与展示。

本页重点介绍 数据模型、行为与展示规范,以便:

  • 浏览器 / 第三方工具(例如 DEX 分析、追踪器)可以一致地摄取并展示 Secure Lock 信息。
  • 开发者可以理解锁仓随时间的行为变化。
  • 终端用户可以快速了解锁定了什么、锁定多久,以及在什么条件下解锁。

提示: 本页为技术参考。为避免歧义,结构体/函数名称与代码示例保持英文不变。

合约源码、ABI 与部署脚本请参阅开源仓库:
https://github.com/cheesepad-ai/cheese-lock.

CheesePad 官方锁仓页面(BSC):

已部署的 CheeseLock 合约

CheeseLock 是 CheesePad 用于安全锁定代币与归属的链上智能合约。

BNB Smart Chain

CheeseLock 合约地址:

0x1E7826b7E3244aDB5e137A5F1CE49095f6857140

在 BscScan 查看

BNB Smart Chain Testnet

CheeseLock 合约地址:

0x1E7826b7E3244aDB5e137A5F1CE49095f6857140

在 BscScan 查看(测试网)

链上合约与网络

Secure Lock 通过链上 locker 合约实现(本文档中称为 locker contract)。

  • 合约名称:通常为 SecureLock(准确名称与 ABI 请见 cheese-lock 仓库)。
  • 网络:该组件 与链无关(chain agnostic),但当前生产部署聚焦于 EVM 链
    产品文档引用:“BSC is live today, but the component is chain agnostic. Use uppercase network tickers.”
  • 官方地址:各支持链的 locker 合约地址维护在 cheese-lock 仓库与 CheesePad 内部部署配置中。

集成时:

  • 始终从以下渠道获取 官方 locker 地址
    • cheese-lock GitHub 仓库,或
    • CheesePad 官方文档/公告,或
    • CheesePad 核心团队的直接确认。
  • 不要依赖第三方复制的合约地址。
  • 请验证:
    • 字节码与相关区块浏览器中发布的源码一致。
    • 地址由 CheesePad 官方团队持有并对外公告。

第三方浏览器(例如 DEX 工具)通常会:

  • 监听 locker 合约事件以捕捉锁仓创建与解锁活动。
  • 将每条链上记录映射到下方描述的 Secure Lock 数据模型
  • 使用 statusscheduleamount 字段渲染锁仓卡片与详情。

支持的锁仓类型

在 Solidity 层面,CheeseLock 合约只暴露 一个 Lock struct
不同的“锁仓类型”由 字段组合与创建函数组合 表达,而不是通过链上 enum。

  • Asset type (token vs LP):
    • 由传入 lockvestingLockmultipleVestingLockisLpToken 标志决定。
    • 合约内部:LP 锁仓通过 _lockLpToken 注册,非 LP 锁仓通过 _lockNormalToken 注册。
    • 对于 LP 锁仓,cumulativeLockInfo[token].factory 会被设置为该 LP 的 factory;普通代币则为 address(0)
  • Schedule type (normal vs vesting):
    • Normal lock:通过 lock(...) 创建,存储为 Lock,其中 tgeBps == 0cycle == 0cycleBps == 0。它会在 tgeDate 一次性解锁。
    • Vesting lock:通过 vestingLock(...)multipleVestingLock(...) 创建,存储为 Lock,其中 tgeBps > 0cycle > 0cycleBps > 0tgeBps + cycleBps <= 10_000。它会随时间 逐步解锁。

此外,产品/UI 层可以自由地将锁仓标记为 “LP Lock”、“Team allocation” 等,但这些标签并不会编码进 Solidity 合约。

核心概念

从高层看,一个 Secure Lock 实例包含:

  • Lock identity:全局唯一标识,以及创建该锁的链上交易。
  • Scope:锁定了哪个代币或 LP 交易对,以及位于哪条链上。
  • Amount:锁定的代币(或 LP 代币)数量,以及可选的 USD 参考值。
  • Unlock schedule:何时、以何种方式可领取锁定资产。
  • Status:锁仓是否处于 active、部分解锁、完全完成或已取消(如适用)。

产品文档强调:

  • 透明:公开的锁仓浏览页,让任何人都能核验项目锁仓。
  • 一致:用于清晰表达时间表与资产类型的 UI chips 与徽章。这些由产品/indexer 层实现,不属于 Solidity 接口的一部分。

链上数据模型

本节记录 CheeseLock 合约 真实的 Solidity structs 与可观察行为
下方内容均直接取自 cheesepad-ai/cheese-lock 的合约代码。

1. Lock struct

Each lock is stored in an array:

struct Lock {
uint256 id;
address token;
address owner;
uint256 amount;
uint256 lockDate;
uint256 tgeDate; // TGE date for vesting locks, unlock date for normal locks
uint256 tgeBps; // In bips. Is 0 for normal locks
uint256 cycle; // Is 0 for normal locks
uint256 cycleBps; // In bips. Is 0 for normal locks
uint256 unlockedAmount;
string description;
}

Field semantics:

  • id: Lock identifier. In this implementation it is locks.length + ID_PADDING at creation time.
  • token: ERC‑20 token address being locked. For LP locks this is the LP token address.
  • owner: Current owner of the lock; has permission to unlock, edit, or transfer ownership.
  • amount: Total amount of tokens associated with this lock (in token smallest units).
  • lockDate: Block timestamp when the lock was created.
  • tgeDate:
    • For normal locks (tgeBps == 0 and cycle == 0 and cycleBps == 0): the unlock timestamp.
    • For vesting locks: the TGE (Token Generation Event) timestamp when the first portion becomes claimable.
  • tgeBps:
    • 0 for normal locks.
    • For vesting locks: basis points (out of 10_000) that unlock at tgeDate.
  • cycle:
    • 0 for normal locks.
    • For vesting locks: cycle duration in seconds.
  • cycleBps:
    • 0 for normal locks.
    • For vesting locks: basis points of amount that unlock on each cycle after TGE.
  • unlockedAmount: Total amount already withdrawn by the owner.
  • description: Free-form text; editable via editLockDescription.

Normal vs vesting locks:

  • lock(...) creates a normal lock:
    • tgeBps = 0, cycle = 0, cycleBps = 0.
    • tgeDate is the single unlock timestamp.
  • vestingLock(...) and multipleVestingLock(...) create vesting locks:
    • Require tgeDate > now, cycle > 0, 0 < tgeBps < 10_000, 0 < cycleBps < 10_000, and tgeBps + cycleBps <= 10_000.

2. CumulativeLockInfo struct

The contract tracks cumulative locked amounts per token:

struct CumulativeLockInfo {
address token;
address factory;
uint256 amount;
}
  • token: The ERC‑20 / LP token address.
  • factory:
    • For LP locks: the LP’s factory (e.g. UniswapV2‑compatible factory), inferred via _parseFactoryAddress.
    • For normal token locks: address(0).
  • amount: Sum of amount minus unlockedAmount over all active locks for this token.

Explorers can use factory != address(0) as a canonical way to detect LP tokens for this locker.

3. Internal sets and indexes

Internally the contract organizes locks using EnumerableSet:

  • By user & asset type:
    • _userLpLockIds[owner] – set of LP lock IDs for a given owner.
    • _userNormalLockIds[owner] – set of non‑LP lock IDs for a given owner.
  • By token type:
    • _lpLockedTokens – set of LP token addresses with non‑zero cumulative locked amount.
    • _normalLockedTokens – set of non‑LP token addresses with non‑zero cumulative locked amount.
  • By token:
    • _tokenToLockIds[token] – set of lock IDs associated with a given token.

These sets are surfaced via various view functions documented below, and are crucial for indexers.

4. Events

The main events emitted by CheeseLock are:

event LockAdded(
uint256 indexed id,
address token,
address owner,
uint256 amount,
uint256 unlockDate
);

event LockUpdated(
uint256 indexed id,
address token,
address owner,
uint256 newAmount,
uint256 newUnlockDate
);

event LockRemoved(
uint256 indexed id,
address token,
address owner,
uint256 amount,
uint256 unlockedAt
);

event LockVested(
uint256 indexed id,
address token,
address owner,
uint256 amount,
uint256 remaining,
uint256 timestamp
);

event LockDescriptionChanged(uint256 lockId);

event LockOwnerChanged(uint256 lockId, address owner, address newOwner);

Indexers SHOULD:

  • Subscribe to LockAdded to detect new locks (both normal and vesting).
  • Listen to LockRemoved and LockVested to track unlocks and remaining balances.
  • Use LockUpdated to pick up changes to amount or tgeDate.
  • Use LockDescriptionChanged to refresh any cached human‑readable description.
  • Use LockOwnerChanged to track ownership transfers, including renounces (newOwner = address(0)).

5. Unlock behavior

Unlocking logic is split between normal and vesting flows:

  • Normal unlock (_normalUnlock):
    • Requires block.timestamp >= userLock.tgeDate.
    • Requires userLock.unlockedAmount == 0 (one‑time full unlock).
    • Transfers amount to msg.sender, sets unlockedAmount = amount, updates cumulative totals, and emits LockRemoved.
  • Vesting unlock (_vestingUnlock):
    • Uses _withdrawableTokens(userLock) to compute how many tokens are currently claimable.
    • If newTotalUnlockAmount == amount, it removes the lock ID from the relevant sets and emits both LockRemoved and LockVested.
    • Always emits LockVested with amount unlocked in this call, remaining amount, and timestamp.

The helper used for vesting calculations:

function _withdrawableTokens(Lock memory userLock) internal view returns (uint256) {
if (userLock.amount == 0) return 0;
if (userLock.unlockedAmount >= userLock.amount) return 0;
if (block.timestamp < userLock.tgeDate) return 0;
if (userLock.cycle == 0) return 0;

uint256 tgeReleaseAmount = FullMath.mulDiv(userLock.amount, userLock.tgeBps, 10_000);
uint256 cycleReleaseAmount = FullMath.mulDiv(userLock.amount, userLock.cycleBps, 10_000);

uint256 currentTotal = 0;
if (block.timestamp >= userLock.tgeDate) {
currentTotal =
(((block.timestamp - userLock.tgeDate) / userLock.cycle) * cycleReleaseAmount) +
tgeReleaseAmount;
}

if (currentTotal > userLock.amount) {
return userLock.amount - userLock.unlockedAmount;
} else {
return currentTotal - userLock.unlockedAmount;
}
}

Informally:

  • Before tgeDate: nothing is withdrawable.
  • At tgeDate: tgeBps of amount becomes available.
  • After each multiple of cycle seconds since tgeDate: an additional cycleBps of amount is released.
  • Total unlocked is capped at amount.

6. Public/external functions relevant for integrators

From the CheeseLock implementation (and its ICheeseLock interface), key functions are:

  • Lock creation
    • lock(owner, token, isLpToken, amount, unlockDate, description) returns (uint256 id)
      • Creates a normal lock that unlocks fully at unlockDate.
    • vestingLock(owner, token, isLpToken, amount, tgeDate, tgeBps, cycle, cycleBps, description) returns (uint256 id)
      • Creates a vesting lock with TGE and cycle‑based vesting.
    • multipleVestingLock(owners[], amounts[], token, isLpToken, tgeDate, tgeBps, cycle, cycleBps, description) returns (uint256[] ids)
      • Batch version of vestingLock for multiple owners.
  • Unlocking
    • unlock(lockId)
      • If lock is vesting (tgeBps > 0), calls _vestingUnlock.
      • Otherwise, calls _normalUnlock.
    • withdrawableTokens(lockId) view returns (uint256)
      • Convenience view returning _withdrawableTokens(getLockById(lockId)).
  • Editing metadata
    • editLock(lockId, newAmount, newUnlockDate)
      • Owner‑only. Can increase amount (not decrease) and/or push tgeDate forward.
    • editLockDescription(lockId, description)
      • Owner‑only. Updates description.
  • Ownership
    • transferLockOwnership(lockId, newOwner)
    • renounceLockOwnership(lockId) (sets owner to address(0)).

Indexing & querying:

  • getTotalLockCount() view returns (uint256)
  • getLockAt(index) view returns (Lock memory)
  • getLockById(lockId) view returns (Lock memory)
  • allLpTokenLockedCount(), allNormalTokenLockedCount()
  • getCumulativeLpTokenLockInfoAt(index), getCumulativeNormalTokenLockInfoAt(index)
  • getCumulativeLpTokenLockInfo(start, end), getCumulativeNormalTokenLockInfo(start, end)
  • totalTokenLockedCount()
  • Per‑user:
    • lpLockCountForUser(user), lpLocksForUser(user), lpLockForUserAtIndex(user, index)
    • normalLockCountForUser(user), normalLocksForUser(user), normalLockForUserAtIndex(user, index)
  • Per‑token:
    • totalLockCountForToken(token)
    • getLocksForToken(token, start, end)

These views give everything a third‑party explorer needs to reconstruct all locks and balances from on-chain state.

索引器示例数据模型(TypeScript)

Below is an example off-chain schema mirroring the Solidity structs and adding a few derived helpers.
Fields marked as “derived” are computed off-chain and are not stored in the contract.

export interface LockRecord {
id: number;
token: string;
owner: string;
amount: string; // raw token units
lockDate: number; // unix seconds
tgeDate: number; // unix seconds
tgeBps: number;
cycle: number; // seconds
cycleBps: number;
unlockedAmount: string; // raw token units
description: string;

// Derived helpers (off-chain only)
isVesting: boolean; // tgeBps > 0 && cycle > 0 && cycleBps > 0
isLpToken: boolean; // cumulativeLockInfo[token].factory != address(0)
}

export interface CumulativeLockInfoRecord {
token: string;
factory: string; // 0x0 for normal tokens
amount: string; // total locked (minus unlocked)
}

使用示例

This section shows how to read lock data from the CheeseLock contract using common Ethereum libraries.

Reading lock data by ID with ethers.js

import { ethers } from "ethers";
import CheeseLockABI from "./abi/CheeseLock.json";

const provider = new ethers.JsonRpcProvider("https://bsc-dataseed1.binance.org/");

// CheeseLock contract address on BNB Smart Chain
const contractAddress = "0x1E7826b7E3244aDB5e137A5F1CE49095f6857140";
const contract = new ethers.Contract(contractAddress, CheeseLockABI, provider);

async function getLockData(lockId: number) {
try {
const lockData = await contract.getLockById(lockId);
console.log("Lock Data:", {
id: lockData.id.toString(), // Unique lock identifier
token: lockData.token, // Token contract address
owner: lockData.owner, // Lock owner address
amount: lockData.amount.toString(), // Total locked amount
lockDate: new Date(Number(lockData.lockDate) * 1000).toISOString(), // Lock creation timestamp
tgeDate: new Date(Number(lockData.tgeDate) * 1000).toISOString(), // TGE / unlock date
tgeBps: lockData.tgeBps.toString(), // TGE unlock percentage in basis points
cycle: lockData.cycle.toString(), // Vesting cycle duration in seconds
cycleBps: lockData.cycleBps.toString(), // Cycle unlock percentage in basis points
unlockedAmount: lockData.unlockedAmount.toString(), // Amount already unlocked
description: lockData.description, // Lock description
});
return lockData;
} catch (error) {
console.error("Error fetching lock data:", error);
}
}

// Example usage
getLockData(1);

Reading lock data by ID with viem

import { createPublicClient, http, getContract } from "viem";
import { bsc } from "viem/chains";
import CheeseLockABI from "./abi/CheeseLock.json";

const publicClient = createPublicClient({
chain: bsc,
transport: http(),
});

const contractAddress = "0x1E7826b7E3244aDB5e137A5F1CE49095f6857140" as const;

const contract = getContract({
address: contractAddress,
abi: CheeseLockABI,
publicClient,
});

async function getLockData(lockId: bigint) {
try {
const lockData = await contract.read.getLockById([lockId]);
console.log("Lock Data:", {
id: lockData.id.toString(), // Unique lock identifier
token: lockData.token, // Token contract address
owner: lockData.owner, // Lock owner address
amount: lockData.amount.toString(), // Total locked amount
lockDate: new Date(Number(lockData.lockDate) * 1000).toISOString(), // Lock creation timestamp
tgeDate: new Date(Number(lockData.tgeDate) * 1000).toISOString(), // TGE / unlock date
tgeBps: lockData.tgeBps.toString(), // TGE unlock percentage in basis points
cycle: lockData.cycle.toString(), // Vesting cycle duration in seconds
cycleBps: lockData.cycleBps.toString(), // Cycle unlock percentage in basis points
unlockedAmount: lockData.unlockedAmount.toString(), // Amount already unlocked
description: lockData.description, // Lock description
});
return lockData;
} catch (error) {
console.error("Error fetching lock data:", error);
}
}

// Example usage
getLockData(1n);

Example: normal lock (non-vesting)

{
"id": 1,
"token": "0xTOKEN_ADDRESS",
"owner": "0xOWNER",
"amount": "1000000000000000000000",
"lockDate": 1704067200,
"tgeDate": 1706745600,
"tgeBps": 0,
"cycle": 0,
"cycleBps": 0,
"unlockedAmount": "0",
"description": "Team lock for 30 days",
"isVesting": false,
"isLpToken": false
}

Example: vesting lock with TGE and cycles

{
"id": 2,
"token": "0xTOKEN_ADDRESS",
"owner": "0xTEAM_MULTISIG",
"amount": "5000000000000000000000000",
"lockDate": 1704067200,
"tgeDate": 1706745600,
"tgeBps": 1000,
"cycle": 2592000,
"cycleBps": 375,
"unlockedAmount": "0",
"description": "Team vesting: 10% at TGE, 3.75% per month",
"isVesting": true,
"isLpToken": false
}

审核锁仓请求(面向 ops / reviewers)

The Secure Lock product documentation suggests the following checklist for manual review:

  1. Confirm the token address matches the Launchpad listing (if applicable).
  2. Ensure liquidity is routed to the locker before the public pool goes live.
  3. Store transaction hashes in the CMS so the leaderboard or analytics teams can surface them later.
  4. Reply via Telegram or official channels if any field is unclear, keeping the same support channels as cheesepad.ai’s footer links.

Third‑party services do not need to perform all of these steps, but they SHOULD at least:

  • Verify that the token address is correct and not a honeypot/impersonator.
  • Verify that the locker contract is the official CheesePad Secure Lock deployment for that chain.
  • Display a clear warning if any inconsistencies are detected.

公开透明与展示清单(面向浏览器 / 分析)

To keep the Secure Lock experience trustworthy and visually consistent across the ecosystem, we recommend:

  • Lock cards / detail pages
    • Display lock cards on routes like /lock/<chain>/<type> using a metric grid similar to CheesePad’s homepage.
    • Show at least: token/LP name, amount, USD reference, lock type, schedule badge, and countdown timer.
  • Links and sharing
    • Include a View on explorer link (e.g. BscScan) for the lock creation transaction and token contract.
    • Include a Share button so teams can promote their proof of lock on social platforms.
  • Countdown timers
    • Show live countdown timers for active locks (time until cliff or full unlock).
    • Use brown or highlight color for active timers and slate / muted text for completed locks, matching the guidance from the official docs.
  • Badges and chips
    • Use clear chips like LP Lock, Team Allocation, Cliff, Daily, Instant, etc.
    • Match the playful chip style used on CheesePad’s own UI so users immediately recognize Secure Lock semantics.

Keeping these standards tight ensures that every Secure Lock integration feels trustworthy, predictable, and on brand, whether users are viewing it on CheesePad directly or through third‑party tools and explorers.