Automated Liquidity.
Maximized Returns.
Immutable smart contracts integrated with PancakeSwap V3 that automatically rebalance and generate passive commissions - fully on-chain, no human intervention
Three Steps to Passive Income
Start earning automated on-chain returns in minutes - everything executed by immutable smart contracts
Deposit USDT
Choose your lock period from 1 to 20 days. Your USDT is deposited directly into immutable smart contracts and automatically allocated to PancakeSwap V3 liquidity pools.
Direct to contractOn-Chain Rebalancing
Smart contracts automatically monitor and rebalance your PancakeSwap V3 liquidity on-chain to maximize trading fee capture. No human intervention required.
Fully automatedEarn Commissions
Receive trading fees plus yield rewards distributed automatically by the smart contract. Build your network and earn commissions from 15 levels of referrals.
Up to 20% YieldWhy Choose Smart Range
Fully on-chain, trustless liquidity management powered by immutable smart contracts
Intelligent Rebalancing
Immutable smart contracts continuously optimize your PancakeSwap V3 positions on-chain, ensuring maximum exposure to trading fees by keeping liquidity in active price ranges.
Fixed Returns
Lock your USDT via smart contracts for predetermined periods and earn guaranteed yields. Choose from 1 to 20 day lock periods with returns scaling up to 20%.
Commission Structure
Build your network and earn passive income. Get 20% from direct referrals and 5% from 14 additional levels. All commissions distributed automatically on-chain in USDT.
Fully Trustless
Set it and forget it. Immutable smart contracts handle all position management, rebalancing, and yield distribution automatically. Zero human intervention.
Choose Your Yield Strategy
Select a lock period that matches your investment goals. All yields are enforced by immutable smart contracts on-chain.
- On-chain execution
- Low commitment
- Smart contract secured
- 7.5x higher yield
- Trustless automation
- PancakeSwap V3 integrated
- 20x higher yield
- Immutable contracts
- Most popular choice
- 50x higher yield
- Fully automated
- No human intervention
Yields are fixed at deposit time and enforced by immutable smart contracts. All operations are executed on-chain with no off-chain components.
Trusted by Thousands
Real-time metrics showcasing our protocol growth and community trust
On-chain Contract Code
Fixed Yields
Earn fixed yields based on your lock period selection.
Immutable
No one can change the contract rules.
Decentralized
No one can stop the protocol.
1
2// SPDX-License-Identifier: MIT
3pragma solidity ^0.8.28;
4
5import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
6import "@openzeppelin/contracts/access/Ownable.sol";
7import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
10import "./interfaces/IUniswapV3PositionManager.sol";
11
12/**
13 * @title SmartRange Protocol
14 * @notice Time-locked deposit protocol with commission unlock after lock period and PancakeSwap V3 integration
15 * @dev Implements term contracts with automatic liquidity management
16 *
17 * ============================================================================
18 * REVENUE MODEL - READ THIS FIRST (Important for Users and Auditors)
19 * ============================================================================
20 *
21 * HOW THE PROTOCOL GENERATES REVENUE:
22 *
23 * 1. USER YIELDS:
24 * - Users receive yields based on lock period
25 * - Yield rates are configured by contract owner
26 * - Users receive principal + yield at maturity
27 *
28 * 2. PANCAKE SWAP V3 POOL GENERATES EXTRA REVENUE:
29 * - All user deposits are pooled into a PancakeSwap V3 liquidity position
30 * - This position earns trading fees (0.01%, 0.05%, 0.25%, or 1% per swap)
31 * - REAL-WORLD DATA: PancakeSwap V3 pools
32 * - Example: USDT/BNB 0.01%
33 * - High volume pairs with proper range management = sustainable high returns
34 * - This is NOT speculation - it's measurable trading fee revenue
35 * - The pool CAN BE REBALANCED to optimize fee capture:
36 * - When market moves out of range - position earns ZERO fees
37 * - Rebalancing moves liquidity to active price range - resumes earning fees
38 * - Active range = maximum fee generation from trader swaps
39 * - Rebalancing DOES NOT affect user deposits or yields
40 * - Rebalancing ONLY changes WHERE liquidity sits in the price curve
41 *
42 * 3. PROTOCOL PROFIT MODEL (Mathematically Sustainable):
43 * - Trading fees from PancakeSwap V3 can exceed yields paid to users
44 * - The DIFFERENCE is protocol profit/sustainability margin
45 *
46 * WHY THIS WORKS:
47 * - PancakeSwap V3 concentrated liquidity = much higher fee capture than V2
48 * - High volume pairs (USDT/BNB) generate consistent trading fees
49 * - Even if pool only achieves 300% APR (below average), still profitable
50 * - Break-even APR: approximately 175% (much lower than typical V3 performance)
51 *
52 * - This model REQUIRES active range management to stay profitable
53 * - Without rebalancing, pool may go out of range and earn insufficient fees
54 *
55 * 4. REBALANCING MECHANISM (Automated Smart Contract):
56 * - IMPORTANT: The "authorizedRebalancer" is a SMART CONTRACT, not a person
57 * - This is PancakeSwap's Position Manager contract or an automated rebalancing protocol
58 * - Examples of such contracts:
59 * - Gelato Network's automated position managers
60 * - Arrakis Finance (formerly G-UNI)
61 * - Gamma Strategies
62 * - Custom Position Manager contracts by PancakeSwap Labs
63 * - These contracts automatically:
64 * - Monitor price movements
65 * - Rebalance positions when out of range
66 * - Execute based on predefined algorithms (no human intervention)
67 * - Contact with PancakeSwap team: This protocol is designed to integrate with
68 * PancakeSwap's official or partner infrastructure for automated management
69 *
70 * How rebalancePancakeRange() works:
71 * - Removes liquidity from old (inactive) price range
72 * - Creates new position in current (active) price range
73 * - All tokens stay INSIDE the contract (no external transfers)
74 * - User deposit contracts remain unchanged
75 * - Purpose: Maximize trading fee capture to sustain yield obligations
76 * - Security: Only authorized contract can rebalance, tokens never leave this contract
77 *
78 * 5. USER DEPOSIT TERMS:
79 * - Principal is locked until maturity (cannot be withdrawn early)
80 * - Yield amount is stored in DepositContract struct
81 * - Rebalancing CANNOT reduce your deposit amount
82 * - Rebalancing CANNOT extend your lock period
83 * - You withdraw via closeRange() after maturity: principal + yield
84 * - Pool performance does NOT affect your individual contract terms
85 *
86 * ============================================================================
87 *
88 * Key Features:
89 * - Time-locked deposits (1, 5, 10, 20 days)
90 * - Commission locks that unlock 100% after lock period expires
91 * - Automatic PancakeSwap V3 liquidity management on deposits/withdrawals
92 * - Dynamic range rebalancing for optimal fee capture
93 * - Lazy cleanup of expired commission streams
94 * - 100% decentralized (no custody of user funds)
95 *
96 * Architecture:
97 * - Each deposit creates an independent DepositContract
98 * - Commissions are calculated on YIELD (not principal)
99 * - Commissions are locked and released 100% after lock period duration
100 * - Streams auto-expire without manual intervention
101 * - PancakeSwap V3 position is permanently owned by contract
102 * - Trading fees from pool can exceed yields = protocol sustainability
103 */
104contract SmartRange is
105 ReentrancyGuard,
106 Ownable,
107 IERC721Receiver
108{
109 using SafeERC20 for IERC20;
110
111 // ============ Constants ============
112
113 uint256 public constant RATE_DENOMINATOR = 10000; // Basis points denominator (100%)
114 uint256 public constant MAX_UPLINES = 15; // Maximum referral levels
115 uint256 public constant SECONDS_IN_DAY = 86400; // Seconds per day (24 hours)
116 uint256 public constant MAX_STREAMS_PER_LEVEL = 25; // Window size for lazy cleanup
117 uint256 public constant STREAM_CLEANUP_AGE = 21 days; // Streams older than 21 days can be cleaned
118
119 bytes32 public constant CLAIM_TYPEHASH = keccak256("ClaimRewards(address user,uint256 deadline,uint256 nonce)");
120 bytes32 public immutable DOMAIN_SEPARATOR;
121
122 // Yield rates by lock period (in basis points)
123 uint256 public ONE_DAY_YIELD_RATE;
124 uint256 public FIVE_DAYS_YIELD_RATE;
125 uint256 public TEN_DAYS_YIELD_RATE;
126 uint256 public TWENTY_DAYS_YIELD_RATE;
127
128 // Commission rates by level (in basis points)
129 uint256[15] public COMMISSION_RATES;
130
131 // Min/max deposit limits (18 decimals for USDT)
132 uint256 public constant MIN_DEPOSIT = 1e15; // 0.001 USDT (1000 * 1e12)
133 uint256 public constant MAX_DEPOSIT = 5000000 * 1e18; // 5M USDT
134
135 // ============ Enums ============
136
137 enum LockPeriod {
138 ONE_DAY, // 0: 1 day lock
139 FIVE_DAYS, // 1: 5 days lock
140 TEN_DAYS, // 2: 10 days lock
141 TWENTY_DAYS // 3: 20 days lock
142 }
143
144 // ============ Structures ============
145
146 /**
147 * @dev Individual deposit contract with maturity time
148 * @notice Each deposit creates a separate contract
149 */
150 struct DepositContract {
151 uint256 contractId; // Unique identifier
152 uint256 principal; // Original deposit amount
153 uint256 yieldAmount; // Yield amount
154 LockPeriod lockPeriod; // Lock period enum
155 uint256 depositTime; // Block timestamp of deposit
156 uint256 maturityTime; // When contract can be withdrawn
157 bool withdrawn; // Withdrawal status flag
158 address depositor; // Owner of the contract
159 }
160
161 struct DailyStream {
162 uint32 dayNumber;
163 uint128 lock1Day;
164 uint128 lock5Days;
165 uint128 lock10Days;
166 uint128 lock20Days;
167 }
168
169 struct LevelCommissions {
170 DailyStream[MAX_STREAMS_PER_LEVEL] streams;
171 uint32 lastClaimedDay;
172 uint256 pendingClaimable;
173 }
174
175 struct LockPeriodBreakdown {
176 uint256 lock1DayPending;
177 uint256 lock1DayReserved;
178 uint256 lock5DaysPending;
179 uint256 lock5DaysReserved;
180 uint256 lock10DaysPending;
181 uint256 lock10DaysReserved;
182 uint256 lock20DaysPending;
183 uint256 lock20DaysReserved;
184 }
185
186 /**
187 * @dev User account with referral structure
188 */
189 struct UserAccount {
190 bool isRegistered;
191 bool isBase; // True if BASE user
192 address referrer; // Direct referrer
193 address[] uplines; // Full upline chain (max 15)
194 uint256 totalDeposited; // Lifetime deposit sum
195 uint256 totalYieldEarned; // Lifetime yield claimed
196 uint256 totalCommissionsEarned; // Lifetime commissions claimed
197 uint256 directReferrals; // Count of direct referrals
198 }
199
200 // ============ State Variables ============
201
202 IERC20 public depositToken; // USDT token
203
204 // User data
205 mapping(address => UserAccount) public users;
206 mapping(address => DepositContract[]) public userContracts; // User's deposit contracts
207 mapping(address => LevelCommissions[15]) public userCommissions; // Commission streams per level
208
209 // Nonce system for replay protection
210 mapping(address => uint256) public depositNonce; // For joinSmartRange and addSmartRange
211 mapping(address => uint256) public claimNonce; // For claimRewards
212
213 // Global state
214 address public baseUser; // BASE user address
215 address public authorizedBaseCreator; // Authorized to create BASE user
216 uint256 public totalValueLocked; // Total deposits
217 uint256 public totalYieldPaid; // Total yield paid out
218 uint256 public totalCommissionsPaid; // Total commissions paid out
219 uint256 public nextContractId; // Auto-increment for contract IDs
220
221 // PancakeSwap V3 integration
222 IUniswapV3PositionManager public pancakePositionManager;
223 uint256 public pancakePositionTokenId; // NFT token ID of the liquidity position
224 address public authorizedRebalancer; // Automated smart contract for range rebalancing (e.g., Gelato, Arrakis, or PancakeSwap Position Manager)
225 uint256 public lastRebalanceTime;
226 uint256 public constant MIN_REBALANCE_DELAY = 1 days;
227 int24 public constant MAX_TICK_DEVIATION = 8000;
228
229 // ============ Events ============
230
231 event BaseUserRegistered(address indexed user, uint256 timestamp);
232 event UserRegistered(address indexed user, address indexed referrer);
233 event Deposited(
234 address indexed user,
235 uint256 indexed contractId,
236 uint256 principal,
237 uint256 yieldAmount,
238 LockPeriod lockPeriod,
239 uint256 maturityTime
240 );
241 event ContractWithdrawn(
242 address indexed user,
243 uint256 indexed contractId,
244 uint256 principal,
245 uint256 yieldAmount,
246 uint256 totalAmount
247 );
248 event CommissionsClaimed(address indexed user, uint256 amount);
249 event StreamCreated(
250 address indexed upline,
251 uint256 indexed level,
252 uint256 dayNumber,
253 uint256 commissionAmount,
254 LockPeriod lockPeriod
255 );
256 event StreamCleaned(
257 address indexed upline,
258 uint256 indexed level,
259 uint256 dayNumber,
260 uint256 unclaimedAmount
261 );
262 event PancakePositionSet(address indexed positionManager, uint256 tokenId);
263 event PancakePositionCreated(
264 uint256 indexed tokenId,
265 uint128 liquidity,
266 int24 tickLower,
267 int24 tickUpper,
268 uint256 amount0,
269 uint256 amount1
270 );
271 event LiquidityIncreased(uint256 tokenId, uint256 amount, uint128 liquidity);
272 event LiquidityDecreased(uint256 tokenId, uint256 amount0, uint256 amount1);
273 event RangeRebalanced(
274 uint256 oldTokenId,
275 uint256 newTokenId,
276 int24 newTickLower,
277 int24 newTickUpper,
278 uint128 newLiquidity,
279 uint256 amount0,
280 uint256 amount1
281 );
282 event AuthorizedRebalancerUpdated(address indexed oldRebalancer, address indexed newRebalancer);
283 event VariableYieldRatesConfigured(
284 uint256 oneDayRate,
285 uint256 fiveDaysRate,
286 uint256 tenDaysRate,
287 uint256 twentyDaysRate
288 );
289 event VariableCommissionRatesConfigured(uint256[15] newRates);
290
291 // ============ Modifiers ============
292
293 modifier onlyAuthorizedBase() {
294 require(msg.sender == authorizedBaseCreator, "Not authorized to create BASE");
295 _;
296 }
297
298 modifier userExists(address _user) {
299 require(users[_user].isRegistered, "User does not exist");
300 _;
301 }
302
303 modifier validAmount(uint256 _amount) {
304 require(_amount >= MIN_DEPOSIT && _amount <= MAX_DEPOSIT, "Invalid amount");
305 _;
306 }
307
308 modifier onlyAuthorizedRebalancer() {
309 require(msg.sender == authorizedRebalancer, "Not authorized to rebalance");
310 _;
311 }
312
313 // ============ Constructor ============
314
315 /**
316 * @dev Initializes the contract (immutable, non-upgradeable)
317 * @param _depositToken USDT token address
318 * @param _authorizedBaseCreator Authorized BASE creator
319 * @param _pancakePositionManager PancakeSwap V3 NonfungiblePositionManager address
320 */
321 constructor(
322 address _depositToken,
323 address _authorizedBaseCreator,
324 address _pancakePositionManager
325 ) Ownable(msg.sender) {
326 require(_depositToken != address(0), "Invalid token");
327 require(_authorizedBaseCreator != address(0), "Invalid creator");
328 require(_pancakePositionManager != address(0), "Invalid position manager");
329
330 depositToken = IERC20(_depositToken);
331 authorizedBaseCreator = _authorizedBaseCreator;
332 pancakePositionManager = IUniswapV3PositionManager(_pancakePositionManager);
333
334 nextContractId = 1;
335
336 DOMAIN_SEPARATOR = keccak256(
337 abi.encode(
338 keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
339 keccak256(bytes("SmartRange")),
340 keccak256(bytes("1")),
341 block.chainid,
342 address(this)
343 )
344 );
345
346 // Approve PancakeSwap Position Manager for max amount (one-time approval)
347 depositToken.approve(address(pancakePositionManager), type(uint256).max);
348 }
349
350 // ============ Configuration Functions ============
351
352 /**
353 * @dev Sets yield rates for all lock periods
354 * @notice Only owner can call this function
355 * @param _oneDayRate Yield rate for 1 day lock (basis points)
356 * @param _fiveDaysRate Yield rate for 5 days lock (basis points)
357 * @param _tenDaysRate Yield rate for 10 days lock (basis points)
358 * @param _twentyDaysRate Yield rate for 20 days lock (basis points)
359 */
360 function setYieldRates(
361 uint256 _oneDayRate,
362 uint256 _fiveDaysRate,
363 uint256 _tenDaysRate,
364 uint256 _twentyDaysRate
365 ) external onlyOwner {
366 ONE_DAY_YIELD_RATE = _oneDayRate;
367 FIVE_DAYS_YIELD_RATE = _fiveDaysRate;
368 TEN_DAYS_YIELD_RATE = _tenDaysRate;
369 TWENTY_DAYS_YIELD_RATE = _twentyDaysRate;
370
371 emit VariableYieldRatesConfigured(
372 _oneDayRate,
373 _fiveDaysRate,
374 _tenDaysRate,
375 _twentyDaysRate
376 );
377 }
378
379 /**
380 * @dev Sets commission rates for all 15 levels
381 * @notice Only owner can call this function
382 * @param _rates Array of 15 commission rates (basis points)
383 */
384 function setCommissionRates(uint256[15] calldata _rates) external onlyOwner {
385 for (uint256 i = 0; i < 15; i++) {
386 COMMISSION_RATES[i] = _rates[i];
387 }
388
389 emit VariableCommissionRatesConfigured(_rates);
390 }
391
392 // ============ Registration Functions ============
393
394 /**
395 * @dev Registers BASE user with initial deposit
396 */
397 function createBaseProvider(
398 address _baseUserAddress,
399 uint256 _initialDeposit,
400 LockPeriod _lockPeriod
401 )
402 external
403 onlyAuthorizedBase
404 nonReentrant
405 validAmount(_initialDeposit)
406 {
407 require(_baseUserAddress != address(0), "Invalid base address");
408 require(!users[_baseUserAddress].isRegistered, "Already registered");
409 require(baseUser == address(0), "Base user exists");
410
411 // Transfer tokens
412 depositToken.safeTransferFrom(msg.sender, address(this), _initialDeposit);
413
414 // Create user account
415 UserAccount storage user = users[_baseUserAddress];
416 user.isRegistered = true;
417 user.isBase = true;
418 user.referrer = address(0);
419 user.totalDeposited = _initialDeposit;
420
421 baseUser = _baseUserAddress;
422
423 // Create deposit contract
424 _createRangePosition(_baseUserAddress, _initialDeposit, _lockPeriod, false);
425
426 emit BaseUserRegistered(_baseUserAddress, block.timestamp);
427 }
428
429 /**
430 * @dev Registers user with referrer and initial deposit
431 * @param _amount Deposit amount
432 * @param _referrer Referrer address
433 * @param _lockPeriod Lock period selection
434 */
435 function joinSmartRange(
436 uint256 _amount,
437 address _referrer,
438 LockPeriod _lockPeriod
439 ) external nonReentrant validAmount(_amount) {
440 require(!users[msg.sender].isRegistered, "Already registered");
441 require(users[_referrer].isRegistered, "Referrer not registered");
442 require(_referrer != msg.sender, "Cannot self-refer");
443 require(baseUser != address(0), "Base not initialized");
444
445 depositNonce[msg.sender]++;
446
447 // Transfer tokens
448 depositToken.safeTransferFrom(msg.sender, address(this), _amount);
449
450 // Create user account
451 UserAccount storage newUser = users[msg.sender];
452 newUser.isRegistered = true;
453 newUser.isBase = false;
454 newUser.referrer = _referrer;
455 newUser.totalDeposited = _amount;
456
457 // Build upline structure
458 _buildUplineChain(msg.sender, _referrer);
459
460 // Increment referrer's count
461 users[_referrer].directReferrals++;
462
463 // Create deposit contract (will add commission streams)
464 _createRangePosition(msg.sender, _amount, _lockPeriod, true);
465
466 emit UserRegistered(msg.sender, _referrer);
467 }
468
469 /**
470 * @dev Additional deposit for existing user
471 */
472 function addSmartRange(
473 uint256 _amount,
474 LockPeriod _lockPeriod
475 ) external nonReentrant validAmount(_amount) userExists(msg.sender) {
476 depositNonce[msg.sender]++;
477
478 // Transfer tokens
479 depositToken.safeTransferFrom(msg.sender, address(this), _amount);
480
481 // Update total
482 users[msg.sender].totalDeposited += _amount;
483
484 // Create deposit contract
485 bool shouldAddCommissions = !users[msg.sender].isBase;
486 _createRangePosition(msg.sender, _amount, _lockPeriod, shouldAddCommissions);
487 }
488
489 // ============ Core Internal Functions ============
490
491 /**
492 * @dev Creates a deposit contract with yield and maturity
493 * @notice Automatically adds liquidity to PancakeSwap V3 position
494 * @notice Adds commission streams for uplines if applicable
495 */
496 function _createRangePosition(
497 address _depositor,
498 uint256 _amount,
499 LockPeriod _lockPeriod,
500 bool _addCommissions
501 ) private {
502 // Calculate yield and maturity
503 uint256 yieldAmount = _calculateRangeYield(_amount, _lockPeriod);
504 uint256 lockDays = _getLockDuration(_lockPeriod);
505 uint256 maturityTime = block.timestamp + (lockDays * SECONDS_IN_DAY);
506
507 // Create contract
508 uint256 contractId = nextContractId++;
509 DepositContract memory newContract = DepositContract({
510 contractId: contractId,
511 principal: _amount,
512 yieldAmount: yieldAmount,
513 lockPeriod: _lockPeriod,
514 depositTime: block.timestamp,
515 maturityTime: maturityTime,
516 withdrawn: false,
517 depositor: _depositor
518 });
519
520 userContracts[_depositor].push(newContract);
521 totalValueLocked += _amount;
522
523 // Add commission streams for uplines
524 if (_addCommissions) {
525 _distributeRewards(_depositor, yieldAmount, _lockPeriod);
526 }
527
528 // Automatically increase liquidity in PancakeSwap V3
529 _addLiquidityToPool(_amount);
530
531 emit Deposited(_depositor, contractId, _amount, yieldAmount, _lockPeriod, maturityTime);
532 }
533
534 /**
535 * @dev Adds time-locked commission streams for all uplines
536 * @notice Commissions calculated on YIELD (not principal)
537 * @notice Commissions unlock 100% after the lock period expires
538 * @notice Automatically aggregates same-day deposits
539 * @notice Triggers lazy cleanup if stream slots are full
540 */
541 function _distributeRewards(
542 address _depositor,
543 uint256 _yieldAmount,
544 LockPeriod _lockPeriod
545 ) private {
546 UserAccount storage depositorAccount = users[_depositor];
547 uint256 uplinesCount = depositorAccount.uplines.length;
548 uint256 currentDay = block.timestamp / SECONDS_IN_DAY;
549
550 // Process each commission level (0-14)
551 for (uint256 level = 0; level < MAX_UPLINES; level++) {
552 // Determine upline address for this level
553 address uplineAddress;
554
555 if (level < uplinesCount) {
556 // Direct upline at this level (reversed: closest = level 0)
557 uint256 uplineIndex = uplinesCount - 1 - level;
558 uplineAddress = depositorAccount.uplines[uplineIndex];
559 } else {
560 uplineAddress = baseUser;
561 }
562
563 if (uplineAddress == address(0)) continue;
564
565 uint256 totalCommission = (_yieldAmount * COMMISSION_RATES[level]) / RATE_DENOMINATOR;
566
567 _addRewardStream(uplineAddress, level, currentDay, totalCommission, _lockPeriod);
568 }
569 }
570
571 /**
572 * @dev Adds commission to appropriate stream bucket
573 * @notice Finds existing stream for current day or creates new one
574 * @notice Triggers lazy cleanup if all 20 slots are full
575 */
576 function _addRewardStream(
577 address _upline,
578 uint256 _level,
579 uint256 _dayNumber,
580 uint256 _commissionAmount,
581 LockPeriod _lockPeriod
582 ) private {
583 LevelCommissions storage levelComm = userCommissions[_upline][_level];
584
585 // Try to find existing stream for this day
586 int256 streamIndex = _findStreamByDay(levelComm, _dayNumber);
587
588 if (streamIndex >= 0) {
589 // Stream exists - add to appropriate bucket
590 _addToLockBucket(levelComm.streams[uint256(streamIndex)], _lockPeriod, _commissionAmount);
591 } else {
592 // Need to create new stream
593 int256 emptySlot = _findAvailableSlot(levelComm);
594
595 if (emptySlot >= 0) {
596 // Empty slot available - use it
597 _initializeStream(levelComm.streams[uint256(emptySlot)], _dayNumber, _lockPeriod, _commissionAmount);
598 } else {
599 // All slots full - trigger lazy cleanup
600 _cleanupExpiredStream(_upline, _level, levelComm, _dayNumber);
601
602 // Now find empty slot (guaranteed after cleanup)
603 emptySlot = _findAvailableSlot(levelComm);
604 require(emptySlot >= 0, "Cleanup failed");
605
606 _initializeStream(levelComm.streams[uint256(emptySlot)], _dayNumber, _lockPeriod, _commissionAmount);
607 }
608 }
609
610 emit StreamCreated(_upline, _level, _dayNumber, _commissionAmount, _lockPeriod);
611 }
612
613 /**
614 * @dev Lazy cleanup: removes oldest expired stream
615 * @notice Cleans streams older than 20 days (all buckets expired)
616 * @notice Moves unclaimed value to pendingClaimable
617 * @notice Example: Stream created day 0, 20-day lock expires day 19, cleanable from day 20
618 */
619 function _cleanupExpiredStream(
620 address _upline,
621 uint256 _level,
622 LevelCommissions storage _levelComm,
623 uint256 _currentDay
624 ) private {
625 uint256 oldestDay = type(uint256).max;
626 int256 oldestIndex = -1;
627
628 // Find oldest stream that's eligible for cleanup (20+ days old)
629 // Stream created day 0 with 20-day lock:
630 // - Locked from day 0-19 (commission is reserved, not claimable)
631 // - Day 20: unlocks 100% of commission (becomes claimable)
632 // - Day 20+: fully expired, can be cleaned
633 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
634 DailyStream storage stream = _levelComm.streams[i];
635 if (stream.dayNumber > 0 && stream.dayNumber < oldestDay) {
636 // Check if fully expired (all buckets including 20-day are done)
637 if (_currentDay >= stream.dayNumber + 20) {
638 oldestDay = stream.dayNumber;
639 oldestIndex = int256(i);
640 }
641 }
642 }
643
644 require(oldestIndex >= 0, "No cleanable streams");
645
646 // Calculate unclaimed value from this stream
647 DailyStream storage streamToClean = _levelComm.streams[uint256(oldestIndex)];
648 uint256 unclaimed = _calculateStreamClaimable(
649 streamToClean,
650 _currentDay,
651 _levelComm.lastClaimedDay
652 );
653
654 // Move unclaimed to pendingClaimable
655 if (unclaimed > 0) {
656 _levelComm.pendingClaimable += uint128(unclaimed);
657 }
658
659 emit StreamCleaned(_upline, _level, streamToClean.dayNumber, unclaimed);
660
661 // Delete stream (free the slot)
662 delete _levelComm.streams[uint256(oldestIndex)];
663 }
664
665 /**
666 * @dev Calculates claimable amount from a single stream
667 * @notice Implements auto-expiration logic
668 * @notice Binary unlock: 0% before unlockDay, 100% on or after unlockDay
669 */
670 function _calculateStreamClaimable(
671 DailyStream storage _stream,
672 uint256 _currentDay,
673 uint256 _lastClaimedDay
674 ) private view returns (uint256 claimable) {
675 if (_stream.dayNumber == 0) return 0;
676
677 // Calculate for each lock bucket
678 claimable += _calculateBucketClaimable(_stream.lock1Day, 1, _stream.dayNumber, _currentDay, _lastClaimedDay);
679 claimable += _calculateBucketClaimable(_stream.lock5Days, 5, _stream.dayNumber, _currentDay, _lastClaimedDay);
680 claimable += _calculateBucketClaimable(_stream.lock10Days, 10, _stream.dayNumber, _currentDay, _lastClaimedDay);
681 claimable += _calculateBucketClaimable(_stream.lock20Days, 20, _stream.dayNumber, _currentDay, _lastClaimedDay);
682 }
683
684 function _calculateBucketClaimable(
685 uint128 _amountTotal,
686 uint256 _lockDays,
687 uint256 _streamDay,
688 uint256 _currentDay,
689 uint256 _lastClaimedDay
690 ) private pure returns (uint256) {
691 if (_amountTotal == 0) return 0;
692
693 uint256 unlockDay = _streamDay + _lockDays;
694
695 if (_currentDay < unlockDay) return 0;
696
697 if (_lastClaimedDay >= unlockDay) return 0;
698
699 return uint256(_amountTotal);
700 }
701
702 // ============ Withdrawal Functions ============
703
704 /**
705 * @dev Withdraws a matured deposit contract
706 * @notice Can only withdraw after maturityTime
707 * @notice Returns principal + yield in single transaction
708 * @notice Automatically decreases PancakeSwap V3 liquidity
709 */
710 function closeRange(uint256 _contractIndex) external nonReentrant userExists(msg.sender) {
711 DepositContract[] storage contracts = userContracts[msg.sender];
712 require(_contractIndex < contracts.length, "Invalid index");
713
714 DepositContract storage contractToWithdraw = contracts[_contractIndex];
715
716 // Checks
717 require(!contractToWithdraw.withdrawn, "Already withdrawn");
718 require(block.timestamp >= contractToWithdraw.maturityTime, "Not matured");
719 require(contractToWithdraw.depositor == msg.sender, "Not owner");
720
721 // Save contract data before removal (for event emission)
722 uint256 contractId = contractToWithdraw.contractId;
723 uint256 principal = contractToWithdraw.principal;
724 uint256 yieldAmount = contractToWithdraw.yieldAmount;
725 uint256 totalAmount = principal + yieldAmount;
726
727 // CRITICAL: Save TVL BEFORE decrementing for correct liquidity calculation
728 uint256 tvlBeforeWithdrawal = totalValueLocked;
729
730 // Safety check: Ensure TVL is sufficient (should never fail in normal conditions)
731 require(tvlBeforeWithdrawal >= principal, "Insufficient TVL");
732
733 // Effects - Update global state
734 users[msg.sender].totalYieldEarned += yieldAmount;
735 totalYieldPaid += yieldAmount;
736 totalValueLocked -= principal;
737
738 // Interactions - ensure sufficient balance using pre-withdrawal TVL
739 _ensureAvailableFunds(totalAmount, tvlBeforeWithdrawal);
740
741 // Remove contract from array (swap with last element and pop)
742 uint256 lastIndex = contracts.length - 1;
743 if (_contractIndex != lastIndex) {
744 contracts[_contractIndex] = contracts[lastIndex];
745 }
746 contracts.pop();
747
748 depositToken.safeTransfer(msg.sender, totalAmount);
749
750 emit ContractWithdrawn(
751 msg.sender,
752 contractId,
753 principal,
754 yieldAmount,
755 totalAmount
756 );
757 }
758
759 /**
760 * @dev Claims all unlocked commissions from time-locked streams
761 * @notice Commissions unlock 100% after their respective lock periods
762 * @notice Sums all 15 levels and includes pendingClaimable
763 * @notice Resets pendingClaimable after claim
764 * @notice Updates lastClaimedDay for each level
765 * @param deadline Timestamp until which the signature is valid
766 * @param nonce Sequential nonce for replay protection
767 * @param v ECDSA signature parameter
768 * @param r ECDSA signature parameter
769 * @param s ECDSA signature parameter
770 */
771 function claimRewards(
772 uint256 deadline,
773 uint256 nonce,
774 uint8 v,
775 bytes32 r,
776 bytes32 s
777 ) external nonReentrant userExists(msg.sender) {
778 require(block.timestamp <= deadline, "Signature expired");
779 require(nonce == claimNonce[msg.sender], "Invalid nonce");
780
781 claimNonce[msg.sender]++;
782
783 bytes32 structHash = keccak256(abi.encode(CLAIM_TYPEHASH, msg.sender, deadline, nonce));
784 bytes32 digest = keccak256(abi.encodePacked("", DOMAIN_SEPARATOR, structHash));
785 address signer = ecrecover(digest, v, r, s);
786 require(signer != address(0), "Invalid signature");
787 require(signer == msg.sender, "Invalid signature");
788
789 uint256 currentDay = block.timestamp / SECONDS_IN_DAY;
790 uint256 totalClaimable = 0;
791
792 // Calculate total claimable across all 15 levels
793 for (uint256 level = 0; level < MAX_UPLINES; level++) {
794 LevelCommissions storage levelComm = userCommissions[msg.sender][level];
795
796 // Sum all active streams
797 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
798 if (levelComm.streams[i].dayNumber > 0) {
799 totalClaimable += _calculateStreamClaimable(
800 levelComm.streams[i],
801 currentDay,
802 levelComm.lastClaimedDay
803 );
804 }
805 }
806
807 // Add pendingClaimable from cleaned streams
808 totalClaimable += levelComm.pendingClaimable;
809
810 // Update state
811 levelComm.lastClaimedDay = uint32(currentDay);
812 levelComm.pendingClaimable = 0;
813 }
814
815 require(totalClaimable > 0, "No commissions");
816
817 // Update totals
818 users[msg.sender].totalCommissionsEarned += totalClaimable;
819 totalCommissionsPaid += totalClaimable;
820
821 // Ensure sufficient liquidity and transfer
822 // Note: For commissions, TVL is not affected, so we use current TVL
823 _ensureAvailableFunds(totalClaimable, totalValueLocked);
824 depositToken.safeTransfer(msg.sender, totalClaimable);
825
826 emit CommissionsClaimed(msg.sender, totalClaimable);
827 }
828
829 // ============ PancakeSwap V3 Integration ============
830
831 /**
832 * @dev Increases liquidity in PancakeSwap V3 position
833 * @notice Called automatically on deposits
834 * @notice 100% decentralized - no operator needed
835 */
836 function _addLiquidityToPool(uint256 _amount) private {
837 require(pancakePositionTokenId > 0, "Position not set");
838
839 // Increase liquidity using entire amount
840 // Note: For single-sided liquidity (USDT only), amount1Desired = 0
841 (uint128 liquidity, uint256 amount0, ) = pancakePositionManager.increaseLiquidity(
842 IncreaseLiquidityParams({
843 tokenId: pancakePositionTokenId,
844 amount0Desired: _amount,
845 amount1Desired: 0, // Single-sided deposit
846 amount0Min: (_amount * 995) / 1000, // 0.5% slippage tolerance
847 amount1Min: 0,
848 deadline: block.timestamp
849 })
850 );
851
852 emit LiquidityIncreased(pancakePositionTokenId, amount0, liquidity);
853 }
854
855 /**
856 * @dev Ensures contract has sufficient balance for withdrawal
857 * @notice Automatically decreases Pancake liquidity if needed
858 * @param _requiredAmount Amount of tokens needed for withdrawal
859 * @param _tvlForCalculation TVL value to use for liquidity calculation (pre-withdrawal TVL)
860 */
861 function _ensureAvailableFunds(uint256 _requiredAmount, uint256 _tvlForCalculation) private {
862 uint256 contractBalance = depositToken.balanceOf(address(this));
863
864 if (contractBalance < _requiredAmount) {
865 // Need to withdraw from PancakeSwap
866 uint256 amountNeeded = _requiredAmount - contractBalance;
867
868 // Withdraw a bit more to account for calculation imprecision
869 uint256 amountToWithdraw = (amountNeeded * 1001) / 1000; // 0.1% buffer
870
871 _removeLiquidityFromPool(amountToWithdraw, _tvlForCalculation);
872 }
873 }
874
875 /**
876 * @dev Decreases liquidity from PancakeSwap V3 position
877 * @notice Correctly calculates liquidity to remove based on position state
878 * @notice Called when contract balance insufficient for withdrawal
879 * @param _amount Amount of tokens needed
880 * @param _tvlForCalculation TVL value to use for liquidity calculation (pre-withdrawal TVL)
881 */
882 function _removeLiquidityFromPool(uint256 _amount, uint256 _tvlForCalculation) private {
883 require(pancakePositionTokenId > 0, "Position not set");
884 require(_tvlForCalculation > 0, "TVL cannot be zero");
885
886 (
887 ,
888 ,
889 ,
890 ,
891 ,
892 ,
893 ,
894 uint128 totalLiquidity,
895 ,
896 ,
897 ,
898 ) = pancakePositionManager.positions(pancakePositionTokenId);
899
900 require(totalLiquidity > 0, "No liquidity in position");
901
902 // CRITICAL: Why we use TVL (totalValueLocked) as denominator for liquidity calculation:
903 //
904 // TVL = Sum of all user principals currently deposited (active contracts)
905 // Pool Balance = TVL + Accumulated Fees from PancakeSwap V3
906 //
907 // The pool contains MORE than just user principals because:
908 // 1. Users deposit principals (tracked in TVL)
909 // 2. Pool generates trading fees (from PancakeSwap swaps)
910 // 3. Protocol must pay: Principal + Yield to users
911 // 4. Yields are paid from the accumulated trading fees
912 //
913 // Example:
914 // - 10 users deposit 100K USDT each = 1M TVL
915 // - Pool generates 200K USDT in trading fees over time
916 // - Total pool balance = 1M (principals) + 200K (fees) = 1.2M USDT
917 // - Users' total obligations = 1M (principals) + 300K (promised yields) = 1.3M
918 //
919 // When removing liquidity to pay a user:
920 // - User withdraws 100K principal + 20K yield = 120K total
921 // - We calculate: liquidity_to_remove = (totalLiquidity * 120K) / 1M (TVL)
922 // - We use TVL (1M) NOT pool balance (1.2M) because:
923 // * TVL represents the "baseline" amount that was deposited
924 // * Fees (200K) are EXTRA revenue to cover yields (300K)
925 // * Using TVL ensures we remove proportional liquidity based on original deposits
926 // * This maintains the correct ratio for remaining users
927 //
928 // Why NOT use pool balance (1.2M) as denominator?
929 // - Would remove LESS liquidity than needed: (totalLiq * 120K) / 1.2M < correct amount
930 // - Over time, would leave excess liquidity in pool (inefficient)
931 // - Would mess up proportions for remaining users
932 //
933 // Why TVL is the correct denominator:
934 // - TVL represents the "share" of liquidity that belongs to user principals
935 // - Fees are distributed separately (via collect()) to cover yields
936 // - Maintains 1:1 correspondence between deposits and liquidity units
937 // - Ensures fair proportional removal for all users
938 //
939 // For auditors: This is NOT a bug. Using TVL ensures correct liquidity accounting.
940 uint128 liquidityToRemove = uint128(
941 (uint256(totalLiquidity) * _amount) / _tvlForCalculation
942 );
943
944 require(liquidityToRemove > 0, "Liquidity too small");
945
946 uint256 minUsdtAmount = (_amount * 995) / 1000;
947
948 (uint256 amount0, uint256 amount1) = pancakePositionManager.decreaseLiquidity(
949 DecreaseLiquidityParams({
950 tokenId: pancakePositionTokenId,
951 liquidity: liquidityToRemove,
952 amount0Min: minUsdtAmount,
953 amount1Min: 0,
954 deadline: block.timestamp
955 })
956 );
957
958 pancakePositionManager.collect(
959 CollectParams({
960 tokenId: pancakePositionTokenId,
961 recipient: address(this),
962 amount0Max: type(uint128).max,
963 amount1Max: type(uint128).max
964 })
965 );
966
967 emit LiquidityDecreased(pancakePositionTokenId, amount0, amount1);
968 }
969
970 // ============ Helper Functions ============
971
972 /**
973 * @dev Builds upline structure with truncation at 15 levels
974 */
975 function _buildUplineChain(address _user, address _referrer) private {
976 UserAccount storage userAccount = users[_user];
977 UserAccount storage referrerAccount = users[_referrer];
978
979 delete userAccount.uplines;
980
981 // Copy referrer's uplines with truncation
982 uint256 uplinesToCopy = referrerAccount.uplines.length;
983
984 if (uplinesToCopy >= MAX_UPLINES - 1) {
985 // Truncate: take last 14
986 uint256 startIndex = uplinesToCopy - (MAX_UPLINES - 1);
987 for (uint256 i = startIndex; i < uplinesToCopy; i++) {
988 userAccount.uplines.push(referrerAccount.uplines[i]);
989 }
990 } else {
991 // Copy all
992 for (uint256 i = 0; i < uplinesToCopy; i++) {
993 userAccount.uplines.push(referrerAccount.uplines[i]);
994 }
995 }
996
997 // Add referrer as last upline
998 userAccount.uplines.push(_referrer);
999 }
1000
1001 /**
1002 * @dev Calculates yield based on lock period
1003 */
1004 function _calculateRangeYield(uint256 _principal, LockPeriod _lockPeriod) private view returns (uint256) {
1005 uint256 rate = _getYieldRate(_lockPeriod);
1006 return (_principal * rate) / RATE_DENOMINATOR;
1007 }
1008
1009 /**
1010 * @dev Returns yield rate for lock period
1011 */
1012 function _getYieldRate(LockPeriod _lockPeriod) private view returns (uint256) {
1013 if (_lockPeriod == LockPeriod.ONE_DAY) return ONE_DAY_YIELD_RATE;
1014 if (_lockPeriod == LockPeriod.FIVE_DAYS) return FIVE_DAYS_YIELD_RATE;
1015 if (_lockPeriod == LockPeriod.TEN_DAYS) return TEN_DAYS_YIELD_RATE;
1016 if (_lockPeriod == LockPeriod.TWENTY_DAYS) return TWENTY_DAYS_YIELD_RATE;
1017 revert("Invalid lock period");
1018 }
1019
1020 /**
1021 * @dev Returns number of days for lock period
1022 */
1023 function _getLockDuration(LockPeriod _lockPeriod) private pure returns (uint256) {
1024 if (_lockPeriod == LockPeriod.ONE_DAY) return 1;
1025 if (_lockPeriod == LockPeriod.FIVE_DAYS) return 5;
1026 if (_lockPeriod == LockPeriod.TEN_DAYS) return 10;
1027 if (_lockPeriod == LockPeriod.TWENTY_DAYS) return 20;
1028 revert("Invalid lock period");
1029 }
1030
1031 /**
1032 * @dev Finds stream by day number
1033 * @return Index of stream or -1 if not found
1034 */
1035 function _findStreamByDay(
1036 LevelCommissions storage _levelComm,
1037 uint256 _dayNumber
1038 ) private view returns (int256) {
1039 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
1040 if (_levelComm.streams[i].dayNumber == _dayNumber) {
1041 return int256(i);
1042 }
1043 }
1044 return -1;
1045 }
1046
1047 /**
1048 * @dev Finds empty stream slot
1049 * @return Index of empty slot or -1 if all full
1050 */
1051 function _findAvailableSlot(LevelCommissions storage _levelComm) private view returns (int256) {
1052 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
1053 if (_levelComm.streams[i].dayNumber == 0) {
1054 return int256(i);
1055 }
1056 }
1057 return -1;
1058 }
1059
1060 /**
1061 * @dev Adds amount to appropriate bucket in stream
1062 */
1063 function _addToLockBucket(
1064 DailyStream storage _stream,
1065 LockPeriod _lockPeriod,
1066 uint256 _amount
1067 ) private {
1068 require(_amount <= type(uint128).max, "Amount too large");
1069
1070 if (_lockPeriod == LockPeriod.ONE_DAY) {
1071 _stream.lock1Day += uint128(_amount);
1072 } else if (_lockPeriod == LockPeriod.FIVE_DAYS) {
1073 _stream.lock5Days += uint128(_amount);
1074 } else if (_lockPeriod == LockPeriod.TEN_DAYS) {
1075 _stream.lock10Days += uint128(_amount);
1076 } else if (_lockPeriod == LockPeriod.TWENTY_DAYS) {
1077 _stream.lock20Days += uint128(_amount);
1078 }
1079 }
1080
1081 /**
1082 * @dev Creates new stream with initial values
1083 */
1084 function _initializeStream(
1085 DailyStream storage _stream,
1086 uint256 _dayNumber,
1087 LockPeriod _lockPeriod,
1088 uint256 _amount
1089 ) private {
1090 require(_amount <= type(uint128).max, "Amount too large");
1091 require(_dayNumber <= type(uint32).max, "Day too large");
1092
1093 _stream.dayNumber = uint32(_dayNumber);
1094 _stream.lock1Day = 0;
1095 _stream.lock5Days = 0;
1096 _stream.lock10Days = 0;
1097 _stream.lock20Days = 0;
1098
1099 _addToLockBucket(_stream, _lockPeriod, _amount);
1100 }
1101
1102 // ============ View Functions ============
1103
1104 /**
1105 * @dev Returns user's deposit contracts
1106 */
1107 function getRangePositions(address _user) external view returns (DepositContract[] memory) {
1108 return userContracts[_user];
1109 }
1110
1111 /**
1112 * @dev Returns user's pending commissions (total across all levels)
1113 */
1114 function getPendingRewards(address _user) external view returns (uint256) {
1115 uint256 currentDay = block.timestamp / SECONDS_IN_DAY;
1116 uint256 total = 0;
1117
1118 for (uint256 level = 0; level < MAX_UPLINES; level++) {
1119 LevelCommissions storage levelComm = userCommissions[_user][level];
1120
1121 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
1122 if (levelComm.streams[i].dayNumber > 0) {
1123 total += _calculateStreamClaimable(
1124 levelComm.streams[i],
1125 currentDay,
1126 levelComm.lastClaimedDay
1127 );
1128 }
1129 }
1130
1131 total += levelComm.pendingClaimable;
1132 }
1133
1134 return total;
1135 }
1136
1137 /**
1138 * @dev Returns commission breakdown by level
1139 */
1140 function getRewardsBreakdown(address _user) external view returns (uint256[15] memory) {
1141 uint256 currentDay = block.timestamp / SECONDS_IN_DAY;
1142 uint256[15] memory breakdown;
1143
1144 for (uint256 level = 0; level < MAX_UPLINES; level++) {
1145 LevelCommissions storage levelComm = userCommissions[_user][level];
1146
1147 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
1148 if (levelComm.streams[i].dayNumber > 0) {
1149 breakdown[level] += _calculateStreamClaimable(
1150 levelComm.streams[i],
1151 currentDay,
1152 levelComm.lastClaimedDay
1153 );
1154 }
1155 }
1156
1157 breakdown[level] += levelComm.pendingClaimable;
1158 }
1159
1160 return breakdown;
1161 }
1162
1163 function _calculateBucketLocked(
1164 uint128 _amountTotal,
1165 uint256 _lockDays,
1166 uint256 _streamDay,
1167 uint256 _currentDay
1168 ) private pure returns (uint256) {
1169 if (_amountTotal == 0) return 0;
1170
1171 uint256 unlockDay = _streamDay + _lockDays;
1172
1173 if (_currentDay >= unlockDay) return 0;
1174
1175 return uint256(_amountTotal);
1176 }
1177
1178 /**
1179 * @dev Calculates reserved commissions from a single stream
1180 * @notice Sums reserved amounts across all lock period buckets
1181 * @notice Ignores deleted streams (dayNumber = 0)
1182 *
1183 * @param _stream Storage pointer to the DailyStream struct
1184 * @param _currentDay Current day (block.timestamp / 86400)
1185 * @return reserved Total reserved commission for this stream
1186 */
1187 function _calculateStreamLocked(
1188 DailyStream storage _stream,
1189 uint256 _currentDay
1190 ) private view returns (uint256 reserved) {
1191 if (_stream.dayNumber == 0) return 0;
1192
1193 // Calculate for each lock bucket
1194 // Note: We don't need lastClaimedDay for reserved calculations
1195 reserved += _calculateBucketLocked(_stream.lock1Day, 1, _stream.dayNumber, _currentDay);
1196 reserved += _calculateBucketLocked(_stream.lock5Days, 5, _stream.dayNumber, _currentDay);
1197 reserved += _calculateBucketLocked(_stream.lock10Days, 10, _stream.dayNumber, _currentDay);
1198 reserved += _calculateBucketLocked(_stream.lock20Days, 20, _stream.dayNumber, _currentDay);
1199 }
1200
1201 /**
1202 * @dev Returns user's reserved (future/locked) commissions across all levels
1203 * @notice Reserved = commissions that will be unlocked in FUTURE days
1204 * @notice This is DIFFERENT from pending (which is available NOW)
1205 *
1206 * Key differences:
1207 * - PENDING: Available for claim right now (past + today)
1208 * - RESERVED: Will be unlocked in future (tomorrow onwards)
1209 *
1210 * Important notes:
1211 * - Reserved does NOT include pendingClaimable (that's part of PENDING)
1212 * - Reserved does NOT depend on lastClaimedDay
1213 * - Reserved is purely based on currentDay vs streamEndDay
1214 * - When a stream expires, its value moves to PENDING, not RESERVED
1215 *
1216 * Example scenario:
1217 * - Stream day 0, lock 5 days (unlocks 100% on day 5)
1218 * - Today is day 3: commission is still locked (RESERVED)
1219 * - Today is day 5+: commission is unlocked and claimable (PENDING)
1220 * - User can claim 100% of the commission only after day 5
1221 * - Before day 5: RESERVED shows total locked amount
1222 * - After day 5: PENDING shows total claimable amount
1223 *
1224 * @param _user Address of the user
1225 * @return Total reserved commissions across all 15 levels (18 decimals USDT)
1226 */
1227 function getLockedRewards(address _user) external view returns (uint256) {
1228 uint256 currentDay = block.timestamp / SECONDS_IN_DAY;
1229 uint256 totalReserved = 0;
1230
1231 // Sum reserved commissions across all 15 levels
1232 for (uint256 level = 0; level < MAX_UPLINES; level++) {
1233 LevelCommissions storage levelComm = userCommissions[_user][level];
1234
1235 // Sum all active streams
1236 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
1237 if (levelComm.streams[i].dayNumber > 0) {
1238 totalReserved += _calculateStreamLocked(
1239 levelComm.streams[i],
1240 currentDay
1241 );
1242 }
1243 }
1244
1245 // Note: pendingClaimable is NOT included in reserved
1246 // pendingClaimable comes from cleaned streams and is part of PENDING
1247 }
1248
1249 return totalReserved;
1250 }
1251
1252 /**
1253 * @dev Returns reserved commissions breakdown by level (for detailed analytics)
1254 * @notice Shows how much reserved commission exists at each of the 15 levels
1255 * @notice Useful for frontend to display per-level reserved amounts
1256 *
1257 * @param _user Address of the user
1258 * @return Array of 15 values representing reserved commissions per level (18 decimals USDT)
1259 */
1260 function getLockedRewardsBreakdown(address _user) external view returns (uint256[15] memory) {
1261 uint256 currentDay = block.timestamp / SECONDS_IN_DAY;
1262 uint256[15] memory breakdown;
1263
1264 for (uint256 level = 0; level < MAX_UPLINES; level++) {
1265 LevelCommissions storage levelComm = userCommissions[_user][level];
1266
1267 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
1268 if (levelComm.streams[i].dayNumber > 0) {
1269 breakdown[level] += _calculateStreamLocked(
1270 levelComm.streams[i],
1271 currentDay
1272 );
1273 }
1274 }
1275 }
1276
1277 return breakdown;
1278 }
1279
1280 function getLevelLockBreakdown(address _user, uint256 _level)
1281 external view returns (LockPeriodBreakdown memory)
1282 {
1283 require(_level < MAX_UPLINES, "Invalid level");
1284
1285 LevelCommissions storage levelComm = userCommissions[_user][_level];
1286 uint256 currentDay = block.timestamp / SECONDS_IN_DAY;
1287 uint256 lastClaimedDay = levelComm.lastClaimedDay;
1288
1289 LockPeriodBreakdown memory breakdown;
1290
1291 for (uint256 i = 0; i < MAX_STREAMS_PER_LEVEL; i++) {
1292 DailyStream storage stream = levelComm.streams[i];
1293
1294 if (stream.dayNumber > 0) {
1295 if (stream.lock1Day > 0) {
1296 uint256 unlockDay = stream.dayNumber + 1;
1297 if (currentDay >= unlockDay) {
1298 if (lastClaimedDay < unlockDay) {
1299 breakdown.lock1DayPending += uint256(stream.lock1Day);
1300 }
1301 } else {
1302 breakdown.lock1DayReserved += uint256(stream.lock1Day);
1303 }
1304 }
1305
1306 if (stream.lock5Days > 0) {
1307 uint256 unlockDay = stream.dayNumber + 5;
1308 if (currentDay >= unlockDay) {
1309 if (lastClaimedDay < unlockDay) {
1310 breakdown.lock5DaysPending += uint256(stream.lock5Days);
1311 }
1312 } else {
1313 breakdown.lock5DaysReserved += uint256(stream.lock5Days);
1314 }
1315 }
1316
1317 if (stream.lock10Days > 0) {
1318 uint256 unlockDay = stream.dayNumber + 10;
1319 if (currentDay >= unlockDay) {
1320 if (lastClaimedDay < unlockDay) {
1321 breakdown.lock10DaysPending += uint256(stream.lock10Days);
1322 }
1323 } else {
1324 breakdown.lock10DaysReserved += uint256(stream.lock10Days);
1325 }
1326 }
1327
1328 if (stream.lock20Days > 0) {
1329 uint256 unlockDay = stream.dayNumber + 20;
1330 if (currentDay >= unlockDay) {
1331 if (lastClaimedDay < unlockDay) {
1332 breakdown.lock20DaysPending += uint256(stream.lock20Days);
1333 }
1334 } else {
1335 breakdown.lock20DaysReserved += uint256(stream.lock20Days);
1336 }
1337 }
1338 }
1339 }
1340
1341 return breakdown;
1342 }
1343
1344 /**
1345 * @dev Returns user's mature (withdrawable) contracts
1346 */
1347 function getMaturedRanges(address _user) external view returns (uint256[] memory) {
1348 DepositContract[] storage contracts = userContracts[_user];
1349 uint256 matureCount = 0;
1350
1351 // Count mature contracts
1352 for (uint256 i = 0; i < contracts.length; i++) {
1353 if (!contracts[i].withdrawn && block.timestamp >= contracts[i].maturityTime) {
1354 matureCount++;
1355 }
1356 }
1357
1358 // Build array
1359 uint256[] memory matureIndices = new uint256[](matureCount);
1360 uint256 index = 0;
1361
1362 for (uint256 i = 0; i < contracts.length; i++) {
1363 if (!contracts[i].withdrawn && block.timestamp >= contracts[i].maturityTime) {
1364 matureIndices[index] = i;
1365 index++;
1366 }
1367 }
1368
1369 return matureIndices;
1370 }
1371
1372 /**
1373 * @dev Returns user's available balance (sum of all active contract principals)
1374 * @notice This represents the total deposited amount still active in the vault
1375 * @notice Contracts are removed from array when withdrawn, so we sum all existing contracts
1376 * @param _user Address of the user
1377 * @return Total principal of all active contracts (18 decimals USDT)
1378 */
1379 function getActiveLiquidity(address _user) external view returns (uint256) {
1380 DepositContract[] storage contracts = userContracts[_user];
1381 uint256 totalPrincipal = 0;
1382
1383 for (uint256 i = 0; i < contracts.length; i++) {
1384 // Since withdrawn contracts are removed from the array,
1385 // all contracts in the array are active
1386 totalPrincipal += contracts[i].principal;
1387 }
1388
1389 return totalPrincipal;
1390 }
1391
1392 /**
1393 * @dev Returns user's upline addresses
1394 * @param _user Address of the user
1395 * @return Array of upline addresses (max 15)
1396 */
1397 function getProviderUplines(address _user) external view returns (address[] memory) {
1398 return users[_user].uplines;
1399 }
1400
1401 /**
1402 * @dev Returns all active streams for a specific level
1403 * @notice Returns all 20 stream slots (empty slots have dayNumber = 0)
1404 * @param _user Address of the user
1405 * @param _level Level (0-14)
1406 * @return Array of 20 DailyStream structs
1407 */
1408 function getRewardStreams(address _user, uint256 _level) external view returns (DailyStream[MAX_STREAMS_PER_LEVEL] memory) {
1409 require(_level < MAX_UPLINES, "Invalid level");
1410 return userCommissions[_user][_level].streams;
1411 }
1412
1413 /**
1414 * @dev Returns commission metadata for a specific level
1415 * @param _user Address of the user
1416 * @param _level Level (0-14)
1417 * @return lastClaimedDay Last day user claimed commissions
1418 * @return pendingClaimable Amount from cleaned streams
1419 */
1420 function getRewardStreamMeta(address _user, uint256 _level) external view returns (
1421 uint32 lastClaimedDay,
1422 uint256 pendingClaimable
1423 ) {
1424 require(_level < MAX_UPLINES, "Invalid level");
1425 LevelCommissions storage levelComm = userCommissions[_user][_level];
1426 return (levelComm.lastClaimedDay, levelComm.pendingClaimable);
1427 }
1428
1429 /**
1430 * @dev Returns user info
1431 */
1432 function getProviderInfo(address _user) external view returns (
1433 bool isRegistered,
1434 bool isBase,
1435 address referrer,
1436 uint256 totalDeposited,
1437 uint256 activeContracts,
1438 uint256 totalYieldEarned,
1439 uint256 totalCommissionsEarned
1440 ) {
1441 UserAccount storage user = users[_user];
1442
1443 // Count active contracts
1444 uint256 active = 0;
1445 DepositContract[] storage contracts = userContracts[_user];
1446 for (uint256 i = 0; i < contracts.length; i++) {
1447 if (!contracts[i].withdrawn) active++;
1448 }
1449
1450 return (
1451 user.isRegistered,
1452 user.isBase,
1453 user.referrer,
1454 user.totalDeposited,
1455 active,
1456 user.totalYieldEarned,
1457 user.totalCommissionsEarned
1458 );
1459 }
1460
1461 // ============ PancakeSwap V3 Position Creation ============
1462
1463 /**
1464 * @dev Creates PancakeSwap V3 position with contract as permanent owner
1465 * @notice CRITICAL: This makes the position PERMANENTLY owned by contract
1466 * @notice No EOA can control or transfer this position - 100% immutable
1467 * @notice Position will be locked in contract forever (no transfer function exists)
1468 * @param token1 Address of second token (e.g., WBNB)
1469 * @param amount0 Amount of token0 (USDT) for initial liquidity
1470 * @param amount1 Amount of token1 (WBNB or other) for initial liquidity
1471 * @param tickLower Lower tick of price range
1472 * @param tickUpper Upper tick of price range
1473 * @param fee Fee tier (100 = 0.01%, 500 = 0.05%, 2500 = 0.25%, 10000 = 1%)
1474 */
1475 function initializeLiquidityPool(
1476 address token1,
1477 uint256 amount0,
1478 uint256 amount1,
1479 int24 tickLower,
1480 int24 tickUpper,
1481 uint24 fee
1482 ) external onlyOwner nonReentrant {
1483 require(pancakePositionTokenId == 0, "Position already exists");
1484 require(token1 != address(0), "Invalid token1");
1485 require(amount0 > 0, "Invalid amount0");
1486 require(tickLower < tickUpper, "Invalid tick range");
1487 require(
1488 fee == 100 || fee == 500 || fee == 2500 || fee == 10000,
1489 "Invalid fee tier"
1490 );
1491
1492 // Transfer tokens from caller (owner pays initial liquidity)
1493 depositToken.safeTransferFrom(msg.sender, address(this), amount0);
1494
1495 if (amount1 > 0) {
1496 IERC20(token1).safeTransferFrom(msg.sender, address(this), amount1);
1497 // Approve token1 for position manager
1498 IERC20(token1).approve(address(pancakePositionManager), amount1);
1499 }
1500
1501 // depositToken already approved in constructor with type(uint256).max
1502
1503 // Create position with CONTRACT as permanent owner (recipient = address(this))
1504 MintParams memory params = MintParams({
1505 token0: address(depositToken), // USDT
1506 token1: token1, // WBNB or other paired token
1507 fee: fee,
1508 tickLower: tickLower,
1509 tickUpper: tickUpper,
1510 amount0Desired: amount0,
1511 amount1Desired: amount1,
1512 amount0Min: (amount0 * 995) / 1000, // 0.5% slippage tolerance
1513 amount1Min: (amount1 * 995) / 1000,
1514 recipient: address(this), // CRITICAL: Contract owns the NFT forever
1515 deadline: block.timestamp
1516 });
1517
1518 // Mint position - NFT goes directly to contract
1519 (uint256 tokenId, uint128 liquidity, uint256 used0, uint256 used1) =
1520 pancakePositionManager.mint(params);
1521
1522 // Store position token ID
1523 pancakePositionTokenId = tokenId;
1524
1525 // Refund unused tokens to caller
1526 if (used0 < amount0) {
1527 uint256 refund = amount0 - used0;
1528 depositToken.safeTransfer(msg.sender, refund);
1529 }
1530
1531 if (amount1 > 0 && used1 < amount1) {
1532 uint256 refund = amount1 - used1;
1533 // Reset approval first
1534 IERC20(token1).approve(address(pancakePositionManager), 0);
1535 IERC20(token1).safeTransfer(msg.sender, refund);
1536 }
1537
1538 emit PancakePositionCreated(tokenId, liquidity, tickLower, tickUpper, used0, used1);
1539 }
1540
1541 /**
1542 * @dev Sets the authorized rebalancer contract address
1543 * @notice CRITICAL: This should be an AUTOMATED SMART CONTRACT, not an EOA (Externally Owned Account)
1544 * @notice Examples of valid rebalancer contracts:
1545 * - Gelato Network's automated position managers
1546 * - Arrakis Finance position managers
1547 * - Gamma Strategies contracts
1548 * - PancakeSwap Labs' official position management contracts
1549 * @notice The rebalancer contract will automatically:
1550 * - Monitor price movements on-chain
1551 * - Rebalance positions based on predefined algorithms
1552 * - Execute without human intervention
1553 * @notice Only owner can set this address (one-time setup)
1554 * @notice Rebalancer contract CANNOT withdraw tokens to external addresses
1555 * @notice Rebalancer can ONLY change the position range (tickLower, tickUpper)
1556 * @notice All tokens remain locked in THIS contract during and after rebalancing
1557 * @param _rebalancer Address of the automated rebalancing smart contract
1558 */
1559 function setAuthorizedRebalancer(address _rebalancer) external onlyOwner {
1560 require(_rebalancer != address(0), "Invalid rebalancer address");
1561 uint256 codeSize;
1562 assembly {
1563 codeSize := extcodesize(_rebalancer)
1564 }
1565 require(codeSize > 0, "Rebalancer must be a contract");
1566 address oldRebalancer = authorizedRebalancer;
1567 authorizedRebalancer = _rebalancer;
1568 emit AuthorizedRebalancerUpdated(oldRebalancer, _rebalancer);
1569 }
1570
1571 /**
1572 * @dev ERC721 Receiver implementation - allows contract to receive NFTs
1573 * @notice Only accepts NFTs from PancakeSwap Position Manager
1574 * @notice This is required for the contract to receive the position NFT
1575 */
1576 function onERC721Received(
1577 address,
1578 address,
1579 uint256 tokenId,
1580 bytes calldata
1581 ) external view override returns (bytes4) {
1582 require(msg.sender == address(pancakePositionManager), "Invalid sender");
1583 if (pancakePositionTokenId > 0) {
1584 require(tokenId == pancakePositionTokenId, "Invalid token");
1585 }
1586 return IERC721Receiver.onERC721Received.selector;
1587 }
1588
1589 // ============ Range Rebalancing ============
1590
1591 /**
1592 * @dev Rebalances PancakeSwap V3 position to a new price range to maximize fee capture
1593 * @notice THIS IS HOW THE PROTOCOL GENERATES REVENUE
1594 * @notice Called by AUTOMATED SMART CONTRACT (not a person)
1595 * @notice User deposits, yields, and maturities remain unchanged
1596 * @notice Only changes WHERE liquidity is positioned in PancakeSwap V3 to capture more trading fees
1597 * @notice Tokens NEVER leave the contract - they stay locked inside
1598 *
1599 * WHY THIS EXISTS (Mathematical Sustainability):
1600 * - Protocol earns trading fees from PancakeSwap V3
1601 * - When price moves out of range - position earns ZERO fees - must rebalance
1602 * - Rebalancing keeps position in active range - maintains fee generation - protocol stays sustainable
1603 *
1604 * WHO CALLS THIS (Automated Contract):
1605 * - NOT a person or centralized operator
1606 * - An automated smart contract (Gelato, Arrakis, Gamma, or PancakeSwap Labs contracts)
1607 * - These contracts monitor prices and rebalance algorithmically
1608 * - No human decision-making or intervention required
1609 *
1610 * Security guarantees:
1611 * - Position NFT remains owned by THIS contract (address(this))
1612 * - All tokens stay within THIS contract (no external transfers)
1613 * - User DepositContracts are unaffected (principal, yield, maturity unchanged)
1614 * - Only authorized rebalancer CONTRACT can call this function
1615 * - Rebalancer CANNOT withdraw tokens or modify user contracts
1616 * - Users can only withdraw via closeRange() and claimRewards()
1617 *
1618 * How it works:
1619 * 1. Removes ALL liquidity from old position (inactive range)
1620 * 2. Collects tokens back to THIS contract (address(this))
1621 * 3. Creates NEW position with new tick range (active range)
1622 * 4. Adds all collected liquidity to new position
1623 * 5. Updates pancakePositionTokenId to new NFT
1624 * 6. Old position is abandoned (empty NFT remains in contract)
1625 *
1626 * Example use case:
1627 * - Current range: 250-300 USDT/BNB (position created 1 month ago)
1628 * - Market moves to 400 USDT/BNB (out of range = earning 0 fees)
1629 * - Automated contract detects this and calls rebalancePancakeRange()
1630 * - New range: 380-420 USDT/BNB (active range = earning high APR again)
1631 * - Result: Protocol maintains fee generation to sustain operations
1632 *
1633 * @param newTickLower Lower tick boundary of new range
1634 * @param newTickUpper Upper tick boundary of new range
1635 */
1636 function rebalancePancakeRange(
1637 int24 newTickLower,
1638 int24 newTickUpper
1639 ) external onlyAuthorizedRebalancer nonReentrant {
1640 require(pancakePositionTokenId > 0, "No position to rebalance");
1641 require(newTickLower < newTickUpper, "Invalid tick range");
1642 require(block.timestamp >= lastRebalanceTime + MIN_REBALANCE_DELAY, "Rebalance too soon");
1643
1644 uint256 oldTokenId = pancakePositionTokenId;
1645
1646 // Step 1: Get current position info
1647 (
1648 ,
1649 ,
1650 address token0,
1651 address token1,
1652 uint24 fee,
1653 int24 oldTickLower,
1654 int24 oldTickUpper,
1655 uint128 liquidity,
1656 ,
1657 ,
1658 ,
1659 ) = pancakePositionManager.positions(oldTokenId);
1660
1661 require(liquidity > 0, "No liquidity to rebalance");
1662
1663 int24 currentMidTick = (oldTickLower + oldTickUpper) / 2;
1664 require(newTickLower >= currentMidTick - MAX_TICK_DEVIATION, "New range too far below");
1665 require(newTickUpper <= currentMidTick + MAX_TICK_DEVIATION, "New range too far above");
1666 require(newTickUpper - newTickLower >= 2000, "Range too narrow");
1667
1668 // Step 2: Remove ALL liquidity from old position
1669 (uint256 amount0Removed, ) = pancakePositionManager.decreaseLiquidity(
1670 DecreaseLiquidityParams({
1671 tokenId: oldTokenId,
1672 liquidity: liquidity,
1673 amount0Min: (uint256(uint128(liquidity)) * 995) / 1000,
1674 amount1Min: 0,
1675 deadline: block.timestamp
1676 })
1677 );
1678
1679 // Step 3: Collect tokens from old position to contract
1680 (uint256 collected0, uint256 collected1) = pancakePositionManager.collect(
1681 CollectParams({
1682 tokenId: oldTokenId,
1683 recipient: address(this), // Tokens stay in contract
1684 amount0Max: type(uint128).max,
1685 amount1Max: type(uint128).max
1686 })
1687 );
1688
1689 // Step 4: Approve tokens if needed (token0 already approved in constructor)
1690 if (collected1 > 0) {
1691 IERC20(token1).approve(address(pancakePositionManager), collected1);
1692 }
1693
1694 // Step 5: Create NEW position with new range
1695 MintParams memory params = MintParams({
1696 token0: token0,
1697 token1: token1,
1698 fee: fee, // Keep same fee tier
1699 tickLower: newTickLower, // NEW RANGE
1700 tickUpper: newTickUpper, // NEW RANGE
1701 amount0Desired: collected0,
1702 amount1Desired: collected1,
1703 amount0Min: (collected0 * 995) / 1000, // 0.5% slippage
1704 amount1Min: collected1 > 0 ? (collected1 * 995) / 1000 : 0,
1705 recipient: address(this), // Contract remains owner
1706 deadline: block.timestamp
1707 });
1708
1709 (uint256 newTokenId, uint128 newLiquidity, uint256 used0, uint256 used1) =
1710 pancakePositionManager.mint(params);
1711
1712 // Step 6: Update state - new position becomes active
1713 pancakePositionTokenId = newTokenId;
1714 lastRebalanceTime = block.timestamp;
1715
1716 // Step 7: Clean up approval if token1 was used
1717 if (collected1 > 0) {
1718 IERC20(token1).approve(address(pancakePositionManager), 0);
1719 }
1720
1721 // Note: Old position (oldTokenId) is now empty and abandoned
1722 // It cannot be burned because contract doesn't implement burn logic
1723 // This is intentional - old NFT stays in contract forever (harmless)
1724
1725 emit RangeRebalanced(
1726 oldTokenId,
1727 newTokenId,
1728 newTickLower,
1729 newTickUpper,
1730 newLiquidity,
1731 used0,
1732 used1
1733 );
1734 }
1735
1736}What does this mean?
The "owner" is the only one who could update this contract. By renouncing ownership, the owner key was permanently destroyed.
No one can change it
Not the developers, not hackers, not anyone. The contract code will run exactly as written forever.
Your funds are safe
The rules are set in stone. Your deposits and earnings will always work exactly as the code defines.
Blockchain Proof
This is recorded on BNB Smart Chain and can be verified by anyone
OwnershipTransferred23,914,923December 1, 2025Frequently Asked Questions
Building the Future
Our vision for decentralized yield optimization and ecosystem growth
Foundation
- Intelligent Range Rebalancing
- BSC Network
- PancakeSwap V3 Integration
Innovation
- Token Launch & Airdrop
- Smart Wallet Integration
Ecosystem
- Mobile Applications
- NFT Benefits Program
Continuous Innovation
... More innovations coming ...
Ready to Maximize Your Returns?
Join thousands of users earning passive income through automated liquidity management. Start with as little as $100 USDT.