pragmasolidity^0.4.23;contractEtherStore{uintpublicwithdrawalLimit=1ether;mapping(address=>uint)publiclastWithdrawTime;mapping(address=>uint)publicbalances;functiondepositFunds()publicpayable{//3balances[msg.sender]+=msg.value;}functionwithdrawFunds(uint_weiToWithdraw)public{//5require(balances[msg.sender]>=_weiToWithdraw);require(_weiToWithdraw<=withdrawalLimit);require(now>=lastWithdrawTime[msg.sender]+1weeks);//5直到这都没有问题require(msg.sender.call.value(_weiToWithdraw)());//6在这里就会出现问题,会调用msg.sender,即合约Attack的fallback函数balances[msg.sender]-=_weiToWithdraw;lastWithdrawTime[msg.sender]=now;}}
避坑技巧
很多方法都可以帮助避免智能合约中潜在的重新入口漏洞。
第一种方法是,当发送以太币到外部合约时,使用内置的transfer()函数。Transfer()函数只发送2300个gas,这不足以使目的地址/合约调用另一个合约(例如,重新进入发送中的合约)。
第二个方法是,在以太币被从合约(或任何外部调用)发送出去之前,确保所有改变状态变量的逻辑发生。在上述的例子中,代码1的第18、19行应该放在第17行之前。将执行外部调用的任何代码作为本地化函数或代码执行的最后一个操作,并将执行外部调用的代码置于未知地址上。这就是所谓的「检查-效应-交互」模式。
第三个方法是,引入一个互斥系统。也就是说,添加一个状态变量,该状态变量在代码执行期间锁定合约,从而防止重新入口的调用。
pragmasolidity^0.4.23;contractEtherStore{boolreEntrancyMutex=false;//解决方法3:引入一个互斥系统uintpublicwithdrawalLimit=1ether;mapping(address=>uint)publiclastWithdrawTime;mapping(address=>uint)publicbalances;functiondepositFunds()publicpayable{balances[msg.sender]+=msg.value;}functionwithdrawFunds(uint_weiToWithdraw)public{require(!reEntrancyMutex);require(balances[msg.sender]>=_weiToWithdraw);require(_weiToWithdraw<=withdrawalLimit);require(now>=lastWithdrawTime[msg.sender]+1weeks);balances[msg.sender]-=_weiToWithdraw;//解决方法2:将所有内部实现都在外部调用中完成lastWithdrawTime[msg.sender]=now;reEntrancyMutex=true;msg.sender.transfer(_weiToWithdraw);//解决方法1:使用transfer(只使用2300个gas)reEntrancyMutex=false;}}真实案例:TheDAO
以太坊虚拟机(EVM)指定整数为固定大小的数据类型。这意味着一个整数变量,只可以表示一定范围的数字。
例如,uint8只能存储的数字范围是[0,255]。试图将256存储到uint8中将导致结果为0。这很可能使Solidity中的变量被利用,如果对用户的输入不做限制,结果就会导致数字超出存储它们的数据类型范围。
坑点分析
当一个操作执行的时候,需要一个固定大小的变量来存储一个数字(或数据片段),如果该数字或数据不在变量数据类型的范围内,将会产生溢出/下溢。
例如,从uint8中(8位的无符号整数,也就是只有正数)的变量0中减去1,就会得到255,这就是下溢。我们已经在uint8的范围内分配了一个数字,结果包含了uint8可以存储的最大数量。类似地,在uint8中添加2^8=256将使变量保持不变,因为我们已经囊括了整个uint8的长度(从数学上来说,这类似于在三角函数的角度上增加2π,sin(x)=sin(x+2π))。
添加大于数据类型范围的数字被称为溢出。比如,如果在uint8中当前为零的值上加257,就会得到数字1。有时,可以把固定类型变量想成循环,我们从零开始,如果我们在最大可能存储的数字之上加上数字,就又从零开始了,反之亦然(我们从最大的数字开始倒数,从0中减去一个数会得到一个较大的值)。
这些类型的漏洞允许攻击者滥用代码并创建一些意想不到的逻辑流:
pragmasolidity^0.4.23;contractToken{//用于转移自己的代币给另一个账户mapping(address=>uint)balances;uintpublictotalSupply;constructor(uint_initialSupply)public{balances[msg.sender]=totalSupply=_initialSupply;}functiontransfer(address_to,uint_value)publicreturns(bool){require(balances[msg.sender]-_value>=0);//如果余额为0,减去任何一个正数,都将导致结果为正数,因为溢出balances[msg.sender]-=_value;//此处也是,向下减溢出仍为正数balances[_to]+=_value;returntrue;}functionbalanceOf(address_owner)publicviewreturns(uintbalance){returnbalances[_owner];}}
防止溢出/下溢漏洞的常规方法是,使用或构建数学库来替代标准的数学运算符,包括加法、减法和乘法(没有除法,因为它不会导致溢出/下溢)。
OppenZepplin在构建和审核安全库方面做了大量的工作,以太坊社区可以充分利用这些库。为了演示在Solidity中如何使用这些库,让我们用Zepplin开源的SafeMath库来修正代码3的合约:
SafeMath.solpragmasolidity^0.4.11;/***@titleSafeMath*@devMathoperationswithsafetychecksthatthrowonerror*/librarySafeMath{functionmul(uint256a,uint256b)internalconstantreturns(uint256){uint256c=a*b;assert(a==0||c/a==b);returnc;}functiondiv(uint256a,uint256b)internalconstantreturns(uint256){//assert(b>0);//Solidityautomaticallythrowswhendividingby0uint256c=a/b;//assert(a==b*c+a%b);//Thereisnocaseinwhichthisdoesn'tholdreturnc;}functionsub(uint256a,uint256b)internalconstantreturns(uint256){assert(b<=a);returna-b;}functionadd(uint256a,uint256b)internalconstantreturns(uint256){uint256c=a+b;assert(c>=a);returnc;}}改后为:
pragmasolidity^0.4.23;import'../node_modules/zeppelin-solidity/contracts/math/SafeMath.sol'contractTimeLock{usingSafeMathforuint;mapping(address=>uint)publicbalances;mapping(address=>uint)publiclockTime;functiondeposit()publicpayable{balances[msg.sender]=balances[msg.sender].add(msg.value);lockTime[msg.sender]=now.add(1weeks);}functionincreaseLockTime(uint_secondsToIncrease)public{lockTime[msg.sender]=lockTime[msg.sender].add(_secondsToIncrease);}functionwithdraw()public{require(balances[msg.sender]>0);require(now>lockTime[msg.sender]);balances[msg.sender]=0;msg.sender.transfer(balances[msg.sender]);}}
通常情况下,当以太币在合约中时,必须执行fallback函数,或者执行合约中定义的另一个函数。
不过这里有两个例外:
1)以太币可以在合约中存在而不执行任何代码;
2)对于依赖于代码执行的合约,每个发送到合约的以太币都可能受到攻击,因为在这种情况下,以太币是被强制送入合约的。
对于强制执行正确的状态转换或验证操作而言,一个常见的防御性技术是非常有用的,那就是变量检查。变量检查涉及到定义一组不变量(不应更改的标称值或参数),并且在一个(或许多)操作之后检查这些不变量是否保持不变。
不变量检查的一个例子是固定发行ERC20代币中的totalSupply。由于任何函数都不应修改这个不变量,因此可以对transfer()函数添加一个检查,以确保totalSupply保持不变,并确保该函数正常工作。
不过,有一个「不变量」对开发者来说特别有吸引力,但实际上却很容易被外部用户操纵。这就是合约中当前存储的以太币。
通常,当开发者第一次学习Solidity时,他们会有一种误解,认为合约只能通过payable函数接受或获得以太币(我真的是这样以为的)。这种误解可能导致合约对其内部的以太币余额作出错误的假设,从而导致一系列的漏洞。而这种漏洞的确凿证据就是错误地使用了this.balance。
错误的使用this.balance会导致严重的漏洞。
以太币可以通过两种方式(强制)发送到合约中,而不使用payable函数或执行合约上的任何代码。
自析构/自杀(与构造函数不同,构造函数用于初始化)
第一种方式是使用析构函数——用于销毁。任何合约都能够实现析构(地址)函数,该函数从合约地址中移除所有字节码,并将存储在那里的所有以太币发送到参数指定的地址。如果这个指定的地址也是一个合约,那么将没有函数(包括出让函数)被调用。
因此,无论合约中可能存在怎样的代码,selfdestruct()都可以用来强制将以太币送到任何合约(这些任何合约就可以被攻击)中,这也包括没有任何支付函数的合约。这样一来,任何攻击者都可以创建带有析构函数的合约,并把以太币发送到合约上,然后调用selfdestruct(target)函数,并强制以太币发送到target合约。
selfdestruct(target)函数:就是将目前的合约销毁并将该合约上的以太币发送给target地址
预先发送的以太币
第二种方法是在不使用selfdestruct()或调用任何支付函数的情况下获得以太币,说白了,就是将合约地址和以太币预加载。因为合约地址是确定的(地址是从创建合约的地址哈希和创建合约的交易nonce计算的。
例如,形如:
address=sha3(rlp.encode([account_address,transaction_nonce]))),
这意味着,任何人都可以在创建合约之前算出地址来,从而将以太币发送到该地址。当合约产生时,就会有一个非0的以太币余额。
自析构/自杀方法举个例子:
其实就是攻击函数自己创建一个合约,该合约中的余额为0.1以太币,然后调用selfdestruct(target)函数,那么就会销毁攻击者自己的这个合约,并且将合约中的0.1以太币强行发送给了target攻击目标合约,那这样,玩家没增加0.5ether将永远都得不到整数值。
更糟糕的是,一个想要报复的攻击者可以强行发送10以太币(或相当数量的以太币,使合约的余额超过finalMileStone),这将永远锁定合约中的所有奖励,claimReward()函数将永远卡在require处。
「非预期的以太币」漏洞,常来自于对this.balance的滥用。在可能的情况下,合约逻辑应避免依赖于合约余额的精确值,因为它可以被人为操纵。
如果应用逻辑基于this.balance,要确保考虑到非预期的余额。
如果需要确切知道以太币的余额,应该使用一个自定义的变量,以便在支付函数中逐步增加,并安全地跟踪存续的以太币。这个变量不会受到通过selfdestruct()强迫发送以太币的影响。
考虑到这一点,代码5EtherGame的合约应修改为:
在允许以太坊开发者模块化他们的代码时,CALL和DELEGATECALL操作是很常见的。标准的外部消息调用由外部合约/函数中运行的CALL操作码来处理。
DELEGATECALL操作码与标准消息调用相同,调用合约中运行目标地址上的代码,不过msg.sender和msg.value保持不变。在目标地址执行的代码是在调用合约的上下文中运行的。这个特性使得开发者可以实现为未来的合约创建可复用的代码。
尽管CALL和DELEGATECALL的作为十分简单,但DELEGATECALL的使用不当,会导致非预期的代码执行。
但是,当它在另一个应用程序中运行时,可能会出现新的漏洞。让我们从斐波那契数列,来看一个相对复杂的例子。
假设下面的库可以生成斐波那契数列,以及类似形式的数列。
pragmasolidity^0.4.23;contractFibonacciLib{uintpublicstart;uintpubliccalculatedFibNumber;functionsetStart(uint_start)public{start=_start;}functionsetFibonacci(uintn)public{calculatedFibNumber=fibonacci(n);}functionfibonacci(uintn)internalreturns(uint){if(n==0)returnstart;elseif(n==1)returnstart+1;elsereturnfibonacci(n-1)+fibonacci(n-2);}}调用上面的库合约:
contractFibonacciBalance{addresspublicfibonacciLibrary;uintpubliccalculatedFibNumber;uintpublicstart=3;uintpublicwithdrawalCounter;bytes4constantfibSig=bytes4(keccak256("setFibonacci(uint)"));constructor(address_fibonacciLibrary)public{fibonacciLibrary=_fibonacciLibrary;}functionwithdraw()public{withdrawalCounter+=1;require(fibonacciLibrary.delegatecall(fibSig,withdrawalCounter),"somethingwrong");msg.sender.transfer(calculatedFibNumber*1ether);}function()public{require(fibonacciLibrary.delegatecall(msg.data));//不当使用1:这里使用了msg.data,使得恶意攻击者能够调用自己想要调用的任意函数,如setStart,而导致下面的更严重后果}}该合约允许参与人从合约中提取以太币,其中以太币的数量等于与参与者提取订单中相应的斐波那契数字;即第一个参与者得到1以太币,第二个参与者得到1以太币,第三个得到2,第四个得到3,第五个得到5等等,直到合余的余额少于被提取的那个斐波那契数字。
不当使用2:位置
出现错误的原因:
状态变量start在库和主调用合约中都被使用了。在库合约中,start用于指定Fibonacci数列的起点,并设置为0,而在主调用合约FibonacciBalance中它被设置为3
主调用合约FibonacciBalance中的fallback函数允许将所有调用传递给库合约,这样就可以调用setStart函数来调用库,用以改变主调用合约FibonacciBalance中的start变量的状态,如果是这样,这将允许黑客提取更多的以太币,因为calculatedFibNumber取决于start变量。
但是实际上,setStart()函数不会(也不能)修改代码7合约中的start变量。这个合约中潜在的漏洞比仅仅修改start变量要糟糕得多。
举个例子,在库合约FibonacciLib中,存在两个状态变量:start和calculatedFibNumber。第一个变量是start,因此它存储在合约的slot[0]中。第二个变量calculatedFibNumber,被放置在下一个可用的存储——slot[1]中。
如果我们查看函数setStart(),它需要一个输入并设置start(不论输入是什么)。因此,这个函数为setStart()函数中提供的任何输入都设置为slot[0]。类似地,setFibonacci()函数也将calculatedFibNumber设置为fibonacci(n)的结果。同样,这只是将存储slot[1]设置为fibonacci(n)的值。
现在再来看看主调用合约FibonacciBalance的合约。slot[0]现在对应于fibonacciLibrary地址且slot[1]对应于calculatedFibNumber。这就是漏洞出现的地方。
delegatecall保留了合约的上下文,运行环境其实为本合约。这意味着通过delegatecall的代码将对主调用合约的状态(如存储)产生作用,即在库合约中如果更改了start的值,那么其实是更改了主调用合约FibonacciBalance环境中的start的值,但是现在这里有个问题:
现在请注意,当我们执行了fibonacciLibrary.delegatecall。这里调用了setFibonacci()函数,它对slot[1]进行了修改(也就是calculatedFibNumber)。这和预期的一样(即在执行之后,calculatedFibNumber得到调整)。
然而,请记住,FibonacciLib合约中的start变量位于slot[0],这是当前合约中的fibonacciLibrary地址。这意味着函数fibonacci()将给出一个意想不到的结果。
因为在当前的调用的上下文中,它引用了start状态变量(引用状态变量并不是根据名字去找,而是根据状态变量存储的位置,即库合约中的start的存储位置为slot[0],那么当使用delegatecall时,就是在主调用合约的slot[0]位置去找,但是在主调用合约中slot[0]位置的值为fibonacciLibrary),这是fibonacciLibrary地址(那么这里fibonacciLibrary地址的值就会被转为uint,当被解释为一个uint时,这个地址通常是相当大的)。
因此,withdraw()函数很可能会恢复原样,即revert,如下图所示,该函数调用会出现错误。因为它不会包含uint(fibonacciLibrary)的以太币数量,而这就是calculatedFibNumber将会返回的值。
然后调用FibonacciBalance合约的withdraw函数时,出现了错误
更糟糕的是,主调用合约FibonacciBalance的合约允许用户通过fallback函数调用所有库合约fibonacciLibrary的函数。正如我们之前讨论过的,这就包括了setStart()函数。在这种情况下,主调用合约FibonacciBalance的slot[0]就是fibonacciLibrary地址。因此,攻击者可以创建一个恶意合约,调用setStart,将地址转换为uint作为参数传给setStart函数,即调用
setStart(
这将会把传入的值放入主调用合约FibonacciBalance的slot[0]位置,即改变fibonacciLibrary状态变量,使其成为攻击者合约的地址。然后,当用户调用withdraw()或fallback函数时,恶意合约就会运行,并盗取合约中的全部余额。就如下面例子所示:
开始时的状态如下图所示:
然后再主调用合约中存入525wei:
部署Attack合约:
然后调用testAttack,将攻击合约的地址作为参数传入,用于将fibonacciLibrary改为攻击合约地址:
这时候再去调用withdraw()函数,因为Attack合约中没有setFibonacci()函数,所以会调用它的fallback函数,则主调用合约中的525wei就会被转到恶意合约中,然后我们也可以看见,当攻击合约fallback函数中将storageSlot1改为3时,真正变化的值是calculatedFibNumber,主调用函数中的525wei也没了:
注意:演示中的一个核心要点是,编译后,我们能得到当前合约的地址,并将该地址复制到输入框中,记得录入地址项时要加英文的双引号,否则会报Errorencodingarguments:SyntaxError:JSONParseerror:Expected']'。
另一个例子:
因为我们最终要控制的目标即主合约的owner对应的存储位为slot[3],所以我们要在前面放两个用于占位的变量
一开始我们部署了两个库合约,并且将它们的合约地址传入主调用函数Preservation中,如上图的timeZone1Library、timeZone2Library所示,然后接下来调用setSecondTime,将攻击合约的地址作为参数_timeStamp传入,结果将会将timeZone1Library的值改为攻击合约的地址,如下图:
此时再运行setFirstTime,那么调用的就是攻击合约的setTime函数,因为storedTime的值没有变,而owner将会被更改成生成Attack合约的恶意攻击者,如下图所示:
Solidity为实现库合约提供了library关键字。这确保了库合约是无状态的和非析构的。确保库的无状态可以减少存储上下文的复杂性。无状态库还可以防止攻击者直接修改库的状态,以实现依赖于库代码的合约。一般来说,当使用DELEGATECALL时,要注意库合约和调用合约中可能调用的上下文,并在可能的情况下建立无状态库。
对于这种漏洞还是需要开发人员按照安全的编写方法正确实现delegatecall的使用,避免遭到恶意的利用,而另一方面就是在这种较复杂的上下文环境下涉及到storage变量时可能造成的变量覆盖,对于这种漏洞感觉如有需要还是避免直接使用delegatecall来进行调用,应该使用library来实现代码的复用,这也是目前在solidity里比较安全的代码复用的方式
其实library使用的基础也是delegatecall,不过它是一种较特别的合约,相比普通合约有几个特别的点,包括没有storage变量,无法继承或被继承,不能接收ether,要使用它来访问storage变量就得靠引用类型的传递了。delegatecall的漏洞可能也就是library没有storage变量等的原因。
真实案例:ParityMultisigWallet的第二次入侵
如果在非预期的上下文中运行,ParityMultisig钱包的第二次攻击就是一个典型的例子
Solidity中的函数具有可见性的特性,它们指明了如何调用函数。可见性决定了一个函数是否可以由用户从外部调用(public)(或由其他派生的合约调用),还是只能在内部(internal)或只能在外部调用(external)。
在Solidity文档中提到四个可见性特性,默认函数是Public。不正确地使用这一函数,可能导致在智能合约中产生一些破坏性的漏洞。
函数的默认可见性是public。因此,不指定任何可见性的函数都可以被外部用户调用。如果开发者忽略了这一特性,本来的私有函数(或者只能在合约自身中调用)就会变成公有函数,问题也会随之而来,比如:
pragmasolidity^0.4.23;contractHashForEther{functionwithdrawWinnings()public{require(uint32(msg.sender)==0);_sendWinnings();//应该为一个内部调用函数,但是这里忘记将其标记为internal,导致所有人都可以取出这个合约中的资金}function_sendWinnings(){msg.sender.transfer(address(this).balance);}}这个合约中,实现的是一个地址猜赏游戏。为了赢得合约的余额,用户必须生成一个以太坊地址,它最后的8个十六进制字符是0。一旦获得,他们可以调用withdrawWinnings函数来获得他们的赏金。
不幸的是,函数的可见性还没有被指定。另外,_sendWinnings()函数是public,因此任何地址都可以调用此函数来窃取赏金。
一种最好的做法是,即使合约中的所有函数都是有意公开的,也必须明确说明合约中所有函数的可见性。最近版本的Solidity将会在编译的函数没有明确的可见性设置时显示警告,以鼓励这种做法。
就是所有函数都要显示表明其的可见性,即使是public也是这样
真实案例:ParityMultiSigWallet的第一次黑客攻击
在以太坊区块链上的所有交易都是确定性状态的转换操作。这意味着每一笔交易都改变了全球的以太坊生态系统状态,并且是以一种可计算的方式进行,没有任何的不确定性。
在以太坊平台上建立的第一批合约中,有一些是关于赌博的。从根本上讲,赌博的根本在于不确定性,这使得在区块链(确定性模型)上建立一个赌博系统相当困难。很明显,不确定性必须来自区块链外部的一个源。
这对于同行之间的赌注是可能的,但是,如果你想要执行一个合约来充当一个赌桌(就像在我们的赌场里玩21点一样),显然是十分困难的。一个常见的陷阱是使用未来的区块变量,例如hash、timestamps、blocknumber或gaslimit。
问题在于,这些变量是由矿工控制的,他们在区块上挖矿,因此并不是真正随机的。例如,考虑一个具有逻辑的轮盘赌智能合约,如果下一个区块哈希以偶数结尾,则返回一个黑数。
一个矿工(或矿工池)可以押注100万美元买黑数。如果他们解决了下一个区块,发现哈希末尾是一个奇数,他们会很乐意不发布这一区块并挖掘下一个块,直到他们找到一个解决方案发现区块哈希尾数是偶数(假设悬赏和费用低于100万美元)为止。
使用过去或现在的变量可能会更具破坏性,此外,使用单个区块变量意味着在一个区块中所有交易的伪随机数都是相同的,因此攻击者可以在一个区块内进行许多交易。
以太坊作为「全球计算机」的好处之一是能够复用代码,并与已经部署在网络上的合约进行交互。因此,大量合约都引用外部合约,在一般操作中使用外部调用与这些合约进行互动。这些外部消息调用可以用某种不明显的方式掩盖黑客的意图。
在Solidity中,任何地址都可以作为一个合约,尤其是当合约的作者试图隐藏恶意代码时。让我们举一个例子来说明这一点,请看下面这段基本实现了Rot13密码的代码:
考虑以下使用此代码进行加密的合约:
contractEncryptionContract{//libraryforencryptionRot13EncryptionencryptionLibrary;//constructor-initialisethelibraryconstructor(Rot13Encryption_encryptionLibrary)public{encryptionLibrary=_encryptionLibrary;}functionencryptPrivateData(stringprivateInfo)public{//potentiallydosomeoperationshereencryptionLibrary.rot13Encrypt(privateInfo);}}这个合约的问题是,encryptionLibrary地址并不是公开的或保证不变的。因此,合约的配置人员可以在指向该合约的构造函数中给出一个地址,如果这个时候这个地址并不是encryptionLibrary的地址,而是
其他合约的地址,并且这个合约中有相同的函数,或者是fallback函数,那么调用encryptPrivateData()是得不到想要的结果的,如下面的合约,如果_encryptionLibrary传入的是合约Blank的地址,那么它调用encryptPrivateData()时只是记录了事件Print("Here")
contractBlank{eventPrint(stringtext);function(){emitPrint("Here");//putmaliciouscodehereanditwillrun}}因此,如果用户可以更改库合约地址encryptionLibrary,那么,他们原则上可以让用户在不知情的情况下运行任意的代码。
因此,开发者要杜绝使用这样的加密合约,因为在区块链上可以看到智能合约的输入参数。此外,Rot密码也并不是一个理想的加密技术。
如上所述,无漏洞合约可以在某些情况下以恶意行为的方式部署。审核员可以公开地核实合约,并使其所有者以恶意方式部署合约,从而导致公开审计的合约具有漏洞或恶意属性。
有许多方法可以防止这些情况发生。
1)一种方法是,使用new关键字来创建合约。在上面的例子中,构造函数可以改成:
constructor()public{encryptionLibrary=newRot13Encryption();}这样Rot13Encryption合约地址就无法被换成其他合约地址
2)对已知的外部合约地址,进行硬编码(怎么做?????)
一般来说,开发者应该仔细地检查调用外部合约的代码。作为一个开发者,在定义外部合约时,最好是让合约公开(除了在honeypot的情况下),以便使用户能够很容易地检查合约中引用的那些代码。
真实案例:重新入口的蜜罐攻击
最近,一些honeypot(蜜罐攻击)已经被放到了主网上。这些合约试图智取那些试图利用这些合约的以太坊黑客,但他们反过来又让以太币失去了它们期望利用的合约。方法就是其中用构造函数中的恶意合约替换了预期的合约。
8.短地址/参数攻击
这种攻击不是专门针对Solidity合约的,而是针对所有可能与合约互动的第三方DApp。
在参数传递给智能合约时,参数将根据ABI规范进行编码。发送短于预期参数长度的编码参数是可能的。
例如,发送一个只有19字节的地址,而不是标准的40个十六进制数20字节。在这种情况下,EVM会把0填充在编码参数的末尾,以补全预期的长度。
当第三方应用程序不验证输入时,这就成为一个问题。
最明显的例子是,当用户请求提款时,不会验证ERC20代币的地址。
请想象一下标准的ERC20transfer函数的接口(注意参数的顺序),如:
functiontransfer(addressto,uinttokens)publicreturns(boolsuccess);
举例说明:
现在交易,一个用户持有大量的代币(如REP),希望提出其中的100个。用户将提交它们的地址:
0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead
以及提取代币数量100。
这时,交易会按照transfer函数指定的顺序编码这些参数,即先是address然后是tokens。编码的结果将是:
a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000000000000000000000056bc75e2d63100000
其中,前四个字节(a9059cbb)是transfer()函数的签名/选择器,第二个32字节是地址,最后的32个字节代表数据类型为uint256的代币。
请注意,末尾的十六进制
56bc75e2d63100000
相当于100个代币(根据REP代币合约的规定,小数点后有18位)。
好了,现在让我们看看如果发送一个缺少1个字节(2个十六进制数字)的地址会发生什么。具体来说,如果攻击者发送
0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde
作为一个地址(缺少了末尾的两位数字),并同样发送取回100个代币的指令。如果这个兑换没有验证这个输入,它将被编码为:
a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeadde0000000000000000000000000000000000000000000000056bc75e2d6310000000
请注意,00已经被填充到编码的末尾,补全了所发送的短地址。当它被发送到智能合约时,地址参数将被解读为:
0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde00
同时,该值会被解读为:
56bc75e2d6310000000(注意这两个多出的0)。
这时,代币的价值已经变成了25,600,翻了256倍。也就是说,用户会提取25,600个代币(而交易所却认为用户只能取回100个)到修改后的地址。
显而易见,在将所有输入发送到区块链之前进行验证,将会有效防止这类攻击。此外,参数排序在这里起着重要的作用。由于填充只发生在最后,智能合约中对参数的仔细排序可以防患于未然。
在Solidity中,有很多方法可以执行外部调用,将以太币传送到外部帐户通常是通过transfer()方法进行的。然而,send()函数也可以使用,并且对于更多用途的外部调用,CALL操作码可以直接用于Solidity中。call()和send()函数返回一个布尔值来表示调用是成功还是失败。
因此,这些函数有一个简单的警告,即如果外部调用失败(初始化call()或send()失败,而不是call()或send()返回false),则执行这些函数的交易将不会恢复。当返回值没有被检查时,会出现一个常见的陷阱,而开发者则预期会出现一个复原,所以一般使用call()或者send(),都会使用require(msg.sender.call())\require(msg.sender.send())来检查,以revert状态。
真实案例:Etherpot和KingoftheEther
外部调用与其他合约的组合以及底层区块链的多用户性质,造成了各种潜在的solidity陷阱,用户通过竞争代码的执行得到了非预期的状态。「重新入口」漏洞就是这种竞争条件的一个例子。
在这一部分,我们将更广泛地讨论可能发生在以太坊区块链上的不同竞争条件。
与大多数主链一样,在以太坊中只有当矿工解决了一个共识机制(PoW),这些交易才被认为是有效的。生成该区块的矿工也会选择将哪些交易包含在该区块中,这通常是由交易的gasPrice决定的。
这里就有一个潜在的攻击向量。攻击者可以监视可能包含问题解决方案的交易池,修改或撤销攻击者的权限或更改合约中对攻击者不利的状态。然后攻击者可以从这个交易获得数据,创建一个自己的交易,并且以更高的价格创建自己的交易,并将该交易包含在原始数据之前的区块中。
让我们通过一个例子来看看这个坑是怎么产生的:
pragmasolidity^0.4.23;contractFindThisHash{//即如果用户能找到一个solution进行hash后的值与给出的hash值相同,那么它就能得到1000weibytes32constantpublichash=0xb5b5b97fafd9855eec9b41f74dfb6c38f5951f9a3ecd7f44d5479b630ee0a;constructor()publicpayable{}//使得合约中有值functionsolve(stringsolution)public{require(hash==keccak256(bytes(solution)));msg.sender.transfer(1000wei);}}让我们假设一个用户发现的解决方案是「Ethereum!」,他们将「Ethereum!」作为参数调用solve()。不幸的是,攻击者已经很聪明地观察到任何提交解决方案者的交易池,这个成功的交易还没有记录到区块中。他们看到了这个解决方案,检查了它的有效性,然后提交一个比原始交易价格更高的交易。
由于gasPrice更高,生成该区块的矿工可能会给攻击者更多的优先权,并在原始提交者之前先接受了他们的交易。攻击者会拿走1000以太币,而导致解决了这个问题的用户反而一无所获。
有两类人可以执行这些正在运行的非法预先交易攻击:1)用户(他们修改交易的gasPrice)和2)矿工本身(他们可以按照他们认为合适的方式在一个区块中重新对交易排序)。
对于第一类来说,他们的合约比第二类合约要糟糕得多,因为矿工只有在解决了一个区块时才能进行攻击,而对于任何一个专门针对某个特定区块的矿工来说,这种攻击都是不可能的实现的。
我们可以将列出一些防坑措施。
1.首先,我们可以采用在合约中创建逻辑,为gasPrice设置一个上限。这使得用户无法提高gasPrice,这可以避免因提高gasPrice获得超出上限的优先交易顺序。这种预防措施只能减少第一类攻击者(任意使用者)。
在这种情况下,矿工仍然可以攻击合约,因为他们可以无论gasPrice如何,都可以随心所欲地在他们所在区块内进行交易。
2.还有另一个方法是尽可能使用commit-reveal。这种方案要求用户使用隐藏的信息(通常是哈希)发送交易。在将交易包含在一个区块之后,用户发送一个交易来显示发送的数据(显式阶段)。这种方法使得矿工和用户无法确定交易的内容,因此不能对交易进行预警。
然而,这种方法不能隐藏交易的价值,智能合约允许用户发送交易,其提交的数据包括了他们愿意花费的以太币数量。然后用户可以发送任意值的交易。在这个阶段,用户可以获得交易中发送的金额与他们愿意支出金额之间的差额。
真实案例:ERC20与Bancor
在以太坊上发币要遵循ERC20标准,这个标准有一个潜在的预先非法交易漏洞,这一漏洞源自approve()函数。
该标准指定的approve()函数为:
functionapprove(address_spender,uint_value)returns(boolsuccess)
Bob一直在仔细地观察这条链,他看到了这个交易,并建立了一个自己花费100个代币的交易。比起Alice,他的gasPrice更高,交易的优先级也更高。一些approve()函数的实现允许鲍勃转移他的100个代币,然后当Alice的交易被提交时,将鲍勃的交易批准为50个代币,实际上让Bob获得了150个代币。
另一个著名的案例是Bancor。IvanBogatty和他的团队记录了最初Bancor实现中的一次的攻击,他在自己的博客详细的记录了这次攻击。从本质上来说,代币的价格是根据交易价值来确定的,用户可以观察Bancor交易的交易池,然后从价格差异中获利。目前Bancor的团队已经解决了这次攻击。
这个漏洞就是通过观察交易池,使自己的交易的gasPrice高于那个限制该交易的交易,这样它就能够在限制之间运行,以此来实现攻击
我们知道,智能合约可以通过多种手段使其变得不可操作。在这里,我将只强调一些可能在区块链中不太明显的Solidity编码方式,这些模式可能导致攻击者发起DOS攻击。
主要包括以下几种。
1.通过外部操作的映射或数组循环。在我的经验中,这种方式的攻击见得太多了。通常情况下,它出现在一个owner希望向他们的投资者分发代币的时候,并且使用了一个与distribute()类似的函数。参见下面代码:
pragmasolidity^0.4.23;contractDistributeTokens{addresspublicowner;address[]investors;uint[]investorTokens;functioninvest()publicpayable{investors.push(msg.sender);investorTokens.push(msg.value*5);}functiondistribute()public{require(msg.sender==owner);for(uinti=0;i 2.所有者操作。所有者在合约中享有特殊特权,并且必须执行一些任务,以便合约进入到下一个状态。一个例子就是一个ICO合约,它要求所有者通过finalize()函数进行操作,使代币可以转让。例如: boolpublicisFinalized=false;addresspublicowner;functionfinalize()public{require(msg.sender==owner);isFinalized=true;}functiontransfer(address_to,uint_value)returns(bool){require(isFinalized);super.transfer(_to,_value)}在这种情况下,如果特权用户owner丢失了他们的密钥,或者变得不活跃,则整个合约就会变得不可操作。而且,如果owner无法调用finalize()函数,就没有可以转移的代币;也就是说,代币生态系统的整个运行都取决于一个单一的地址。 在发送以太币的例子中,用户可以创建一个不接受以太币的合约。如果一份合约需要将以太币送到这个地址,以便进入一个新的状态的话,那么合约永远不会达到这一新状态,因为以太币永远不可能被送到合约中。 2)在上面的第二个例子中,要求特权用户更改合约状态。在这个例子中,当owner丧失能力时,可以使用故障保护装置。一个解决方案是将owner设置为一个多重签名合约。 require(msg.sender==owner||now>unlockTime) 真实案例:GovernMental GovernMental是一个老式的庞氏骗局,积累了大量的以太币。不幸的是,它很容易受到本节中提到的DOS漏洞的影响。 一个Reddit帖子描述了合约是如何要求删除一个大的映射,这种映射的删除导致当时的gas成本超过了区块gas的限制,因此无法取回以太币。 合约地址是: 0xF45717552f12Ef7cb65e95476F217Ea008167Ae3 可以从 0x0d80d67202bd9cb6773df8dd2020e7190a1b0793e8ec4fc105257e8128f0506b 中看到交易,最终得到所有以太币共使用了2.5Mgas。 正如上面所说,如果矿工动机不纯,就可以操纵block.timestamp。让我们构建一个简单的游戏,这个游戏很容易被矿工利用。 pragmasolidity^0.4.23;contractRoulette{uintpublicpastBlockTime;constructor()publicpayable{}function()publicpayable{require(msg.value==10ether);require(now!=pastBlockTime);pastBlockTime=now;if(now%15==0){msg.sender.transfer(address(this).balance);}}}这个合约就像一个简单的彩票系统。每个区块中的一个交易都可以赌10以太币来得到赢得合约余额。这里的假设是,block.timestamp对于最后两位数字是均匀分布的。如果是这样的话,那么中奖的几率将是1/15。 在这样做的时候,他们可能会赢得锁定在这份合约中的以太币,同时获得全部的回报。由于每个区块只允许一个人下注,这也很容易受到非法预先交易的攻击。 **真实案例:GovernMental** 构造函数是一种特殊的函数,通常在初始化合约时执行关键的任务。在solidityv0.4.22之前,构造函数被定义为与包含它们的合约具有相同名称的函数。 因此,当一个合约名称在开发过程中发生变化时,如果构造函数的名称没有改变,它就变成了一个正常的、可调用的函数。可以想象,这会导致一些有意思的合约攻击。 正如上面所说,如果我们修改了合约的名称,或者在构造函数名称中有一些笔误,这样构造函数就不再匹配合约的名称,从而会变成一个正常的函数。这会导致可怕的后果,尤其是当构造函数执行特权操作的时侯。请看以下合约: pragmasolidity^0.4.23;contractOwnerWallet{addresspublicowner;functionownerWallet(address_owner)public{//应该写成OwnerWallet才是构造函数owner=_owner;}function()payablepublic{}functionwithdraw()public{require(msg.sender==owner);msg.sender.transfer(address(this).balance);}} 这份合约的功能是收集以太币。通过调用withdraw()函数,只允许所有者撤回所有的以太币。问题是,建构函数并非完全以合约的名称命名。具体来说,OwnerWallet和ownerWallet是不一样的。 因此,任何用户都可以调用ownerWallet()函数,将自己定位为所有者,然后通过调用withdraw()来获取合约中的所有以太币。 不过,这个问题已经在Solidity0.4.22版本的编译器中得到了解决。这个版本引入了一个构造函数关键字,用该关键字来指定构造函数,而不是要求函数的名称与合约名相匹配。建议使用此关键字指定构造函数,以防止上面强调的命名问题。 真实案例:Rubixi Rubixi的合约代码是另一个出现这种漏洞的「金字塔计划」。它最初叫做DynamicPyramid,但是在被部署到Rubixi之前,合约名字已经改变了。而构造函数的名称没有改变,允许任何用户成为创建者。 关于这个bug的一些有趣讨论可以在一些比特币论坛上找到。最终,它允许用户争夺创建者的地位,从金字塔计划中获得费用。 EVM将数据存为storage或memory。在开发合约时,准确地理解如何使用这个操作至关重要。否则可以因为利用不适当地初始化变量来产生有漏洞的合约。 函数中的局部变量根据它们的类型默认为存在内存中。未初始化的本地存储变量可以指向合约中其他意想不到的存储变量,从而导致有意或无意的漏洞。 让我们考虑下面这个相对简单的名称注册合约: pragmasolidity^0.4.23;contractNameRegistrar{boolpublicunlocked=false;structNameRecord{bytes32name;addressmappedAddress;}mapping(address=>NameRecord)publicregisteredNameRecord;mapping(bytes32=>address)publicresolve;functionregister(bytes32_name,address_mappedAddress)public{NameRecordnewRecord;newRecord.name=_name;newRecord.mappedAddress=_mappedAddress;resolve[_name]=_mappedAddress;registeredNameRecord[msg.sender]=newRecord;require(unlocked);//如果不是解锁状态是不可以register的}}这个简单的名称注册合约只有一个函数。当合约解锁时,它允许任何人注册一个名称(作为bytes32哈希),并将该名称映射到地址上。 不幸的是,这个注册器最初是锁定的,最后的require阻止了register()函数添加名称记录。然而,在这个合约中存在一个漏洞,它允许名称注册,而不顾及unlocked的变量。 为了讨论这个漏洞,首先我们需要了解存储在Solidity中是如何工作的。简单来说,状态变量按照合约中出现的顺序保存在slot中(它们可以组合在一起,但不是在这个例子中的问题,所以不过多讨论)。 因此,解锁存在于slot0中,registeredNameRecord存在于slot1中,resolve存在于slot2中(结构体不考虑在内,为什么呢????????)。每个slot都是32字节大小(我们现在忽略了映射的复杂性)。 布尔值unlocked,对于false看起来像0x000...0(64个0,不包括0x)或对于true来说是0x000...1(63个0)。正如你所看到的,在这个特殊的例子中存在着巨大的存储空间。 我们需要的下一个信息是Solidity默认的复杂数据类型(如结构)NameRecordnewRecord;,在初始化时作为局部变量存储它们。因此,新记录在NameRecordnewRecord;默认为storage。这种漏洞是由于newRecord没有初始化而引起的。因为它默认为存储,它成为一个指向存储的指针,因为它是未初始化的,它指向了slot0(即存储解锁的地方)。 nameRecord.name 并为 _mappedAddress设置了 nameRecord.mappedAddress 这实际上改变slot0和slot1的存储位置,这两个位置同时修改了已解锁的存储空间和与 registeredNameRecord 这意味着,只需通过寄存器函数的bytes32名称参数,就可以直接修改解锁。因此,如果名称的最后一个字节是非零的,它将修改存储slot0的最后一个字节,并直接将unlocked更改为true。 这样的_name值将在unlocked为true的时候通过require()。在Remix中尝试一下这个例子。 注意,如果_name使用了以下值的函数: 0x0000000000000000000000000000000000000000000000000000000000000001 则会通过执行。 一开始为: 然后调用函数register,参数为(0x0000000000000000000000000000000000000000000000000000000000000001,0xca35b7d915458ef540ade6068dfe2f44e8fa733c) 可见unlocked变为了true: Solidity的编译器将未初始化的存储变量作为了警告,因此开发者在构建智能合约时应该注意这些警告。当前版本的mist(0.10)不允许编译这些合约。在处理复杂类型时,要明确使用内存还是存储,以确保它们按预期运行。 那就显示初始化: 但是这样会报错: NameRecordstoragenewRecord=NameRecord(_name,_mappedAddress); NameRecordmemorynewRecord=NameRecord(_name,_mappedAddress); 真实案例:蜜罐OpenAddressLottery和CryptoRoulette 有一个名为OpenAdditsLottery的Honeypot使用了另外一个未初始化的存储变量,从一些可能的黑客那里收集以太币。 这份合约相当有深度,在Reddit上有一个深度讨论的帖子,感兴趣的话可以去研究一下。 Reddit地址: 另一个honeypot叫CryptoRoulette,也利用了这个技巧来收集一些以太币。你可以在下面地址找到详细的解读: 在Solidityv0.4.24中,还不支持定点或浮点数。这意味着浮点表示必须在Solidity中使用整数类型。如果实现不当,这可能会导致错误/漏洞。 由于Solidity中没有定点类型,开发者必须使用标准的整型数据类型来实现他们自己的数据。在这个过程中,可能会遇到很多陷阱。 比如下面代码所示的(请忽略溢出和下溢): pragmasolidity^0.4.23;contractFunWithNumbers{uintconstantpublictokensPerEth=10;uintconstantpublicweiPerEth=1e18;mapping(address=>uint)publicbalances;functionbuyTokens()publicpayable{uinttokens=msg.value/weiPerEth*tokensPerEth;//7balances[msg.sender]+=tokens;}functionsellTokens(uinttokens)public{require(balances[msg.sender]>=tokens);uinteth=tokens/tokensPerEth;//13balances[msg.sender]-=tokens;msg.sender.transfer(eth*weiPerEth);}}这个简单的代币买卖合约在购买和出售代币过程中有一些明显的问题。虽然买卖代币的数学计算是正确的,但缺少浮点数会导致错误的结果。例如,当在第7行上购买代币时,如果值小于1以太币,初始除法的结果是0,最后乘法的结果也为0(例如200wei除以1e18,weiPerEth等于0)。 同样地,在13行,当出售代币时,任何小于10的代币也会导致结果为0。事实上,这里的四舍五入总是在往下走,所以卖出29个代币,就会产生2以太币。 因此,这份合约的问题是其精度仅限于最近的以太币(例如1e18wei)。当你需要更高的精度时,或者在处理ERC20代币中的小数时,有时就会很头疼。 在智能合约中保持正确的精度是非常重要的,尤其是在处理反映经济决策的比率和利率的问题时,应该确保所使用的任何比率或利率允许大数字。 1)例如,在上面的例子中,我们使用了tokensPerEth作为利率。但如果使用weiPerTokens会更好,因为它是一个很大的数字。为了解决代币的数量,我们可以做 msg.sender/weiPerTokens 这将得到一个更精确的结果。 2)另一个需要牢记的是操作的顺序。在上面的例子中,购买代币的计算是 msg.value/weiPerEth*tokenPerEth 请注意,除法发生在乘法之前。如果计算先执行乘法,然后进行除法,那么这个例子就会更加精确, 即msg.value*tokenpereth/weipereth 最后,在定义数字的任意精度时,需要将变量转换为更高的精度,执行所有的数学操作,然后在需要的时候,再转换回输出的精度。通常使用uint256(因为它们最适合gas的使用),在uint256的范围内,大约有60个数量级,其中一些可以专门用于精确的数学运算。 在这种情况下,最好将所有变量保持在稳定的高精度,并在外部应用程序中转换回较低的精度(这实际上就是ERC20代币合约中小数变量的工作原理)。为了了解如何实现这一点以及库是如何做到这一点的,推荐查看MakerDAODSMath。 真实案例:Ethstick 其实,我没有找到一个特别好的例子来说明四舍五入在合约中引起的问题,但我肯定有很多这样的例子。 如果非要说一下的话,那我们就说下Ethstick好了。这个合约不使用任何扩展的精度,然而它却处理了wei。因此,这份合约会存在四舍五入的问题,但只是在精度的微观层面上。 它还有一些更严重的缺陷,但这些都与区块链上获得熵的难度有关。 Solidity有一个全局变量tx.origin,它遍历整个调用堆栈,并返回原先发送调用(或事务)的帐户地址。在智能合约中使用此变量进行身份验证会使合约很容易受到类似网络钓鱼的攻击。 例如下面合约: contractAttackContract{PhishablephishableContract;addresspublicattracker;constructor(Phishable_phishableContract,address_attackerAddress)public{phishableContract=_phishableContract;attracker=_attackerAddress;}function()public{phishableContract.withdrawAll(attracker);}}为了利用这个合约,攻击者会先对其进行部署,然后说服Phishable合约的所有者向这份合约发送某些数量的以太币。攻击者可以把这个合约伪装成他们自己的私人地址,然后让受害者向地址发送某种形式的交易。 如果不是特别谨慎,几乎不可能注意到代码中有攻击者的地址。而且攻击者也可能会把它当做一个多重签名钱包或者一些高级的存储钱包。 无论什么时候,只要受害者向AttackContract地址发送一个交易(有足够的gas),它都将调用fallback函数,fallback函数又调用Phishable合约的withdrawal()函数,调用参数为attacker。 这样一来就会造成,从Phishable合约中取回所有的资金到了攻击者的地址上。因为这是受害者第一个初始化调用的地址(即Phishable合约的拥有者)。因此,tx.origin会等于owner,这样,在Phishable合约第11行上的require将会顺利执行。 例如,如果一个人想要拒绝外部合约调用当前的合约,可以通过require(tx.origin==msg.sender)实现这一要求。这就阻止了中间合约被用来调用当前的合约,从而将合约限制为无码地址。 真实案例:未知 关于这个坑的真实案例,目前还没有发现。 常混以太坊社区的人,不难发现以太坊有一些有趣的「怪癖」。如果利用好这些「怪癖」,则对智能合约开发很有帮助。 无键(Keyless)以太币 合约地址是确定的,这意味着在实际创建地址之前就可以先对其进行计算。创建合约的地址和产生其他合约的合约也是这种情况。事实上,一份已创建的合约地址由以下函数决定: keccak256(rlp.encode([ Keccak256(rlp.encode(accountaddress,transactionnonce)) 基本上,一个合约的地址仅仅是一个kecca256哈希,它创建了与帐户交易随机数的联系。对于合约也是如此,但不包括那些合约nonce从1开始而地址的交易nonce从0开始的合约。 这也就是说,给定一个以太坊地址,我们就可以计算出这个地址可能产生的所有合约地址。例如,如果地址0x123000...000是为了在其第100次交易中创建一个合约,它将通过 kecca256(rlp.encode[0x123...000,100]) 创建合约地址,从而得到合约地址 0xed4cafc88a13f5d58a163e61591b9385b6fe6d1a 这意味着,你可以将以太币发送到一个预先确定的地址(一个没有私人密钥的地址),然后通过稍后在同一个地址上创建的一个合约再取回以太币。构造函数可以用来返回所有预先发送的以太币。 因此,即使有人获取了你所有的以太坊私钥,也很难发现你的以太坊地址或者访问这些隐藏的以太币。事实上,如果攻击者花费了大量的交易,以至于nonce需要访问被你所用到的以太币,它也还是不可能恢复你隐藏的以太币。 我们可以用用下面合约来说明这一点: pragmasolidity^0.4.23;contractKeylessHiddenEthCreator{uintpubliccurrentContractNonce=1;functionfutureAddresses(uint8nonce)publicviewreturns(address){//你自己选择nonce,然后根据你选择的nonce以及你自己的账户来生成一个合约地址if(nonce==0){returnaddress(keccak256(0xd6,0x94,this,0x80));}returnaddress(keccak256(0xd6,0x94,this,nonce));}functionretrieveHiddenEther(addressbeneficiary)publicreturns(address){//beneficiary即受益人currentContractNonce+=1;//比如如果上面的nonce选择的是2,那么只需要调用一次retrieveHiddenEther函数就能够得到藏在上面生成的合约中的以太币returnnewRecoverContract(beneficiary);}function()payable{}}contractRecoverContract{constructor(addressbeneficiary)public{selfdestruct(beneficiary);}}这个合约允许你存储无密钥的以太币。这个函数可以用来计算第一个127个合约地址,并且可以通过指定nonce来产生。 那么如何避免这一情况呢?我们可以将以太币发送到标准以太坊账户中的地址,然后在正确的nonce中恢复它。但是要小心,如果你意外地超过了需要回收自己以太币的交易nonce,你的以太币将永远丢失。 一次性地址 交易签名采用了椭圆曲线数字签名算法(ECDSA)。按照惯例,为了在以太坊上发送一个已验证的交易,开发者可以使用以太坊私钥签署一个消息。 换句话说,你签署的信息是以太坊交易的组件,包括to、value、gas、gasPrice、nonce和data字段。以太坊签名的结果是三个数字v、r和s。感兴趣的话,可以读一下以太坊的黄皮书。 因此,一个以太坊交易的签名由一个消息和数字v、r以及s组成。我们可以通过使用消息(即交易的详细信息)、r和s来检查签名是否有效。如果派生的以太坊地址与交易的from字段匹配,那么我们就知道r和s是由拥有(或已经获得)私钥的人创建的,因此签名是有效的。 接着,我们考虑一下,假设我们没有私钥,而是为任意交易编写r和s的值。这个交易参数如下: *{to:"0xa9e",value:10e18,nonce:0}* 知道了这个地址,我们就可以发送10以太币到0x54321的地址,而不需要拥有地址的私钥,并且可以发送交易: *{to:"0xa9e",value:10e18,nonce:0,from:"0x54321"}* 这样我们就可以把钱从随机地址0x54321支付到我们选择的地址0xa9e中了。因此,我们可以设法将以太币储存在一个地址上(没有私钥),并使用一次性交易来收回以太币。 单笔交易空投 「空投」是指在一大群人中分配代币的过程。一般空投通过大量的交易进行处理,每次交易都更新单个或者一批用户的余额。这对于以太坊区块链来说既昂贵又费力。 不过,有一种替代方法,在这种方法中,用户的余额可以用单个交易的代币来完成。 方法是,创建一个Merkle树,其中作为叶子节点的所有用户的地址和余额都被记录在案。这项工作将在链下完成。 Merkle树可以公开发出,然后可以创建一个智能合约,其中包含merkle树的根哈希,它允许用户提交merkle证明来获取它们的代币。因此,一个单一的交易就会允许所有用户兑换他们空投的代币。