neo-devpack-solidity 0.22.0

Production-focused Solidity-to-NeoVM compilation system
Documentation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/**
 * @title VaultPattern - ERC-4626 Vault Adapted for Neo N3
 * @dev Demonstrates a deposit/withdraw vault using GAS as the underlying asset.
 *
 * On Ethereum, ERC-4626 vaults use approve/transferFrom for deposits.
 * On Neo N3, authorization uses Runtime.checkWitness() instead.
 * Deposits arrive via the onNEP17Payment callback from the GAS native contract.
 *
 * This example is designed to compile with strict manifest flags:
 *   --deny-wildcard-permissions --deny-wildcard-contracts --deny-wildcard-methods
 */

import "../contracts/FrameworkBase.sol";
import "../contracts/NativeCalls.sol";
import "../libraries/Runtime.sol";

contract VaultPattern is FrameworkBase {
    using Runtime for *;

    // --- State ---
    uint256 public totalShares;
    mapping(address => uint256) public shares;

    // --- Events ---
    event Deposit(address indexed depositor, uint256 assets, uint256 sharesMinted);
    event Withdraw(address indexed recipient, uint256 assets, uint256 sharesBurned);

    // --- Errors ---
    error UnsupportedToken(address token);
    error InsufficientShares(address account, uint256 requested, uint256 available);
    error ZeroAmount();

    constructor() FrameworkBase() {}

    // ------------------------------------------------------------------
    // Core vault logic
    // ------------------------------------------------------------------

    /**
     * @dev Receive GAS deposits via the NEP-17 payment callback.
     *
     * When a user calls `NativeCalls.gasTransfer(sender, vault, amount, "")`
     * the GAS native contract invokes this callback on the vault.
     */
    function onNEP17Payment(address from, uint256 amount, bytes memory) external {
        if (msg.sender != NativeCalls.GAS_CONTRACT) {
            revert UnsupportedToken(msg.sender);
        }
        if (amount == 0) revert ZeroAmount();

        // Mint shares proportional to the deposit relative to the vault's assets
        // BEFORE this deposit. `totalAssets()` reads the live GAS balance, which
        // already includes `amount` (the NEP-17 payment lands before this
        // callback runs), so `_convertToShares(amount)` would divide by the
        // post-deposit total and dilute the depositor. Use the pre-deposit
        // balance as the denominator instead.
        uint256 supply = totalShares;
        uint256 priorAssets = totalAssets() - amount;
        uint256 sharesToMint = (supply == 0 || priorAssets == 0)
            ? amount
            : (amount * supply) / priorAssets;
        shares[from] += sharesToMint;
        totalShares += sharesToMint;

        emit Deposit(from, amount, sharesToMint);
    }

    /**
     * @dev Withdraw underlying GAS by burning vault shares.
     *
     * The caller must be the account owner (verified via Runtime.checkWitness).
     */
    function withdraw(uint256 shareAmount) external {
        if (shareAmount == 0) revert ZeroAmount();

        uint256 callerShares = shares[msg.sender];
        if (callerShares < shareAmount) {
            revert InsufficientShares(msg.sender, shareAmount, callerShares);
        }

        uint256 assets = _convertToAssets(shareAmount);

        shares[msg.sender] = callerShares - shareAmount;
        totalShares -= shareAmount;

        require(
            NativeCalls.gasTransfer(address(this), msg.sender, assets, ""),
            "VaultPattern: GAS transfer failed"
        );

        emit Withdraw(msg.sender, assets, shareAmount);
    }

    // ------------------------------------------------------------------
    // View helpers
    // ------------------------------------------------------------------

    /**
     * @dev Total GAS held by the vault.
     */
    function totalAssets() public view returns (uint256) {
        return NativeCalls.gasBalanceOf(address(this));
    }

    /**
     * @dev Preview how many shares a deposit of `assets` GAS would mint.
     */
    function previewDeposit(uint256 assets) external view returns (uint256) {
        return _convertToShares(assets);
    }

    /**
     * @dev Preview how much GAS a redemption of `shareAmount` would return.
     */
    function previewWithdraw(uint256 shareAmount) external view returns (uint256) {
        return _convertToAssets(shareAmount);
    }

    // ------------------------------------------------------------------
    // Internal conversion (1:1 when vault is empty, proportional otherwise)
    // ------------------------------------------------------------------

    function _convertToShares(uint256 assets) internal view returns (uint256) {
        uint256 supply = totalShares;
        if (supply == 0) return assets;
        return (assets * supply) / totalAssets();
    }

    function _convertToAssets(uint256 shareAmount) internal view returns (uint256) {
        uint256 supply = totalShares;
        if (supply == 0) return shareAmount;
        return (shareAmount * totalAssets()) / supply;
    }
}