无多签硬件钱包时,如何实现以太坊通证的多签合约?
【本文目标】 1) 了解目前辉哥调研的多签硬件钱包的现状; 2) 提供一个智能合约多签钱包的实现方案和测试结果;
【前置条件】 1)存在一个已发布的ERC20通证合约,例如本文举例的CLB通证。
有一天,老板给辉哥提了一个需求,希望能够实现一个安全的代币支出多签功能,便于基金会治理审核。 汇总而言,产品需求描述如下: (1)治理委员会由4人组成,一项CLB代币支出需要所有4人同意才可以转账打出; (2)为了避嫌中心化作恶,治理委员会的退出只能由委员本人账号操作有效; (3)当治理委员会无法正常运作时,老板有权撤回治理委员会管理的剩余代币。
辉哥首先完成市场上商用硬件钱包的调研工作。 结论: 目前(2018.08.24)世面上暂时没有体验好的多签硬钱包,不过等1个月后比特派硬件钱包更新会支持ETH和ERC20代币的多重签名功能。 具体信息:
多签合约核心代码如下:
contract ColorbayMultiSign {
using SafeMath for uint256;
uint256 public MAX_OWNER_COUNT = 50;
event Confirmation(address indexed sender, uint256 indexed transactionId);
event Revocation(address indexed sender, uint256 indexed transactionId);
event Submission(uint256 indexed transactionId);
event Execution(uint256 indexed transactionId);
event ExecutionSuccess(uint256 indexed transactionId);
event ExecutionFailure(uint256 indexed transactionId);
event OwnerAddition(address indexed owner);
event OwnerRemoval(address indexed owner);
event RequirementChange(uint256 required);
mapping (uint256 => Transaction) public transactions;
mapping (uint256 => mapping(address => bool)) public confirmations;
mapping (address => bool) public isOwner;
address[] public owners;
uint256 public required;
uint256 public transactionCount;
address public creator;
ERC20 public token;
struct Transaction {
address destination;
uint256 value;
bool executed;
}
/*omit some modifier function*/
/**
* @dev Contract constructor sets initial owners and required number of confirmations.
* @param _owners List of initial owners.
* @param _required Number of required confirmations.
*/
constructor(address _token, address[] _owners, uint256 _required) public validRequirement(_owners.length, _required)
{
token = ERC20(_token);
require(_owners.length <= 100);
for (uint256 i=0; i<_owners.length; i++) {
require(!isOwner[_owners[i]] && _owners[i] != address(0));
isOwner[_owners[i]] = true;
}
owners = _owners;
required = _required;
creator = msg.sender;
}
/**
* @dev Allows to remove an owner. Transaction has to be sent by wallet.
* @param owner Address of owner.
*/
function removeOwner(address owner) public ownerExists(owner)
{
/*only owner can delete itself*/
require(owner == msg.sender);
isOwner[owner] = false;
for (uint256 i=0; i<owners.length.sub(1); i++) {
if (owners[i] == owner) {
owners[i] = owners[owners.length.sub(1)];
break;
}
}
owners.length = owners.length.sub(1);
if (required > owners.length) {
changeRequirement(owners.length);
}
emit OwnerRemoval(owner);
}
/**
* @dev Withdraw the token remained to the constructor address.
*/
function withdrawToken() public onlyCreator{
if( 0 < token.balanceOf(address(this))) {
token.transfer(creator, token.balanceOf(address(this)));
}
}
/**
* @dev Allows to change the number of required confirmations. Transaction has to be sent by wallet.
* @param _required Number of required confirmations.
*/
function changeRequirement(uint256 _required) private validRequirement(owners.length, _required)
{
required = _required;
emit RequirementChange(_required);
}
/**
* @dev Allows an owner to submit and confirm a transaction.
* @param destination Transaction target address.
* @param value Transaction ether value.
* @return Returns transaction ID.
*/
function submitTransaction(address destination, uint256 value) public returns (uint256 transactionId)
{
transactionId = addTransaction(destination, value);
confirmTransaction(transactionId);
}
/**
* @dev Allows an owner to confirm a transaction.
* @param transactionId Transaction ID.
*/
function confirmTransaction(uint256 transactionId) public ownerExists(msg.sender) transactionExists(transactionId) notConfirmed(transactionId, msg.sender)
{
confirmations[transactionId][msg.sender] = true;
emit Confirmation(msg.sender, transactionId);
executeTransaction(transactionId);
}
/**
* @dev Allows an owner to revoke a confirmation for a transaction.
* @param transactionId Transaction ID.
*/
function revokeConfirmation(uint256 transactionId) public ownerExists(msg.sender) confirmed(transactionId, msg.sender) notExecuted(transactionId)
{
confirmations[transactionId][msg.sender] = false;
emit Revocation(msg.sender, transactionId);
}
/**
* @dev Allows anyone to execute a confirmed transaction.
* @param transactionId Transaction ID.
*/
function executeTransaction(uint256 transactionId) public notExecuted(transactionId)
{
if (isConfirmed(transactionId)) {
Transaction storage ta = transactions[transactionId];
ta.executed = true;
if(token.transfer(ta.destination, ta.value)) {
emit ExecutionSuccess(transactionId);
} else {
emit ExecutionFailure(transactionId);
ta.executed = false;
}
}
}
/**
* @dev Returns the confirmation status of a transaction.
* @param transactionId Transaction ID.
* @return Confirmation status.
*/
function isConfirmed(uint256 transactionId) public view returns (bool)
{
uint256 count = 0;
for (uint256 i=0; i<owners.length; i++) {
if (confirmations[transactionId][owners[i]]) {
count = count.add(1);
}
if (count == required) {
return true;
}
}
}
/**
* @dev Adds a new transaction to the transaction mapping, if transaction does not exist yet.
* @param destination Transaction target address.
* @param value Transaction ether value.
* @return Returns transaction ID.
*/
function addTransaction(address destination, uint256 value) internal notNull(destination) returns (uint256 transactionId)
{
transactionId = transactionCount;
transactions[transactionId] = Transaction({
destination: destination,
value: value,
executed: false
});
transactionCount = transactionCount.add(1);
emit Submission(transactionId);
}
/**
* Web3 call functions
* @dev Returns number of confirmations of a transaction.
* @param transactionId Transaction ID.
* @return Number of confirmations.
*/
function getConfirmationCount(uint256 transactionId) public view returns (uint256 count)
{
for (uint256 i=0; i<owners.length; i++) {
if (confirmations[transactionId][owners[i]]) {
count = count.add(1);
}
}
}
/**
* @dev Returns total number of transactions after filers are applied.
* @param pending Include pending transactions.
* @param executed Include executed transactions.
* @return Total number of transactions after filters are applied.
*/
function getTransactionCount(bool pending, bool executed) public view returns (uint256 count)
{
for (uint256 i=0; i<transactionCount; i++) {
if (pending && !transactions[i].executed || executed && transactions[i].executed) {
count = count.add(1);
}
}
}
/**
* @dev Returns list of owners.
* @return List of owner addresses.
*/
function getOwners() public view returns (address[])
{
return owners;
}
/**
* @dev Returns array with owner addresses, which confirmed transaction.
* @param transactionId Transaction ID.
* @return Returns array of owner addresses.
*/
function getConfirmations(uint256 transactionId) public view returns (address[] _confirmations)
{
address[] memory confirmationsTemp = new address[](owners.length);
uint256 count = 0;
for (uint256 i=0; i<owners.length; i++) {
if (confirmations[transactionId][owners[i]]) {
confirmationsTemp[count] = owners[i];
count = count.add(1);
}
}
_confirmations = new address[](count);
for (i=0; i<count; i++) {
_confirmations[i] = confirmationsTemp[i];
}
}
/**
* @dev Returns list of transaction IDs in defined range.
* @param from Index start position of transaction array.
* @param to Index end position of transaction array.
* @param pending Include pending transactions.
* @param executed Include executed transactions.
* @return Returns array of transaction IDs.
*/
function getTransactionIds(uint256 from, uint256 to, bool pending, bool executed) public view returns (uint256[] _transactionIds)
{
uint256[] memory transactionIdsTemp = new uint256[](transactionCount);
uint256 count = 0;
for (uint256 i=0; i<transactionCount; i++) {
if (pending && !transactions[i].executed || executed && transactions[i].executed)
{
transactionIdsTemp[count] = i;
count = count.add(1);
}
}
_transactionIds = new uint256[](to.sub(from));
for (i=from; i<to; i++) {
_transactionIds[i.sub(from)] = transactionIdsTemp[i];
}
}
}
各个函数的定义说明参考类图:
多签合约类图.png
核心函数说明:
限于文章篇幅,辉哥并没有把所有的多签合约代码提取出来。如需获取完整代码文件请加入知识星球获取。
编译通过后,按照实际业务场景,辉哥做了一下完整测试。测试流程如下:
业务流程.png
具体的操作流程如下,均达到预期目标,本多签合约具备商用能力。
代币
彩贝发行总量为 1,000,000,000 个token
激励 占比35%,即350,000,000个token
私募 占比20%,即200,000,000个token
团队及基金会 占比25%,即250,000,000个token
社区培养及推广 占比20%,即200,000,000个token
账号
治理审批委员会成员:
0xca3...a733c
0x147...c160c
0x4b0...4d2db
0x583...40225
申请人:
阿汤哥 0xdd8...92148
管理员0xca35b7d915458ef540ade6068dfe2f44e8fa733c
辉哥0x14723a09acff6d2a60dcdf7aa4aff308fddc160c
欧阳哥哥0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db
ELLA0x583031d1113ad414f02576bd6afabfb302140225
阿汤哥0xdd870fa1b7c4700f2bd7f44238821c26f7392148
1) 创建代币合约Colorbay
切换到管理员账号0xca3...a733c
下,创建代币合约Colorbay,创建完成后,复制合约地址备用。试用者也可以创建自己的ERCC20合约,记住地址即可。
0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
2、创建多签合约
切换到管理员账号0xca3...a733c
下,创建多签合约。输入CLB合约地址,将审批委员会名单导入(管理员、辉哥、欧阳哥哥、ELLA
),配置需要4
个确认个数(也称必签数,即需要审批4次才能通过)
constructor("0x692a70d2e424a56d2c6c27aa97d1a86395877b3a", ["0xca35b7d915458ef540ade6068dfe2f44e8fa733c","0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db","0x583031d1113ad414f02576bd6afabfb302140225"], "4")
如果必签数
0xbbf289d846208c16edc8474705c748aff07732db
3、给多签合约地址充值CLB
回到CLB合约中,切换到管理员账号0xca3...a733c
下,给多签合约地址充值2000000
CLB
transfer("0xbbf289d846208c16edc8474705c748aff07732db", "2000000,000000000000000000") #约定:运行时,去除数额中的逗号,否则会出错
4、查询多签合约地址代币余额
balanceOf("0xbbf289d846208c16edc8474705c748aff07732db")
5、查询剩余发行总量
balanceOf("0xca35b7d915458ef540ade6068dfe2f44e8fa733c")
仅审批委员会成员可操作
1、作为申请人阿汤哥0xdd8...92148
,向管理员提出申请,需要10000
个CLB做为活动运营激励。切换到管理员账号0xca3...a733c
下(可以是任意审批委员会账号)
submitTransaction("0xdd870fa1b7c4700f2bd7f44238821c26f7392148", "10000,000000000000000000")
编号为0的事务申请提交完成后,即管理员已经审批通过了这个事务,待其他3个审批通过。
2、作为申请人阿汤哥0xdd8...92148
,直接自己发起待审批事务。切换到阿汤哥账号0xca3...a733c
下(任意非审批委员会账号),将会报错,因为普通用户不能发起待审批事务
submitTransaction("0xdd870fa1b7c4700f2bd7f44238821c26f7392148", "10000,0000000000000000000000")
3、查询当前编号为0的事务有几人审批确认了
getConfirmationCount(0)
4、查询当前编号为0的事务有哪些委员做审批确认了
getConfirmations(0)
1、编号为0的事务还需要3个审批确认,现在辉哥、欧阳哥哥开始审批编号为0的事务,分别切换到辉哥账号、欧阳哥哥账号0xca3...a733c
下,
confirmTransaction(0)
辉哥再次审批将会报错,因为已经审批过了
重复用例3第3、4步
2、辉哥后悔了,想要撤销审批,切换到辉哥账号0xca3...a733c
下:
revokeConfirmation(0)
重复用例3第3、4步
3、审批委员会说服了辉哥,切换到辉哥账号0xca3...a733c
下,辉哥再次做审批通过操作:
重复用例5第1步
1、切换到ELLA账号0x583...40225
下,完成最后1次审批确认,同时将执行转账操作(已经满足4次确认)
2、查询阿汤哥到账情况
回到CLB合约,查询阿汤哥账号0xdd8...92148
余额
balanceOf("0xdd870fa1b7c4700f2bd7f44238821c26f7392148")
3、查询发行总量
重复用例1第5条
4、查询编号为0的事务是否已经处于完成状态
isConfirmed(0)
说明:不管是在审批流程中操作,还是在审批结束后操作,移除成员时,遵循以下规则:
removeOwner("0x14723a09acff6d2a60dcdf7aa4aff308fddc160c")
后来,该治理委员会开始无法有效运作,部分人员审批并不及时,严重影响了业务进展。老板决策把剩余代币打回到管理账号,结束该审计委员会工作和审批权限。
withdrawToken()
本文编码为欧阳哥哥实现,辉哥参与需求设计和验收测试。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!