Address
0xadfb8bb0dd67b3c3d838b2f30cdd923c8117ad1dCurrent 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"));
}
}