Description:
Non-Fungible Token (NFT) contract following ERC721 standard.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
/**
* @title HavenV3
* @dev Universal time-locked storage with NFT receipts
* Lock any asset - POL, USDC, DAI, WETH, or any ERC-20 token
* Each deposit receives an NFT receipt as proof
*/
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function decimals() external view returns (uint8);
function symbol() external view returns (string memory);
}
contract HavenV3 {
// Haven development fund (receives unclaimed deposits)
address public constant HAVEN_FUND = 0xdd638480A7C8f2Bd12fF57957a7e2550D47C8B55;
// NFT Receipt Counter
uint256 private _nextTokenId = 1;
// Struct to hold each deposit
struct Deposit {
uint256 amount;
uint256 unlockTime;
uint256 beneficiaryUnlockTime;
bool withdrawn;
address token; // 0x0 = POL, otherwise ERC-20 address
uint256 nftTokenId;
string tokenSymbol;
uint8 tokenDecimals;
}
// NFT Receipt Data
struct NFTReceipt {
address depositor;
uint256 depositId;
uint256 mintTime;
bool redeemed;
}
// Storage
mapping(address => Deposit[]) public deposits;
mapping(address => address) public beneficiaries;
mapping(uint256 => NFTReceipt) public nftReceipts;
mapping(uint256 => address) public nftOwners;
mapping(address => uint256) public nftBalances;
mapping(uint256 => address) public nftApprovals;
mapping(address => mapping(address => bool)) public nftOperatorApprovals;
// Events
event Locked(
address indexed user,
uint256 amount,
uint256 unlockTime,
uint256 beneficiaryUnlockTime,
uint256 depositId,
address indexed token,
uint256 nftTokenId
);
event Withdrawn(
address indexed user,
uint256 amount,
uint256 depositId,
uint256 nftTokenId
);
event BeneficiaryWithdrawn(
address indexed depositor,
address indexed beneficiary,
uint256 amount,
uint256 depositId,
uint256 nftTokenId
);
event LockExtended(
address indexed user,
uint256 depositId,
uint256 newUnlockTime,
uint256 newBeneficiaryUnlockTime
);
event BeneficiarySet(
address indexed user,
address indexed beneficiary
);
// ERC-721 Events
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// ============================================
// BENEFICIARY MANAGEMENT
// ============================================
function setBeneficiary(address beneficiary) public {
beneficiaries[msg.sender] = beneficiary;
emit BeneficiarySet(msg.sender, beneficiary);
}
function getBeneficiary(address user) public view returns(address) {
address beneficiary = beneficiaries[user];
return beneficiary == address(0) ? HAVEN_FUND : beneficiary;
}
// ============================================
// UNIVERSAL LOCK FUNCTION
// ============================================
/**
* @dev Lock any asset - POL or ERC-20 tokens
* @param token Address of token to lock (use 0x0 for POL)
* @param amount Amount to lock (ignored for POL, use msg.value instead)
* @param lockDuration Duration in seconds
* @param gracePeriodDays Days after unlock before beneficiary can claim
*
* Examples:
* Lock POL: lock(0x0, 0, duration, grace) + send POL
* Lock USDC: approve first, then lock(usdcAddress, amount, duration, grace)
*/
function lock(
address token,
uint256 amount,
uint256 lockDuration,
uint256 gracePeriodDays
) public payable {
require(lockDuration >= 3600, "Min 1 hour");
require(gracePeriodDays >= 1 && gracePeriodDays <= 365, "Grace period: 1-365 days");
uint256 actualAmount;
string memory tokenSymbol;
uint8 tokenDecimals;
// Handle POL (address 0x0)
if (token == address(0)) {
require(msg.value > 0, "Must send POL");
actualAmount = msg.value;
tokenSymbol = "POL";
tokenDecimals = 18;
}
// Handle ERC-20 tokens
else {
require(amount > 0, "Amount must be > 0");
require(msg.value == 0, "Don't send POL for token locks");
// Get token info with fallbacks
try IERC20(token).symbol() returns (string memory s) {
tokenSymbol = s;
} catch {
tokenSymbol = "TOKEN";
}
try IERC20(token).decimals() returns (uint8 d) {
tokenDecimals = d;
} catch {
tokenDecimals = 18;
}
// Transfer tokens from user
require(
IERC20(token).transferFrom(msg.sender, address(this), amount),
"Transfer failed"
);
actualAmount = amount;
}
// Calculate unlock times
uint256 unlockTime = block.timestamp + lockDuration;
uint256 beneficiaryUnlockTime = unlockTime + (gracePeriodDays * 1 days);
// Mint NFT receipt
uint256 nftTokenId = _mintNFT(msg.sender, deposits[msg.sender].length);
// Store deposit
deposits[msg.sender].push(Deposit({
amount: actualAmount,
unlockTime: unlockTime,
beneficiaryUnlockTime: beneficiaryUnlockTime,
withdrawn: false,
token: token,
nftTokenId: nftTokenId,
tokenSymbol: tokenSymbol,
tokenDecimals: tokenDecimals
}));
uint256 depositId = deposits[msg.sender].length - 1;
emit Locked(msg.sender, actualAmount, unlockTime, beneficiaryUnlockTime, depositId, token, nftTokenId);
}
// ============================================
// WITHDRAWAL FUNCTIONS
// ============================================
function withdraw(uint256 depositId) public {
require(depositId < deposits[msg.sender].length, "Invalid deposit");
Deposit storage deposit = deposits[msg.sender][depositId];
require(!deposit.withdrawn, "Already withdrawn");
require(block.timestamp >= deposit.unlockTime, "Still locked");
require(deposit.amount > 0, "No funds");
// Update state before transfer (reentrancy protection)
deposit.withdrawn = true;
uint256 amountToSend = deposit.amount;
address tokenAddress = deposit.token;
uint256 nftTokenId = deposit.nftTokenId;
// Burn NFT receipt
_redeemNFT(nftTokenId);
// Transfer funds
_transferOut(tokenAddress, msg.sender, amountToSend);
emit Withdrawn(msg.sender, amountToSend, depositId, nftTokenId);
}
function withdrawAsBeneficiary(address depositor, uint256 depositId) public {
require(depositId < deposits[depositor].length, "Invalid deposit");
Deposit storage deposit = deposits[depositor][depositId];
address beneficiary = getBeneficiary(depositor);
require(msg.sender == beneficiary, "Not beneficiary");
require(!deposit.withdrawn, "Already withdrawn");
require(block.timestamp >= deposit.beneficiaryUnlockTime, "Grace period active");
require(deposit.amount > 0, "No funds");
// Update state before transfer
deposit.withdrawn = true;
uint256 amountToSend = deposit.amount;
address tokenAddress = deposit.token;
uint256 nftTokenId = deposit.nftTokenId;
// Burn NFT receipt
_redeemNFT(nftTokenId);
// Transfer to beneficiary
_transferOut(tokenAddress, beneficiary, amountToSend);
emit BeneficiaryWithdrawn(depositor, beneficiary, amountToSend, depositId, nftTokenId);
}
function extendLock(uint256 depositId, uint256 additionalTime) public {
require(depositId < deposits[msg.sender].length, "Invalid deposit");
Deposit storage deposit = deposits[msg.sender][depositId];
require(!deposit.withdrawn, "Already withdrawn");
require(additionalTime > 0, "Must add time");
deposit.unlockTime += additionalTime;
deposit.beneficiaryUnlockTime += additionalTime;
emit LockExtended(msg.sender, depositId, deposit.unlockTime, deposit.beneficiaryUnlockTime);
}
// ============================================
// INTERNAL HELPERS
// ============================================
function _transferOut(address token, address to, uint256 amount) internal {
if (token == address(0)) {
// POL transfer
(bool success, ) = payable(to).call{ value: amount }("");
require(success, "Transfer failed");
} else {
// ERC-20 transfer
require(IERC20(token).transfer(to, amount), "Transfer failed");
}
}
// ============================================
// VIEW FUNCTIONS
// ============================================
function getDepositCount(address user) public view returns(uint256) {
return deposits[user].length;
}
function getDepositFull(address user, uint256 depositId)
public
view
returns(
uint256 amount,
uint256 unlockTime,
uint256 beneficiaryUnlockTime,
bool withdrawn,
address token,
uint256 nftTokenId,
string memory tokenSymbol,
uint8 tokenDecimals
)
{
require(depositId < deposits[user].length, "Invalid deposit");
Deposit memory deposit = deposits[user][depositId];
return (
deposit.amount,
deposit.unlockTime,
deposit.beneficiaryUnlockTime,
deposit.withdrawn,
deposit.token,
deposit.nftTokenId,
deposit.tokenSymbol,
deposit.tokenDecimals
);
}
function isUnlocked(address user, uint256 depositId) public view returns(bool) {
require(depositId < deposits[user].length, "Invalid deposit");
return block.timestamp >= deposits[user][depositId].unlockTime;
}
function isBeneficiaryUnlocked(address user, uint256 depositId) public view returns(bool) {
require(depositId < deposits[user].length, "Invalid deposit");
return block.timestamp >= deposits[user][depositId].beneficiaryUnlockTime;
}
function timeUntilUnlock(address user, uint256 depositId) public view returns(uint256) {
require(depositId < deposits[user].length, "Invalid deposit");
Deposit memory deposit = deposits[user][depositId];
if (block.timestamp >= deposit.unlockTime) return 0;
return deposit.unlockTime - block.timestamp;
}
function timeUntilBeneficiaryUnlock(address user, uint256 depositId) public view returns(uint256) {
require(depositId < deposits[user].length, "Invalid deposit");
Deposit memory deposit = deposits[user][depositId];
if (block.timestamp >= deposit.beneficiaryUnlockTime) return 0;
return deposit.beneficiaryUnlockTime - block.timestamp;
}
function getActiveDeposits(address user) public view returns(uint256[] memory) {
uint256 activeCount = 0;
for (uint256 i = 0; i < deposits[user].length; i++) {
if (!deposits[user][i].withdrawn) activeCount++;
}
uint256[] memory activeIds = new uint256[](activeCount);
uint256 currentIndex = 0;
for (uint256 i = 0; i < deposits[user].length; i++) {
if (!deposits[user][i].withdrawn) {
activeIds[currentIndex] = i;
currentIndex++;
}
}
return activeIds;
}
function getTotalLocked(address user, address token) public view returns(uint256) {
uint256 total = 0;
for (uint256 i = 0; i < deposits[user].length; i++) {
if (!deposits[user][i].withdrawn && deposits[user][i].token == token) {
total += deposits[user][i].amount;
}
}
return total;
}
// ============================================
// NFT RECEIPT FUNCTIONS (ERC-721)
// ============================================
function _mintNFT(address to, uint256 depositId) internal returns (uint256) {
uint256 tokenId = _nextTokenId++;
nftOwners[tokenId] = to;
nftBalances[to]++;
nftReceipts[tokenId] = NFTReceipt({
depositor: to,
depositId: depositId,
mintTime: block.timestamp,
redeemed: false
});
emit Transfer(address(0), to, tokenId);
return tokenId;
}
function _redeemNFT(uint256 tokenId) internal {
require(nftReceipts[tokenId].depositor != address(0), "NFT doesn't exist");
require(!nftReceipts[tokenId].redeemed, "Already redeemed");
address owner = nftOwners[tokenId];
nftReceipts[tokenId].redeemed = true;
delete nftOwners[tokenId];
delete nftApprovals[tokenId];
nftBalances[owner]--;
emit Transfer(owner, address(0), tokenId);
}
function getReceiptData(uint256 tokenId)
public
view
returns(address depositor, uint256 depositId, uint256 mintTime, bool redeemed)
{
NFTReceipt memory receipt = nftReceipts[tokenId];
return (receipt.depositor, receipt.depositId, receipt.mintTime, receipt.redeemed);
}
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = nftOwners[tokenId];
require(owner != address(0), "Invalid token");
return owner;
}
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "Invalid address");
return nftBalances[owner];
}
function name() public pure returns (string memory) {
return "Haven Deposit Receipt";
}
function symbol() public pure returns (string memory) {
return "HAVEN-R";
}
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(nftOwners[tokenId] != address(0), "Invalid token");
NFTReceipt memory receipt = nftReceipts[tokenId];
Deposit memory deposit = deposits[receipt.depositor][receipt.depositId];
string memory json = string(abi.encodePacked(
'{"name":"Haven Receipt #', _toString(tokenId),
'","description":"Proof of locked assets in Haven protocol",',
'"attributes":[',
'{"trait_type":"Amount","value":"', _toString(deposit.amount), '"},',
'{"trait_type":"Asset","value":"', deposit.tokenSymbol, '"},',
'{"trait_type":"Unlock Time","value":"', _toString(deposit.unlockTime), '"},',
'{"trait_type":"Status","value":"', deposit.withdrawn ? "Withdrawn" : "Locked", '"}',
']}'
));
return string(abi.encodePacked("data:application/json;utf8,", json));
}
function _toString(uint256 value) internal pure returns (string memory) {
if (value == 0) return "0";
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
function transferFrom(address from, address to, uint256 tokenId) public {
require(nftOwners[tokenId] == from, "Not owner");
require(to != address(0), "Invalid recipient");
require(
msg.sender == from ||
msg.sender == nftApprovals[tokenId] ||
nftOperatorApprovals[from][msg.sender],
"Not authorized"
);
delete nftApprovals[tokenId];
nftOwners[tokenId] = to;
nftBalances[from]--;
nftBalances[to]++;
emit Transfer(from, to, tokenId);
}
function approve(address to, uint256 tokenId) public {
address owner = nftOwners[tokenId];
require(msg.sender == owner || nftOperatorApprovals[owner][msg.sender], "Not authorized");
nftApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
function setApprovalForAll(address operator, bool approved) public {
nftOperatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function getApproved(uint256 tokenId) public view returns (address) {
require(nftOwners[tokenId] != address(0), "Invalid token");
return nftApprovals[tokenId];
}
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return nftOperatorApprovals[owner][operator];
}
}
Submitted on: 2025-10-23 18:24:55
Comments
Log in to comment.
No comments yet.