pragma solidity ^0.8.17; contract CryptoCTF10 { enum PlayerStatus { Unverified, Verified, Banned } struct Player { PlayerStatus status; uint score; } modifier onlyExistingContest(uint contestID) { require(contests[contestID].admin != address(0), "Unknown contest ID"); _; } modifier onlyAdmin(uint contestID) { require(msg.sender == contests[contestID].admin, "You are not the admin of this contest"); _; } modifier onlyOpen(uint contestID) { require(block.timestamp < contests[contestID].deadline, "Submissions are not open for this contest at this time"); _; } modifier onlyExistingChallenge(uint contestID, uint challengeID) { require(contests[contestID].challenges[challengeID].obscuredFlag != address(0), "Unknown challenge ID"); _; } struct Challenge { address obscuredFlag; // essentially the public key of the flag, the flag being a private key uint worth; uint256 descriptionFingerprint; bool onlyFirstSolver; string skill; } struct Contest { address admin; mapping (uint => Challenge) challenges; mapping (address => Player) players; uint256 deadline; mapping (address => mapping (uint => bool)) solves; // address -> challengeID -> solved/not mapping (uint => bool) anySolves; // challengeID -> solved/not string password; } mapping (uint => Contest) public contests; event ChallengeAddedOrUpdated(uint contestID, uint indexed challengeID); event ChallengeSolved(uint contestID, uint indexed challengeID, address indexed solver); function createContest(uint contestID, string memory password) external { require(contests[contestID].admin == address(0), "This contest ID has already been registered"); contests[contestID].admin = msg.sender; contests[contestID].password = password; } function setAdmin(uint contestID, address newAdmin) external onlyExistingContest(contestID) onlyAdmin(contestID) { require(newAdmin != address(0)); contests[contestID].admin = newAdmin; } function setContestDeadline(uint contestID, uint256 deadline) external onlyExistingContest(contestID) onlyAdmin(contestID) { contests[contestID].deadline = deadline; } function addOrUpdateChallenge(uint contestID, uint challengeID, address obscuredFlag, uint worth, uint256 descriptionFingerprint, bool onlyFirstSolver, string memory skill) external onlyExistingContest(contestID) onlyAdmin(contestID) { require(obscuredFlag != address(0), "The obscured flag value must not be 0"); contests[contestID].challenges[challengeID] = Challenge(obscuredFlag, worth, descriptionFingerprint, onlyFirstSolver, skill); emit ChallengeAddedOrUpdated(contestID, challengeID); } function register(uint contestID, string memory password) external onlyExistingContest(contestID) { require(contests[contestID].players[msg.sender].status == PlayerStatus.Unverified, "You are already registered or banned in this contest"); require(keccak256(abi.encodePacked(password)) == keccak256(abi.encodePacked(contests[contestID].password)), "Wrong password"); contests[contestID].players[msg.sender].status = PlayerStatus.Verified; } function setPlayerStatus(uint contestID, address player, PlayerStatus status) external onlyExistingContest(contestID) onlyAdmin(contestID) { contests[contestID].players[player].status = status; } function submitFlag(uint contestID, uint challengeID, bytes memory signature) external onlyExistingContest(contestID) onlyExistingChallenge(contestID, challengeID) onlyOpen(contestID) { require(contests[contestID].players[msg.sender].status == PlayerStatus.Verified, "You are unverified or banned in this contest"); // the correct signature is an ECDSA signature where (1) the message (hash) is the sender address and (2) the private key is the flag; // (2) is checked by testing against the public key, which can then be public information address recoveredSigner = recoverSigner(bytes32(uint256(uint160(msg.sender))), signature); require(recoveredSigner != address(0), "Invalid signature"); require(recoveredSigner == contests[contestID].challenges[challengeID].obscuredFlag, "Wrong answer"); require(!contests[contestID].solves[msg.sender][challengeID], "You have already solved this challenge of this contest"); if (!contests[contestID].anySolves[challengeID] || !contests[contestID].challenges[challengeID].onlyFirstSolver) { contests[contestID].players[msg.sender].score += contests[contestID].challenges[challengeID].worth; } contests[contestID].solves[msg.sender][challengeID] = true; contests[contestID].anySolves[challengeID] = true; emit ChallengeSolved(contestID, challengeID, msg.sender); } function recoverSigner(bytes32 messageHash, bytes memory signature) public pure returns (address) { (bytes32 r, bytes32 s, uint8 v) = splitSignature(signature); return ecrecover(messageHash, v, r, s); } function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v) { require(sig.length == 65, "Invalid signature length"); assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) } } function getContestDeadline(uint contestID) external view onlyExistingContest(contestID) returns (uint256) { return contests[contestID].deadline; } function getPlayerStatus(uint contestID, address player) external view onlyExistingContest(contestID) returns (PlayerStatus) { return contests[contestID].players[player].status; } function getPlayerScore(uint contestID, address player) external view onlyExistingContest(contestID) returns (uint) { return contests[contestID].players[player].score; } }