aragon 这个DAO的工具来说,还是很好用的。各方面都封装得很好;
主要由kernel + acl + apps组成;
kernel 简单来说, kernel负责存储各个app的地址(app-id => address),app的部署方式为(proxy delegatecall), app的proxy中的delegatecall(implement, msg.data)中的implement将会到kernel中读取对应app-id的base逻辑的合约地址。
acl acl负责完成权限管理。app的一切修改数据的操作都需要具有对应的权限才可以。如一个transfer方法的签名为
1 2 3 4 function transfer(address _token, address _to, uint256 _value)         external         authP(TRANSFER_ROLE, arr(_token, _to, _value)) 				{} 
authP便是权限查询。
一个权限 Permission 主要由下面几个概念来组成
manager管理者,管理者可将该权限授予某实体地址,以及取消某实体地址的该权限role 权限名称,如TRANSFER_ROLEapp,该权限作用的appentity,拥有该权限的实体 
创建一个permission时,就需要包含这几个信息;如 app = vault, role = TRANSFER_ROLE,则代表该权限是要使用(transfer)vault中的资产权限;创建(create)完成一个permission后,manager可通过grant来对权限进行授予或取消;
app 更详细的文档 👈。这里列出基本的几个
基本的部署示例 这里将部署一个投票app,当投票通过后,使用vault中资产的例子。
部署Kernel和ACL
执行kernel.initialize(acl, rootAddress)来配置kernel中的acl实例和操作地址,该方法中会执行acl.initialize(rootAddress)以及createPermission(rootAddress, aclAddress, CREATE_PERMISSIONS_ROLE, rootAddress)创建权限管理;
初始化时的rootAddress将会具有以及管理createPermission的权限。
部署Voting app,投票通过后,将会调用vault中的transfer方法,所以voting要具有对应的权限。
上面提到rootAddress有管理createPermission的权限,所以首先使用rootAddress将createPermission授权给voting,之后voting再创建transfer权限。
Grant the Voting app the ability to call createPermission(): grantPermission(votingAppAddress, aclAddress, CREATE_PERMISSIONS_ROLE) (must be executed by rootAddress)
 
部署Vault 合约,该合约具有transfer方法
所以现在voting就要创建自己去执行vault的transfer方法的权限
Create a new vote via the Voting app to create the TRANSFER_ROLE permission: createPermission(votingAppAddress, vaultAppAddress, TRANSFER_ROLE, votingAppAddress)
 
当投票通过后,voting就可以调用vault中任何由 TRANSFER_ROLE 权限修饰的方法了,在这个例子中,只有transfer方法
Vault中的资产就被Voting控制了,当用户想使用Vault的资产时,在Voting app上创建一个新的执行Vault的transfer的vote,当投票通过时,transfer将会被执行
voting app可以取消transfer的权限
 
详细的部署示例 模板Reputation部署过程(in templates)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 //create token => in MiniMeTokenFactory.createCloneToken MiniMeToken newToken = new MiniMeToken(...) newToken.changeController(msg.sender); //create dao kernal acl.. Kernel dao = Kernel(new KernelProxy(baseKernel)); dao.initialize(baseACL, _root); //kernel的initialize,上述2步骤 //初始化Vault资金池,如果使用了Agent就使用支持Agent的合约 Vault agentOrVault = _useAgentAsVault ? _installDefaultAgentApp(_dao) : _installVaultApp(_dao); //创建Finance app Finance finance = _installFinanceApp(_dao, agentOrVault, _financePeriod == 0 ? DEFAULT_FINANCE_PERIOD : _financePeriod); //创建Token Manager app TokenManager tokenManager = _installTokenManagerApp(_dao, token, TOKEN_TRANSFERABLE, TOKEN_MAX_PER_ACCOUNT); //创建Voting app Voting voting = _installVotingApp(_dao, token, _votingSettings); //给初始化时的地址mint币,因为整个系统都是基于权限,所以会先创建mint权限给当前,挖完再取消掉权限 _mintTokens(_acl, tokenManager, _holders, _stakes); //创建相关的权限 _setupPermissions(_acl, agentOrVault, voting, finance, tokenManager, _useAgentAsVault); 
上面创建相关的权限的细节在这里👇
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function _setupPermissions(     ACL _acl,     Vault _agentOrVault,     Voting _voting,     Finance _finance,     TokenManager _tokenManager,     bool _useAgentAsVault )     internal {     if (_useAgentAsVault) {     		//创建voting app具有transfer vault 资产的权限         _createAgentPermissions(_acl, Agent(_agentOrVault), _voting, _voting);     }     //设置finance具有transfer vault 资产的权限     _createVaultPermissions(_acl, _agentOrVault, _finance, _voting);     //设置由voting来触发finance中EXECUTE_PAYMENTS_ROLE、MANAGE_PAYMENTS_ROLE权限     _createFinancePermissions(_acl, _finance, _voting, _voting);     //设置voting具有在finance中创建payment的权限,CREATE_PAYMENTS_ROLE权限     _createFinanceCreatePaymentsPermission(_acl, _finance, _voting, address(this));     //设置voting 具有evmscriptsregistery中的REGISTRY_MANAGER_ROLE、REGISTRY_ADD_EXECUTOR_ROLE权限     _createEvmScriptsRegistryPermissions(_acl, _voting, _voting);     //设置voting具有voting的MODIFY_QUORUM_ROLE、MODIFY_SUPPORT_ROLE权限     //以及_tokenManager具有voting的CREATE_VOTES_ROLE权限     _createVotingPermissions(_acl, _voting, _voting, _tokenManager, _voting);     //创建voting app 具有mint和burn token的权限,也就是投票通过后可以mint 和burn token     _createTokenManagerPermissions(_acl, _tokenManager, _voting, _voting); } 
script分析 
example来自一个由vote发起的普通new vote交易 ;这是通过aragon网页创建的一个普通投票,内容填写的是”mint”
使用forward进行new vote时,首先要编码要调用的script为new vote,其次,new vote的签名为function _newVote(bytes _executionScript, string _metadata), 需要传入vote通过后的执行script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //对data数据进行解析 0x d948d468 //forward 0000000000000000000000000000000000000000000000000000000000000020 00000000000000000000000000000000000000000000000000000000000000e0 //script 00000001 //executorId ,rinkeby测试网上的id好像只有1 dcc728ad010792caef6b73ab04633a22c4a9eef4 //execute  contract address,这里是vote的合约地址 000000c4 // call data length, 后面的数据的长度 d5db2c80 //execute method signature, vote中要调用方法的签名,这里对应是newVote方法 0000000000000000000000000000000000000000000000000000000000000040 0000000000000000000000000000000000000000000000000000000000000080 0000000000000000000000000000000000000000000000000000000000000004 //script 00000001 //executorId 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000 46d696e74 //new vote的第二个参数,ascii为mint(字符) 00000000000000000000000000000000000000000000000000000000 
example再看一个通过token manager创建的new vote,当投票通过后,将会给地址转账, 交易 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0x d948d468 //forward 0000000000000000000000000000000000000000000000000000000000000020 00000000000000000000000000000000000000000000000000000000000000c0 00000001 //executorId dcc728ad010792caef6b73ab04633a22c4a9eef4 //vote的合约地址 000000a4 //call data length d948d468 //newVote方法 0000000000000000000000000000000000000000000000000000000000000020 0000000000000000000000000000000000000000000000000000000000000060 //new vote 方法参数1,执行脚本 00000001  //executorId ab3eca07408b8aa4db5915b2a12b193b25716c8c //execute contract address token manager的proxy 00000044 //call data length 40c10f19 //方法签名 mint(address,uint256) 000000000000000000000000 b7e84ea36c789dd576ffe46419245ea28a1e0e88 //地址 0000000000000000000000000000000000000000000000000000000000000001  //数量1 
 
Dot Voting app 选项脚本分析 首先简单恶补一下方法数据参数(abi)编码规则。dot app的代码分析可跳到示例4
任何数据的单元都为32字节 
数据类型分为动态类型和非动态类型,string, array等非固定长度的数据类型为动态类型;动态类型需额外存储数据长度 
abi编码为方法签名(4字节)+数据,以下省略方法签名 
 
示例1 1 function slice(uint32[2]) 
测试数据为 [2]uint32{1, 2}
分析,uint32[2] 虽然是个数组,但是数组长度固定,数组中的数据长度也固定,所以不需要额外的数据来表示长度。
编码结果(hex)为00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
可以看到1和2的编码长度单元都是32bytes(hex长度64)
后续为了方便查看,我以32bytes换行展示。
示例2 1 function slice(uint32[]) 
测试数据为 []uint32{1, 2}
分析,这里做了一点小改动,将数组改成了任意长度。所以这里类型更改为动态类型,需要额外的数据来表示长度。
编码结果(hex)为
1 2 3 4 0000000000000000000000000000000000000000000000000000000000000020 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000000000000000000000000000000000000000000002 
可以看到,数据中多了0x20和0x02, 0x02可以理解是数据的长度为2,那么0x20 代表什么呢。
存在动态类型的编码时,会增加动态类型数据的位置偏移索引值,这个0x20就代表数据开始于当前位置再往后偏移0x20的位置,也就是向后偏移32bytes,是不是就刚好是数据1开始的位置。
0x20是怎么得出来的呢。
编码时,会按照参数顺序依次编码,遇到动态类型,就编码位置偏移索引值,遇到非动态类型,就编码数据,最后,将动态类型的数据再依次编码。 那么位置偏移索引值怎么计算呢?
按照上面说的,先将非动态类型的数据编码后,最后编码动态类型的数据,并且遇到动态类型,会编码一个位置索引值(32bytes),所以第一个动态类型的位置索引值 = 动态类型参数的个数 * 32 + 非动态类型数据的具体长度(以32为单元的长度)。如果存在第二个动态类型参数呢,第二个动态类型的位置索引值 = 第一个动态类型的位置索引值 + 第一个动态类型数据的具体长度。
简单看一下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 func  (arguments Arguments)  Pack (args ...interface {})  ([]byte , error) 	 	abiArgs := arguments 	if  len (args) != len (abiArgs) { 		return  nil , fmt.Errorf("argument count mismatch: %d for %d" , len (args), len (abiArgs)) 	} 	 	 	var  variableInput []byte  	 	inputOffset := 0    	for  _, abiArg := range  abiArgs { 		inputOffset += getTypeSize(abiArg.Type)  	} 	var  ret []byte  	for  i, a := range  args { 		input := abiArgs[i] 		 		packed, err := input.Type.pack(reflect.ValueOf(a)) 		if  err != nil  { 			return  nil , err 		} 		 		if  isDynamicType(input.Type) { 			 			ret = append (ret, packNum(reflect.ValueOf(inputOffset))...) 			 			inputOffset += len (packed) 			 			variableInput = append (variableInput, packed...) 		} else  { 			 			ret = append (ret, packed...) 		} 	} 	 	ret = append (ret, variableInput...) 	return  ret, nil  } 
这种编码方式倒是很神奇,通过读取位置索引值,就能索引到数据真正的地方。
示例3 1 function slice(uint32[] a, uint32 b) 
测试数据为[]uint32{1, 2}, 2
编码结果(hex)为
1 2 3 4 5 0000000000000000000000000000000000000000000000000000000000000040 //类型占位和偏移 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000000000000000000000000000000000000000000002 
按照编码规则,
首先遇到动态数组,所以需要所有类型占位和,有两个参数,第一个是动态数组,所以占位是32, 第二个是uint32,占位也是32,所以占位和为64。也就是0x40 
其次,编码动态数组的数据长度,长度为2 
然后,编码第二个参数,2 
最后,将动态数据编码,1和2 
 
按照占位和偏移的含义,0x40也就是数组数据开始的地方,向后偏移64 bytes,也就刚好是数组数据1开始的位置。
下面举个多动态数组的例子,也就是DOT Voting的例子吧
示例4 Dot  voting的数据结构中,提供多选项,每个选项的信息可包含一个地址,一个描述性的info,两个id。(id两级我有点不太理解是做什么用的…
1 2 3 4 5 6 7 8 9 function setSignal(         address[] _addr, //选项地址数组         uint256[] _signal,  //选票结束后,执行脚本时赋值为选项对应的票数(未达到选票条件的选项票数为0)         uint256[] _infoIndices, //每个info信息长度         string _candidateInfo,  //多个info拼接在一起,按长度可截取出来         string description,         uint256[] _level1Id,         uint256[] _level2Id     ) 
按照aragon的结构,和Voting一样,交易数据为调用某合约的forward方法,通过执行器调用对应合约的对应方法。
我在aragon上发起了一个投票 ,描述为Test Voting, 三个选项,分别为A B C。然后这笔交易 的data如下,以这个数据为例介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 //这里先按照上面介绍的编码形式简单看看数据 MethodID: 0xd948d468 //forward(bytes s) [0]:  0000000000000000000000000000000000000000000000000000000000000020 //s索引位置为32bytes [1]:  00000000000000000000000000000000000000000000000000000000000004c0 //bytes的长度 [2]:  0000000101b79e559f9c2c9b6b50bd24330add6ddac6e78f000004a4d5db2c80 // 这里是特定编码 exec 					//00000001 + 调用合约01b79e559f9c2c9b6b50bd24330add6ddac6e78f + 调用方法 d5db2c80,后面就是调用方法的参数,这里是调用dot vote的newVote(bytes _executionScript, string _metadata) [3]:  0000000000000000000000000000000000000000000000000000000000000040 //两个参数都是动态类型,故总索引值为32 + 32 [4]:  0000000000000000000000000000000000000000000000000000000000000460 //第二个动态类型位置 [5]:  0000000000000000000000000000000000000000000000000000000000000400 //参数1的长度 [6]:  0000000101b79e559f9c2c9b6b50bd24330add6ddac6e78f000003e400000000 // 参数1又是一个exec script,同上,调用方法是 00000000 ? /**后面就是具体的要解析出多选项的数据了,参照编码方法为function setSignal(         address[] _addr,         uint256[] _signal,         uint256[] _infoIndices,         string _candidateInfo,         string description,         uint256[] _level1Id,         uint256[] _level2Id     )**/ [7]:  00000000000000000000000000000000000000000000000000000000000000e0 //参数1位置,7个动态7*32 [8]:  0000000000000000000000000000000000000000000000000000000000000160 //参数2位置 [9]:  00000000000000000000000000000000000000000000000000000000000001e0 //参数3位置 [10]: 0000000000000000000000000000000000000000000000000000000000000260 //参数4位置 [11]: 00000000000000000000000000000000000000000000000000000000000002a0 // ... [12]: 00000000000000000000000000000000000000000000000000000000000002e0 // ... [13]: 0000000000000000000000000000000000000000000000000000000000000360 // 参数7位置(位置结束 [14]: 0000000000000000000000000000000000000000000000000000000000000003 //参数1长度(address) [15]: 0000000000000000000000000000000000000000000000000000000000000000 //address{0} [16]: 0000000000000000000000000000000000000000000000000000000000000001 //address{1} [17]: 0000000000000000000000000000000000000000000000000000000000000002 //address{2} [18]: 0000000000000000000000000000000000000000000000000000000000000003 //参数2长度 [19]: 0000000000000000000000000000000000000000000000000000000000000000 // 参数2数据[0, 0, 0] [20]: 0000000000000000000000000000000000000000000000000000000000000000 [21]: 0000000000000000000000000000000000000000000000000000000000000000 [22]: 0000000000000000000000000000000000000000000000000000000000000003 //参数3长度 [23]: 0000000000000000000000000000000000000000000000000000000000000001 //参数3数据[1, 1, 1] [24]: 0000000000000000000000000000000000000000000000000000000000000001 [25]: 0000000000000000000000000000000000000000000000000000000000000001 [26]: 0000000000000000000000000000000000000000000000000000000000000003 //参数4长度为3bytes [27]: 4142430000000000000000000000000000000000000000000000000000000000 //参数4数据[41,42,43],即A,B,C [28]: 000000000000000000000000000000000000000000000000000000000000000b  //参数5长度为b [29]: 5465737420566f74696e67000000000000000000000000000000000000000000 //参数5数据Test Voting [30]: 0000000000000000000000000000000000000000000000000000000000000003 //参数6为[0,0,0] [31]: 0000000000000000000000000000000000000000000000000000000000000000 [32]: 0000000000000000000000000000000000000000000000000000000000000000 [33]: 0000000000000000000000000000000000000000000000000000000000000000 [34]: 0000000000000000000000000000000000000000000000000000000000000003 //参数7位[0,0,0] [35]: 0000000000000000000000000000000000000000000000000000000000000000 [36]: 0000000000000000000000000000000000000000000000000000000000000000 [37]: 0000000000000000000000000000000000000000000000000000000000000000 //new vote的第二个参数_metadata编码, 数据Test Voting [38]: 000000000000000000000000000000000000000000000000000000000000000b [39]: 5465737420566f74696e67000000000000000000000000000000000000000000 
然后看看合约代码怎么解析出选项的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 //_executionScript为上述6到39 function _extractOptions(bytes _executionScript, uint256 _actionId) internal {     Action storage actionInstance = actions[_actionId];     //calldataLength 0x03e4, 0x4 为0x00000001, 0x14为合约地址     uint256 calldataLength = uint256(_executionScript.uint32At(0x4 + 0x14));     //startOffset为7开始的位置     uint256 startOffset = 0x04 + 0x14 + 0x04; //0x04 calldata length     //OPTION_ADDR_PARAM_LOC = 1,firstParamOffset 为0xe0 + 0x20, 即14开始的位置,对应address数据     uint256 firstParamOffset = _goToParamOffset(OPTION_ADDR_PARAM_LOC, _executionScript);     //DESCRIPTION_PARAM_LOC = 5,fifthParamOffset为28开始的位置,对应descript数据     uint256 fifthParamOffset = _goToParamOffset(DESCRIPTION_PARAM_LOC, _executionScript);     uint256 currentOffset = firstParamOffset;     require(startOffset + calldataLength == _executionScript.length); // solium-disable-line error-reason    //optionLength为address数组的长度     uint256 optionLength = _executionScript.uint256At(currentOffset);    //currentOffset向后偏移32, 指向address数组开始的位置     currentOffset = currentOffset + 0x20;     //开始解析多选项     _iterateExtraction(_actionId, _executionScript, currentOffset, optionLength);    //解析description     uint256 descriptionStart = fifthParamOffset + 0x20;     uint256 descriptionEnd = descriptionStart + (_executionScript.uint256At(fifthParamOffset));     actionInstance.description = substring(_executionScript, descriptionStart, descriptionEnd); } //将多选项解析出来,  _currentOffset指向address数组开始的位置,_optionLength为address数组的长度 function _iterateExtraction(uint256 _actionId, bytes _executionScript, uint256 _currentOffset, uint256 _optionLength) internal {     uint256 currentOffset = _currentOffset;     address currentOption;     string memory info;     uint256 infoEnd;     bytes32 externalId1;     bytes32 externalId2;     uint256 idOffset;     //OPTION_INFO_PARAM_LOC=4, infoStart为参数4数据开始的位置再后偏移32位(长度),也就是27的位置     uint256 infoStart = _goToParamOffset(OPTION_INFO_PARAM_LOC,_executionScript) + 0x20;     emit OptionQty(_optionLength);     //以address数组的长度作为遍历次数,长度即为多选的长度     for (uint256 i = 0 ; i < _optionLength; i++) {         //以32作为单元存储长度20的地址,前12都是0补齐,所以skip 12,将地址读出来         currentOption = _executionScript.addressAt(currentOffset + 0x0C);         emit Address(currentOption);         //infoEnd = 参数4数据真实的位置 + 参数3的第i个值(参数3为info长度         infoEnd = infoStart + _executionScript.uint256At(currentOffset + (0x20 * 2 * (_optionLength + 1) ));         //单个选择项的info=_candidateInfo,按_infoIndices数组对应的值作为长度进行截取         info = substring(_executionScript, infoStart, infoEnd);         currentOffset = currentOffset + 0x20;         infoStart = infoEnd;         //后面就是按索引值取对应的externalId了         idOffset = _goToParamOffset(EX_ID1_PARAM_LOC, _executionScript) + 0x20 * (i + 1);         externalId1 = bytes32(_executionScript.uint256At(idOffset));         idOffset = _goToParamOffset(EX_ID2_PARAM_LOC, _executionScript) + 0x20 * (i + 1);         externalId2 = bytes32(_executionScript.uint256At(idOffset));         addOption(_actionId, info, currentOption, externalId1, externalId2);         }     } 
选票结束并执行后,会将不符合票数条件的票数置为0,并执行script脚本,