EVM 合约
EVM合约是聪网智能合约的一种执行类型。EVM执行Solidity/EVM状态机,聪网UTXO模型负责真实资产来源和资产结算。
EVM合约遵循智能合约的通用协议。本文只定义EVM合约特有规则。
合约类型
EVM合约类型为:
ContractTypeEVMEVM合约兼容EVM执行语义、ABI、Solidity开发模型和EVM事件模型,但不采用以太坊账户资产模型。EVM合约内部状态可以维护应用账本,真实资产余额以合约地址UTXO集合为准。
地址规则
EVM内部地址保持20字节,以兼容Solidity和ABI。
msg.sender是20字节EVM地址。address(this)是20字节EVM合约地址。event topic中的indexed address按EVM ABI编码。
CREATE和CREATE2生成20字节EVM合约地址。
聪网外部合约地址使用:
ca/tc + version + ContractTypeEVM + hashEVM合约地址hash为20字节EVM地址。EVM内部20字节地址和聪网外部ca/tc地址必须支持无损转换。
chain id、vm version和code hash不放入地址hash:
chain id由网络和地址前缀区分。
vm version由地址version、type和协议升级规则表达。
code hash由部署交易、合约状态和state root承诺表达。
部署规则
CONTRACT_DEPLOY用于部署EVM合约。
EVM部署payload包含:
init code。
constructor参数。
deployer EVM address。
gas limit。
部署nonce。
部署成功后生成:
20字节EVM合约地址。
聪网
ca/tc合约地址。code hash。
初始storage root。
部署结果必须通过同一区块内的canonical CONTRACT_RESULT记录。
调用规则
CONTRACT_INVOKE用于调用EVM合约。
EVM调用payload包含:
calldata。
gas limit。
call nonce。
调用交易必须输出到被调用合约地址,作为gas/funding输出。被调用合约地址由Call TX输出解析,不写入OP_RETURN。
EVM msg.sender由调用交易最后一个输入的前序输出地址确定,再映射为20字节EVM地址。节点不得从witness公钥推导msg.sender。
EVM合约的默认调用表示向合约地址发起一次空calldata调用。输出中的satoshi作为msg.value,其他资产仍由聪网资产层和预编译资产接口表达。
EVM calldata直接作为EVM调用输入,不在协议层预定义业务action和参数。合约如果需要读取本次Call TX转入合约地址的资产数量,应通过预编译资产接口查询funding output,而不是要求用户在calldata中重复填写同一经济参数。
StateDB和执行器
EVM执行器第一阶段基于go-ethereum core/vm。
实现要求:
不修改EVM opcode语义。
实现聪网自定义StateDB。
StateDB维护storage、code、log和state root。
合约资产由UTXO集合表达,不由EVM account balance表达。
资产规则
资产名称在ABI中按字符串编码,使用聪网资产层AssetName.String()结果。
satoshi资产名称固定为:
第一阶段支持聪网已识别并能以UTXO表达的资产:
satoshi。
ORDX资产。
Runes资产。
BRC20资产。
其他已被聪网资产层识别的资产。
EVM合约内部可以维护ERC20或其他应用层余额。该余额不等同于聪网原生资产余额。需要兑现为聪网资产转移时,合约必须通过资产接口生成Asset Intent,并由CONTRACT_RESULT结算。
预编译资产接口
EVM不能直接花费UTXO,只能生成Asset Intent。底层资产接口通过预编译合约提供。
预编译合约负责:
查询合约UTXO资产状态。
生成资产转移intent。
参与Result TX确定性验证。
当前资产预编译地址为:
第一阶段接口:
balanceOf(address owner, string assetName) returns (string):查询指定EVM地址对应合约地址当前可用的指定资产余额。fundingAssetAmount(string assetName) returns (string):查询本次Call TX funding output中指定资产的剩余可认领数量。若assetName为统一gas资产,返回值会扣除本次调用预留的Result费用后再提供给合约。fundingSats() returns (uint256):查询本次Call TX funding output中的白聪数量。claimFundingAsset(string assetName, string amount) returns (bool):合约明确认领本次funding中的指定资产数量。只有被认领的funding资产进入本次合约业务处理;未认领的多余资产按framework规则退款或保留为非业务资产。callerAddress() returns (string):返回按通用规则解析出的聪网caller地址。transferAsset(string assetName, string to, string amount, bytes extraData) returns (bool):声明一笔资产转移intent。transferAssets(string[] assetNames, string[] recipients, string[] amounts, bytes[] extraData) returns (bool):声明多笔资产转移intent。该接口用于close或批量结算时一次性表达多个接收者和多种资产。compareAmount(string left, string right) returns (int256):按Decimal语义比较两个资产数量字符串。addAmount、subAmount、mulAmount、divAmount:按Decimal语义计算资产数量字符串。uintToAmount(uint256)、amountToUintFloor(string)、amountToUintCeil(string):在整数和资产数量字符串之间转换。
所有资产数量在预编译资产接口中优先使用字符串表达。EVM合约内部可以为了业务计算临时转换为uint256,但最终资产转移intent必须回到资产字符串,并由Result TX按资产精度截断或校验。
msg.value和balance
msg.value和balancemsg.value表示本次Call TX转入合约地址的satoshi数量。
address.balance返回satoshi余额。address(this).balance表示当前合约地址可支配的satoshi总量,该数值由合约地址UTXO集合统计得到。
多资产余额必须通过预编译合约查询。合约storage中的资产记录如果与UTXO统计不一致,Result TX验证以UTXO资产状态和预编译接口为准。
合约信息和状态视图接口
为了让钱包、应用前端和浏览器在不知道具体Solidity源码的情况下展示基础信息,建议EVM合约实现一组轻量只读接口:
节点查询EVM合约状态时会尝试调用这些接口。没有实现contractName()的合约,基础显示名称返回unknown。managedAsset*接口用于返回合约自己认为正在管理的资产名称和数量;浏览器仍可以同时展示合约地址UTXO中的实际资产余额,两者不要求完全相等,但差异应被视为需要关注的合约状态信息。
合约可以额外实现:
stateView()返回合约自定义JSON字符串,用于展示订单簿、池子、用户份额或其他业务视图。该视图不替代共识状态,只是对合约storage和资产状态的可读投影。
部署工具和源码metadata
钱包可以提供Solidity源码编译入口。编译参数不由用户自由选择,而由聪网当前支持的EVM编译配置决定,并展示给用户。当前测试阶段默认配置为:
solcVersion:0.8.30。evmVersion:paris。optimizer开启,
runs=200。metadata
bytecodeHash=none。只支持单文件源码,不允许import。
部署交易确认后,钱包或部署工具可以把合约地址、Solidity源码、ABI、compiler config、constructor参数和init/runtime code hash提交给L2 indexer的metadata接口。indexer保存metadata用于浏览器、应用前端和钱包展示;metadata不是共识状态,不能替代链上deploy交易、EVM code hash和state root。
Gas
EVM保留gas机制。
规则:
opcode gas成本沿用geth。
每个部署和调用必须声明gas limit。
执行超过gas limit时结果为out of gas。
gas费用由调用方承担。
区块存在EVM gas总上限。
gas price第一阶段采用协议固定值。
gas费用使用统一gas资产支付。
gas费用归出块节点。
默认gas上限参数:
单次deploy/invoke gas上限为
50,000,000。单次触发器执行gas上限为
5,000,000。单区块EVM执行总gas上限为
1,000,000,000。
触发器
EVM合约可以通过协议预编译合约注册高度触发器。触发器记录在EVM状态中,并参与EVM state root。
高度触发器至少包含:
trigger id。
目标合约地址。
触发高度。
触发执行gas limit。
触发时调用的calldata。
出块节点在构造每个区块时必须检查EVM状态中已经到期的触发器。即使候选区块没有普通EVM deploy/invoke交易,只要存在到期触发器,也必须执行触发器并在需要结算时生成canonical CONTRACT_RESULT。
验证节点遇到没有同区块EVM deploy/invoke的EVM Result TX时,必须通过Result TX花费的EVM合约UTXO确定合约地址,并基于上一EVM状态、当前区块高度和区块确认时间重放触发器。触发器不存在、未到期、已被移除、gas limit无效或重放结果不一致时,区块无效。
触发器注册和执行必须同时满足:
gas limit必须为正数。
gas limit不能超过单次触发器执行gas上限。
合约必须有足够gas资金覆盖触发器执行和Result TX打包费用。
区块内所有EVM执行的累计gas不能超过单区块EVM执行总gas上限。
Result TX和state root
EVM合约不接受外部提交的CONTRACT_RESULT进入mempool。
出块节点执行EVM后,根据Asset Intent构造canonical CONTRACT_RESULT。验证节点独立重放EVM,并比较Result TX和state root。
EVM state root、模版合约state root和Agent合约state root合成统一contract state root,写入coinbase。
关闭规则
EVM合约使用通用close语义。deployer发起合约关闭时,EVM执行器会先尝试调用合约的:
合约应在close()中通过预编译资产接口为自己管理的资产生成必要的转移intent,例如返还LP、订单、押金或其他有明确归属的资产。close()执行完成后,framework再按通用规则处理合约管理资产中的剩余利润,以及合约地址上超出runtime管理记录的非管理资产。
如果EVM合约没有实现close()或close()失败,节点仍会按协议规则处理Result验证;但作为可公开使用的合约,应实现明确的close(),并使用transferAssets一次性表达多资产、多接收者的关闭分配。
Last updated