Skip to content

Common Patterns

This section highlights common Solidity design patterns and explains how they are adapted for NeoVM using Neo DevPack for Solidity.

The Withdrawal Pattern

On Ethereum, pushing funds to another address using address.transfer() is discouraged due to reentrancy risks and fixed gas limits. The recommended approach is a "pull" pattern where users call a withdraw() function.

On Neo N3, value transfer is handled via NEP-17 token transfers, which inherently use a callback pattern (onNEP17Payment).

Neo Adaptation: Because NEP-17 transfers invoke the recipient's callback, reentrancy is still a theoretical possibility on Neo if the recipient is a contract. You should still utilize the Checks-Effects-Interactions pattern.

solidity
import {NativeCalls} from "@neo/NativeCalls.sol";
import {Runtime} from "@neo/Runtime.sol";

contract PullPayment {
    mapping(address => uint256) public balances;

    function withdraw() public {
        address payee = Runtime.getCallingScriptHash();
        uint256 amount = balances[payee];
        
        require(amount > 0, "No funds to withdraw");
        
        // Effects
        balances[payee] = 0;
        
        // Interactions
        bool success = NativeCalls.gasTransfer(address(this), payee, amount, "");
        require(success, "Transfer failed");
    }
}

Receiving Funds (onNEP17Payment)

💡 NeoVM Difference

Ethereum contracts use receive() external payable or fallback() external payable to accept native Ether. Neo contracts must implement onNEP17Payment to receive NEP-17 tokens (including GAS).

solidity
contract Treasury {
    uint256 public totalDeposits;

    /// @notice Equivalent to `receive() external payable`
    function onNEP17Payment(address from, uint256 amount, bytes memory data) external {
        // Only accept GAS
        require(msg.sender == NativeContracts.GAS, "Only GAS accepted");
        
        totalDeposits += amount;
    }
}

Restricting Access

Access restriction is one of the most common patterns. On Neo, this relies on Runtime.checkWitness() rather than msg.sender.

solidity
import {Runtime} from "@neo/Runtime.sol";

contract Owned {
    address public owner;

    constructor() {
        // Initialize owner during deployment
        owner = Runtime.getCallingScriptHash();
    }

    modifier onlyOwner() {
        require(Runtime.checkWitness(owner), "Unauthorized: Must be signed by owner");
        _;
    }

    function changeOwner(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

Contract Upgrades

Proxy patterns (like Transparent Proxies or UUPS) using delegatecall are heavily used on Ethereum. NeoVM blocks delegatecall but offers a native upgrade syscall.

Neo Adaptation: Use ContractManagement.update to replace the contract bytecode while preserving the storage state.

solidity
import {ContractManagement} from "@neo/ContractManagement.sol";

contract Upgradeable {
    address public admin;

    constructor() {
        admin = Runtime.getCallingScriptHash();
    }

    function upgrade(bytes memory nefFile, bytes memory manifest) public {
        require(Runtime.checkWitness(admin), "Unauthorized");
        
        // Native in-place upgrade. Storage remains intact.
        ContractManagement.update(nefFile, manifest);
    }
}

MIT Licensed