IFR Lock Mechanism
Generic token lock for proving commitment — no rewards, no vesting, no app logic. Lock IFR, gain premium access.
1. Overview
IFRLock is a generic token lock contract. It has no rewards system, no vesting schedule, and no embedded application logic. Users lock IFR tokens to prove commitment, and external applications query isLocked() to gate premium access.
The contract acts as a neutral truth layer — it stores locked amounts and exposes read functions. What those locks mean is entirely up to the applications consuming them.
Important: IFRLock must be set as feeExempt on InfernoToken. Without this, fee deductions cause balance mismatches that prevent unlocking. See Section 8 for details.
Wallets may show a third-party risk or untrusted-contract warning while external reputation data is still updating. Before confirming, verify that the spender is IFRLock 0x769928aBDfc949D0718d8766a1C2d7dBb63954Eb and that the approval amount is the exact lock amount, not unlimited.
2. How the Lock Works
The lock flow is straightforward: approve, lock, query, unlock. Here is the step-by-step process:
-
Approve — User approves IFRLock to spend their IFR tokens.
token.approve(lockAddress, amount) -
Lock — User calls
lock(amount). Tokens transfer from user to contract. ALockDatastruct is stored on-chain. -
Record — Contract records the lock:
amountaccumulates across calls, whilelockedAtis overwritten with the timestamp of the latest increase. -
Query — Applications call
isLocked(user, minAmount)to check whether the user qualifies for premium access. -
Unlock — User calls
unlock(). All locked tokens are returned to the user. TheLockDatastruct is deleted.
Visual Flow
3. Key Functions
All public and external functions exposed by IFRLock:
| Function | Description |
|---|---|
lock(uint256 amount) |
Lock IFR tokens (requires prior approval). Accumulates on multiple calls — calling lock twice adds to the existing balance. |
lockWithType(uint256 amount, bytes32 lockType) |
Lock with an app-specific bytes32 tag emitted in the Locked event. The tag is not stored in the lock record; resolvers that need it must index the event. |
unlock() |
Withdraw all locked IFR back to the caller. Deletes the LockData struct entirely. |
isLocked(address user, uint256 minAmount) → bool |
Central query function for resolvers. Returns true if the user has at least minAmount locked. |
lockedBalance(address user) → uint256 |
Returns the total locked amount for a given user. |
lockInfo(address user) → (uint256, uint256) |
Returns full lock details: (amount, lockedAt) timestamp pair. |
totalLocked() → uint256 |
Returns the total IFR locked across all users in the contract. |
4. Multi-App Concept
The lockType parameter (bytes32) allows different applications to tag locks with an identifier. This is purely metadata — the contract itself does not enforce any logic based on the lock type.
Example Lock Types
// App-specific lock type tags
bytes32 partnerPremium = keccak256("partner_premium");
bytes32 app2Pro = keccak256("app2_pro");
bytes32 daoMembership = keccak256("dao_membership");
// Lock with type
lock.lockWithType(amount, partnerPremium);
Key principle: one user, one lock amount. The locked balance is shared across all applications querying the same IFRLock contract. If a user locks 10,000 IFR for one builder's premium tier, an app requiring 5,000 IFR for its pro tier will also see that user as qualified.
Design note: Lock types are metadata hints for off-chain resolvers. The on-chain contract treats all locks identically — isLocked() checks amount only, regardless of lock type.
5. Resolver Architecture
The IFR Lock system follows a three-layer architecture that cleanly separates concerns between on-chain truth, stateless bridging, and off-chain application logic.
isLocked() and lockedBalance(). Contains no application logic, no user IDs, no access control decisions. Pure data.
isLocked() on each request — fully stateless, no database. Privacy-neutral: knows wallets but not identities.
This architecture ensures that the on-chain layer remains minimal and auditable, while applications retain full flexibility over their business logic.
6. Code Examples
Complete ethers.js v6 examples for interacting with the IFRLock contract. IFR uses 9 decimals (not 18).
Connect to IFRLock
const { ethers } = require("ethers");
const LOCK_ADDRESS = "0x769928aBDfc949D0718d8766a1C2d7dBb63954Eb";
const LOCK_ABI = [
"function lock(uint256 amount) external",
"function lockWithType(uint256 amount, bytes32 lockType) external",
"function unlock() external",
"function isLocked(address user, uint256 minAmount) view returns (bool)",
"function lockedBalance(address user) view returns (uint256)",
"function lockInfo(address user) view returns (uint256 amount, uint256 lockedAt)",
"function totalLocked() view returns (uint256)"
];
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const lock = new ethers.Contract(LOCK_ADDRESS, LOCK_ABI, signer);
Lock 10,000 IFR
// IFR uses 9 decimals
const amount = ethers.parseUnits("10000", 9);
// Step 1: Approve IFRLock to spend tokens
const token = new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, signer);
const approveTx = await token.approve(LOCK_ADDRESS, amount);
await approveTx.wait();
console.log("Approved:", approveTx.hash);
// Step 2: Lock tokens
const lockTx = await lock.lock(amount);
await lockTx.wait();
console.log("Locked:", lockTx.hash);
Check Lock Status
const userAddress = "0x...";
const minRequired = ethers.parseUnits("5000", 9);
// Check if user qualifies for premium (>= 5000 IFR locked)
const isActive = await lock.isLocked(userAddress, minRequired);
console.log("Premium active:", isActive);
// Get exact locked balance
const balance = await lock.lockedBalance(userAddress);
console.log("Locked balance:", ethers.formatUnits(balance, 9), "IFR");
// Get full lock info
const [lockedAmount, lockedAt] = await lock.lockInfo(userAddress);
console.log("Amount:", ethers.formatUnits(lockedAmount, 9), "IFR");
console.log("Locked at:", new Date(Number(lockedAt) * 1000).toISOString());
Unlock All Tokens
const unlockTx = await lock.unlock();
await unlockTx.wait();
console.log("Unlocked:", unlockTx.hash);
// Verify: balance should be 0
const remaining = await lock.lockedBalance(signer.address);
console.log("Remaining locked:", ethers.formatUnits(remaining, 9), "IFR");
7. Security Notes
- ReentrancyGuard is applied to both
lock()andunlock()functions, preventing reentrancy attacks. - No admin force-unlock: No administrator, guardian, or governance proposal can force-unlock user funds. Only the user who locked can call
unlock(). - Emergency pause: The guardian can pause the contract, but this only affects
lock(). Theunlock()function is always available, even when paused — users can always withdraw their tokens. - feeExempt requirement: The IFRLock contract must be set as
feeExempton InfernoToken. Without this, fee deductions during transfer cause a balance mismatch that breaks unlock. See Section 8. - Guardian scope: The guardian can pause new locks but cannot touch, move, or redirect locked funds under any circumstance.
Key guarantee: Users retain full custody of their locked tokens at all times. The contract is a vault with a single key — the user's wallet.
8. feeExempt Requirement
InfernoToken applies a 3.5% fee on transfers. If IFRLock is not marked as feeExempt, this fee is deducted when tokens move in and out of the contract — causing a critical accounting mismatch.
User locks 5,000 IFR
3.5% fee deducted on transfer in
Contract receives ~4,825 IFR
User calls unlock() — contract tries to send 5,000 IFR
Transaction reverts. Contract only holds 4,825 but owes 5,000.
User locks 5,000 IFR
No fee deducted (exempt)
Contract receives 5,000 IFR
User calls unlock() — contract sends 5,000 IFR
Perfect round-trip. Full amount returned.
Setting feeExempt requires a Governance proposal with a 48-hour timelock. This is a one-time configuration step during deployment.
// Governance proposal to set IFRLock as feeExempt
// This is executed via the 48h timelock governance process
await governance.propose(
tokenAddress, // target
0, // value
encodeFunctionData("setFeeExempt", [LOCK_ADDRESS, true]), // calldata
"Set IFRLock as feeExempt for correct lock/unlock accounting"
);
9. Lock Economics
The IFR Lock mechanism is a Refundable Deposit Model, not staking. Key distinctions:
- No yield / no APY: Locking IFR does not generate rewards for the locker. There is no staking, no slashing, no impermanent loss.
- Purpose: Locking grants tier-based access to builder products (Benefits Network, Creator content, premium features).
- Refundable: Users can call
unlock()at any time to retrieve their full locked amount. - Current first-party baseline: 1,000 IFR (Bronze Tier) for the default IFR premium flow. Builder products can set their own lock thresholds through off-chain/resolver policy without changing the IFRLock contract. Higher example tiers: 2,500 / 5,000 / 10,000 IFR.
| Tier | Minimum Lock | Typical Benefit |
|---|---|---|
| Bronze | 1,000 IFR current default | 5–10% discount |
| Silver | 2,500 IFR | 10–15% discount |
| Gold | 5,000 IFR | 15–20% discount |
| Platinum | 10,000 IFR | 20–25% discount |
10. Locks and Partner Rewards
A lock never earns a reward — not for the locker and not for a builder. Partner rewards from the 40M PartnerVault are not active: no partner is registered and no reward has been paid.
Under the decided model (2026-10-03), a registered pilot partner receives IFR only for
verified checkout redemptions: the reward is valued in EUR, paid in IFR at the published
7-day TWAP at settlement, capped by a fixed per-partner budget and settled per Governance proposal
(recordMilestone), vesting over 6–12 months. The pool is never refilled.
Contract note: PartnerVault also contains recordLockReward, which would pay
lockAmount × rewardBps / 10000 (currently 1500 bps). The project does not use this path,
because a lock can be opened and closed again to farm it. A user's locked IFR always stays the user's own
and is fully refundable via unlock().
11. isLocked() Verification
Builders and businesses verify lock status with a single on-chain read call:
const { ethers } = require("ethers");
const provider = new ethers.JsonRpcProvider(RPC_URL);
const ifrLock = new ethers.Contract(IFRLOCK_ADDRESS, [
"function isLocked(address user, uint256 minAmount) view returns (bool)"
], provider);
// Example: check if wallet has at least 1,000 IFR locked (9 decimals)
const minAmount = ethers.parseUnits("1000", 9);
const locked = await ifrLock.isLocked(walletAddress, minAmount);
// Returns: true or false
This is a view function — no gas cost, no transaction, instant response. Any frontend, backend, or smart contract can call it permissionlessly.
12. Anti-Gaming
- No flashloan attacks: Lock is persistent (stored in contract state), not per-transaction. A flashloan cannot fake a lock status because
isLocked()reads stored balances, not transient ones. - authorizedCaller pattern: Only the Governance admin or a caller whitelisted by Governance can call
recordLockReward(); no caller is whitelisted, and project policy (model B) uses neither path. - Anti-double-count:
walletRewardClaimed[wallet][partnerId]mapping ensures each wallet can only be rewarded once per builder. Re-locking after unlock does not generate a second reward. - Algorithmic throttle (implemented, not active on Mainnet): once Governance sets
ifrLock, the effective reward rate decreases as more IFR is locked protocol-wide (1% → 50% lock ratio scales reward fromrewardBpsdown toMIN_REWARD_BPS). On MainnetifrLockis unset (address(0)), so the flatrewardBpsof 1500 applies.