Created
July 23, 2020 09:58
-
-
Save codemedici/07704c90e2c6fe5ca3054b1f61b76a69 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.6.0; | |
/* | |
* @dev Provides information about the current execution context, including the | |
* sender of the transaction and its data. While these are generally available | |
* via msg.sender and msg.data, they should not be accessed in such a direct | |
* manner, since when dealing with GSN meta-transactions the account sending and | |
* paying for execution may not be the actual sender (as far as an application | |
* is concerned). | |
* | |
* This contract is only required for intermediate, library-like contracts. | |
*/ | |
abstract contract Context { | |
function _msgSender() internal view virtual returns (address payable) { | |
return msg.sender; | |
} | |
function _msgData() internal view virtual returns (bytes memory) { | |
this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 | |
return msg.data; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.6.0; // TODO change this to v 0.6 by upgrading compiler version in truffle-config.js | |
import "./Ownable.sol"; | |
contract Leaderboard is Ownable { | |
// levelsRegistered's key address is the level while the value address is the user's, useed to check if level already exists and its owner (player) | |
// i.e. contains all deployed contracts, used to check that the submitted level's address belongs to the user | |
mapping(address => address) public levelsRegistered; | |
mapping(uint8 => _Level) public levels; // levels deployed by Owner, i.e. 'official' levels containing challenge | |
mapping(address => _Player) public players; | |
mapping(address => address) public linkedLeaderboard; | |
uint256 public listSize; | |
address constant GUARD = address(1); | |
address constant NULL = address(0); | |
struct _Player { | |
string username; | |
uint256 score; | |
mapping(uint8 => address) playerLevels; // contains the address of the latest deployed contract for each level name | |
mapping(uint8 => bool) solved; // use to check the user can't submit solution twice, true if solved | |
} | |
struct _Level { | |
string levelName; | |
uint256 levelPoints; | |
bytes32 levelBytesHash; // used in registerLevel() to check the user is deoloying the right contract, i.e. not cheating. | |
// address levelAddress; | |
} | |
constructor() public { | |
linkedLeaderboard[GUARD] = GUARD; | |
// players[msg.sender].username = 'Extropy'; | |
} | |
// https://solidity.readthedocs.io/en/v0.5.3/assembly.html#example | |
function at(address _addr) public view returns(bytes memory o_code) { | |
assembly { | |
// retrieve the size of the code, this needs assembly | |
let size := extcodesize(_addr) | |
// allocate output byte array - this could also be done without assembly | |
// by using o_code = new bytes(size) | |
o_code := mload(0x40) | |
// new "memory end" including padding | |
mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) | |
// store length in memory | |
mstore(o_code, size) | |
// actually retrieve the code, this needs assembly | |
extcodecopy(_addr, add(o_code, 0x20), 0, size) | |
} | |
} | |
function setUsername(string calldata _username) external { | |
players[msg.sender].username = _username; | |
// TODO (optional) only allow unique usernames | |
} | |
function registerLevel(address _deployedAt, uint8 _levelNumber) public { | |
// public because called internally as well | |
// submitting a solution requires registering a level so that user's can't submit others' solutions | |
require(levelsRegistered[_deployedAt] == NULL); // cannot be registered already | |
// the ABI bytecode hashes of the original and deployed contracts must match | |
require(levels[_levelNumber].levelBytesHash == keccak256(at(_deployedAt))); | |
// register globally so that player's can't register it and submit each others' solutions | |
levelsRegistered[_deployedAt] = msg.sender; | |
// register to _Player struct so that _levelNumber can be used to call submitSolution() | |
players[msg.sender].playerLevels[_levelNumber] = _deployedAt; | |
} | |
function verifyIndex(address prevPlayer, uint256 newValue, address nextPlayer) internal view returns(bool) { | |
return (prevPlayer == GUARD || players[prevPlayer].score >= newValue) && | |
(nextPlayer == GUARD || newValue > players[nextPlayer].score); | |
} | |
function addPlayer(address _newPrevPlayer) internal { | |
// this function adds a new player to the sorted mapping, i.e. it | |
// The previous player must exist | |
require(linkedLeaderboard[_newPrevPlayer] != NULL); | |
// _newPrevPlayer is the player whose score is immediately greater or equal to the current player. | |
// this means that looping through the list must be done client side by web3 and it will be possible to insert the player using linked lists | |
// only if it passes the check by verifyIndex. | |
require(verifyIndex(_newPrevPlayer, players[msg.sender].score, linkedLeaderboard[_newPrevPlayer])); | |
// point msg.sender to whatever addr _newPrevPlayer was pointing to | |
linkedLeaderboard[msg.sender] = linkedLeaderboard[_newPrevPlayer]; | |
// point _newPrevPlayer to msg.sender | |
// i.e. msg.sender is now after _newPrevPlayer in the scores, sorted in descending order | |
linkedLeaderboard[_newPrevPlayer] = msg.sender; | |
listSize++; | |
} | |
function updateScore( | |
address oldCandidatePlayer, | |
address newCandidatePlayer | |
) internal { | |
// the msg.sender must already exist | |
require(linkedLeaderboard[msg.sender] != NULL); | |
require(linkedLeaderboard[oldCandidatePlayer] != NULL); | |
require(linkedLeaderboard[newCandidatePlayer] != NULL); | |
if (oldCandidatePlayer == newCandidatePlayer) { | |
require(linkedLeaderboard[oldCandidatePlayer] == msg.sender); // require(_isPrevStudent(msg.sender, oldCandidatePlayer)); | |
require(verifyIndex(newCandidatePlayer, players[msg.sender].score, linkedLeaderboard[newCandidatePlayer])); | |
// players[msg.sender] = newScore; | |
} else { | |
removePlayer(oldCandidatePlayer); | |
addPlayer(newCandidatePlayer); | |
} | |
} | |
function removePlayer(address candidatePlayer) internal { | |
require(linkedLeaderboard[msg.sender] != NULL); | |
require(linkedLeaderboard[candidatePlayer] == msg.sender); // require(_isPrevStudent(msg.sender, candidateStudent)); | |
linkedLeaderboard[candidatePlayer] = linkedLeaderboard[msg.sender]; | |
linkedLeaderboard[msg.sender] = NULL; | |
listSize--; | |
} | |
function getTop(uint256 k) public view returns(address[] memory) { | |
// TODO needs to be deprecated as it is cheaper to do this client side | |
require(k <= listSize); | |
address[] memory playerLists = new address[](k); | |
address currentAddress = linkedLeaderboard[GUARD]; | |
for(uint256 i = 0; i < k; ++i) { | |
playerLists[i] = currentAddress; | |
currentAddress = linkedLeaderboard[currentAddress]; | |
} | |
return playerLists; | |
} | |
function submitSolution(uint8 _levelNumber) external { | |
address _registeredLevel = players[msg.sender].playerLevels[_levelNumber]; | |
// level must be registered by user first | |
require(_registeredLevel != NULL); | |
// checks that it hasn't been solved yet | |
require(players[msg.sender].solved[_levelNumber] == false); | |
// check return value of public var from deployed contract's instance | |
(bool _success, bytes memory _result) = address(_registeredLevel).call(abi.encodeWithSignature("levelComplete()")); | |
(bool levelSolved) = abi.decode(_result, (bool)); | |
require(levelSolved); | |
players[msg.sender].solved[_levelNumber] = true; // mark the level as solved | |
// Add the points to the Player | |
players[msg.sender].score += levels[_levelNumber].levelPoints; | |
// now that points have been added, check the new score on the frontend and then call updateLeaderboard() | |
} | |
// @param _newPrevPlayer is the player with the score immediately higher than or equal to the current player's new score (after submitting a solution) | |
// @param _oldPrevPlayer is the player pointing to the current player's old score which needs to be updated with new score | |
// if the player is not in the leaderboard yet, _oldPrevPlayer will not be used, i.e. pass in 0x0 for simplicity | |
function updateLeaderboard(address _newPrevPlayer, address _oldPrevPlayer) external { | |
// there is no need to check for players[msg.sender].solved[_levelNumber] since verifyIndex takes care of checking the provided leaderboard index matches the player's score | |
if (linkedLeaderboard[msg.sender] == NULL) { | |
// if the player is not in the leaderboard yet | |
addPlayer(_newPrevPlayer); | |
} else { | |
// if linkedLeaderboard[msg.sender] is not null addr it means that is already in the scoreboard | |
// _newPrevPlayer point to the new location on the scoreboard (another address), but we also need to remove the old instance first | |
updateScore(_oldPrevPlayer, _newPrevPlayer); | |
} | |
} | |
function editLevel(string calldata _levelName, uint8 _levelNumber, uint256 _levelPoints, address _levelAddress) external onlyOwner { | |
// @note This will override a level's mapping if it already exists | |
// @note We don't actually deploy any level, we just keep a struct with the details necessary to stop users from cheating | |
// @dev technically we could calculate the hash of the level's bytes offline but providing a function to do so is more consistent, | |
// @audit consider if disclosing (in call parameter) the address of others' levels could incentivise cheating or malicious behavior | |
// @TODO (optional) create a public function to let users deploy their own level, and populate the list on the frontend, let users vote on others' levels | |
bytes memory _byteCode = at(_levelAddress); | |
bytes32 _levelBytesHash = keccak256(_byteCode); | |
levels[_levelNumber] = _Level(_levelName, _levelPoints, _levelBytesHash); | |
// Finally call registerLevel as the owner so that users cannot use this deployed contract to submit their solution | |
registerLevel(_levelAddress, _levelNumber); | |
} | |
function getUsername(address _account) public view returns(string memory) { | |
// string memory usr = players[_account].username; | |
return(players[_account].username); | |
} | |
function getScore(address _account) public view returns(uint256) { | |
// string memory usr = players[_account].username; | |
return(players[_account].score); | |
} | |
// I couldn't find a way to get the _Player mapping's value via web3, so I created getter functions | |
function getPlayerLevels(address _p, uint8 _l) public view returns(address) { | |
address _playerLevel = players[_p].playerLevels[_l]; | |
return _playerLevel; | |
} | |
function getPlayerSolved(address _p, uint8 _l) public view returns(bool) { | |
// @audit is there any reason why we need address _p? i.e. why not use msg.sender | |
bool _playerSolved = players[_p].solved[_l]; | |
return _playerSolved; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.6.0; | |
import "./Context.sol"; | |
/** | |
* @dev Contract module which provides a basic access control mechanism, where | |
* there is an account (an owner) that can be granted exclusive access to | |
* specific functions. | |
* | |
* By default, the owner account will be the one that deploys the contract. This | |
* can later be changed with {transferOwnership}. | |
* | |
* This module is used through inheritance. It will make available the modifier | |
* `onlyOwner`, which can be applied to your functions to restrict their use to | |
* the owner. | |
*/ | |
contract Ownable is Context { | |
address private _owner; | |
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); | |
/** | |
* @dev Initializes the contract setting the deployer as the initial owner. | |
*/ | |
constructor () internal { | |
address msgSender = _msgSender(); | |
_owner = msgSender; | |
emit OwnershipTransferred(address(0), msgSender); | |
} | |
/** | |
* @dev Returns the address of the current owner. | |
*/ | |
function owner() public view returns (address) { | |
return _owner; | |
} | |
/** | |
* @dev Throws if called by any account other than the owner. | |
*/ | |
modifier onlyOwner() { | |
require(_owner == _msgSender(), "Ownable: caller is not the owner"); | |
_; | |
} | |
/** | |
* @dev Leaves the contract without owner. It will not be possible to call | |
* `onlyOwner` functions anymore. Can only be called by the current owner. | |
* | |
* NOTE: Renouncing ownership will leave the contract without an owner, | |
* thereby removing any functionality that is only available to the owner. | |
*/ | |
function renounceOwnership() public virtual onlyOwner { | |
emit OwnershipTransferred(_owner, address(0)); | |
_owner = address(0); | |
} | |
/** | |
* @dev Transfers ownership of the contract to a new account (`newOwner`). | |
* Can only be called by the current owner. | |
*/ | |
function transferOwnership(address newOwner) public virtual onlyOwner { | |
require(newOwner != address(0), "Ownable: new owner is the zero address"); | |
emit OwnershipTransferred(_owner, newOwner); | |
_owner = newOwner; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment