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