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.
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).
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.
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.
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);
}
}