diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ee0459 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ + +.woke-build +.woke-logs +.env +pytypes +__pycache__/ +*.py[cod] +.hypothesis/ +woke-coverage.cov \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2cf038 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Hackee v3 + +## Goal + +## Recommendations + +## Setup + diff --git a/contracts/Dungeon.sol b/contracts/Dungeon.sol new file mode 100644 index 0000000..6c34cf7 --- /dev/null +++ b/contracts/Dungeon.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: None + +pragma solidity =0.8.20; + +/* NPCs */ +import "./NPC.sol"; +import "./Villager.sol"; +import "./Mathematican.sol"; +import "./FinalBoss.sol"; + +/* Objects */ +import "./Spawner.sol"; + +/* Helpers */ +import "woke/console.sol"; + +contract Dungeon { + Spawner public $spawner; + NPC public $storyTeller; + NPC public $mathematican; + + mapping(bytes4 => bool) public $quests; + uint256 private $traps; + address public $challenger; + + event Subtitles(string subtitles); + + /* + Modifiers + */ + + modifier takeQuest() { + require($quests[msg.sig] == false, "Quest already taken"); + $quests[msg.sig] = true; + _; + } + + modifier onlyHumans() { + uint256 size; + assembly { size := extcodesize(caller()) } + require(size <= 0, "You're trying to trick me with a contract!"); + _; + } + + modifier cleanRoom() { + require( + $quests[this.removeWeb1.selector] == true && + $quests[this.removeWeb2.selector] == true && + $quests[this.removeWeb3.selector] == true, + "These functions cannot be called" + ); + _; + } + + modifier altarFound() { + require( + $quests[this.dodgeTraps.selector] == true, + "These functions cannot be called" + ); + _; + } + + modifier lootGained() { + require( + $quests[this.lockedChest.selector] == true, + "These functions cannot be called" + ); + _; + } + + modifier challenger() { + require(msg.sender == $challenger, "You're not the challenger"); + _; + } + + constructor() { + // init dungeon + $spawner = new Spawner(); + $storyTeller = NPC(_spawn(keccak256("storyTeller"), type(Villager).creationCode)); + $mathematican = NPC(_spawn(keccak256("mathematican"), type(Mathematican).creationCode)); + $traps = 3; + // mint potential rewards + _mint(address(this), 100_000); + } + + /* + Collect tokens from dungeon + */ + + // get 300 tokens + function removeWeb1(uint256 slashes) external takeQuest { + unchecked { + _attempt(slashes == 666, 300, msg.sender, "Devil's cut!", "Caught in web1"); + } + } + + // get 500 tokens + function removeWeb2(uint256 slashes) external takeQuest { + unchecked { + _attempt((((((slashes * 3) ^ 2) % 1000) + $traps) * 2) == 666, 500, msg.sender, "Devil's cut!", "Caught in web2"); + } + } + + // get 3000 tokens + function removeWeb3(uint256 slashes) external takeQuest { + unchecked { + _attempt(abi.decode($mathematican.interaction(abi.encode(slashes)), (uint256)) == 666, 3000, msg.sender, "Devil's cut!", "Caught in web3"); + } + } + + // get 750 tokens + function findMoreTraps() external takeQuest { + uint256 traps = uint256(block.timestamp/* + block.prevrandao */) % 6; + traps == 0 || traps == 1 ? traps += 2 : traps; + $traps = traps; + _attempt(traps == 5, 750, msg.sender, "Nicely done!", "You missed some traps."); + } + + // get 1000 tokens + function etherForTokens() external takeQuest { + _attempt(address(this).balance == 1000, 1000, msg.sender, "Etheeer!", "You failed."); + _muhahaha(); + } + + // get 1500 tokens + function lockedChest(uint256 pin) external takeQuest { + require(pin != 1234, "You're cheatin'"); + uint32 allowed_range_pin = uint32(pin); + _attempt(allowed_range_pin == 1234, 1500, msg.sender, "Gotcha.", "Incorrect pin."); + } + + // get something + function gamble(function(address, uint256) external rollin, uint256 buyIn) external takeQuest { + rollin(address(this), (buyIn / 1000) * buyIn); + _attempt(buyIn <= 2000, buyIn, msg.sender, "Nice game.", "You're too greedy."); + } + + // one wish in front of altar + function spawnNPC(bytes32 secretSubstance, bytes memory lifeform) altarFound takeQuest public { + $spawner.spawnNPC(secretSubstance, lifeform); + } + + // get 2500 tokens + function assignChallenger() external cleanRoom onlyHumans takeQuest { + $challenger = msg.sender; + _attempt(1 == 1, 2500, msg.sender, "Buy something useful.", "Wut?"); + } + + // get 10000 tokens + function spawnFinalBoss() external altarFound cleanRoom lootGained challenger takeQuest { + NPC finalBoss = NPC(_spawn(keccak256("finalBoss"), type(FinalBoss).creationCode)); + bytes memory result = finalBoss.interaction(abi.encodePacked(msg.sender)); + _attempt(abi.decode(result, (bool)) == true, 10000, msg.sender, "What a legend!", "You have been defecated"); + } + + // get something + function dodgeTraps() external { + if ($traps == 0) { + $quests[msg.sig] = true; + emit Subtitles("You dodged all traps."); + return; + } + _attempt($traps >= 0, 250 * $traps, msg.sender, "Doge!", "You failed."); + _muhahaha(); + $traps--; + } + + /* + Helper part + */ + + // TODO this will be deprecated + function getTraps() external view returns (uint256) { + return $traps; + } + + function _attempt(bool condition, uint256 value, address to, string memory good, string memory bad) private { + if (condition) { + this.transfer(to, value); + emit Subtitles(good); + } else { + this._burn(address(this), value); + emit Subtitles(bad); + } + } + + function _spawn(bytes32 secretSubstance, bytes memory lifeform) private returns (address payable) { + return payable($spawner.spawnNPC(secretSubstance, lifeform)); + } + + function _muhahaha() private { + msg.sender.call{value: 0}(""); + } + + /* + Evaluation part + */ + + function evaluate(address hackeer) external view returns (uint256) { + return $balances[hackeer]; + } + + /* + ERC20 part + */ + + uint256 public $totalSupply; + mapping(address => uint256) public $balances; + mapping(address => mapping(address => uint256)) public $allowed; + + function balanceOf(address _owner) public view returns (uint256 balance) { + return $balances[_owner]; + } + + function transfer(address _to, uint256 _value) public returns (bool success) { + require($balances[msg.sender] >= _value); + $balances[msg.sender] -= _value; + $balances[_to] += _value; + return true; + } + + function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { + require($balances[_from] >= _value && $allowed[_from][msg.sender] >= _value); + $balances[_to] += _value; + $balances[_from] -= _value; + $allowed[_from][msg.sender] -= _value; + return true; + } + + function approve(address _spender, uint256 _value) public returns (bool success) { + $allowed[msg.sender][_spender] = _value; + return true; + } + + function allowance(address _owner, address _spender) public view returns (uint256 remaining) { + return $allowed[_owner][_spender]; + } + + function _mint(address _to, uint256 _amount) private { + $totalSupply += _amount; + $balances[_to] += _amount; + } + + function _burn(address _from, uint256 _amount) external { + require(_from == address(this), "You don't have such power."); + require($balances[_from] >= _amount); + $totalSupply -= _amount; + $balances[_from] -= _amount; + } + + receive() external payable { + revert("You can't bribe the dungeon!"); + } + + fallback() external { + revert("Fall back stranger."); + } + +} \ No newline at end of file diff --git a/contracts/Mathematican.sol b/contracts/Mathematican.sol new file mode 100644 index 0000000..ab9ae93 --- /dev/null +++ b/contracts/Mathematican.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: None + +pragma solidity =0.8.20; + +import "./NPC.sol"; + +contract Mathematican is NPC { + function interaction(bytes memory input) external override returns (bytes memory output){ + uint256 x = abi.decode(input, (uint256)); + uint256 y; + assembly { + let arg := x + x := sub(x,1) + x := or(x, div(x, 0x02)) + x := or(x, div(x, 0x04)) + x := or(x, div(x, 0x10)) + x := or(x, div(x, 0x100)) + x := or(x, div(x, 0x10000)) + x := or(x, div(x, 0x100000000)) + x := or(x, div(x, 0x10000000000000000)) + x := or(x, div(x, 0x100000000000000000000000000000000)) + x := add(x, 1) + let m := mload(0x40) + mstore(m, 0xf8f9cbfae6cc78fbefe7cdc3a1793dfcf4f0e8bbd8cec470b6a28a7a5a3e1efd) + mstore(add(m,0x20), 0xf5ecf1b3e9debc68e1d9cfabc5997135bfb7a7a3938b7b606b5b4b3f2f1f0ffe) + mstore(add(m,0x40), 0xf6e4ed9ff2d6b458eadcdf97bd91692de2d4da8fd2d0ac50c6ae9a8272523616) + mstore(add(m,0x60), 0xc8c0b887b0a8a4489c948c7f847c6125746c645c544c444038302820181008ff) + mstore(add(m,0x80), 0xf7cae577eec2a03cf3bad76fb589591debb2dd67e0aa9834bea6925f6a4a2e0e) + mstore(add(m,0xa0), 0xe39ed557db96902cd38ed14fad815115c786af479b7e83247363534337271707) + mstore(add(m,0xc0), 0xc976c13bb96e881cb166a933a55e490d9d56952b8d4e801485467d2362422606) + mstore(add(m,0xe0), 0x753a6d1b65325d0c552a4d1345224105391a310b29122104190a110309020100) + mstore(0x40, add(m, 0x100)) + let magic := 0x818283848586878898a8b8c8d8e8f929395969799a9b9d9e9faaeb6bedeeff + let shift := 0x100000000000000000000000000000000000000000000000000000000000000 + let a := div(mul(x, magic), shift) + y := div(mload(add(m,sub(255,a))), shift) + y := add(y, mul(256, gt(arg, 0x8000000000000000000000000000000000000000000000000000000000000000))) + } + output = abi.encode(y); + } +} \ No newline at end of file diff --git a/contracts/NPC.sol b/contracts/NPC.sol new file mode 100644 index 0000000..76607dd --- /dev/null +++ b/contracts/NPC.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: None + +pragma solidity =0.8.20; + +contract NPC { + function interaction(bytes memory input) external virtual returns (bytes memory output) { + return bytes(hex"42"); + } + + function kill(address backpack) external { + selfdestruct(payable(backpack)); + } + + function givelife(bytes memory lifeform) external payable { + assembly { + if iszero(create(0, add(lifeform, 32), mload(lifeform))) { + revert(0, 0) + } + selfdestruct(caller()) + } + } + + receive () external payable {} +} \ No newline at end of file diff --git a/contracts/Spawner.sol b/contracts/Spawner.sol new file mode 100644 index 0000000..26e2ebc --- /dev/null +++ b/contracts/Spawner.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: None + +pragma solidity =0.8.20; + +import "./NPC.sol"; + +import 'woke/console.sol'; + +contract Spawner { + address private $dungeon; + + constructor() { + $dungeon = msg.sender; + } + + modifier onlyDungeon() { + require(msg.sender == $dungeon, "Only dungeon can spawn NPCs"); + _; + } + + function spawnNPC(bytes32 secretSubstance, bytes memory lifeform) public onlyDungeon returns (address) { + NPC npc = new NPC{ salt: secretSubstance }(); + npc.givelife(lifeform); + return address(uint160(uint256(keccak256(abi.encodePacked(hex'd6_94', uint160(uint256(keccak256(abi.encodePacked(hex'ff', address(this), secretSubstance, keccak256(type(NPC).creationCode))))), hex'01'))))); + } + + function whereSpawner() external view returns (address) { + return $dungeon; + } +} diff --git a/contracts/Villager.sol b/contracts/Villager.sol new file mode 100644 index 0000000..3b82167 --- /dev/null +++ b/contracts/Villager.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: None + +pragma solidity =0.8.20; + +import "./NPC.sol"; + +contract Villager is NPC { + function interaction(bytes memory) external override returns (bytes memory output) { + return "Just go to the dungeon, stranger, you'll die or get tokens." + " Everything what you find in the dungeon can be considered as yours." + " Beware of traps and webs and try to defeat the dungeon boss." + " If you do so, whole Etherea will be grateful to you." + " I know about some (hopefully) useful maps of the dungeon for you." + " They are in a locked chest in the dungeon." + " Farewell."; + } +} diff --git a/contracts/lockedChest.dungeon b/contracts/lockedChest.dungeon new file mode 100644 index 0000000..a927422 Binary files /dev/null and b/contracts/lockedChest.dungeon differ diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/deploy.py b/scripts/deploy.py new file mode 100644 index 0000000..ac211bd --- /dev/null +++ b/scripts/deploy.py @@ -0,0 +1,8 @@ +from woke.deployment import * + +NODE_URL = "ENTER_NODE_URL_HERE" + + +@default_chain.connect(NODE_URL) +def main(): + default_chain.set_default_accounts(Account.from_alias("deployment")) diff --git a/woke.toml b/woke.toml new file mode 100644 index 0000000..029e64d --- /dev/null +++ b/woke.toml @@ -0,0 +1,24 @@ +[compiler.solc] +ignore_paths = ["node_modules", ".woke-build", "venv", "lib"] +include_paths = ["node_modules"] +evm_version = "shanghai" + +[compiler.solc.optimizer] +enabled = false +runs = 200 + +[detectors] +exclude = [] +ignore_paths = ["node_modules", ".woke-build", "venv", "lib"] + +[testing] +cmd = "anvil" + +[testing.anvil] +cmd_args = "--prune-history 100 --transaction-block-keeper 10 --steps-tracing --silent" + +[testing.ganache] +cmd_args = "-k istanbul -q" + +[testing.hardhat] +cmd_args = "" \ No newline at end of file