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.
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:
amount(accumulates across multiple lock calls) andlockedAt(block timestamp of the first lock). -
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 tag. Functionally identical to lock() but stores a lockType for resolver metadata. |
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 partner'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 v5 examples for interacting with the IFRLock contract. IFR uses 9 decimals (not 18).
Connect to IFRLock
const { ethers } = require("ethers");
const LOCK_ADDRESS = "0x0Cab0A9440643128540222acC6eF5028736675d3";
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.providers.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.utils.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.utils.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.utils.formatUnits(balance, 9), "IFR");
// Get full lock info
const [lockedAmount, lockedAt] = await lock.lockInfo(userAddress);
console.log("Amount:", ethers.utils.formatUnits(lockedAmount, 9), "IFR");
console.log("Locked at:", new Date(lockedAt.toNumber() * 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.utils.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 partner products (Benefits Network, Creator content, premium features).
- Refundable: Users can call
unlock()at any time to retrieve their full locked amount. - Minimum lock: 1,000 IFR (Bronze Tier). Higher tiers at 2,500 / 5,000 / 10,000 IFR.
| Tier | Minimum Lock | Typical Benefit |
|---|---|---|
| Bronze | 1,000 IFR | 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. Lock & Creator Rewards Mechanism
When a user locks IFR for a registered partner/creator, the PartnerVault records a one-time reward from the 40M Partner Ecosystem Pool:
- User A locks 10,000 IFR → Gold Tier
- Creator B is registered with
rewardBps = 1500(15%) - PartnerVault:
recordLockReward(creatorB, 10000e9, userA) - Creator B earns: 10,000 × 15% = 1,500 IFR from the Partner Pool
- Net deflation effect: 10,000 locked − 1,500 rewarded = 8,500 IFR effectively removed from circulation
- Reward vesting: 1,500 IFR over 6–12 months (milestone-based)
Important: The reward comes from the dedicated 40M Partner Pool — not from the user's locked tokens.
The user's full 10,000 IFR remains locked and is fully refundable via unlock().
11. isLocked() Verification
Partners 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);
// 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 whitelisted addresses (set via Governance) can call
recordLockReward(). Random contracts cannot trigger rewards. - Anti-double-count:
walletRewardClaimed[wallet][partnerId]mapping ensures each wallet can only be rewarded once per partner. Re-locking after unlock does not generate a second reward. - Algorithmic throttle: As more IFR is locked protocol-wide, the effective reward rate decreases automatically (1% → 50% lock ratio scales reward from
rewardBpsdown toMIN_REWARD_BPS).