Gnosis Safe 功能流程
- EIP712
- ABI
- 创建保险箱,Safe合约
- 用户自定义构造函数与转移资产
- 用户批量构造交易,整合到一笔交易,multisend
- 自己做了一个测试网站,方便理解abi,encode编码
EIP712
EIP712是一种签名标准,主要是针对明文。EIP712详细解释
- EIP712签名的结构由三部分组成,分别是domainData,types,sign_message。
签名的原理就是拿用户的私钥,对一串32个字节的哈希值进行ECDSA算法计算,得出来一个65个字节的值,由r,s,v组成。
智能合约验签的原理就是拿到这个签名进行算法解析,接出来先是公钥,然后推出用户地址。这就是验签。
//基础准备工作,ethers.js或者web3.js都可以,这里拿ethers.js举例,直接前端引入,可以存储到本地 加载速度更快更安全,如果用外链cdn加速有时候可能会加载失败。<script src="https://cdn.ethers.io/lib/ethers-5.6.9.umd.min.js"type="application/javascript"></script> const provider = new ethers.providers.Web3Provider(window.ethereum)//只要浏览器下载了小狐狸插件,window全局对象中都会有ethereum这个对象,如果是初学者,稍微百度一下window全局对象 const accounts = await provider.send("eth_requestAccounts", [])//如果没有链接钱包会自动弹出来小狐狸连接框,如果链接钱包了就没什么显示 abiCoder = ethers.utils.defaultAbiCoder//ethers.js自带的编码器,就是用来编码的,编码就是把要交易的信息打包成一个类似string的东西,在合约中为bytes,0x十六进制开头 var signer_712 = provider.getSigner()//这个signer_712变量其实是拿到了当前链接小狐狸的账号,只包含一个用户地址,不包含多个。 const domainData = {chainId: 4,//这个是int类型的,指定链id,很重要,比如ETH主网就必须是1否则弹不出来小狐狸,是对应上的。verifyingContract: "0x7Fbf984cd60d763Eb94C4C466013f1Ee90dd78Cf",//这个是Safe保险箱合约地址};const types = { SafeTx://这里要和合约匹配上,gnosis safe其中rinkeby一个合约地址为0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552,可以去观察SafeTx这里的细节 [{ name: 'to',type: 'address'},{ type: 'uint256', name: 'value' },{ type: 'bytes', name: 'data' },{ type: 'uint8', name: 'operation' },{ type: 'uint256', name: 'safeTxGas' },{ type: 'uint256', name: 'baseGas' },{ type: 'uint256', name: 'gasPrice' },{ type: 'address', name: 'gasToken' },{ type: 'address', name: 'refundReceiver' },{ type: 'uint256', name: 'nonce' }]}//这个只是定义所有消息数据的类型。var sign_message = {to: sign_to_address,//类似0x40A2aCCbd92BCA938b02010E17A5b8929b49130Dvalue:0,data: total_multi_data,//类似0x8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000013200d1d8a3af43788876089b57a214544f9fff9ed08500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000f94e4af78b3f222fa0f8be0f47b6ec6960866e0b0000000000000000000000000000000000000000000000004279842b41585000operation: 1,//或者0,0表示call,1表示delegatecall,这个需要一定基础的消化的safeTxGas:0,baseGas: 0,gasPrice:0,gasToken:'0x0000000000000000000000000000000000000000',refundReceiver: "0x0000000000000000000000000000000000000000",nonce:sign_nonce ,//类似17,18 在gnosis 保险箱合约中 表示交易提交的次数};var signature = await signer_712._signTypedData(domainData, types, sign_message)//这里的signature就是此用户签名信息。用户签名之后,需要把这个签名信息向后端发起接口请求,让后端中心化服务器存储的。
ABI
- ABI是前端与区块链智能合约链接的接口,特别重要。 ABI介绍及解释
ABI全名:Application Binary Interface,应用二进制接口文件。智能合约的接口描述,描述了字段名称、字段类型、方法名称、参数名称、参数类型、方法返回值类型等。
当合约被编译后,对应的abi文件也就确定了。
在前端代码中,大部分用abi无非就两种,一个是前端构造合约实例中,参数带abi文件,这个abi是已经发布的合约中自带的。一种是abi,encode的这种工具。例如,
var contracts_gnosis = {gnosis_factory:{address:'0xa6b71e26c5e0845f74c812102ca7114b6a896ab2',abi:[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"},{"indexed":false,"internalType":"address","name":"singleton","type":"address"}],"name":"ProxyCreation","type":"event"},{"inputs":[{"internalType":"address","name":"_singleton","type":"address"},{"internalType":"bytes","name":"initializer","type":"bytes"},{"internalType":"uint256","name":"saltNonce","type":"uint256"}],"name":"calculateCreateProxyWithNonceAddress","outputs":[{"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"singleton","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"createProxy","outputs":[{"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_singleton","type":"address"},{"internalType":"bytes","name":"initializer","type":"bytes"},{"internalType":"uint256","name":"saltNonce","type":"uint256"},{"internalType":"contract IProxyCreationCallback","name":"callback","type":"address"}],"name":"createProxyWithCallback","outputs":[{"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_singleton","type":"address"},{"internalType":"bytes","name":"initializer","type":"bytes"},{"internalType":"uint256","name":"saltNonce","type":"uint256"}],"name":"createProxyWithNonce","outputs":[{"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"proxyCreationCode","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"proxyRuntimeCode","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"pure","type":"function"}]}}var contract_gnosis_factory = new ethers.Contract(contracts_gnosis.gnosis_factory.address,contracts_gnosis.gnosis_factory.abi,signer_create);//contracts_gnosis.gnosis_factory.abi就包含了很多个函数,是列表里面带json格式,大约长这样,//[{"inputs":[{"internalType":"address","name":"_singleton","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"stateMutability":"payable","type":"fallback"}]//他其实也可以自由定义,可以增加,也可以删减,就是一个接口。await contract_gnosis_factory.createProxyWithNonce(gnosis_safe_address,gnosisSafeData_final,salt); }
创建保险箱,Safe合约
- 这是Gnosis safe最开始的功能,创建保险箱合约。用户点击创建按钮,提交DAO组织用户们的地址和阈值,就可以创建保险箱合约了。保险箱合约的用处就是主要是管理共同资产,然后代替DAO成员意向执行共同决策的。
async function CreateSafe(){//创建保险箱合约var ethereum = window.ethereum;//只要浏览器下载了小狐狸插件,window全局对象中都会有ethereum这个对象。var provider = new ethers.providers.Web3Provider(window.ethereum)//把ethers的provider设置成小狐狸钱包的环境。await provider.send("eth_requestAccounts", []); //如果没有链接钱包会自动弹出来链接框,如果链接钱包了则什么事也不干。var signer_create =provider.getSigner()//这个signer_create变量其实是拿到了当前链接小狐狸的账号,只包含一个用户地址,不包含多个。abiCoder = ethers.utils.defaultAbiCoder//ethers.js自带的编码器,就是用来编码的,编码就是把要交易的信息打包成一个类似string的东西,在合约中为bytes,0x十六进制开头var contract_gnosis_factory = new ethers.Contract(contracts_gnosis.gnosis_factory.address,contracts_gnosis.gnosis_factory.abi,signer_create);//这里的功能是链接合约,生成合约对象了,最后的signer_create参数是当前部署者地址的对象,也就是说返回的contract_gonsis_factory这个对象已经具备了很强大的和合约交互的功能了。var safeAccounts = [ '0x4d2E1A38d07Eadf5C62CfDaF93547DAe09F1EF83', '0x592916d0D7fcaec0A0A0504134364721Aafd5e87', '0xF94e4af78b3f222fa0F8Be0F47b6Ec6960866E0b', ]//这里我测试是写死的,其实这个不应该是写死的,是最开始的DAO创始人去设置的。在前端中到时候可以写一个列表,前端接受用户输入的地址,直接保存就可以,等创建保险箱成功后,再去把信息反馈给后端。var numConfirmations = 1;这个是阈值,就是DAO中有几个人同意就可以执行交易,如果这里是1,那么团队中只要有一个人签名就可以执行了。var ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';这个是0地址,要满足gnosis合约规定方式var EMPTY_DATA = '0x';//这个是0字节,要满足gnosis合约规定方式var fallbackHandler_address = '0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4';//gnosis好像意思是这个是fallback处理的地址,我感觉没什么用,在合约交互的时候没走过这个合约。 var gnosisSafeData = abiCoder.encode(['address[]','uint256','address','bytes','address','address','uint256','address'],[safeAccounts,numConfirmations, ZERO_ADDRESS, EMPTY_DATA, fallbackHandler_address, ZERO_ADDRESS, 0, ZERO_ADDRESS]);//这个就是编码(其实编码就是压缩,没有很多其他的操作),把这些东西压缩编码一下,var gnosisSafeData_final = '0xb63e800d'+gnosisSafeData.substring(2);//0xb63e800d=web3.eth.abi.encodeFunctionSignature('myMethod(uint256,string)'),这个就是在合约字节码中找函数的对应的位置var gnosis_safe_address = "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552";//这个是rinkeby测试网指定的一个主合约地址,写死。var salt = 1657266633780;//这个其实是时间戳,是要变化的,也是传入调用合约函数的最后一个参数,具体值是当前的时间,date.now()await contract_gnosis_factory.createProxyWithNonce(gnosis_safe_address,gnosisSafeData_final,salt);//这个是ethers调用合约的标准方法,createProxyWithNonce这个是合约中定义的函数名,其实他这里是从ABI上找的,记得刚开始contract_gnosis_factory的变量中带了abi的,如果那个abi没有这个方法,小狐狸钱包都起不来。}
用户自定义构造函数与转移资产
1>用户自定义构造函数
这里的构造函数更应该说是指定某些函数功能交互,就是任意合约的任意函数(可执行),哪怕不是转账任意的都行。
Safe保险箱内的用户可以构造任意的链上交易,意思就是比如这条链上随便有个NFT合约,有个投票合约,有个质押合约,我让这个Safe保险箱执行指定合约一个可执行函数都是可以的。
var contracts_gnosis_safe = {gnosis:{address:'0x7Fbf984cd60d763Eb94C4C466013f1Ee90dd78Cf',abi:[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"AddedOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"approvedHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"owner","type":"address"}],"name":"ApproveHash","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"handler","type":"address"}],"name":"ChangedFallbackHandler","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"guard","type":"address"}],"name":"ChangedGuard","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"threshold","type":"uint256"}],"name":"ChangedThreshold","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"module","type":"address"}],"name":"DisabledModule","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"module","type":"address"}],"name":"EnabledModule","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"txHash","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"payment","type":"uint256"}],"name":"ExecutionFailure","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"module","type":"address"}],"name":"ExecutionFromModuleFailure","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"module","type":"address"}],"name":"ExecutionFromModuleSuccess","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"txHash","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"payment","type":"uint256"}],"name":"ExecutionSuccess","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"RemovedOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"SafeReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"initiator","type":"address"},{"indexed":false,"internalType":"address[]","name":"owners","type":"address[]"},{"indexed":false,"internalType":"uint256","name":"threshold","type":"uint256"},{"indexed":false,"internalType":"address","name":"initializer","type":"address"},{"indexed":false,"internalType":"address","name":"fallbackHandler","type":"address"}],"name":"SafeSetup","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"msgHash","type":"bytes32"}],"name":"SignMsg","type":"event"},{"stateMutability":"nonpayable","type":"fallback"},{"inputs":[],"name":"VERSION","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"_threshold","type":"uint256"}],"name":"addOwnerWithThreshold","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hashToApprove","type":"bytes32"}],"name":"approveHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"approvedHashes","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_threshold","type":"uint256"}],"name":"changeThreshold","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"bytes","name":"signatures","type":"bytes"},{"internalType":"uint256","name":"requiredSignatures","type":"uint256"}],"name":"checkNSignatures","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"bytes","name":"signatures","type":"bytes"}],"name":"checkSignatures","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"prevModule","type":"address"},{"internalType":"address","name":"module","type":"address"}],"name":"disableModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"domainSeparator","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"module","type":"address"}],"name":"enableModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"},{"internalType":"uint256","name":"safeTxGas","type":"uint256"},{"internalType":"uint256","name":"baseGas","type":"uint256"},{"internalType":"uint256","name":"gasPrice","type":"uint256"},{"internalType":"address","name":"gasToken","type":"address"},{"internalType":"address","name":"refundReceiver","type":"address"},{"internalType":"uint256","name":"_nonce","type":"uint256"}],"name":"encodeTransactionData","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"},{"internalType":"uint256","name":"safeTxGas","type":"uint256"},{"internalType":"uint256","name":"baseGas","type":"uint256"},{"internalType":"uint256","name":"gasPrice","type":"uint256"},{"internalType":"address","name":"gasToken","type":"address"},{"internalType":"address payable","name":"refundReceiver","type":"address"},{"internalType":"bytes","name":"signatures","type":"bytes"}],"name":"execTransaction","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"}],"name":"execTransactionFromModule","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"}],"name":"execTransactionFromModuleReturnData","outputs":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getChainId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"start","type":"address"},{"internalType":"uint256","name":"pageSize","type":"uint256"}],"name":"getModulesPaginated","outputs":[{"internalType":"address[]","name":"array","type":"address[]"},{"internalType":"address","name":"next","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOwners","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"offset","type":"uint256"},{"internalType":"uint256","name":"length","type":"uint256"}],"name":"getStorageAt","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getThreshold","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"},{"internalType":"uint256","name":"safeTxGas","type":"uint256"},{"internalType":"uint256","name":"baseGas","type":"uint256"},{"internalType":"uint256","name":"gasPrice","type":"uint256"},{"internalType":"address","name":"gasToken","type":"address"},{"internalType":"address","name":"refundReceiver","type":"address"},{"internalType":"uint256","name":"_nonce","type":"uint256"}],"name":"getTransactionHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"module","type":"address"}],"name":"isModuleEnabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"isOwner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"prevOwner","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"_threshold","type":"uint256"}],"name":"removeOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"}],"name":"requiredTxGas","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"handler","type":"address"}],"name":"setFallbackHandler","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"guard","type":"address"}],"name":"setGuard","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_owners","type":"address[]"},{"internalType":"uint256","name":"_threshold","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"address","name":"fallbackHandler","type":"address"},{"internalType":"address","name":"paymentToken","type":"address"},{"internalType":"uint256","name":"payment","type":"uint256"},{"internalType":"address payable","name":"paymentReceiver","type":"address"}],"name":"setup","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"signedMessages","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"targetContract","type":"address"},{"internalType":"bytes","name":"calldataPayload","type":"bytes"}],"name":"simulateAndRevert","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"prevOwner","type":"address"},{"internalType":"address","name":"oldOwner","type":"address"},{"internalType":"address","name":"newOwner","type":"address"}],"name":"swapOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]}keccak_function_value = getFunctionSeletor(abi_single)//这个是函数选择器,生成是8位16进制。类似0x1a19f89document.getElementById("keccak_function").innerHTML = keccak_function_value;var encode_para_data =abiCoder.encode(matchObj_copy,matchObj_paras);//这个是对交易数据进行encode编码//举个例子,abiCoder.encode(['address', 'address', 'uint256'],//[['0x4d2E1A38d07Eadf5C62CfDaF93547DAe09F1EF83', '0x592916d0D7fcaec0A0A0504134364721Aafd5e87', 6000]])//encode_para_data = 0x0000000000000000000000004d2e1a38d07eadf5c62cfdaf93547dae09f1ef83000000000000000000000000592916d0d7fcaec0a0a0504134364721aafd5e870000000000000000000000000000000000000000000000000000000000001770//encode都是32个字节0x20, 64位一个slot(插槽)进行编码,encodePacked并不按照32个字节(64位16进制)进行编码。//这里是不带函数选择器的,类似0x1a19f89var single_final =keccak_function_value + encode_para_data.substring(2);//把完整的交易打包,完整的交易由函数选择器+参数组成,keccak_function_value这个是函数选择器,类似长这个样子0x1a19f89,mencode_para_data.substring(2)这个是压缩后的参数。//keccak_function_value类似为0x1a19f894//single_final的值类似为0x1a19f8940000000000000000000000004d2e1a38d07eadf5c62cfdaf93547dae09f1ef83000000000000000000000000592916d0d7fcaec0a0a0504134364721aafd5e870000000000000000000000000000000000000000000000000000000000001770//0x1a19f894 //4个字节,8位也就是长度为8的16进制//0000000000000000000000004d2e1a38d07eadf5c62cfdaf93547dae09f1ef83//一个solt,32个字节,64位16进制,合约函数中表示from//000000000000000000000000592916d0d7fcaec0a0a0504134364721aafd5e87//一个solt,32个字节,64位16进制,合约函数中表示to//0000000000000000000000000000000000000000000000000000000000001770//这个是value,1*16**3+7*16**2+7*16=4096+1792+112=6000//这就是一笔交易的函数信息了single_data = single_final//如果对一笔交易来讲,这个single_final就是最后要签名的数据中的data了。(这个single_data只是你要签名信息sign_message中的一部分,是sign_message的第三个参数)var dom_address = document.getElementById('Contract_address').value;//这里sign_to_address 是指你要执行的这个函数是哪个合约里的,这里要填写合约地址。sign_to_address = dom_address;sign_message = {to: sign_to_address,value:0,data: single_data,operation: 0,safeTxGas:0,baseGas: 0,gasPrice:0,gasToken:'0x0000000000000000000000000000000000000000',refundReceiver: "0x0000000000000000000000000000000000000000",nonce:sign_nonce ,};//这是最后要签名的信息sign_messagevar contracts_safe_execTransaction = new ethers.Contract(contracts_gnosis_safe.gnosis.address,contracts_gnosis_safe.gnosis.abi,signer_ExecTransaction)var to = sign_to_address;var valueInWei = 0;var data = sign_single_data;var operation = 0;var safeTxGas = 0;var baseGas = 0;var gasPrice = 0;var gasToken = "0x0000000000000000000000000000000000000000";var refundReceiver = "0x0000000000000000000000000000000000000000";var sigs = sig_s;//这些变量就是拿到最后要执行合约交易的参数,contracts_safe_execTransaction这个是合约对象,已经包含了用户要执行合约的地址,其余的变量都是要传入的参数。try{await contracts_safe_execTransaction.execTransaction(to,valueInWei,data,operation,safeTxGas,baseGas,gasPrice,gasToken,refundReceiver,sigs);//这块就是要唤醒小狐狸执行交易合约的代码 alert('success') nonce ++;}catch{alert('执行交易失败')}//其实真正这个合约中只有一个fallback函数,rinkeby上地址为0x7Fbf984cd60d763Eb94C4C466013f1Ee90dd78Cf,这就是我自己的保险箱Safe合约,没有这个execTransaction函数,在创建保险箱的时候走的是0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2这个合约,但是Safe合约里fallback函数里面用了内联汇编就可以让他指向内部的singleton合约地址,地址为0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552,让这个singleton合约去验证多重签名和执行其他合约的函数,是这个意思。但是如果在构造合约实例的时候,配置的abi中没有这个execTransaction函数是会报错的。
2>转移资产
可以理解成,转移资产和用户自己构造交易在合约层面是一模一样的,都是一样的步骤。区别就是一个是用户自定义指定合约与函数,需要他自己填写函数的参数。转移资产功能只需要用户填写to地址(给谁转账),以及对应的资产数量。转移资产这种方式其实是前端提前写好的。转移资产基本都是走transfer函数,从Safe合约中转给to地址,from是Safe合约。用户只需要添加to地址与数量,转移资产功能可以转ETH主币也可以转ERC20,721,1155。
如果是转ETH,同理,用户需要添加to地址以及对应的数量,前端去动态encode生成data(接受ETH value数量)。
用户批量构造交易,整合到一笔交易,multisend
*1>每一次用户自定义构造交易
2>encodePacked和encode操作是类似的,只不过encodePacked在encode基础上针对参数类型进行了缩放。比如encodePacked(uint8),传入参数是1,那么得出的结果就是0x01,如果是encodePacked(uint16),传入参数也是1的话,得出结果是0x0001,但是encode的话,全部都是64位16进制,得出的结果是0x0000000000000000000000000000000000000000000000000000000000000001.
3>总体的流程是,用户自定义构造多笔交易,这一步只需要encode,再进行encodePacked成完整的一笔交易,然后把这个完整的每笔encodePacked之后交易一个个拼接,组成最后的多笔交易的final_multi_data。(比如用户要构造三笔交易,他先构造了一笔,前端做的事就是先encode这个data,然后把data和其他的参数一起进行encodePacked,得到对应的信息,然后构造第二笔也是一样的操作,接着把上笔完整交易encodePacked后返回的东西和这笔完整交易encodePacked后返回的东西进行直接拼接。
最后对总的信息再进行encode成bytes类型。
var uint8_operation = document.getElementById('operation').value//这个是用户在执行交易对这个合约的操作,是0或者1,暂时不用管,都写0,0在gnosis合约中表示call,1在gnosis合约定义位delegatecallvar address_to = document.getElementById('to').value//这个合约函数要执行的时候指定的合约地址,比如想法是在A合约里面有个f1函数,执行A合约里面的f1函数,我不能用f1函数去指定B合约。类似0xd1d8a3af43788876089b57a214544f9fff9ed085var uint256_value = document.getElementById('value').value * 10 ** 18//这个是转移eth的数量,这里10 **18 是精度,就是如果页面中直接给了一个值5000,如果不带这个10**18,结果会是0.000000000000005个ETH。var uint_data_length = ((single_data.length)-2)/2//这里是算单次交易转换成data的字节长度。2位16进制=1个字节,-2是把0x去掉,/2是拿到字节长度,而不是位长度。var single_data = keccak_function_value + encode_para_data.substring(2)//这single_data是单笔交易,不包含operation,函数所在的合约地址,转账ETH金额//其中keccak_function_value是函数选择器,类似0x1a19f894//encode_para_data 类似0x0000000000000000000000004d2e1a38d07eadf5c62cfdaf93547dae09f1ef83000000000000000000000000592916d0d7fcaec0a0a0504134364721aafd5e870000000000000000000000000000000000000000000000000000000000001770,substring(2)是把前两位0x去掉var single_total_OAV = ethers.utils.solidityPack(["uint8", "address", "uint256","uint256", "bytes"], //ethers.utils.solidityPack就是encodePacked功能[uint8_operation,address_to,uint256_value,uint_data_length,single_data])console.log('single_total_OAV',single_total_OAV)multi_data_final = multi_data_final + single_total_OAV.substring(2)//multi_data_final 最开始是"0x"//multi_data_final 才是最后想要拿到的一次性执行多笔交易的完整的data,但是还需要最后再encode成bytes类型一下。//single_total_OAV类似长这样:0x00d1d8a3af43788876089b57a214544f9fff9ed085000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000641a19f8940000000000000000000000004d2e1a38d07eadf5c62cfdaf93547dae09f1ef83000000000000000000000000592916d0d7fcaec0a0a0504134364721aafd5e870000000000000000000000000000000000000000000000000000000000001770//把single_total_OAV的值分离开来就是//00 //uint8 encodePacked之后的值为0,0表示call,1表示delegatecall 。 值为uint8_operation//d1d8a3af43788876089b57a214544f9fff9ed085//这个表示用户自定义想执行的哪个合约地址 ,值为address_to//0000000000000000000000000000000000000000000000000000000000000000 //32字节,表示转多少wei的ETH//0000000000000000000000000000000000000000000000000000000000000064 //32字节, 64表示 16*6+4=100,也就是交易数据带函数选择器一共是100个字节,比如交易数据就是//1a19f894//4个字节,函数选择器//0000000000000000000000004d2e1a38d07eadf5c62cfdaf93547dae09f1ef83 //32个字节,这个函数中表示from地址//000000000000000000000000592916d0d7fcaec0a0a0504134364721aafd5e87 //32个字节,这个函数中表示to地址//0000000000000000000000000000000000000000000000000000000000001770 //32个字节,这个函数中表示转账的数量// 从1a19f894一直到最后的1770就是完整的一个交易数据,4+32*3=100 个字节, 16*6+4=100个字节。var final_encode_multi_data = abiCoder.encode(['bytes'],[multi_data_final]);//把multi_data_final encode成bytes类型的total_multi_data = '0x8d80ff0a' + final_encode_multi_data.substring(2)//这个total_multi_data 是最后multisend EIP712签名的那个data,这个才是最终的,其他的所有encode编码都是为这个data服务。指向的是固定的一个合约,帮你执行这些交易,函数也是固定的所以这里直接固定0x8d80ffoa就好,0x8d80ffoa的来源就是keccak256( function multiSend(bytes memory transactions)),然后去bytes4,取前4个字节,前8位16进制,因为哈希过后是固定长度的32字节。var contracts_safe_execTransaction = new ethers.Contract(contracts_gnosis_safe.gnosis.address,contracts_gnosis_safe.gnosis.abi,signer_ExecTransaction)var to = sign_to_address;//如果这步是执行multisend的话,只能是前端自己去配置,比如rinkeby上的合约地址就是0x40A2aCCbd92BCA938b02010E17A5b8929b49130D,不同的网络要配置不同的合约,合约里面必须用内联汇编把交易分离,有时间我会讲一下内联汇编是如何通过evm code把data分离,我会着重讲内存布局var valueInWei = 0;var data = total_multi_data; //这里要传入合约参数第三个参数就是data,值为total_multi_datavar operation = 1;var safeTxGas = 0;var baseGas = 0;var gasPrice = 0;var gasToken = "0x0000000000000000000000000000000000000000";var refundReceiver = "0x0000000000000000000000000000000000000000";var sigs = sig_s;//这里假设用户都签完名了//这个就是执行最后交易唤醒小狐狸的函数方法,最关键的就是data,然后传入对应解析data的sign_to_address这个合约地址,操作operation为1,和用户验签后返回的signature,65字节*DAO成员通过人数阈值的一串bytes,变成sigs,把sigs带入调用合约中的最后一个参数,就可以执行多个合约多个函数的交易了。await contracts_safe_execTransaction.execTransaction(to,valueInWei,data,operation,safeTxGas,baseGas,gasPrice,gasToken,refundReceiver,sigs);
自己做了一个测试网站,方便理解abi,encode编码
我自己有一个网站,主要是通过ABI产生data值的,网站为:http://114.116.97.234/Gnosis/eip712_gnosis_ethers