Show / Hide Table of Contents

合约编写对比 (Neo N3 vs. Neo Legacy)

本文档总结了 Neo N3 与 Neo Legacy在合约编写方面的主要差异, 以便于开发人员将智能合约从Neo Legacy 迁移到 Neo N3。由于 Neo N3 仍在不断更新中,本文仅列举了改动较大或影响较大的变更,并将持续更新。

开发环境

.NET Core

在 Neo N3 中,.NET Core 版本从 3.0 升级为 6.0,需要更新 SDK。

Visual Studio 扩展

在 Neo N3 中,Visual Studio 扩展更新,需卸载旧的 NeoContractPlugin 插件,编译、安装最新的 NeoContractPlugin。

新建的合约模板有重大更新,详见合约模板

编译器

在Neo N3中,旧版的 neon(Neo.Compiler.MSIL) 已经废弃,需使用最新的 nccs (Neo.Compiler.CSharp)编译器,其主要变化如下:

  • 新的 nccs 取消了对 Visual Basic 语言的支持

  • 新的 nccs 编译时无需 IL 中间语言,直接将 C# 代码编译为智能合约

  • 可直接编译解决方案、项目、C# 文件

  • 支持更多 C# 功能

  • 确定性编译。代码和编译器确定的情况下,编译后的合约即是确定的

  • 将 abi 文件升级为 manifest 文件

  • 将 nvm 文件升级为 nef 文件

  • 编译目录从 bin/debug 修改为 bin/sc

合约模板

命名空间

Neo Legacy:

using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Services.Neo;
using System;

Neo N3:

using Neo;
using Neo.SmartContract;
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Native;
using Neo.SmartContract.Framework.Services;
using System;

合约特性

Neo LegacyNeo N3
合约信息在合约部署时需填写合约特性,如名称、作者、邮箱等将合约特性添加到合约文件中,以 C# 特性 的方式编写。例如:
[ManifestExtra("Author", "Neo")]
[ManifestExtra("Email", " dev@neo.org ")]
[ContractTrust("*")]
[ContractPermission("*", "*")]
[SupportedStandards("NEP-17")]
[ManifestExtra("Description", "This is a contract example")]
public class Contract1 : SmartContract
合约功能部署合约时需要声明合约功能,如是否使用存储区、是否可以动态调用、是否可以接受 NEP-5 资产。默认所有合约均可以使用存储区和动态调用,通过实现 OnNEP17Payment 方法以决定是否接受 NEP-17 资产,通过实现 OnNEP11Payment 方法以决定是否接受 NEP-11(NFT 标准)资产。
声明支持的NEP代码示例
public static string[] SupportedStandards()
{
string[] result = { "NEP-5", "NEP-7", "NEP-10" };
return result;
}
直接在合约类名上添加 [SupportedStandards("NEP-17")] 特性。

静态变量的声明

Neo Legacy

private static readonly byte[] InitialOwnerScriptHash = "AJhZmdHxW44FWMiMxD5bTiF7UgHcp3g2Fr".ToScriptHash();

Neo N3

[InitialValue("NiNmXL8FjEUEs1nfX9uHFBNaenxDHJtmuB", ContractParameterType.Hash160)]
static readonly UInt160 Owner = default;

方法和事件

Neo LegacyNeo N3
main已移除,开发者不再需要写冗余的 main 方法来进行合约方法的跳转
Verify在Main 方法中判断,例如:
public static object Main(string method, object[] args)
{
if (Runtime.Trigger == TriggerType.Verification)
{
return IsOwner();
)
}
独立的方法:
public static bool Verify() => IsOwner();
方法名为了让方法名符合智能合约的命名规则,会这么声明方法:
[DisplayName("balanceOf")]
public static BigInteger BalanceOf(byte[] account)
自动将方法名首字母编译为小写,开发者不用再通过 DisplayName 的方式将方法名首字母改为小写,但 DisplayName 这种写法仍然可用。
部署初始化变量会放到一个单独的方法中,部署后手动调用;添加 _deploy 方法,部署后自动执行。
升级和销毁需要自行编写升级(Update)和销毁(Destroy)方法合约模板中内置了升级和销毁方法
transfer事件名 transfer ,事件名为全小写 Transfer ,首字母大写

权限管理

用户签名

Neo Legacy 默认调用链中的所有合约均可使用用户签名;

Neo N3 新增签名作用域(WitnessScope)概念,默认只允入口合约才能使用用户签名,并允许用户修改。

权限与信任

Neo Legacy 中有动态调用功能的合约均可互相调用,需开发者在代码中自行控制权限,但作用有限;

Neo N3:

  • 增加了对合约调用权限的限制,需要先声明权限(Permission),再调用;

  • 增加了合约组(Groups)和信任(Trusts)的概念,以便钱包提供更好的安全提示;

  • 增加了调用标志(CallFlag)的概念,以限制被调用的合约的行为。

安全方法

Neo N3 新增安全方法,在方法中添加 [Safe] 特性,就会以只读的方式执行合约。

合约框架

原生合约

Neo N3 新增了大量原生合约,将 Neo Legacy 中的大量互操作服务转移到了原生合约中,具体变化如下:

  • 将 Blockchain 类升级为 Ledger 原生合约,如: Blockchain.GetBlock() 改为 Ledger.GetBlock()

  • 新增 ContractManagement 原生合约,来查询合约以及管理合约的升级和销毁。

  • 将 Blockchain 类中的合约部分移入 ContractManagement 原生合约,如: Blockchain.GetContract() 改为 ContractManagement .GetContract()

  • 新增 CryptoLib 原生合约,将 Sha256、ripemd160、VerifyWithECDsa 等方法移到该合约中。

  • 新增 StdLib 原生合约,将序列化、反序列化、数据转换等方法从 Helper 类中移到该合约中。

  • 新增 NEO、GAS、Oracle、Policy、RoleManagement 原生合约。

  • 将选举、投票、提取 GAS等功能从单独的特殊交易移到了 NEO 原生合约中。

其它类

  • Runtime 类有大量更新,增加更多运行时状态。

  • Transaction 类有大量更新,以适配 Neo N3 交易的数据结构。

  • 新增 Crypto 类,将部分 SmartContract 类提供的方法移到 Crypto 类中。

  • 移除 Account 类

  • 移除 Asset 类

  • 移除 Header 类

  • 移除 InvocationTransaction 类

  • 移除 TransactionAttribute 类

  • 移除 TransactionInput 类

  • 移除 TransactionOutput 类

存储区操作

Neo LegacyNeo N3
StorageMap不能作为静态变量,要写到每个方法中支持声明静态的 StorageMap
存储区查找找不到对应的 key时,返回 byte[]找不到对应的 key时返回 null,需要开发者自行判断是否为空,否则可能出现空引用异常
存储区数字转换通过 ToBigInteger 方法转换通过 (BigInteger) 强制转换
Storage.CurrentContext.CreateMap(string name) 方法改为 StorageMap 的构造方法

Neo Legacy :

public static readonly string mapName = "asset";

public static void Put(byte[] key, BigInteger value) => Storage.CurrentContext.CreateMap(mapName).Put(key, value);

public static BigInteger Get(byte[] key) => Storage.CurrentContext.CreateMap(mapName).Get(key).ToBigInteger();

Neo N3:

public static readonly string mapName = "asset";

public static void Put(UInt160 key, BigInteger value) => assetMap.Put(key, value);

public static BigInteger Get(UInt160 key)
{
    var value = assetMap.Get(key);
    return value is null ? 0 : (BigInteger)value;
}

TokenSale 操作

合约编写

Neo Legacy:

由于 UTXO 资产和合约资产两种资产模型的关系,进行 Token Sale 比较复杂。一般要写 mintTokens 方法,获得调用合约的交易,再从交易输入中分析出发送者,从交易输出中分析出转账金额和资产名称,最后将合约资产转给发送者。

Neo N3:

实现 OnNEP17Payment 即可完成 TokenSale 操作,发送者、转账金额再也不用从交易中逐个分析再累加汇总,而是可以直接从参数中拿到。示例代码如下:

public static void OnNEP17Payment(UInt160 from, BigInteger amount, object data)
{
    if (Runtime.CallingScriptHash == NEO.Hash)
    {
        Mint(amount * TokensPerNEO);
    }
    else if (Runtime.CallingScriptHash == GAS.Hash)
    {
        if (from != null) Mint(amount * TokensPerGAS);
    }
}

用户操作

Neo Legacy:

首先用户通过发起 InvocationTransaction,构造交易输入和交易输出,将 NEO/GAS 转给合约地址,并且调用合约地址的 mintTokens 方法完成 TokenSale 操作。

Neo N3 :

用户只需将 NEO/GAS 打到合约地址,此操作会触发合约的 OnNEP17Payment 方法,就能完成 TokenSale 操作。

异常处理

Neo Legacy:

调用合约时,如合约执行遇到异常,异常的消息并不会输出。

Neo N3:

调用合约时,异常消息会输出到调用结果中。

静态调用

Neo Legacy :

[Appcall("XXXXXXXXXX")]//ScriptHash
public static extern int AnotherContract(string arg);

public static void Main()
{
    AnotherContract("Hello");    
}

Neo N3 :

[Contract("0102030405060708090A0102030405060708090A")]
public class Contract1
{
    public static extern void MyMethod();
}

public static void Call()
{
    Contract1.MyMethod();
}

动态调用

Neo Legacy:

delegate object Dyncall(string method, object[] args);
public static object Main(string operation, object[] args)
{
    var dyncall = (Dyncall)target.ToDelegate();
    var newArgs = new object[1];
    var method = (string)args[0];
    newArgs[0] = args[1];
    dyncall(method, newArgs);
}

Neo N3:

可以直接调用 Contract.Call() 完成合约的动态调用:

public static bool Transfer(UInt160 from, UInt160 to, BigInteger amount, object data)
{
    Contract.Call(to, "onNEP17Payment", CallFlags.All, new object[] { from, amount, data });
}