Dice Game

김현학·2024년 6월 26일
0

speedrun-ethereum

목록 보기
5/5

챌린지를 수행하며 기억에 남은 내용만 간단히 회고한다.

Contract

  • 이전에 작성한 것과 동일한 기준으로 external을 인터페이스에 담았고, public은 없어서 추상 계약은 건너뛰었다.
  • 함수의 기능만 놓고 본다면 다르게 정의할 수 있었지만, 테스트를 통과해야하므로 해당 명세에 맞춰 정의했다.
pragma solidity >=0.8.0 <0.9.0; //Do not change the solidity version as it negativly impacts submission grading
//SPDX-License-Identifier: MIT

import "./DiceGame.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

interface IRiggedRoll {
	receive() external payable;

	function withdraw(address payable to, uint256 amount) external;

	function riggedRoll() external payable;
}
  • Ownable 계약으로 정의되어, 배포 시에 정해둔 운영 계정으로 소유권을 넘기므로, onlyOwnder modifier를 사용하여 인가를 구현했다.
  • riggedRoll은 다양한 방법으로 구현할 수 있지만, 결국 이 역시도 테스트의 요구 사항에 맞추기 위해 다음 부분들을 신경썼다.
    1. value로 전달되는 eth 말고, 계정에 존재하는 eth를 기준으로 수행 가능 여부를 판단한다.
    2. 조건 충족 시rollTheDice()를 수행하는 것뿐만 아니라, 불충족 시 트랜잭션을 revert한다.
      • 트랜잭션을 최소화하는 방향은 바람직하다고 생각한다.
contract RiggedRoll is IRiggedRoll, Ownable {
	DiceGame public diceGame;

	constructor(address payable diceGameAddress) {
		diceGame = DiceGame(diceGameAddress);
	}

	// +---------------------------------------------------+
	// |                     EXTERNALS                     |
	// +---------------------------------------------------+

	// enable the contract to receive incoming Ether
	receive() external payable {}

	// transfer Ether from the rigged contract to the owner
	function withdraw(
		address payable to,
		uint256 amount
	) external override onlyOwner {
		to.transfer(amount);
	}

	// only initiate a roll when it guarantees a win.
	function riggedRoll() external payable override {
		uint256 cost = 0.002 ether;
		require(address(this).balance >= cost, "Insufficient balance to play");
		require(canWin(), "You can't win this roll");
		diceGame.rollTheDice{ value: cost }();
	}
  • 현재 네트워크에서 트랜잭션이 수행됨에 따라 block.number가 증가하고, 본 계약에 대한 트랜잭션을 진행함에 따라 nonce가 증가하므로, (물론 예상 불가능한 무작위성은 아니지만) 난수를 생성할 수 있다.
    • block.number 만으로도 난수를 만들 수 있다고 생각했는데, nonce를 사용해야 한다면 replay attack을 방지하기 위함이 아닐까 추측해보았다. (일단 이 방법 자체가 보안상 안전하지 않음은 자명하다.)

안전한 난수?

  • 블록체인 자체가 탈중앙화를 지향하고, 모든 노드에 대해 동일한 결과를 보장해야 하므로, 보안적으로 보다 안전한 난수를 만드는 것은 쉽지 않은 문제이다.
  • 지금으로서는 Chainlink를 활용하여 난수 생성 람다를 호출하거나, Chainlink VRF를 사용하는 방식이 가장 단순해보인다.

    이론에 대한 내용들은 링크를 참조

	// +---------------------------------------------------+
	// |                     INTERNALS                     |
	// +---------------------------------------------------+

	// check if the roll will be a win
	function canWin() private view returns (bool) {
		return predictRoll() <= 5;
	}

	// predict the randomness in the DiceGame contract
	function predictRoll() private view returns (uint256) {
		bytes32 prevHash = blockhash(block.number - 1);
		bytes32 hash = keccak256(
			abi.encodePacked(prevHash, address(diceGame), diceGame.nonce())
		);
		uint256 roll = uint256(hash) % 16;
		return roll;
	}
}

0개의 댓글