HavenV3

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];
    }
}

Tags:
ERC721, NFT, Non-Fungible|addr:0xe85de24b8723a1db034e5719474a042fff1e3f35|verified:true|block:23631146|tx:0x162d7871fabf4e9730d7b5e7472b9656a5f30ac72eeb8a48d750d1e6617792e7|first_check:1761236692

Submitted on: 2025-10-23 18:24:55

Comments

Log in to comment.

No comments yet.