diff --git a/README.md b/README.md index fed67e5..0b3af49 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ -# DCTF +# DCTF Beta -Decentralized CTF engine, used by CCTF \ No newline at end of file +Decentralized CTF engine, used by CCTF + +Testnet deployment on Moonbase Alpha: https://moonbase.moonscan.io/address/0x919f68cc35ce5d49a45c94dc44e7bf444f9a7531 + +Contributors: six, Silur, Anonz team, Metafaka team, BMEta team \ No newline at end of file diff --git a/contracts/CCTF_2022.sol b/contracts/CCTF_2022.sol new file mode 100644 index 0000000..018dca2 --- /dev/null +++ b/contracts/CCTF_2022.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// Authors: six and Silur +pragma solidity ^0.8.16; + +contract CCTF9 { + address public admin; + uint256 public volStart; + uint256 public volMaxPoints; + uint256 public powDiff; + bool public started; + + enum PlayerStatus { + Unverified, + Verified, + Banned + } + + struct Player { + PlayerStatus status; + uint256 points; + } + + modifier onlyAdmin { + require(msg.sender == admin, "Not admin"); + _; + } + + modifier onlyActive { + require(started == true, "CCTF not started."); + _; + } + + struct Flag { + address signer; + bool onlyFirstSolver; + uint256 points; + string skill_name; + } + + mapping (address => Player) public players; + mapping (uint256 => Flag) public flags; + + event CCTFStarted(uint256 timestamp); + event FlagAdded(uint256 indexed flagId, address flagSigner); + event FlagRemoved(uint256 indexed flagId); + event FlagSolved(uint256 indexed flagId, address indexed solver); + + constructor(uint256 _volMaxPoints, uint256 _powDiff) { + admin = msg.sender; + volMaxPoints = _volMaxPoints; + powDiff = _powDiff; + started = false; + } + + function setAdmin(address _admin) external onlyAdmin { + require(_admin != address(0)); + admin = _admin; + } + + function setCCTFStatus(bool _started) external onlyAdmin { + started = _started; + } + + function setFlag(uint256 _flagId, address _flagSigner, bool _onlyFirstSolver, uint256 _points, string memory _skill) external onlyAdmin{ + flags[_flagId] = Flag(_flagSigner, _onlyFirstSolver, _points, _skill); + emit FlagAdded(_flagId, _flagSigner); + } + + function setPowDiff(uint256 _powDiff) external onlyAdmin { + powDiff = _powDiff; + } + + + function register(string memory _RTFM) external { + require(players[msg.sender].status == PlayerStatus.Unverified, 'Already registered or banned'); + //uint256 pow = uint256(keccak256(abi.encodePacked("CCTF", msg.sender,"registration", nonce))); + //require(pow < powDiff, "invalid pow"); + require(keccak256(abi.encodePacked('I_read_it')) == keccak256(abi.encodePacked(_RTFM))); // PoW can be used for harder challenges, this is Entry! + players[msg.sender].status = PlayerStatus.Verified; + } + + function setPlayerStatus(address player, PlayerStatus status) external onlyAdmin { + players[player].status = status; + } + + +////////// Submit flags + mapping(bytes32 => bool) usedNs; // Against replay attack (we only check message signer) + mapping (address => mapping (uint256 => bool)) Solves; // address -> challenge ID -> solved/not + uint256 public submission_success_count = 0; // For statistics + + function SubmitFlag(bytes32 _message, bytes memory signature, uint256 _submitFor) external onlyActive { + require(players[msg.sender].status == PlayerStatus.Verified, "You are not even playing"); + require(bytes32(_message).length <= 256, "Too long message."); + require(!usedNs[_message]); + usedNs[_message] = true; + require(recoverSigner(_message, signature) == flags[_submitFor].signer, "Not signed with the correct key."); + require(Solves[msg.sender][_submitFor] == false); + + Solves[msg.sender][_submitFor] = true; + players[msg.sender].points += flags[_submitFor].points; + players[msg.sender].points = players[msg.sender].points < volMaxPoints ? players[msg.sender].points : volMaxPoints; + + if (flags[_submitFor].onlyFirstSolver) { + flags[_submitFor].points = 0; + } + + submission_success_count = submission_success_count + 1; + emit FlagSolved(_submitFor, msg.sender); + } + + function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + return ecrecover(_ethSignedMessageHash, 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))) + } + } + +////////// Check status, scores, etc + function getPlayerStatus(address _player) external view returns (PlayerStatus) { + return players[_player].status; + } + + function getPlayerPoints(address _player) external view returns (uint256) { + return players[_player].points < volMaxPoints ? players[_player].points : volMaxPoints; + } + + function getSuccessfulSubmissionCount() external view returns (uint256){ + return submission_success_count; + } +} diff --git a/contracts/CCTF_by_Anonz.sol b/contracts/CCTF_by_Anonz.sol new file mode 100644 index 0000000..58d93df --- /dev/null +++ b/contracts/CCTF_by_Anonz.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// Authors: Anonz team, developed at Polkadot Metaverse Championship, as part of their CCTF track solution. +// Based on Six's and Silur's CCTF 2022 code. +pragma solidity ^0.8.16; + +contract CryptoCTFX { + 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(contests[contestID].submissionsOpen, "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; // public key of the flag + uint worth; + uint256 descriptionFingerprint; + bool onlyFirstSolver; + string skill; + } + + struct Contest { + address admin; + mapping (uint => Challenge) challenges; + mapping (address => Player) players; + bool submissionsOpen; + mapping (address => mapping (uint => bool)) solves; // address -> challengeID -> solved/not + mapping (uint => bool) anySolves; // challengeID -> solved/not + } + + 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) external { + require(contests[contestID].admin == address(0), "This contest ID has already been registered"); + contests[contestID].admin = msg.sender; + contests[contestID].submissionsOpen = false; + } + + function setAdmin(uint contestID, address newAdmin) external onlyExistingContest(contestID) onlyAdmin(contestID) { + require(newAdmin != address(0)); + contests[contestID].admin = newAdmin; + } + + function setSubmissionsStatus(uint contestID, bool open) external onlyExistingContest(contestID) onlyAdmin(contestID) { + contests[contestID].submissionsOpen = open; + } + + 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("I_read_it")) == keccak256(abi.encodePacked(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(abi.encodePacked(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 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; + } +} diff --git a/frontend/abi.js b/frontend/abi.js new file mode 100644 index 0000000..c703f66 --- /dev/null +++ b/frontend/abi.js @@ -0,0 +1,798 @@ +myABI = [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_volMaxPoints", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_powDiff", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "CCTFStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "flagSigner", + "type": "address" + } + ], + "name": "FlagAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + } + ], + "name": "FlagRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "solver", + "type": "address" + } + ], + "name": "FlagSolved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum CCTF9.PlayerStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "PlayerStatusChanged", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_message", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "_submitFor", + "type": "uint256" + } + ], + "name": "SubmitFlag", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "TIME_DECAY_MAX", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "endTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "flagCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "flagList", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "flags", + "outputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "getFlags", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "internalType": "struct CCTF9.Flag[]", + "name": "flagListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFlags", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "internalType": "struct CCTF9.Flag[]", + "name": "flagListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_player", + "type": "address" + } + ], + "name": "getPlayerPoints", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_player", + "type": "address" + } + ], + "name": "getPlayerStatus", + "outputs": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "getPlayers", + "outputs": [ + { + "components": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256[2][]", + "name": "pointsPerFlag", + "type": "uint256[2][]" + } + ], + "internalType": "struct CCTF9.Player[]", + "name": "playerListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPlayers", + "outputs": [ + { + "components": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256[2][]", + "name": "pointsPerFlag", + "type": "uint256[2][]" + } + ], + "internalType": "struct CCTF9.Player[]", + "name": "playerListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSuccessfulSubmissionCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "playerCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "playerList", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "players", + "outputs": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "powDiff", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_ethSignedMessageHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "recoverSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_RTFM", + "type": "string" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + } + ], + "name": "register", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_admin", + "type": "address" + } + ], + "name": "setAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_endTime", + "type": "uint256" + } + ], + "name": "setCCTFEndTime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_paused", + "type": "bool" + } + ], + "name": "setCCTFPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_startTime", + "type": "uint256" + } + ], + "name": "setCCTFStartTime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_flagId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_flagSigner", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "_flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_skill", + "type": "string" + } + ], + "name": "setFlag", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + } + ], + "name": "setPlayerStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_powDiff", + "type": "uint256" + } + ], + "name": "setPowDiff", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "sig", + "type": "bytes" + } + ], + "name": "splitSignature", + "outputs": [ + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "startTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "started", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "submission_success_count", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "volMaxPoints", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "volStart", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/frontend/cctf-icon.png b/frontend/cctf-icon.png new file mode 100644 index 0000000..5ae4a6e Binary files /dev/null and b/frontend/cctf-icon.png differ diff --git a/frontend/cctf-logo.png b/frontend/cctf-logo.png new file mode 100644 index 0000000..036bc8d Binary files /dev/null and b/frontend/cctf-logo.png differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..99a0374 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,121 @@ + + + + CCTF Frontend by Metafaka + + + + + + +
+ + + +
+
+ +
+
+ PLAYERS
+ loading... +
+
+ +
+
+ FOUND FLAGS
+ loading... +
+
+ +
+
+ --:--:-- +
+
+ +
+
+ + +
+ + Getting Started +
+
+
+
+ +
+
+

My Account

+
+ +
+ + +
+
+ +
+
Loading...
+

Leaderboard

+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/frontend/metafaka.png b/frontend/metafaka.png new file mode 100644 index 0000000..d9e12f5 Binary files /dev/null and b/frontend/metafaka.png differ diff --git a/frontend/metamask.svg b/frontend/metamask.svg new file mode 100644 index 0000000..963c73e --- /dev/null +++ b/frontend/metamask.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/script.js b/frontend/script.js new file mode 100644 index 0000000..c0cc5db --- /dev/null +++ b/frontend/script.js @@ -0,0 +1,330 @@ +"use strict"; + +document.addEventListener('DOMContentLoaded', function () { + + const contractAddress = '0xc8bd0A15E35Ddc1692D907583696443e2970779b'; + const nullAddress = '0x0000000000000000000000000000000000000000'; + var userAccount = nullAddress; + + var btnRegister = document.getElementById("register"); + var btnAccountEnable = document.getElementById("account_enable"); + + btnRegister.addEventListener('click', register); + btnAccountEnable.addEventListener('click', accountEnable); + + var funcType; + var mark; + var expirationTime; + var timerIsRunning = false; + var players = []; + + // Modern dapp browsers... + if(window.ethereum) { + console.log("Modern dapp browser detected"); + window.web3 = new Web3(window.ethereum); + checkNetworkType(); + ethereum.on('accountsChanged', (accounts) => accountChange(accounts[0])); + ethereum.on('chainChanged', (_chainId) => window.location.reload()); + } + // Legacy dapp browsers... + else if(window.web3) { + console.log("Legacy dapp browser detected"); + window.web3 = new Web3(web3.currentProvider); + // Acccounts always exposed + checkNetworkType(); + } + // Non-dapp browsers... + else { + console.log("Non-Ethereum browser detected. You should consider trying MetaMask!"); + window.web3 = new Web3(new Web3.providers.WebsocketProvider('wss://goerli.infura.io/ws/v3/d82552d5ad5841e5acbbe7a8417a23ad')); + } + + function switchToGoerli() { + const iconUrl = ""; + const goerli = { + chainId: "0x5", + chainName: "Goerli test network", + rpcUrls: ["https://goerli.infura.io/v3/"], + nativeCurrency: { + name: "GoerliETH", + symbol: "ETH", + decimals: 18, + }, + blockExplorerUrls: ["https://goerli.etherscan.io"], + iconUrls: [iconUrl], + }; + + const { request } = window.ethereum; + request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: goerli.chainId }], + }).catch(e => { + // This error code indicates that the chain has not been added to MetaMask. + if (e.code === 4902) { + return request({ + method: "wallet_addEthereumChain", + params: [goerli], + }); + } else { + throw e; + } + }); + } + + function checkNetworkType() { + web3.eth.net.getId() + .then(function(result) { + if(result != 5) { /* Goerli Mainnet */ + alert("Please select Goerli test network on your Metamask!"); + console.log('switchToGoerli'); + switchToGoerli(); + } + console.log('Network Id:', result); + }); + } + + function displayShortAddress(address) { + return(address.slice(0, 6) + "..." + address.slice(-4)) + } + + function setUserAccount() { + var x = userAccount.toString(); + document.getElementById("user").textContent = displayShortAddress(x); + if(userAccount != nullAddress) { + document.getElementById("account_locked").style.display = "none"; + document.getElementById("account_unlocked").style.display = "block"; + } + else { + document.getElementById("account_locked").style.display = "block"; + document.getElementById("account_unlocked").style.display = "none"; + } + } + + function getPlayerStatus() { + myContract.methods.getPlayerStatus(userAccount).call() + .then(function(result) { + console.log("Player status: " + result); + if(result == 0) { + document.getElementById("unverified").style.display = "block"; + document.getElementById("verified").style.display = "none"; + document.getElementById("banned").style.display = "none"; + } + else if(result == 1) { + document.getElementById("unverified").style.display = "none"; + document.getElementById("verified").style.display = "block"; + document.getElementById("banned").style.display = "none"; + } + else { + document.getElementById("unverified").style.display = "none"; + document.getElementById("verified").style.display = "none"; + document.getElementById("banned").style.display = "block"; + } + }); + } + + function accountChange(newAccount) { + if(web3.utils.isAddress(newAccount)) { + userAccount = web3.utils.toChecksumAddress(newAccount); + } + else userAccount = nullAddress; + console.log('ACCOUNT: ' + userAccount); + console.log('CheckNetworkType'); + checkNetworkType(); + setUserAccount(); + getPlayerStatus(); + } + + async function getAccount(type) { + if (window.ethereum) { // for modern DApps browser + try { + await window.ethereum.request({ method: 'eth_requestAccounts' }); + } catch (error) { + console.error(error); + } + } + if (window.web3){ + await window.web3.eth.getAccounts().then(it => { + accountChange(it[0]); + if (type == "REGISTER") registerPlayer(); + }); + } + else { + alert('No Metamask or Ethereum wallet found. Read "Getting Started" for more information'); + console.log('No Metamask or Ethereum wallet found. Read "Getting Started" for more information'); + } + } + + function register() { + funcType = "REGISTER"; + getAccount(funcType); + } + + function accountEnable() { + funcType = "NONE"; + getAccount(funcType); + } + + function setPlayers(data) { + document.getElementById("players").textContent = data; + } + + function setSubmissions(data) { + document.getElementById("submissions").textContent = data; + } + + function setExpiration(newExpiration) { + if(! timerIsRunning) startTimer(); + expirationTime = newExpiration; + updateClock(); + } + + function setRank(data) { + var text = ""; + text += "PlayerStatusNameFlags foundPoints"; + + data.forEach(item => { + console.log(item[2]); + text += "" + item[0] + "" + item[2] + "" + item[3] + "" + item[1] + ""; + }); + + document.getElementById("loading").style.display = "none"; + document.getElementById("rank_list").innerHTML = text; + } + + function registerPlayer() { + var rtfm = 'I_read_it'; // todo: Checkbox check removed for easier testing + var playerName = document.getElementById("playerName").value; + myContract.methods.register(rtfm, playerName).send({from: userAccount}) + .then(function() { + console.log("register()"); + rank(); + getPlayerStatus(); + getPlayers(); + }); + } + + function isGameGoing() { + myContract.methods.started().call() + .then(function(result) { + console.log("is going: " + result); + return(result); + }); + } + + function startTimer() { + mark = setInterval(updateClock, 1000); + timerIsRunning = true; + } + + function stopTimer() { + clearInterval(mark); + timerIsRunning = false; + } + + function updateClock() { + const remainingTime = expirationTime - Math.floor((new Date()).getTime() / 1000); + + if(remainingTime > 0) { + //var hour = Math.floor(remainingTime / (60 * 60)) % 24; + var hour = Math.floor(remainingTime / (60 * 60)); + var min = Math.floor(remainingTime / 60) % 60; + var sec = remainingTime % 60; + if(hour < 10) hour = '0' + hour; + if(min < 10) min = '0' + min; + if(sec < 10) sec = '0' + sec; + document.getElementById("remaining_time").textContent = hour + ':' + min + ':' + sec; + } + else gameOver(); + } + + function gameOver() { + stopTimer(); + document.getElementById("remaining_time").textContent = "--:--:--"; + if(isGameGoing()) { + document.getElementById("remaining_time").textContent = "Game Over"; + } + else { + document.getElementById("remaining_time").textContent = "Not Started"; + } + } + + function expiration() { + myContract.methods.endTime().call() + .then(function(result) { + console.log("expiration: " + result); + setExpiration(result); + }); + } + + function getPlayers() { + myContract.methods.playerCount().call() + .then(function(result) { + console.log("playerCount: " + result); + setPlayers(result); + }); + } + + function submissions() { + myContract.methods.getSuccessfulSubmissionCount().call() + .then(function(result) { + console.log("SuccessfulSubmissionCount: " + result); + setSubmissions(result); + }); + } + + function rank() { + myContract.methods.getPlayers().call() + .then(function(result) { + console.log("Players: " + result); + setRank(result); + }); + } + + //=============================================================================================== + // Get the modal + var modal = document.getElementById('myModal'); + + // Get the button that opens the modal + var btn = document.getElementById("myBtn"); + + // Get the element that closes the modal + var span = document.getElementsByClassName("close")[0]; + + // When the user clicks the button, open the modal + btn.onclick = function() { + modal.style.display = "block"; + } + + // When the user clicks on (x), close the modal + span.onclick = function() { + modal.style.display = "none"; + } + + // When the user clicks anywhere outside of the modal, close it + window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + } + } + + //=============================================================================================== + var myContract; + myContract = new web3.eth.Contract(myABI, contractAddress); + + myContract.events.PlayerStatusChanged() + .on("data", function(event) { + var data = event.returnValues; + console.log("PlayerStatusChanged: " + data); + rank(); + }) + .on("error", function(error) { + var text = error; + console.log("PlayerStatusChanged: " + text); + }) + .on("error", console.error); + + expiration(); + getPlayers(); + submissions(); + rank(); +}); diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..90e2584 --- /dev/null +++ b/frontend/style.css @@ -0,0 +1,360 @@ +body { + margin: 0; + overflow: hidden; + font-family: 'Roboto Mono', monospace; + color: #FFFFFF; +} + +.top { + background: #45484d; + background: linear-gradient(to right, #000000 220px,#2D3436 50%,#2D3436 calc(50% + 220px),#000000 100%); + position: absolute; + display: flex; + width: 100%; + height: 70px; +} + +.top_logo { + width: 220px; + align-items: center; + display: flex; + font-size: 32px; + justify-content: center; +} + +.top_left { + width: calc(50% - 205px); + align-items: center; + display: flex; +} + +.top_center { + width: 190px; + align-items: center; + display: flex; + justify-content: center; +} + +.top_right { + width: calc(50% - 205px); + align-items: center; + display: flex; + justify-content: flex-end; +} + +.left { + background-color: #000000; + position: absolute; + left: 0; top: 70px; bottom: 0; + width: 220px; + overflow-y: auto; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 10+ */ +} + +.left::-webkit-scrollbar { + display: none; +} + +.main { + background: #bcbfbe; + background: radial-gradient(ellipse at center, #2D3436 0%,#000000 100%); + position: absolute; + left: 220px; top: 70px; right: 0; bottom: 0; +} + +.left .activated { + border: 1px solid #2FA232; + background: #FFFFFF; + color: #2FA232; +} + +.topitem { + display: inline-flex; + align-items: center; + white-space: nowrap; +} + +#playerName { + border: 0; + border-radius: 0.25rem; + font-family: 'Roboto Mono', monospace; + line-height: 1.5; + white-space: nowrap; + text-decoration: none; + margin: 10px 0; + padding: 0.25rem 0.5rem; + width: 93%; +} + +#register { + border: 0; + border-radius: 0.25rem; + color: #FFFFFF; + background: #2FA232; + background: linear-gradient(225deg, #166D3B 0%, #2FA232 100%); + font-family: 'Roboto Mono', monospace; + font-size: 1rem; + line-height: 1.5; + white-space: nowrap; + text-decoration: none; + margin: 10px 0; + padding: 0.25rem 0.5rem; + width: 100%; + cursor: pointer; +} + +#register:hover { + background: #2FA232; + background: linear-gradient(45deg, #166D3B 0%, #2FA232 100%); +} + +.left-content { + margin: 14px; +} + +#header_uppercase { + font-size: 13px; + color: #CCCCCC; + font-family: monospace; + white-space: nowrap; +} + +#account_enable { + border: 1px solid #dcdcdc; + border-radius: 0.25rem; + background: #f4f6f7; + font-family: 'Roboto Mono', monospace; + font-size: 1rem; + white-space: nowrap; + text-decoration: none; + padding: 0.25rem 0.5rem; + margin: 10px 0; + width: 100%; + cursor: pointer; +} + +#account_enable:hover { + background: #fafafb; + border-color: #2FA232; + color: #2FA232; +} + +#metamask { + width: 20px; + vertical-align: text-top; + padding-right: 10px; +} + + + + +/* The Modal (background) */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content */ +.modal-content { + position: relative; + background-color: #000000; + margin: 70px auto; + padding: 0; + max-width: 800px; + border: 3px solid #2FA232; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); + -webkit-animation-name: animatetop; + -webkit-animation-duration: 0.4s; + animation-name: animatetop; + animation-duration: 0.4s +} + +/* Add Animation */ +@-webkit-keyframes animatetop { + from {top:-300px; opacity:0} + to {top:0; opacity:1} +} + +@keyframes animatetop { + from {top:-300px; opacity:0} + to {top:0; opacity:1} +} + +/* The Close Button */ +.close { + float: right; + font-size: 28px; +} + +.close:hover, +.close:focus { + color: #BBBBBB; + text-decoration: none; + cursor: pointer; +} + +.modal-header { + padding: 1px 12px 1px 24px; +} + +.modal-body {padding: 2px 16px;} + +.modal-footer { + padding: 16px; +} + +.top a:link, .top a:visited, .top a:hover, .top a:active { + color: white; + text-decoration: none; +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #d8dadb; + margin-top: 1em; + padding: 0; +} +.buttons { + text-align: center; +} + +.buttons i { + font-size: 24px; + margin: 2px 0; + background-color: #2FA232; + background-image: linear-gradient(45deg, #8929AD 0%, #2FA232 50%, #43B7B8 100%); + background-size: 100%; + -webkit-background-clip: text; + -moz-background-clip: text; + -webkit-text-fill-color: transparent; + -moz-text-fill-color: transparent; +} + +li { + padding-bottom: 10px; +} + +.form-switch { + display: inline-block; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.form-switch i { + position: relative; + display: inline-block; + width: 46px; + height: 26px; + background-color: #e6e6e6; + border-radius: 23px; + vertical-align: text-bottom; + transition: all 0.3s linear; +} + +.form-switch i::before { + content: ""; + position: absolute; + left: 0; + width: 42px; + height: 22px; + background-color: #fff; + border-radius: 11px; + transform: translate3d(2px, 2px, 0) scale3d(1, 1, 1); + transition: all 0.25s linear; +} + +.form-switch i::after { + content: ""; + position: absolute; + left: 0; + width: 22px; + height: 22px; + background-color: #fff; + border-radius: 11px; + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6); + transform: translate3d(2px, 2px, 0); + transition: all 0.2s ease-in-out; +} + +.form-switch:active i::after { + width: 28px; + transform: translate3d(2px, 2px, 0); +} + +.form-switch:active input:checked + i::after { transform: translate3d(16px, 2px, 0); } + +.form-switch input { display: none; } + +.form-switch input:checked + i { background-color: #2FA232; } + +.form-switch input:checked + i::before { transform: translate3d(18px, 2px, 0) scale3d(0, 0, 0); } + +.form-switch input:checked + i::after { transform: translate3d(22px, 2px, 0); } + +h1 { + font-size: 16px; + margin-top: 1em; + margin-bottom: 0; + margin-left: 0; + margin-right: 0; + color: #2FA232; +} + +#loading { + position: absolute; + top: 50%; + left: 50%; + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + text-align: center; + text-shadow: 2px 2px 4px #FFFFFF; +} + +.dropdown-p { + text-align: center; +} + +#rank_list { + width: 90%; + border-collapse: collapse; + border-style: hidden; + text-align: center; + margin-left: auto; + margin-right: auto; +} + +#rank_list th { + border: 1px solid #5f5f5f; + padding: 8px; + min-width: 40px; +} + +#rank_list td { + border: 1px solid #5f5f5f; + padding: 8px; + min-width: 40px; +} + +.center { + display: block; + margin-left: auto; + margin-right: auto; + width: 20%; + } + +#rules { + color: #cccccc; + font-size: 14px; +}