目录
- 一、原则
- 二、查询里的异常处理
- 三、小数
- 四、访问子合约的变量
- 五、实现结构体继承的功能
- 六、address转string
- 七、字符串判等比较
- 八、简单的地址合法性校验
- 九、字符串拼接
- 十、实现Set集合
- 十一、参数是结构体的event的签名
最近在开发一组自主链权限治理的Solidity合约,业务上涉及治理委员会、权重、提案、撤销、投票和决议等,技术上也有一些新的注意点。
一、原则
solidity合约要尽可能简单简洁,如果可以依赖SDK层或者应用层,考虑尽可能丢弃掉较多的计算逻辑,以及过于细致的复杂校验。
尽可能在remix中调测,迅速排除语法的问题。
二、查询里的异常处理
交易里的异常处理建议使用require
来保证可以回滚已修改状态,对于查询里的异常处理,没有找到标准的方式,所以只要合理即可,可以返回错误码,也可以require抛出异常。
三、小数
solidity里的小数支持不太友好,所以一般都是默认约定乘以10的n倍,使用整数来间接代表小数。
在治理合约的业务里,如果SDK层要计算某个提案在每次投票后的实时的同意率(便于实时看到),不论保留几位小数,都要注意不要用四舍五入,而应该使用小数位后几位去尾
的舍入方式。
举个例子,假设当前设置了提案投票的同意率阈值是70%(也就是0.7),某个提案当前的同意加权票数是69999,参与加权票数是100000,二者相除得到实时的同意率为0.69999。
R = 所有同意委员的加权票数 所有投票委员的加权票数 R = \frac {所有同意委员的加权票数}{所有投票委员的加权票数} R=所有投票委员的加权票数所有同意委员的加权票数
如果四舍五入,结果都是0.7,而实际上并没有达到真正的70%阈值,存在误导性;而如果去尾,结果都会小于0.7,不会存在误导性。
四、访问子合约的变量
主合约ParentContract使用子合约ChildContract的变量childVariable,需要带括号。
// 子合约
pragma solidity ^0.8.0;contract ChildContract {uint256 public childVariable;constructor(uint256 initialValue) {childVariable = initialValue;}
}// 父合约
pragma solidity ^0.8.0;contract ParentContract {ChildContract public childContract;constructor(uint256 initialValue) {// 创建一个子合约实例childContract = new ChildContract(initialValue);}// 访问子合约的变量,带括号访问function getChildVariable() public view returns (uint256) {return childContract.childVariable();}
}
五、实现结构体继承的功能
solidity并非高级语言,没有结构体之间继承的概念,如果非要用,可以通过solidity中字节码指令的方式基本实现。
比如业务提案有多种类型,每种类型的业务参数也不尽相同,考虑将公共的参数抽象到父结构体的概念中,将所有类型的提案作为子结构体的概念,并通过组合的形式内含一个父结构体。如下:
// 提案父结构体
struct BaseProposal {uint256 proposalId; // 提案IDaddress proposer; // 提案人ProposalType proposalType; // 提案类型【根据不同提案类型,在提案通过后执行相应动作】ProposalStatus proposalStatus; // 提案状态uint blockNum; // 块高
}
// 变更委员权重业务提案
struct GovernorProposal {BaseProposal baseProposal;address account; // 委员账号地址uint weight; // 委员权重
}
// 系统配置业务提案
struct SystemConfigProposal {BaseProposal baseProposal;string key; // Kstring value; // V
}
使用这种方式的初衷是什么?
对治理业务来说,委员发起的每一个提案,都需要保存在storage空间中,如果不抽象一个父结构体,可能需要为每一种类型的提案都创建一个mapping保存提案id和提案对象的映射,如果提案类型太多,也就需要定义更多mapping,看起来不够优雅,也会导致有的查询需求必须组合所有的mapping才能满足,稍微麻烦一些。而如果抽象一个父结构体,只需要一个mapping即可,通过提案id查询也更方便。
// 所有的业务提案
mapping(uint256 => ProposalCommon.BaseProposal) internal _proposalMap;
下面举例发起一个设置系统配置的提案,重点关注保存新提案_proposalMap.storageSystemConfigProposal(proposalId)
,
// 发起一个设置系统配置的提案
function proposeSetSystemConfig(string memory key, string memory value) public onlyGovernor returns(uint256) {uint256 proposalId;ProposalCommon.ProposalStatus proposalStatus;// _proposalMgr子合约创建新提案(proposalId, proposalStatus) = _proposalMgr.createProposal(msg.sender, blockLimit, ProposalCommon.LockType.SystemConfig, address(_systemConfigBuiltin), ProposalCommon.ProposalType.SetSystemConfig);// 保存新提案ProposalCommon.SystemConfigProposal storage proposal = _proposalMap.storageSystemConfigProposal(proposalId);proposal.baseProposal = ProposalCommon.BaseProposal({proposalId: proposalId,proposer: msg.sender,proposalType: proposalType,proposalStatus: proposalStatus,blockNum: block.number});proposal.key = key;proposal.value = value;// 如果提案状态是approved则执行业务操作execPropose(proposalId);return proposalId;
}
storageSystemConfigProposal
具体实现如下:
pragma solidity >= 0.7.0 < 0.9.0;import "./ProposalCommon.sol";// 从mapping中存取某种业务提案的派生结构体
library LProposalCasting {function storageSystemConfigProposal(mapping(uint256 => ProposalCommon.BaseProposal) storage proposalMap, uint256 proposalId)internalviewreturns (ProposalCommon.SystemConfigProposal storage proposal) {assembly {let ptr := mload(0x40)mstore(add(ptr, 0x00), proposalId)mstore(add(ptr, 0x20), proposalMap.slot)proposal.slot := keccak256(ptr, 0x40)}}
}
具体存取每一个子提案到父提案_proposalMap的实现原理参考https://medium.com/@sina.tadayyon/struct-inheritance-in-solidity-143c712f9092,打不开尝试翻墙。
六、address转string
function toString(address _address) internal view returns (string memory) {bytes32 value = bytes32(uint256(uint160(_address)));bytes memory alphabet = "0123456789abcdef";bytes memory str = new bytes(42);str[0] = '0';str[1] = 'x';for (uint256 i = 0; i < 20; i++) {str[2 + i * 2] = alphabet[uint8(value[i + 12] >> 4)];str[3 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)];}return string(str);
}
七、字符串判等比较
keccak256(bytes(_systemConfigBuiltin.get("enable_governance"))) == keccak256(bytes("true"))
八、简单的地址合法性校验
function checkAddress(address contractOrAccountAddress) public view returns (bool) {// 检查合约地址和账号地址,长度=42,且以0x开头string memory addressStr = toString(contractOrAccountAddress);return bytes(addressStr).length == 42 && bytes(addressStr)[0] == 0x30 && bytes(addressStr)[1] == 0x78;
}
九、字符串拼接
拼接字符串str1和str2,
string(abi.encodePacked(str1, str2))
拼接字符串str1和整数number,
function concatStringAndUint(string memory str1, uint256 number) public pure returns (string memory) {string memory str2 = uintToString(number);// 连接两个字符串string memory result = string(abi.encodePacked(str1, str2));return result;
}
十、实现Set集合
solidity里没有Set集合,需要单独实现。
pragma solidity ^0.8.0;library LibAddressSet {struct AddressSet {mapping(address => uint256) indexMapping;address[] values;}function add(AddressSet storage self, address value) internal {require(value != address(0x0), "LibAddressSet: value can't be 0x0");require(!contains(self, value),"LibAddressSet: value already exists in the set.");self.values.push(value);self.indexMapping[value] = self.values.length;}function contains(AddressSet storage self, address value)internalviewreturns (bool){return self.indexMapping[value] != 0;}function remove(AddressSet storage self, address value) internal {require(contains(self, value), "LibAddressSet: value doesn't exist.");uint256 toDeleteindexMapping = self.indexMapping[value] - 1;uint256 lastindexMapping = self.values.length - 1;address lastValue = self.values[lastindexMapping];self.values[toDeleteindexMapping] = lastValue;self.indexMapping[lastValue] = toDeleteindexMapping + 1;delete self.indexMapping[value];self.values.pop();}function getSize(AddressSet storage self) internal view returns (uint256) {return self.values.length;}function get(AddressSet storage self, uint256 index)internalviewreturns (address){return self.values[index];}function getAll(AddressSet storage self)internalviewreturns (address[] memory){address[] memory output = new address[](self.values.length);for (uint256 i; i < self.values.length; i++) {output[i] = self.values[i];}return output;}
}
十一、参数是结构体的event的签名
定义了一个事件LogSet7,参数是结构体NameChangeInfo,如下:
pragma solidity ^0.4.25;
pragma experimental ABIEncoderV2;contract Hello {string name;event LogSet7(NameChangeInfo nameChangeInfo);constructor() public {name = "hello";}struct NameChangeInfo {string oldName;string newName;}function set(string newName) public {emit LogSet7(NameChangeInfo(name, newName));name = newName;}
}
事件LogSet7(DecisionProposal decisionProposal)
的签名,下面的都不正确:
- LogSet7(NameChangeInfo nameChangeInfo)
- LogSet7(NameChangeInfo)
- LogSet7(struct)
- LogSet7(tuple)
正确是应该是LogSet7((string,string))
,将结构体的参数类型列表拼起来。
-... . - - . .-. .- -. -.. -... . - - . .-.