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.

Sepolia 0x0Cab0A9440643128540222acC6eF5028736675d3

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:

  1. Approve — User approves IFRLock to spend their IFR tokens.
    token.approve(lockAddress, amount)
  2. Lock — User calls lock(amount). Tokens transfer from user to contract. A LockData struct is stored on-chain.
  3. Record — Contract records the lock: amount (accumulates across multiple lock calls) and lockedAt (block timestamp of the first lock).
  4. Query — Applications call isLocked(user, minAmount) to check whether the user qualifies for premium access.
  5. Unlock — User calls unlock(). All locked tokens are returned to the user. The LockData struct is deleted.

Visual Flow

Approve
Lock IFR
isLocked = true
Premium Active
Unlock
Standard Plan

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.

Layer 1 — Truth Layer (On-Chain)
IFRLock Contract
Stores locked amounts. Exposes isLocked() and lockedBalance(). Contains no application logic, no user IDs, no access control decisions. Pure data.
Layer 2 — Bridge Layer (Stateless)
License Resolver Service
Maps wallet addresses to product entitlements. Queries isLocked() on each request — fully stateless, no database. Privacy-neutral: knows wallets but not identities.
Layer 3 — Service Layer (Off-Chain)
Partner Applications
Each app decides its own minimum lock amount for premium features. Queries the resolver API. Fully decoupled from the contract — can change thresholds without any on-chain transaction.

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

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.

Without feeExempt

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.

With feeExempt

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:

TierMinimum LockTypical Benefit
Bronze1,000 IFR5–10% discount
Silver2,500 IFR10–15% discount
Gold5,000 IFR15–20% discount
Platinum10,000 IFR20–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:

  1. User A locks 10,000 IFR → Gold Tier
  2. Creator B is registered with rewardBps = 1500 (15%)
  3. PartnerVault: recordLockReward(creatorB, 10000e9, userA)
  4. Creator B earns: 10,000 × 15% = 1,500 IFR from the Partner Pool
  5. Net deflation effect: 10,000 locked − 1,500 rewarded = 8,500 IFR effectively removed from circulation
  6. 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