Integration Guide

Create a Lock

struct Schedule {
    uint64 tgeDate;   // first release timestamp, must be in the future
    uint16 tgeBps;    // released at tgeDate; 10000 = full lock
    uint64 cycle;     // seconds between later releases
    uint16 cycleBps;  // released per cycle
}

struct LockParams {
    address taxToken;
    address beneficiary;
    uint256 amount;      // pulled from the caller; the lock records what actually arrives
    Schedule schedule;
}

function lock(LockParams calldata params) external payable returns (uint256 lockId);

Approve the locker for amount, then call lock with msg.value equal to the current defaultFee.creationFee (0 today). The lock records the amount that actually arrived, so a token that taxes the transfer in records less than amount. lockId is the index into the lock array and is emitted in FlapLockCreated.

The first lock of a token opens its pool: the registry is asked whether the token is a Flap token, which version it is and where its Dividend contract lives. A token the Portal does not know opens a pool with no dividend wiring.

Entry Points

FunctionWho may callWhat it does
claim(lockId)beneficiarySettles pending dividends, then pays all currently releasable principal. Reverts NothingToClaim when nothing is releasable
collect(lockId)anyonePays the lock's pending dividends to its beneficiary. Returns (0, 0) when there are none
transferBeneficiary(lockId, to)beneficiaryHands the whole lock to to, including unclaimed released principal and unsettled dividends. Not available on custom-fee locks
renounceBeneficiary(lockId)beneficiaryIrreversible. Settles dividends, then burns all remaining principal, released or not, to 0xdead

Order of operations: claim includes dividend settlement, so collect alone is only needed while nothing is releasable. Call claim (or collect) before transferBeneficiary to keep what has been earned, and claim before renounceBeneficiary so released principal is not burned.

Read a Lock

function getLock(uint256 lockId) external view returns (Lock memory);
function unlockedAmount(uint256 lockId) external view returns (uint256);  // released so far, claimed or not
function claimable(uint256 lockId) external view returns (uint256);       // released and not yet claimed
function pendingDividend(uint256 lockId) external view returns (uint256); // estimate for UIs; collect is exact
function locksLength() external view returns (uint256);
struct Lock {
    address taxToken;
    address beneficiary;   // address(0) once renounced
    uint256 locked;        // principal held for this lock, after the principal fee
    uint256 claimed;
    uint256 shares;        // locked - claimed while live, 0 after renounce
    uint256 rewardDebt;
    uint16  dividendBps;   // fee snapshotted at creation
    bool    fromCustomFee;
    Schedule schedule;
}

Whether a token's locks earn dividends: pools(token).dividendContract is non-zero when the pool is wired to a Dividend contract, zero when the token locks as principal only.

Check Admission

Admission lives on FlapTokenRegistry, not the locker. Before the first lock of a token, check it there:

function allowedTokenVersions(uint8 version) external view returns (bool);   // Flap token versions that may open pools
function allowedDividendTokens(address token) external view returns (bool);  // dividend tokens admitted by the registry owner
function query(address token) external view
    returns (bool isAllow, address dividendContract, address dividendToken, address quoteToken);
function isDividendTokenAllowed(address token, address quoteToken, address dividendToken) external view returns (bool);

query is what lock asks before opening a pool: isAllow is false for a Flap token whose version is closed (TokenNotAllowed), and dividendContract is zero when the token locks as principal only. isDividendTokenAllowed is the full dividend-token rule (quote token, the token itself, Flap-launched tokens and the owner allowlist); pass the quoteToken returned by query. allowedDividendTokens shows the owner allowlist alone.

Custom Fee

A negotiated fee is an EIP-712 message signed by feeSigner and submitted together with the lock:

function lockWithCustomFee(LockParams calldata params, CustomFee calldata customFee, bytes calldata signature)
    external payable returns (uint256 lockId);

Domain: name = "FlapTokenLocker", version = "1", chainId = 56 (mainnet) or 97 (testnet), verifyingContract = <locker address>.

CustomFee(address beneficiary,address taxToken,address lockBeneficiary,uint256 amount,
          uint64 tgeDate,uint16 tgeBps,uint64 cycle,uint16 cycleBps,
          uint96 creationFee,uint16 principalBps,uint16 dividendBps,uint256 nonce,uint256 deadline)

beneficiary is the only address allowed to submit the fee; every other field must match the submitted LockParams, so an offer signed for one deal cannot be spent on another. Each signature is consumed once, expires at deadline, and both bps fields stay under the 10% cap. Rotating feeSigner invalidates every unconsumed offer; re-request if it changed since issuance. Custom-fee locks cannot be transferred.

Events

EventEmitted on
FlapLockCreatedlock / lockWithCustomFee; carries the full Schedule, so an indexer can compute vesting from this event alone
FlapClaimedclaim
FlapDividendsSettledevery dividend payout, whichever entry point triggered it
FlapCollectedan explicit collect call
FlapPokedevery time a pool pulls dividends
FlapBeneficiaryTransferredtransferBeneficiary and renounceBeneficiary (new beneficiary address(0))
FlapRenouncedrenounceBeneficiary

Errors

ErrorMeaning
TokenNotAllowedFlap token whose version is not open in the registry
DividendTokenNotSupportedThe token's dividend token is not admitted (or has no code)
PortalUnavailableThe registry could not reach the Portal; retry
InvalidScheduleTGE in the past, bps over 10000, or a partial release without a cycle
ZeroAmountNothing arrived at the locker
WrongCreationFeemsg.value differs from the creation fee
NotBeneficiary, NothingToClaimCaller is not the beneficiary; nothing is releasable
CustomFeeLockNotTransferableCustom-fee locks cannot change beneficiary
InvalidSignature, SignatureExpired, SignatureUsed, NotFeeBeneficiary, Fee*Mismatch, FeeOverCapCustom-fee validation