Skip to main content
PulseScanner.io

Address

0xadfb8bb0dd67b3c3d838b2f30cdd923c8117ad1d
Current Holdings
$0.00
TXs sent
not counted
First Active
2026-07-24
block 27,113,108
Last Active
54 days ago
block 27,113,967
Funded By
not identified

Net worth historyi

No net-worth snapshots recorded yet
exact matchAquarianConvictionLocksolc 0.8.26+commit.8a97fa7aruntime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

/**
* @title AquarianConvictionLock
* @notice Amount-based conviction NFTs for $AQI on PulseChain
*/
contract AquarianConvictionLock is ERC721, ReentrancyGuard, Ownable {
   using SafeERC20 for IERC20;
   using Strings for uint256;

   // ============ ADDRESSES ============
   IERC20 public immutable aquarian;
   address public constant AQUARIAN_ADDRESS = 0x4F9c985a762875f0A12C11eBA850b2945aEFD964;

   address public constant RESERVE_VAULT    = 0x77ca0Bf25b19aE23474E2249bA52d1cE03760C70;
   address public constant DIAMOND_VAULT    = 0x5DD357Debb23305126604D2eBc13160b513bbACA;
   address public constant CONVICTION_VAULT = 0x87fA4f1403a01b82cBc8E1d147f3E945679373fb;

   address public constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;

   // ============ CONSTANTS ============
   uint256 public constant MIN_LOCK_DAYS = 15;
   uint256 public constant BPS_DENOMINATOR = 10000;
   uint256 public constant EARLY_RETURN_BPS = 7000; // 70%
   uint256 public constant BURN_BPS = 1000;         // 10%
   uint256 private constant ACC_PRECISION = 1e18;

   uint256 public constant TIER_1M  = 1_000_000 ether;
   uint256 public constant TIER_5M  = 5_000_000 ether;
   uint256 public constant TIER_15M = 15_000_000 ether;
   uint256 public constant TIER_25M = 25_000_000 ether;
   uint256 public constant TIER_55M = 55_000_000 ether;

   // ============ STRUCTS ============
   struct Lock {
       uint128 amount;
       uint64  startTime;
       uint64  unlockTime;
       uint32  durationDays;
       uint16  multiplier;
       uint16  tier;
       uint256 rewardDebt;
       bool    paired;
       uint256 edition;
   }

   // ============ STATE ============
   mapping(uint256 => Lock) public locks;
   uint256 public nextTokenId = 1;
   mapping(uint16 => uint256) public tierEditions;

   mapping(address => uint256) public activeWeight;
   uint256 public totalActiveWeight;
   uint256 public rewardPerWeightStored;
   uint256 public rewardPoolBalance;

   uint256 public totalLocked;
   uint256 public totalBurned;
   uint256 public totalDistributed;

   // ============ EVENTS ============
   event Locked(
       uint256 indexed tokenId,
       address indexed owner,
       uint256 amount,
       uint256 durationDays,
       uint16 multiplier,
       uint16 tier,
       uint256 edition,
       uint256 unlockTime
   );
   event EarlyUnlocked(uint256 indexed tokenId, address indexed owner, uint256 returned, uint256 burned, uint256 toPool);
   event MatureUnlocked(uint256 indexed tokenId, address indexed owner, uint256 amount);
   event RewardsClaimed(uint256 indexed tokenId, address indexed owner, uint256 amount);
   event Paired(uint256 indexed aquarianTokenId, address indexed vault, uint256 vaultTokenId);
   event RewardPoolSeeded(uint256 amount);

   // ============ ERRORS ============
   error InvalidAmount();
   error InvalidDuration();
   error NotOwner();
   error StillLocked();
   error AlreadyMatured();
   error Soulbound();
   error NothingToClaim();
   error InvalidVault();
   error NotMature();
   error AlreadyPaired();
   error NotPairedOwner();

   constructor() ERC721("Aquarian Conviction Lock", "AQILOCK") Ownable(msg.sender) {
       aquarian = IERC20(AQUARIAN_ADDRESS);
   }

   // ============ CORE FUNCTIONS ============

   function lock(uint256 amount, uint256 durationDays) external nonReentrant returns (uint256 tokenId) {
       uint16 tier = _resolveTier(amount);
       if (durationDays < MIN_LOCK_DAYS) revert InvalidDuration();

       uint16 multiplier = _multiplierFor(durationDays);
       uint256 weight = amount * multiplier;

       aquarian.safeTransferFrom(msg.sender, address(this), amount);

       tokenId = nextTokenId++;
       uint256 edition = ++tierEditions[tier];
       uint256 unlockTime = block.timestamp + (durationDays * 1 days);

       locks[tokenId] = Lock({
           amount: uint128(amount),
           startTime: uint64(block.timestamp),
           unlockTime: uint64(unlockTime),
           durationDays: uint32(durationDays),
           multiplier: multiplier,
           tier: tier,
           rewardDebt: (weight * rewardPerWeightStored) / ACC_PRECISION,
           paired: false,
           edition: edition
       });

       totalLocked += amount;
       totalActiveWeight += weight;
       activeWeight[msg.sender] += weight;

       _safeMint(msg.sender, tokenId);

       emit Locked(tokenId, msg.sender, amount, durationDays, multiplier, tier, edition, unlockTime);
   }

   function earlyUnlock(uint256 tokenId) external nonReentrant {
       address holder = ownerOf(tokenId);
       if (holder != msg.sender) revert NotOwner();

       Lock memory l = locks[tokenId];
       if (block.timestamp >= l.unlockTime) revert AlreadyMatured();

       uint256 weight = uint256(l.amount) * l.multiplier;
       _payRewards(tokenId, holder, l);

       uint256 returned = (uint256(l.amount) * EARLY_RETURN_BPS) / BPS_DENOMINATOR;
       uint256 burned   = (uint256(l.amount) * BURN_BPS) / BPS_DENOMINATOR;
       uint256 toPool   = uint256(l.amount) - returned - burned;

       totalLocked -= l.amount;
       totalActiveWeight -= weight;
       activeWeight[holder] -= weight;

       aquarian.safeTransfer(BURN_ADDRESS, burned);
       totalBurned += burned;

       if (toPool > 0 && totalActiveWeight > 0) {
           rewardPerWeightStored += (toPool * ACC_PRECISION) / totalActiveWeight;
           rewardPoolBalance += toPool;
           totalDistributed += toPool;
       } else if (toPool > 0) {
           aquarian.safeTransfer(BURN_ADDRESS, toPool);
           totalBurned += toPool;
       }

       delete locks[tokenId];
       _burn(tokenId);

       aquarian.safeTransfer(holder, returned);
       emit EarlyUnlocked(tokenId, holder, returned, burned, toPool);
   }

   function unlock(uint256 tokenId) external nonReentrant {
       address holder = ownerOf(tokenId);
       if (holder != msg.sender) revert NotOwner();

       Lock storage l = locks[tokenId];
       if (block.timestamp < l.unlockTime) revert StillLocked();

       uint256 weight = uint256(l.amount) * l.multiplier;
       _payRewards(tokenId, holder, l);

       if (!l.paired) {
           totalActiveWeight -= weight;
           activeWeight[holder] -= weight;
       }

       totalLocked -= l.amount;
       uint256 amount = l.amount;

       if (!l.paired) {
           delete locks[tokenId];
           _burn(tokenId);
       } else {
           l.amount = 0;
       }

       aquarian.safeTransfer(holder, amount);
       emit MatureUnlocked(tokenId, holder, amount);
   }

   function claim(uint256 tokenId) external nonReentrant {
       address holder = ownerOf(tokenId);
       if (holder != msg.sender) revert NotOwner();

       Lock storage l = locks[tokenId];
       uint256 owed = _pending(l);
       if (owed == 0) revert NothingToClaim();
       if (rewardPoolBalance < owed) revert NothingToClaim();

       l.rewardDebt = (uint256(l.amount) * l.multiplier * rewardPerWeightStored) / ACC_PRECISION;
       rewardPoolBalance -= owed;

       aquarian.safeTransfer(holder, owed);
       emit RewardsClaimed(tokenId, holder, owed);
   }

   function pair(uint256 aquarianTokenId, address vault, uint256 vaultTokenId) external {
       address holder = ownerOf(aquarianTokenId);
       if (holder != msg.sender) revert NotOwner();

       Lock storage l = locks[aquarianTokenId];
       if (block.timestamp < l.unlockTime) revert NotMature();
       if (l.paired) revert AlreadyPaired();

       if (vault != RESERVE_VAULT && vault != DIAMOND_VAULT && vault != CONVICTION_VAULT) {
           revert InvalidVault();
       }

       if (IERC721(vault).ownerOf(vaultTokenId) != msg.sender) revert NotPairedOwner();

       l.paired = true;
       emit Paired(aquarianTokenId, vault, vaultTokenId);
   }

   // ============ VIEW FUNCTIONS ============

   function pendingRewards(uint256 tokenId) external view returns (uint256) {
       return _pending(locks[tokenId]);
   }

   function isMature(uint256 tokenId) external view returns (bool) {
       return block.timestamp >= locks[tokenId].unlockTime;
   }

   function getLock(uint256 tokenId) external view returns (Lock memory) {
       return locks[tokenId];
   }

   // ============ INTERNAL ============

   function _resolveTier(uint256 amount) internal pure returns (uint16) {
       if (amount == TIER_1M)  return 0;
       if (amount == TIER_5M)  return 1;
       if (amount == TIER_15M) return 2;
       if (amount == TIER_25M) return 3;
       if (amount == TIER_55M) return 4;
       revert InvalidAmount();
   }

   function _multiplierFor(uint256 days_) internal pure returns (uint16) {
       if (days_ >= 90) return 9;
       if (days_ >= 60) return 6;
       if (days_ >= 30) return 3;
       return 1;
   }

   function _pending(Lock memory l) internal view returns (uint256) {
       if (l.amount == 0 && !l.paired) return 0;
       uint256 weight = uint256(l.amount) * l.multiplier;
       if (l.paired && l.amount == 0) weight = 0;
       uint256 accumulated = (weight * rewardPerWeightStored) / ACC_PRECISION;
       if (accumulated <= l.rewardDebt) return 0;
       return accumulated - l.rewardDebt;
   }

   function _payRewards(uint256 tokenId, address holder, Lock memory l) internal {
       uint256 owed = _pending(l);
       if (owed == 0 || rewardPoolBalance < owed) return;

       locks[tokenId].rewardDebt = (uint256(l.amount) * l.multiplier * rewardPerWeightStored) / ACC_PRECISION;
       rewardPoolBalance -= owed;
       aquarian.safeTransfer(holder, owed);
       emit RewardsClaimed(tokenId, holder, owed);
   }

   function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
       address from = _ownerOf(tokenId);
       if (from != address(0) && to != address(0)) {
           if (block.timestamp < locks[tokenId].unlockTime) revert Soulbound();
       }
       return super._update(to, tokenId, auth);
   }

   // ============ OWNER ============

   function seedRewardPool(uint256 amount) external onlyOwner {
       aquarian.safeTransferFrom(msg.sender, address(this), amount);
       rewardPoolBalance += amount;
       emit RewardPoolSeeded(amount);
   }

   // ============ METADATA (Simplified for reliable compilation) ============

   function tokenURI(uint256 tokenId) public view override returns (string memory) {
       _requireOwned(tokenId);
       return string(
           abi.encodePacked(
               "data:application/json;base64,",
               Base64.encode(bytes(_buildJSON(tokenId)))
           )
       );
   }

   function _buildJSON(uint256 tokenId) internal view returns (string memory) {
       Lock memory l = locks[tokenId];
       string memory tierName = _tierName(l.tier);
       string memory status = block.timestamp >= l.unlockTime
           ? (l.paired ? "Mature + Paired" : "Mature")
           : "Locked";

       return string(
           abi.encodePacked(
               '{"name":"Aquarian Conviction Lock #', tokenId.toString(),
               ' - ', tierName,
               '","description":"AQUARIAN Conviction Lock NFT. Locked AQI with chosen duration and multiplier.",',
               '"attributes":[',
               '{"trait_type":"Tier","value":"', tierName, '"},',
               '{"trait_type":"Amount","value":"', _formatAmount(l.amount), '"},',
               '{"trait_type":"Duration","value":"', uint256(l.durationDays).toString(), ' days"},',
               '{"trait_type":"Multiplier","value":"', uint256(l.multiplier).toString(), 'x"},',
               '{"trait_type":"Edition","value":"', l.edition.toString(), '"},',
               '{"trait_type":"Status","value":"', status, '"}',
               ']}'
           )
       );
   }

   function _tierName(uint16 tier) internal pure returns (string memory) {
       if (tier == 0) return "1M Bronze";
       if (tier == 1) return "5M Silver";
       if (tier == 2) return "15M Gold";
       if (tier == 3) return "25M Platinum";
       return "55M Diamond";
   }

   function _formatAmount(uint256 amountWei) internal pure returns (string memory) {
       uint256 whole = amountWei / 1e18;
       if (whole >= 1_000_000) {
           return string(abi.encodePacked((whole / 1_000_000).toString(), "M AQI"));
       }
       return string(abi.encodePacked(whole.toString(), " AQI"));
   }
}