├── contracts ├── @player-contracts │ └── .gitkeep ├── the-secret-source │ ├── README.md │ └── TheSecretSource.sol ├── lock-pool │ ├── README.md │ └── LockPool.sol ├── nft-whitelist │ ├── README.md │ ├── NFTWhitelist.sol │ └── NFTMarket.sol ├── the-collector │ ├── README.md │ ├── TheCollectorNFT.sol │ └── TheCollectorMarket.sol ├── ValixPlaygroundToken.sol ├── extra-bank │ ├── utils │ │ ├── AddOn.sol │ │ ├── Factory.sol │ │ └── AddOnFactory.sol │ ├── README.md │ └── ExtraBank.sol ├── freeze-the-flow │ ├── README.md │ ├── VaultHelper.sol │ └── Vault.sol ├── bullied-boy │ ├── README.md │ └── SchoolRunner.sol ├── alice-in-the-dark │ ├── README.md │ └── DressColor.sol ├── poor-boy │ ├── README.md │ ├── RichBoyNFT.sol │ ├── RichBoyTraitNFT.sol │ └── RichBoyNFTLendingPool.sol ├── trick-or-thieve │ ├── README.md │ ├── ValixPlaygroundTokenLite.sol │ └── NftMarketplace.sol ├── ValixPlaygroundNFT.sol └── lotto888 │ ├── README.md │ ├── GamblingOracle.sol │ └── Lotto888.sol ├── cover.png ├── .gitignore ├── tsconfig.json ├── hardhat.config.ts ├── test ├── alice-in-the-dark │ └── alice-in-the-dark.challenge.ts ├── the-secret-source │ └── the-secret-source.challenge.ts ├── extra-bank │ ├── utils │ │ └── AddOn.json │ └── extra-bank.challenge.ts ├── the-collector │ └── the-collector.challenge.ts ├── poor-boy │ └── poor-boy.challenge.ts ├── lotto888 │ └── lotto888.challenge.ts ├── lock-pool │ └── lock-pool.challenge.ts ├── freeze-the-flow │ └── freeze-the-flow.challenge.ts ├── trick-or-thieve │ └── trick-or-thieve.challenge.ts ├── nft-whitelist │ └── nft-whitelist.challenge.ts └── bullied-boy │ └── bullied-boy.challenge.ts ├── package.json └── README.md /contracts/@player-contracts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valixconsulting/valix-ctf-playground/HEAD/cover.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | 8 | # Hardhat files 9 | cache 10 | artifacts 11 | 12 | -------------------------------------------------------------------------------- /contracts/the-secret-source/README.md: -------------------------------------------------------------------------------- 1 | # The Secret Source Challenge 2 | Welcome to The Secret Source challenge! We keep the top secret in the contract. Whoever guesses correctly the secret source will win the 1 million tokens reward. 3 | -------------------------------------------------------------------------------- /contracts/lock-pool/README.md: -------------------------------------------------------------------------------- 1 | # Lock Pool Challenge 2 | The lock pool provides rewards to users who lock 10 VLXP tokens. 3 | After a certain period, users can withdraw and claim the NFT. 4 | 5 | Could you retrieve all of the locked tokens from the pool? You start with 10 VLXP. -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } -------------------------------------------------------------------------------- /contracts/nft-whitelist/README.md: -------------------------------------------------------------------------------- 1 | # NFT Whitelist Challenge 2 | A new NFT Market has been opened!, each NFT is available for sale in the marketplace at 1 ETH. 3 | In addition, who is in the whitelists can claim the NFT without spending any ETH. 4 | 5 | To pass this challenge, the player must own 1 NFT. 6 | -------------------------------------------------------------------------------- /contracts/the-collector/README.md: -------------------------------------------------------------------------------- 1 | # The Collector Challenge 2 | The Collector NFT Market has been opened!, each NFT is available for sale in the marketplace start at 100 tokens. 3 | The more you own, the cheaper you can buy, the lowest at 25 tokens. 4 | 5 | To pass this challenge, the player must own 4 NFTs with just 100 tokens. -------------------------------------------------------------------------------- /contracts/ValixPlaygroundToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract ValixPlaygroundToken is ERC20 { 7 | constructor() ERC20("ValixPlaygroundToken", "VLXP") { 8 | _mint(msg.sender, type(uint256).max); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import '@openzeppelin/hardhat-upgrades'; 4 | 5 | const config: HardhatUserConfig = { 6 | solidity: { 7 | compilers: [ 8 | { version: "0.7.6" }, 9 | { version: "0.8.19" } 10 | ], 11 | }, 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /contracts/extra-bank/utils/AddOn.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | contract AddOn { 5 | event Message(string message); 6 | 7 | function addOn() external { 8 | emit Message("Excuted code approved by Bank"); 9 | } 10 | 11 | function emergencyKill() external { 12 | selfdestruct(payable(address(0))); 13 | } 14 | } -------------------------------------------------------------------------------- /contracts/freeze-the-flow/README.md: -------------------------------------------------------------------------------- 1 | # Fleeze The Flow Challenge 2 | There is an ether vault with depositors. Users can deposit and withdraw their funds, but they must use the VaultHelper contract for these transactions. 3 | 4 | The VaultHelper contract acts as a gateway to the vault and cannot receive any ether. 5 | 6 | To pass the challenge, your have to disable the deposit feature of the VaultHelper contract. -------------------------------------------------------------------------------- /contracts/bullied-boy/README.md: -------------------------------------------------------------------------------- 1 | # Bullied Boy Challenge 2 | Once upon a time, there was a boy who had bullying from his classmates. The relentless bullying had pushed him to the last position in a running competition. 3 | 4 | Motivated by a strong desire for revenge, he was determined to block every other student from receiving any rewards. His goal was to ensure that nobody else could taste victory, not even the class teacher. -------------------------------------------------------------------------------- /contracts/alice-in-the-dark/README.md: -------------------------------------------------------------------------------- 1 | # Alice in The Dark 2 | Alice hates dark colors, she loves to dress up in bright colors every day, starting today with the pink dress 👚. 3 | She lets anyone introduce exciting colors with the RGB format by adding a little color to change the color of the dress. And of course! Must not be dark color. 4 | 5 | To pass this challenge, the player must tease Alice by suggesting a black dress for her 😛. -------------------------------------------------------------------------------- /contracts/poor-boy/README.md: -------------------------------------------------------------------------------- 1 | # Poor Boy - Challenge 2 | 3 | You're a young guy living in poverty, feeling really lonely in the tough world of Decentralized.
4 | You've got very little money, can't use any services, but there's still a tiny bit of hope.
5 | In this challenging place, you need to use your skills and what little you have to improve your life.
6 | Become a premium member and gain access to valuable resources. -------------------------------------------------------------------------------- /contracts/trick-or-thieve/README.md: -------------------------------------------------------------------------------- 1 | # Trick or Thieve Challenge 2 | The NFT marketplace provides features for sellers and buyers to list and buy items, respectively. Buyers must purchase the VLXP-L token to acquire the listed NFT at the seller's set price. 3 | 4 | Now, this marketplace has been launched for just a minute 🚀, and one seller has already listed their token. 5 | 6 | Could you retrieve the token? You start with **NOTHING!** -------------------------------------------------------------------------------- /contracts/extra-bank/utils/Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "./AddOnFactory.sol"; 5 | 6 | contract Factory { 7 | address private _owner = msg.sender; 8 | event CreateAddOnFactory(address addr); 9 | 10 | function createFactory( 11 | bytes32 _salt 12 | ) external{ 13 | require(msg.sender == _owner, "Permission denied"); 14 | address factoryAddress = address(new AddOnFactory{salt: _salt}()); 15 | emit CreateAddOnFactory(factoryAddress); 16 | } 17 | } -------------------------------------------------------------------------------- /contracts/extra-bank/README.md: -------------------------------------------------------------------------------- 1 | # Extra Bank Challenge 2 | The bank contract allows users to deposit and withdraw, 3 | and both the owner and operator can add new features or additional features to this contract. 4 | 5 | You served the operator of this bank and have added one feature to the bank contract. 6 | However, your time as the operator came to an end quicker. 🚪😞 7 | 8 | NOW!, You see, 9 | `I may have left a little surprise behind, something that could turn this bank into a TORNADO of chaos` 🌪️ 10 | 11 | Could you take ownership of the bank and all the funds? -------------------------------------------------------------------------------- /contracts/ValixPlaygroundNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract ValixPlaygroundNFT is ERC721, Ownable { 8 | uint256 public tokenIdCounter; 9 | constructor() ERC721("ValixPlaygroundNFT", "VLXPNFT") {} 10 | 11 | function safeMint(address to) public onlyOwner returns (uint256 tokenId) { 12 | tokenId = tokenIdCounter; 13 | _safeMint(to, tokenId); 14 | ++tokenIdCounter; 15 | } 16 | } -------------------------------------------------------------------------------- /contracts/alice-in-the-dark/DressColor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | 4 | contract DressColor { 5 | bytes3 public color; 6 | 7 | constructor(uint8, uint8, uint8) { 8 | assembly { 9 | let p := mload(0x40) 10 | mstore(p, shl(0xf8, mload(0x80))) 11 | mstore(add(p, 1), shl(0xf8, mload(0xa0))) 12 | mstore(add(p, 2), shl(0xf8, mload(0xc0))) 13 | 14 | sstore(0, shr(0xe8, mload(p))) 15 | } 16 | } 17 | 18 | function fill(bytes3 colorToFill) external { 19 | color = bytes3(uint24(color) + uint24(colorToFill)); 20 | } 21 | } -------------------------------------------------------------------------------- /contracts/lotto888/README.md: -------------------------------------------------------------------------------- 1 | # Lotto888 - Challenge 2 | 3 | In the shady underworld of gambling, the Mafia unveiled "Lotto888" their cryptic lottery. 4 |
5 | Brave souls dared to bet 0.1 Ether, praying for luck. Those who matched the magic number whispered, 💰"I'm a winner!"💰 — and walked away with an easy 1 Ether, leaving the Mafia both baffled and amused. 6 |

7 | ***Caution***: Welcome to the wild world of gambling, where luck can be as tricky as a cat playing hide-and-seek.
8 | Remember, the Mafia enjoys playing pranks like party tricks.
9 | If you don't win this time, just hang in there and stay tuned for the next block!—it might be your lucky moment to outsmart the Mafia! 10 | -------------------------------------------------------------------------------- /contracts/the-collector/TheCollectorNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/utils/Counters.sol"; 7 | 8 | contract TheCollectorNFT is ERC721, Ownable { 9 | using Counters for Counters.Counter; 10 | 11 | Counters.Counter private _tokenIdCounter; 12 | 13 | constructor() ERC721("TheCollectorNFT", "TCNFT") {} 14 | 15 | function safeMint(address to) external onlyOwner returns (uint256 tokenId){ 16 | tokenId = _tokenIdCounter.current(); 17 | _tokenIdCounter.increment(); 18 | _safeMint(to, tokenId); 19 | } 20 | } -------------------------------------------------------------------------------- /contracts/poor-boy/RichBoyNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract RichBoyNFT is ERC721, Ownable { 8 | uint256 public tokenIdCounter; 9 | 10 | constructor() ERC721("RichBoyNFT", "RBNFT") { 11 | tokenIdCounter = 1; 12 | } 13 | 14 | function allocateForLendingPool(address lendingPool) external onlyOwner { 15 | _safeMint(lendingPool, 0); 16 | } 17 | 18 | function mint(address to) external payable { 19 | require(msg.value == 1 ether, "Please send 1 ETH"); 20 | uint256 tokenId = tokenIdCounter; 21 | tokenIdCounter++; 22 | _safeMint(to, tokenId); 23 | } 24 | } -------------------------------------------------------------------------------- /contracts/extra-bank/utils/AddOnFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | contract AddOnFactory { 5 | address private _owner; 6 | 7 | event CreateAddOn(address addr); 8 | 9 | constructor() { 10 | _owner = tx.origin; 11 | } 12 | 13 | function createAddOn(bytes calldata _bytecode) external { 14 | require(msg.sender == _owner, "Permission denied"); 15 | address addr; 16 | bytes memory bytecode = abi.encodePacked(_bytecode); 17 | assembly { 18 | addr := create(0, add(bytecode, 0x20), mload(bytecode)) 19 | if iszero(extcodesize(addr)) { 20 | revert(0, 0) 21 | } 22 | } 23 | emit CreateAddOn(addr); 24 | } 25 | 26 | function emergencyKill() external { 27 | selfdestruct(payable(address(0))); 28 | } 29 | } -------------------------------------------------------------------------------- /contracts/lotto888/GamblingOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | contract GamblingOracle is Ownable { 7 | mapping(address => bool) public authorized; 8 | 9 | modifier onlyAuthorized() { 10 | require(authorized[msg.sender], "Allow only authorized"); 11 | _; 12 | } 13 | 14 | function addAuthorized(address contractAddr) external onlyOwner() { 15 | authorized[contractAddr] = true; 16 | } 17 | 18 | function removeAuthorized(address contractAddr) external onlyOwner() { 19 | authorized[contractAddr] = false; 20 | } 21 | 22 | function getLuckyNumber(uint256 guess) external view onlyAuthorized() returns (uint256) { 23 | return uint256(keccak256(abi.encodePacked(block.timestamp, block.number, guess))) % 10; 24 | } 25 | } -------------------------------------------------------------------------------- /contracts/poor-boy/RichBoyTraitNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "./RichBoyNFT.sol"; 6 | 7 | contract RichBoyTraitNFT is ERC721 { 8 | RichBoyNFT public rbNFT; 9 | mapping (address => bool) public alreadyMinted; 10 | uint256 public tokenIdCounter; 11 | uint256 public constant PREMIUM_MEMBER_REQUIREMENT = 2; 12 | 13 | constructor(address rbNFTAddress) ERC721("RichBoyTraitNFT", "RBTNFT") { 14 | rbNFT = RichBoyNFT(rbNFTAddress); 15 | } 16 | 17 | function mint() external { 18 | require(!alreadyMinted[msg.sender], "Only One Token/Account"); 19 | require(rbNFT.balanceOf(msg.sender) > 0, "Allows Only RichBoyTraitNFT Owner"); 20 | 21 | alreadyMinted[msg.sender] = true; 22 | uint256 tokenId = tokenIdCounter; 23 | tokenIdCounter++; 24 | _safeMint(msg.sender, tokenId); 25 | } 26 | 27 | function isPremiumMember(address member) external view returns (bool) { 28 | return balanceOf(member) >= PREMIUM_MEMBER_REQUIREMENT; 29 | } 30 | } -------------------------------------------------------------------------------- /test/alice-in-the-dark/alice-in-the-dark.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; 4 | import { DressColor } from '../../typechain-types'; 5 | 6 | describe("Challenge - Alice in The Dark", function () { 7 | let alice: SignerWithAddress; 8 | let player: SignerWithAddress; 9 | let dressColor: DressColor; 10 | 11 | before(async function () { 12 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 13 | [alice, player] = await ethers.getSigners(); 14 | 15 | // Deploy the DressColor contract with her favorite color (pink) 16 | const ColorBoard = await (await ethers.getContractFactory('DressColor', alice)); 17 | dressColor = await ColorBoard.deploy(255, 0, 255); 18 | }); 19 | 20 | it('Execution', async function () { 21 | /** CODE YOUR SOLUTION HERE */ 22 | }); 23 | 24 | after(async function () { 25 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 26 | 27 | // Player must suggest the black dress to Alice 28 | expect(await dressColor.color()).to.be.eq('0x000000'); 29 | }); 30 | }); -------------------------------------------------------------------------------- /contracts/the-secret-source/TheSecretSource.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "../ValixPlaygroundToken.sol"; 6 | 7 | contract TheSecretSource is Ownable { 8 | uint256 public constant REWARD_AMOUNT = 1_000_000 * 1e18; 9 | ValixPlaygroundToken public immutable token; 10 | bytes32 private secretSource; 11 | address public winner; 12 | 13 | event Guess(address indexed sender, bytes32 secret, bool isWinner); 14 | 15 | constructor(ValixPlaygroundToken _token) { 16 | token = _token; 17 | } 18 | 19 | function setSecret(string memory secret) external onlyOwner { 20 | require(uint256(secretSource) == 0, "Already set"); 21 | secretSource = keccak256(abi.encode(secret, block.timestamp)); 22 | } 23 | 24 | function guess(bytes32 secret) payable external { 25 | require(uint256(secretSource) > 0, "Not ready to play"); 26 | require(winner == address(0x0), "Game ended"); 27 | 28 | if (secret == secretSource) { 29 | winner = msg.sender; 30 | token.transfer(msg.sender, REWARD_AMOUNT); 31 | 32 | emit Guess(msg.sender, secret, true); 33 | }else { 34 | emit Guess(msg.sender, secret, false); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /contracts/the-collector/TheCollectorMarket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "./TheCollectorNFT.sol"; 5 | import "../ValixPlaygroundToken.sol"; 6 | 7 | contract TheCollectorMarket { 8 | uint256 public constant BASE_PRICE = 100e18; 9 | uint256 public constant MINIMUM_PRICE = 25e18; 10 | TheCollectorNFT public immutable nft; 11 | ValixPlaygroundToken public immutable token; 12 | 13 | event Buy(address indexed sender, uint256 indexed tokenId, uint256 price); 14 | 15 | constructor(ValixPlaygroundToken _token, TheCollectorNFT _nft) { 16 | token = _token; 17 | nft = _nft; 18 | } 19 | 20 | function calculatePrice() public view returns (uint256) { 21 | uint256 balances = nft.balanceOf(msg.sender); 22 | if (balances > 0) { 23 | uint256 reducedPrice = BASE_PRICE / balances; 24 | uint256 newPrice = reducedPrice <= MINIMUM_PRICE ? MINIMUM_PRICE : reducedPrice; 25 | 26 | return newPrice; 27 | } else { 28 | return BASE_PRICE; 29 | } 30 | } 31 | 32 | function buy() external { 33 | uint256 tokenId = nft.safeMint(msg.sender); 34 | uint256 price = calculatePrice(); 35 | token.transferFrom(msg.sender, address(this), price); 36 | emit Buy(msg.sender, tokenId, price); 37 | } 38 | } -------------------------------------------------------------------------------- /contracts/lotto888/Lotto888.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "./GamblingOracle.sol"; 6 | 7 | contract Lotto888 is Ownable { 8 | GamblingOracle public oracle; 9 | mapping(address => bool) public winner; 10 | 11 | event Deposit(uint256 value); 12 | event BetPlaced(address indexed player, uint256 guess, uint256 random, bool isWinner); 13 | constructor(address _oracle) { 14 | oracle = GamblingOracle(_oracle); 15 | } 16 | 17 | function placeBet(uint256 guess) external payable { 18 | require(msg.value == 0.1 ether, "Require 0.1 ether to place a bet"); 19 | require(!winner[msg.sender], "Winner only bet once"); 20 | 21 | uint256 answer = oracle.getLuckyNumber(guess); 22 | if (guess == answer) { 23 | winner[msg.sender] = true; 24 | (bool whocares, ) = payable(msg.sender).call{value: 1 ether}(""); 25 | emit BetPlaced(msg.sender, guess, answer, true); 26 | } else { 27 | emit BetPlaced(msg.sender, guess, answer, false); 28 | } 29 | } 30 | 31 | function balance() external view returns (uint256) { 32 | return address(this).balance; 33 | } 34 | 35 | function deposit() external payable onlyOwner() { 36 | emit Deposit(msg.value); 37 | } 38 | } -------------------------------------------------------------------------------- /contracts/freeze-the-flow/VaultHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "./Vault.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract VaultHelper is Ownable { 8 | 9 | Vault public vault; 10 | 11 | event SetVault(address prevVault, address newVault); 12 | 13 | constructor(Vault _vault) { 14 | vault = _vault; 15 | } 16 | 17 | function depositToVault( 18 | address _user 19 | ) external payable { 20 | require(msg.sender == _user, "Sender can only deposit for self"); 21 | require(msg.value > 0, "No forwarded deposited value"); 22 | require(_user != address(0), "Invalid Address"); 23 | vault.deposit{value: msg.value}(_user); 24 | 25 | require(address(this).balance == 0); 26 | } 27 | 28 | function withdrawFromVault( 29 | address _user, 30 | uint256 _amount 31 | ) external payable { 32 | require(msg.sender == _user, "Sender can only withdraw for self"); 33 | require(_user != address(0), "Invalid Address"); 34 | vault.withdraw(_user, _amount); 35 | } 36 | 37 | function setVaultHelper(address _newVault) external onlyOwner { 38 | address prevVault = address(vault); 39 | vault = Vault(_newVault); 40 | 41 | emit SetVault(prevVault, _newVault); 42 | } 43 | 44 | receive() external payable { revert(); } 45 | } -------------------------------------------------------------------------------- /contracts/bullied-boy/SchoolRunner.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | contract SchoolRunner is Ownable { 7 | uint256 public constant FIRST_PLACE_REWARD = 5 ether; 8 | uint256 public constant SECOND_PLACE_REWARD = 4 ether; 9 | uint256 public constant THIRD_PLACE_REWARD = 3 ether; 10 | uint256 public constant FOURTH_PLACE_REWARD = 2 ether; 11 | uint256 public constant FIFTH_PLACE_REWARD = 1 ether; 12 | 13 | uint256 public positionCount; 14 | mapping(uint256 => address) public studentPositions; 15 | 16 | function register() external { 17 | require(positionCount < 5); 18 | studentPositions[positionCount] = msg.sender; 19 | positionCount++; 20 | } 21 | 22 | function depositReward() external payable onlyOwner { 23 | require(msg.value == 15 ether); 24 | } 25 | 26 | function distributeReward() external onlyOwner { 27 | uint256[5] memory rewards = [ 28 | FIRST_PLACE_REWARD, 29 | SECOND_PLACE_REWARD, 30 | THIRD_PLACE_REWARD, 31 | FOURTH_PLACE_REWARD, 32 | FIFTH_PLACE_REWARD 33 | ]; 34 | 35 | for (uint256 i; i < 5; i++) { 36 | address student = studentPositions[i]; 37 | uint256 amount = rewards[i]; 38 | (bool sent, ) = student.call{value: amount}(""); 39 | require(sent); 40 | } 41 | } 42 | 43 | receive() external payable {} 44 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valix-ctf-playground", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "nft-whitelist": "npx hardhat test test/nft-whitelist/nft-whitelist.challenge.ts", 6 | "the-collector": "npx hardhat test test/the-collector/the-collector.challenge.ts", 7 | "lock-pool": "npx hardhat test test/lock-pool/lock-pool.challenge.ts", 8 | "extra-bank": "npx hardhat test test/extra-bank/extra-bank.challenge.ts", 9 | "poor-boy": "npx hardhat test test/poor-boy/poor-boy.challenge.ts", 10 | "bullied-boy": "npx hardhat test test/bullied-boy/bullied-boy.challenge.ts", 11 | "the-secret-source": "npx hardhat test test/the-secret-source/the-secret-source.challenge.ts", 12 | "trick-or-thieve": "npx hardhat test test/trick-or-thieve/trick-or-thieve.challenge.ts", 13 | "alice-in-the-dark": "npx hardhat test test/alice-in-the-dark/alice-in-the-dark.challenge.ts", 14 | "freeze-the-flow": "npx hardhat test test/freeze-the-flow/freeze-the-flow.challenge.ts", 15 | "lotto888": "npx hardhat test test/lotto888/lotto888.challenge.ts" 16 | }, 17 | "devDependencies": { 18 | "hardhat": "2.17.1", 19 | "@nomicfoundation/hardhat-toolbox": "2.0.2", 20 | "@typechain/ethers-v5": "10.2.1", 21 | "@typechain/hardhat": "6.1.6", 22 | "typechain": "8.3.1", 23 | "@openzeppelin/contracts": "4.9.3", 24 | "@openzeppelin/contracts-upgradeable": "4.9.3", 25 | "@openzeppelin/hardhat-upgrades": "1.28.0" 26 | } 27 | } -------------------------------------------------------------------------------- /contracts/poor-boy/RichBoyNFTLendingPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 5 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 6 | import "./RichBoyNFT.sol"; 7 | 8 | contract RichBoyNFTLendingPool is IERC721Receiver { 9 | uint256 public constant FEE = 0.1 ether; 10 | RichBoyNFT public rbNFT; 11 | 12 | error RepayFailed(); 13 | 14 | constructor(address rbNFTAddress) { 15 | rbNFT = RichBoyNFT(rbNFTAddress); 16 | } 17 | 18 | function nftInPoolBalance() public view returns (uint256) { 19 | return rbNFT.balanceOf(address(this)); 20 | } 21 | 22 | function flashLoan( 23 | address receiver, 24 | uint256 tokenId 25 | ) external payable returns (bool) { 26 | require(msg.value == FEE, "Incorrect fee"); 27 | uint256 nftInPoolBalanceBefore = nftInPoolBalance(); 28 | 29 | rbNFT.safeTransferFrom(address(this), receiver, tokenId); 30 | 31 | uint256 nftInPoolBalanceAfter = nftInPoolBalance(); 32 | if (nftInPoolBalanceAfter < nftInPoolBalanceBefore) 33 | revert RepayFailed(); 34 | 35 | return true; 36 | } 37 | 38 | function onERC721Received( 39 | address operator, 40 | address from, 41 | uint256 tokenId, 42 | bytes calldata data 43 | ) external returns (bytes4) { 44 | return IERC721Receiver.onERC721Received.selector; 45 | } 46 | 47 | receive() external payable {} 48 | } -------------------------------------------------------------------------------- /contracts/nft-whitelist/NFTWhitelist.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 7 | 8 | contract NFTWhitelist is Initializable, OwnableUpgradeable, UUPSUpgradeable { 9 | mapping(address => bool) private whitelists; 10 | 11 | function init() external initializer { 12 | __Ownable_init(); 13 | __UUPSUpgradeable_init(); 14 | } 15 | 16 | function add(address user) external onlyOwner { 17 | require(whitelists[user] == false, "already in whitelist"); 18 | whitelists[user] = true; 19 | } 20 | 21 | function remove(address user) external onlyOwner { 22 | require(whitelists[user] == true, "not in whitelist"); 23 | whitelists[user] = false; 24 | } 25 | 26 | function hasWhitelist(address user) external view returns (bool) { 27 | return whitelists[user]; 28 | } 29 | 30 | function upgradeTo(address newImplementation) public override { 31 | _authorizeUpgrade(newImplementation); 32 | _upgradeTo(newImplementation); 33 | } 34 | 35 | function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { 36 | _authorizeUpgrade(newImplementation); 37 | _upgradeToAndCallUUPS(newImplementation, data, true); 38 | } 39 | 40 | function _authorizeUpgrade(address imp) internal override onlyOwner {} 41 | } 42 | -------------------------------------------------------------------------------- /contracts/nft-whitelist/NFTMarket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 7 | import "../ValixPlaygroundNFT.sol"; 8 | import "./NFTWhitelist.sol"; 9 | 10 | contract NFTMarket { 11 | ValixPlaygroundNFT nft; 12 | NFTWhitelist whitelist; 13 | mapping(address => bool) private minted; 14 | error PermissionDenied(); 15 | constructor(ValixPlaygroundNFT _nft, NFTWhitelist _whitelist) { 16 | nft = _nft; 17 | whitelist = _whitelist; 18 | } 19 | 20 | function buy() payable external { 21 | require(minted[msg.sender] == false, "already minted"); 22 | require(msg.value == 1 ether, "insufficient"); 23 | 24 | minted[msg.sender] = true; 25 | nft.safeMint(msg.sender); 26 | } 27 | 28 | function claim() external { 29 | require(minted[msg.sender] == false, "already minted"); 30 | if (!hasWhitelist(msg.sender)) { 31 | revert PermissionDenied(); 32 | } 33 | 34 | minted[msg.sender] = true; 35 | nft.safeMint(msg.sender); 36 | } 37 | 38 | function hasWhitelist(address user) public view returns (bool) { 39 | assembly { 40 | let target := sload(1) 41 | let p := mload(0x40) 42 | mstore(p, shl(0xe0, 0x8cdb7e8b)) 43 | mstore(add(p, 0x04), user) 44 | if iszero(staticcall(gas(), target, p, 0x24, p, 0x20)) {return(0, 0)} 45 | if and(not(iszero(returndatasize())), iszero(mload(p))) {return(0, 0)} 46 | } 47 | return true; 48 | } 49 | } -------------------------------------------------------------------------------- /test/the-secret-source/the-secret-source.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 4 | import { TheSecretSource } from '../../typechain-types'; 5 | import { ValixPlaygroundToken } from '../../typechain-types'; 6 | 7 | describe('Challenge - The Secret Source', function () { 8 | let deployer: SignerWithAddress; 9 | let player: SignerWithAddress; 10 | let theSecretSource: TheSecretSource; 11 | let token: ValixPlaygroundToken; 12 | 13 | const REWARD_AMOUNT: bigint = 1_000_000n * 10n ** 18n; 14 | 15 | before(async function () { 16 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 17 | [deployer, player] = await ethers.getSigners(); 18 | 19 | // Deploy Token contract 20 | const ValixPlaygroundToken = await (await ethers.getContractFactory('ValixPlaygroundToken', deployer)); 21 | token = await ValixPlaygroundToken.deploy(); 22 | 23 | // Deploy The Secret Source contract 24 | const TheSecretSource = await (await ethers.getContractFactory('TheSecretSource', deployer)); 25 | theSecretSource = await TheSecretSource.deploy(token.address); 26 | 27 | // Set the secret source 28 | await theSecretSource.setSecret("nothing"); 29 | 30 | // Initialize the winner's reward 31 | await token.transfer(theSecretSource.address, REWARD_AMOUNT); 32 | }); 33 | 34 | it('Execution', async function () { 35 | /** CODE YOUR SOLUTION HERE */ 36 | }); 37 | 38 | after(async function () { 39 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 40 | 41 | //Player must become the winner and retrieved reward 42 | expect(await token.balanceOf(player.address)).to.eq(REWARD_AMOUNT); 43 | }); 44 | }); -------------------------------------------------------------------------------- /contracts/freeze-the-flow/Vault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | contract Vault is Ownable { 7 | address public vaultHelpaer; 8 | mapping(address => uint256) private balance; 9 | 10 | event Deposit(address sender, uint256 amount); 11 | event Withdraw(address sender, uint256 amount); 12 | event SetVaultHelper(address prevVaultHelper, address newVaultHelper); 13 | 14 | modifier onlyVaultHelper() { 15 | require(msg.sender == address(vaultHelpaer), "Only VaultHelper can call to this action"); 16 | _; 17 | } 18 | 19 | function deposit(address _to) external payable onlyVaultHelper { 20 | require(_to != address(0), "Invalid address"); 21 | require(msg.value > 0, "No deposit"); 22 | balance[_to] += msg.value; 23 | 24 | emit Deposit(_to, msg.value); 25 | } 26 | 27 | function withdraw(address _to, uint256 _amount) external onlyVaultHelper { 28 | require(_to != address(0), "Invalid address"); 29 | require(_amount > 0, "Amount withdraw should above zero"); 30 | 31 | require(balance[_to] >= _amount); 32 | balance[_to] -= _amount; 33 | payable(_to).transfer(_amount); 34 | 35 | emit Withdraw(_to, _amount); 36 | } 37 | 38 | function setVaultHelper(address _newVaultHelper) external onlyOwner { 39 | address prevVaultHelper = address(vaultHelpaer); 40 | vaultHelpaer = _newVaultHelper; 41 | 42 | emit SetVaultHelper(prevVaultHelper, _newVaultHelper); 43 | } 44 | 45 | function getUserBalance(address _user) public view returns(uint256) { 46 | return balance[_user]; 47 | } 48 | 49 | function getVaultBalance() public view returns(uint256) { 50 | return address(this).balance; 51 | } 52 | } -------------------------------------------------------------------------------- /test/extra-bank/utils/AddOn.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "anonymous": false, 5 | "inputs": [ 6 | { 7 | "indexed": false, 8 | "internalType": "string", 9 | "name": "message", 10 | "type": "string" 11 | } 12 | ], 13 | "name": "Message", 14 | "type": "event" 15 | }, 16 | { 17 | "inputs": [], 18 | "name": "addOn", 19 | "outputs": [], 20 | "stateMutability": "nonpayable", 21 | "type": "function" 22 | }, 23 | { 24 | "inputs": [], 25 | "name": "emergencyKill", 26 | "outputs": [], 27 | "stateMutability": "nonpayable", 28 | "type": "function" 29 | } 30 | ], 31 | "bytecode": "0x608060405234801561001057600080fd5b50610153806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806328df3f221461003b578063bead851714610045575b600080fd5b61004361004f565b005b61004d610086565b005b7f51a7f65c6325882f237d4aeb43228179cfad48b868511d508e24b4437a81913760405161007c906100fd565b60405180910390a1565b600073ffffffffffffffffffffffffffffffffffffffff16ff5b600082825260208201905092915050565b7f4578637574656420636f646520617070726f7665642062792042616e6b000000600082015250565b60006100e7601d836100a0565b91506100f2826100b1565b602082019050919050565b60006020820190508181036000830152610116816100da565b905091905056fea2646970667358221220c2fbc744339ab43a4b3cb26290ed45abfd14d6db62a7adbbd136c2008211b78a64736f6c63430008130033", 32 | "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c806328df3f221461003b578063bead851714610045575b600080fd5b61004361004f565b005b61004d610086565b005b7f51a7f65c6325882f237d4aeb43228179cfad48b868511d508e24b4437a81913760405161007c906100fd565b60405180910390a1565b600073ffffffffffffffffffffffffffffffffffffffff16ff5b600082825260208201905092915050565b7f4578637574656420636f646520617070726f7665642062792042616e6b000000600082015250565b60006100e7601d836100a0565b91506100f2826100b1565b602082019050919050565b60006020820190508181036000830152610116816100da565b905091905056fea2646970667358221220c2fbc744339ab43a4b3cb26290ed45abfd14d6db62a7adbbd136c2008211b78a64736f6c63430008130033", 33 | "linkReferences": {}, 34 | "deployedLinkReferences": {} 35 | } 36 | -------------------------------------------------------------------------------- /test/the-collector/the-collector.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; 4 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 5 | import { TheCollectorNFT } from '../../typechain-types'; 6 | import { ValixPlaygroundToken } from '../../typechain-types'; 7 | import { TheCollectorMarket } from '../../typechain-types'; 8 | 9 | describe('Challenge - The Collector', function () { 10 | let player: SignerWithAddress; 11 | let nft: TheCollectorNFT; 12 | let token: ValixPlaygroundToken; 13 | let theCollectorMarket: TheCollectorMarket; 14 | 15 | const TOKENS_FOR_PLAYER: bigint = 100n * 10n ** 18n; 16 | const ETH_FOR_PLAYER: bigint = 1n * 10n ** 18n; 17 | 18 | before(async function () { 19 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 20 | const [deployer] = await ethers.getSigners(); 21 | [, player] = await ethers.getSigners(); 22 | 23 | // Deploy Token contract 24 | const ValixPlaygroundToken = await (await ethers.getContractFactory('ValixPlaygroundToken', deployer)); 25 | token = await ValixPlaygroundToken.deploy(); 26 | 27 | // Deploy The Collector NFT contract 28 | const TheCollectorNFT = await (await ethers.getContractFactory('TheCollectorNFT', deployer)); 29 | nft = await TheCollectorNFT.deploy(); 30 | 31 | // Deploy the The Collector Market contract 32 | const TheCollectorMarket = await (await ethers.getContractFactory('TheCollectorMarket', deployer)); 33 | theCollectorMarket = await TheCollectorMarket.deploy(token.address, nft.address); 34 | await nft.transferOwnership(theCollectorMarket.address); 35 | 36 | // Setup initial token balances of player 37 | setBalance(player.address, ETH_FOR_PLAYER); 38 | await token.transfer(player.address, TOKENS_FOR_PLAYER); 39 | }); 40 | 41 | it('Execution', async function () { 42 | /** CODE YOUR SOLUTION HERE */ 43 | }); 44 | 45 | after(async function () { 46 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 47 | 48 | // Player must owned 4 NFTs 49 | expect(await nft.balanceOf(player.address)).to.eq(4); 50 | }); 51 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](cover.png) 2 | # Valix CTF Playground 3 | An interactive Ethereum-based platform for mastering offensive security in Smart Contracts. 4 | 5 | # Disclaimer 6 | The smart contracts in this repository are intended for learning the offensive security aspects of Ethereum smart contracts. Some contracts are vulnerable, some are simplified for minimal, some contain malicious code. Hence, do not use the source code in this repository in your production. 7 | 8 | Nonetheless, feel free to contact **Valix Consulting** for your smart contract consulting and auditing services.🕵 9 | 10 | # Challenges 11 | | # | Name | 12 | | ------------- | ------------- | 13 | | 1 | Extra Bank | 14 | | 2 | Lock Pool | 15 | | 3 | NFT Whitelist | 16 | | 4 | Poor Boy | 17 | | 5 | The Collector | 18 | | 6 | Bullied Boy | 19 | | 7 | The Secret Source | 20 | | 8 | Trick or Thieve | 21 | | 9 | Lotto888 | 22 | | 10 | Alice in The Dark | 23 | | 11 | Freeze The Flow | 24 | 25 | # How to play 26 | 1. Clone this repository 27 | 2. Install dependencies with `npm install` 28 | 3. Code your solution in the `*.challenge.js` file (inside each challenge's folder in the test folder). 29 | 4. In all challenges you must use the account called `player`. In Ethers, that may translate to using `.connect(player)`. 30 | 5. Run the challenge with `npm run <>` If the test is executed successfully, you've passed! 31 | 32 | # Important Notes 33 | - Solidity is absolutely required. 34 | - Typescript knowledge is beneficial but not a must-have 35 | - To code the solutions, you may need to read Ethers and Hardhat docs. 36 | - Some challenges require you to code and deploy custom smart contracts. Keep them in the `contracts/@player-contracts` 37 | 38 | # About Valix Consulting 39 | **Valix Consulting** is a blockchain and smart contract security firm offering a wide range of cybersecurity consulting services. Our specialists, combined with technical expertise with industry knowledge and support staff, strive to deliver consistently superior quality services. 40 | 41 | For any business inquiries, please get in touch with us via [Twitter](https://twitter.com/valixconsulting), [Facebook](https://www.facebook.com/ValixConsulting), or [info@valix.io](mailto:info@valix.io). 42 | 43 | # Inspiration 44 | Inspiration by [Damn Vulnerable DeFi](https://www.damnvulnerabledefi.xyz/). -------------------------------------------------------------------------------- /contracts/trick-or-thieve/ValixPlaygroundTokenLite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract ValixPlaygroundTokenLite is IERC20, Ownable { 8 | uint256 public totalSupply; 9 | mapping(address => uint256) public balanceOf; 10 | mapping(address => mapping(address => uint256)) public allowance; 11 | 12 | string public name = "ValixPlaygroundTokenLite"; 13 | string public symbol = "VLXP-l"; 14 | uint8 public decimals = 18; 15 | 16 | function transfer(address to, uint256 amount) external returns (bool) { 17 | require(to != address(0), "Invalid address"); 18 | 19 | if (balanceOf[msg.sender] < amount) { 20 | return false; 21 | } 22 | 23 | balanceOf[msg.sender] -= amount; 24 | balanceOf[to] += amount; 25 | 26 | emit Transfer(msg.sender, to, amount); 27 | return true; 28 | } 29 | 30 | function approve(address spender, uint256 amount) external returns (bool) { 31 | allowance[msg.sender][spender] = amount; 32 | emit Approval(msg.sender, spender, amount); 33 | return true; 34 | } 35 | 36 | function transferFrom( 37 | address from, 38 | address to, 39 | uint256 amount 40 | ) external returns (bool) { 41 | require(from != address(0) && to != address(0), "Invalid address"); 42 | 43 | if (allowance[from][msg.sender] < amount) { 44 | return false; 45 | } 46 | 47 | if (balanceOf[from] < amount) { 48 | return false; 49 | } 50 | 51 | allowance[from][msg.sender] -= amount; 52 | balanceOf[from] -= amount; 53 | balanceOf[to] += amount; 54 | 55 | emit Transfer(from, to, amount); 56 | return true; 57 | } 58 | 59 | function mint(address to, uint256 amount) external onlyOwner { 60 | require(to != address(0), "Invalid address"); 61 | balanceOf[to] += amount; 62 | totalSupply += amount; 63 | emit Transfer(address(0), msg.sender, amount); 64 | } 65 | 66 | function burn(uint256 amount) external { 67 | balanceOf[msg.sender] -= amount; 68 | totalSupply -= amount; 69 | emit Transfer(msg.sender, address(0), amount); 70 | } 71 | } -------------------------------------------------------------------------------- /test/poor-boy/poor-boy.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 4 | const { setBalance } = require("@nomicfoundation/hardhat-network-helpers"); 5 | import { RichBoyNFT } from '../../typechain-types'; 6 | import { RichBoyTraitNFT } from '../../typechain-types'; 7 | import { RichBoyNFTLendingPool } from '../../typechain-types'; 8 | 9 | describe("Challenge - Poor Boy", function () { 10 | let deployer: SignerWithAddress; 11 | let player: SignerWithAddress; 12 | let rbNFT: RichBoyNFT; 13 | let rbTraitNFT: RichBoyTraitNFT; 14 | let rbNFTLendingPool: RichBoyNFTLendingPool; 15 | 16 | const AMOUNT_OF_NFTS_IN_LENDING_POOL = 1; 17 | const PLAYER_INITIAL_ETH_BALANCE = 2n * 10n ** 17n; // 0.2 ETHER 18 | const LENDING_POOL_FEE = 1n * 10n ** 17n; // 0.1 ETHER 19 | 20 | before(async function () { 21 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 22 | [deployer, player] = await ethers.getSigners(); 23 | 24 | // Player starts with limited ETH balance 25 | setBalance(player.address, PLAYER_INITIAL_ETH_BALANCE); 26 | expect(await ethers.provider.getBalance(player.address)).to.eq(PLAYER_INITIAL_ETH_BALANCE); 27 | 28 | const RichBoyNFT = await (await ethers.getContractFactory('RichBoyNFT', deployer)); 29 | rbNFT = await RichBoyNFT.deploy(); 30 | 31 | const RichBoyTraitNFT = await (await ethers.getContractFactory('RichBoyTraitNFT', deployer)); 32 | rbTraitNFT = await RichBoyTraitNFT.deploy(rbNFT.address); 33 | 34 | const RichBoyNFTLendingPool = await (await ethers.getContractFactory('RichBoyNFTLendingPool', deployer)); 35 | rbNFTLendingPool = await RichBoyNFTLendingPool.deploy(rbNFT.address); 36 | 37 | // Allocate RichBoyNFT for lending pool 38 | await rbNFT.allocateForLendingPool(rbNFTLendingPool.address); 39 | expect(await rbNFT.balanceOf(rbNFTLendingPool.address)).to.equal(AMOUNT_OF_NFTS_IN_LENDING_POOL); 40 | }); 41 | 42 | it('Execution', async function () { 43 | /** CODE YOUR SOLUTION HERE */ 44 | 45 | }); 46 | 47 | after(async function () { 48 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 49 | expect(await rbTraitNFT.balanceOf(player.address)).to.equal(2); 50 | expect(await rbTraitNFT.isPremiumMember(player.address)).to.equal(true); 51 | }); 52 | }); -------------------------------------------------------------------------------- /test/lotto888/lotto888.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 4 | const { setBalance } = require("@nomicfoundation/hardhat-network-helpers"); 5 | import { GamblingOracle } from '../../typechain-types'; 6 | import { Lotto888 } from '../../typechain-types'; 7 | 8 | describe("Challenge - Lotto888", function () { 9 | let deployer: SignerWithAddress; 10 | let player: SignerWithAddress; 11 | let oracle: GamblingOracle; 12 | let lotto888: Lotto888; 13 | 14 | const ONE_ETHER = 10n ** 18n; 15 | const POOL_INITIAL_ETH_BALANCE = 10n * 10n ** 18n 16 | const PLAYER_INITIAL_ETH_BALANCE = 2n * 10n ** 17n; // 0.2 ETHER 17 | 18 | before(async function () { 19 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 20 | [deployer, player] = await ethers.getSigners(); 21 | // Player starts with limited ETH balance 22 | setBalance(player.address, PLAYER_INITIAL_ETH_BALANCE); 23 | expect(await ethers.provider.getBalance(player.address)).to.eq(PLAYER_INITIAL_ETH_BALANCE); 24 | 25 | const GamblingOracle = await (await ethers.getContractFactory('GamblingOracle', deployer)); 26 | oracle = await GamblingOracle.deploy(); 27 | 28 | const Lotto888 = await (await ethers.getContractFactory('Lotto888', deployer)); 29 | lotto888 = await Lotto888.deploy(oracle.address); 30 | 31 | // Deposit initial balance for Lotto888 pool 32 | await lotto888.deposit({value: POOL_INITIAL_ETH_BALANCE}); 33 | expect(await lotto888.balance()).to.eq(POOL_INITIAL_ETH_BALANCE); 34 | 35 | // Set authorized contract 36 | await oracle.addAuthorized(lotto888.address); 37 | expect(await oracle.authorized(lotto888.address)).to.eq(true); 38 | }); 39 | 40 | it('Execution', async function () { 41 | /** CODE YOUR SOLUTION HERE */ 42 | 43 | }); 44 | 45 | after(async function () { 46 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 47 | // Take money from the pool 48 | expect(await ethers.provider.getBalance(player.address)).to.greaterThan(PLAYER_INITIAL_ETH_BALANCE); 49 | 50 | // Special Bonus! 51 | // Uncomment the line below and drain the pool until the balance is less than 1 ether in a single transaction. 52 | // expect(await lotto888.balance()).to.lessThan(ONE_ETHER); 53 | }); 54 | }); -------------------------------------------------------------------------------- /contracts/extra-bank/ExtraBank.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | contract ExtraBank { 5 | address public owner; 6 | address public operator; 7 | 8 | address public addOnContract; 9 | mapping(address => uint256) private balance; 10 | 11 | event Deposit(address sender, uint256 amount); 12 | event Withdraw(address sender, uint256 amount); 13 | event SetOpeartor(address prevOperator, address newOperator); 14 | event SetAddOn(address prevAddOn, address newAddOn); 15 | 16 | constructor(address _operator) { 17 | require(_operator != address(0), "Invald address"); 18 | owner = msg.sender; 19 | operator = _operator; 20 | } 21 | 22 | function deposit() external payable{ 23 | require(msg.value > 0, "No deposit"); 24 | balance[msg.sender] += msg.value; 25 | 26 | emit Deposit(msg.sender, msg.value); 27 | } 28 | 29 | function withdraw(uint256 _amount) external { 30 | require(balance[msg.sender] >= _amount); 31 | balance[msg.sender] -= _amount; 32 | payable(msg.sender).transfer(_amount); 33 | 34 | emit Withdraw(msg.sender, _amount); 35 | } 36 | 37 | function emergencyWithdraw() external { 38 | require(msg.sender == owner, "Permission denied"); 39 | payable(msg.sender).transfer(address(this).balance); 40 | 41 | emit Withdraw(owner, address(this).balance); 42 | } 43 | 44 | function getUserBalance(address _user) public view returns(uint256){ 45 | return balance[_user]; 46 | } 47 | 48 | function getBankBalance() public view returns(uint256){ 49 | return address(this).balance; 50 | } 51 | 52 | function setOperator(address _newOperator) external { 53 | address prevOperator = operator; 54 | operator = _newOperator; 55 | 56 | emit SetOpeartor(prevOperator, operator); 57 | } 58 | 59 | function setAddOn(address _newAddOn) external { 60 | require(msg.sender == operator || msg.sender == owner, "Permission denied"); 61 | address prevAddOn = _newAddOn; 62 | addOnContract = _newAddOn; 63 | 64 | emit SetAddOn(prevAddOn, _newAddOn); 65 | } 66 | 67 | function executeAddOn() external returns (bytes memory){ 68 | (bool success, bytes memory data) = addOnContract.delegatecall(abi.encodeWithSignature("addOn()")); 69 | require(success, "delegatecall failed"); 70 | 71 | return data; 72 | } 73 | } -------------------------------------------------------------------------------- /test/lock-pool/lock-pool.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 4 | import { LockPool } from '../../typechain-types'; 5 | import { ValixPlaygroundToken } from '../../typechain-types'; 6 | import { ValixPlaygroundNFT } from '../../typechain-types'; 7 | 8 | describe('Challenge - Lock Pool', function () { 9 | let deployer: SignerWithAddress; 10 | let player: SignerWithAddress; 11 | 12 | let pool: LockPool; 13 | let lockToken: ValixPlaygroundToken; 14 | let rewardToken: ValixPlaygroundNFT; 15 | 16 | const TOKENS_IN_POOL: bigint = 100n * 10n ** 18n; 17 | const TOKENS_FOR_PLAYER: bigint = 10n * 10n ** 18n; 18 | 19 | before(async function () { 20 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 21 | [deployer, player] = await ethers.getSigners(); 22 | 23 | const ValixPlaygroundToken = await (await ethers.getContractFactory('ValixPlaygroundToken', deployer)); 24 | lockToken = await ValixPlaygroundToken.deploy(); 25 | 26 | const ValixPlaygroundNFT = await (await ethers.getContractFactory('ValixPlaygroundNFT', deployer)); 27 | rewardToken = await ValixPlaygroundNFT.deploy(); 28 | 29 | const LockPool = await (await ethers.getContractFactory('LockPool', deployer)); 30 | pool = await LockPool.deploy(lockToken.address, rewardToken.address); 31 | 32 | await rewardToken.connect(deployer).transferOwnership(pool.address); 33 | 34 | await lockToken.transfer(pool.address, TOKENS_IN_POOL); 35 | await lockToken.transfer(player.address, TOKENS_FOR_PLAYER); 36 | 37 | expect(await pool.lockToken()).to.eq(lockToken.address); 38 | expect(await pool.rewardToken()).to.eq(rewardToken.address); 39 | 40 | expect(await lockToken.balanceOf(pool.address)).to.equal(TOKENS_IN_POOL); 41 | expect(await lockToken.balanceOf(player.address)).to.equal(TOKENS_FOR_PLAYER); 42 | 43 | expect(await rewardToken.owner()).to.eq(pool.address); 44 | }); 45 | 46 | it('Execution', async function () { 47 | /** CODE YOUR SOLUTION HERE */ 48 | 49 | }); 50 | 51 | after(async function () { 52 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 53 | 54 | //Player has taken all tokens from the pool 55 | expect( 56 | await lockToken.balanceOf(player.address) 57 | ).to.equal(TOKENS_IN_POOL + TOKENS_FOR_PLAYER); 58 | expect( 59 | await lockToken.balanceOf(pool.address) 60 | ).to.equal(0); 61 | }); 62 | }); -------------------------------------------------------------------------------- /test/freeze-the-flow/freeze-the-flow.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 4 | 5 | import { Vault } from '../../typechain-types'; 6 | import { VaultHelper } from '../../typechain-types'; 7 | 8 | describe('Challenge - Fleeze The Flow', function () { 9 | let deployer: SignerWithAddress; 10 | let player: SignerWithAddress; 11 | let someUser: SignerWithAddress; 12 | 13 | let vault: Vault; 14 | let vaultHelper: VaultHelper; 15 | 16 | before(async function () { 17 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 18 | [deployer, player, someUser] = await ethers.getSigners(); 19 | 20 | // Deploy VaultHelper & Vault Contracts 21 | const Vault = await (await ethers.getContractFactory('Vault', deployer)); 22 | vault = await Vault.deploy(); 23 | 24 | const VaultHelper = await (await ethers.getContractFactory('VaultHelper', deployer)); 25 | vaultHelper = await VaultHelper.deploy(vault.address); 26 | 27 | await vault.setVaultHelper(vaultHelper.address); 28 | 29 | expect(await vault.owner()).to.eq(deployer.address); 30 | expect(await vaultHelper.owner()).to.eq(deployer.address); 31 | 32 | expect(await vault.vaultHelpaer()).to.eq(vaultHelper.address); 33 | expect(await vaultHelper.vault()).to.eq(vault.address); 34 | 35 | // someUser interact with the Vault contract through VaultHelper contract; 36 | await vaultHelper.connect(someUser).depositToVault(someUser.address, { value: ethers.utils.parseEther("100") }); 37 | expect(await vault.getUserBalance(someUser.address)).to.eq(ethers.utils.parseEther("100")); 38 | expect(await vault.getVaultBalance()).to.eq(ethers.utils.parseEther("100")); 39 | 40 | // someUser cannot directly interact to Vault contract 41 | await expect(vault.connect(someUser).deposit(someUser.address, { value: ethers.utils.parseEther("100") })).to.be.reverted; 42 | // someUser cannot directly send ether to VaultHelper contract 43 | await expect(someUser.sendTransaction({ 44 | to: vaultHelper.address, 45 | value: ethers.utils.parseEther("100"), 46 | })).to.be.reverted; 47 | }); 48 | 49 | it('Execution', async function () { 50 | /** CODE YOUR SOLUTION HERE */ 51 | 52 | }); 53 | 54 | after(async function () { 55 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 56 | 57 | // Player has stopped the deposit process 58 | await expect(vaultHelper.connect(someUser).depositToVault(someUser.address, { value: ethers.utils.parseEther("100") })).to.be.reverted; 59 | }); 60 | }); -------------------------------------------------------------------------------- /contracts/lock-pool/LockPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/utils/Address.sol"; 5 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 6 | import "../ValixPlaygroundToken.sol"; 7 | import "../ValixPlaygroundNFT.sol"; 8 | 9 | contract LockPool is ReentrancyGuard{ 10 | ValixPlaygroundToken public immutable lockToken; 11 | ValixPlaygroundNFT public immutable rewardToken; 12 | 13 | address public owner; 14 | 15 | uint256 public constant REWARD_DURATION = 17 days; 16 | uint256 public constant OPEN_DURATION = 2 days; 17 | uint256 public constant REQUIRED_LOCK_AMOUNT = 10 * 1e18; 18 | 19 | uint256 public immutable openPeriod; 20 | uint256 public immutable rewardApplicableAt; 21 | 22 | mapping(address => uint) public balanceOf; 23 | 24 | event Lock(address sender, uint256 amount, uint256 ts); 25 | event EmergencyWithdraw(address sender, address to, uint256 amount, uint256 ts); 26 | event WithdrawAndClaim(address sender, uint256 tokenId, uint256 ts); 27 | 28 | constructor(ValixPlaygroundToken _lockToken, ValixPlaygroundNFT _rewardToken) { 29 | owner = msg.sender; 30 | 31 | lockToken = _lockToken; 32 | rewardToken = _rewardToken; 33 | 34 | openPeriod = block.timestamp + OPEN_DURATION; 35 | rewardApplicableAt = block.timestamp + REWARD_DURATION; 36 | } 37 | 38 | modifier onlyOwner() { 39 | require(msg.sender == owner, "not authorized"); 40 | _; 41 | } 42 | 43 | function lock() external { 44 | require(block.timestamp < openPeriod, "LockPool: Open period is closed"); 45 | balanceOf[msg.sender] += REQUIRED_LOCK_AMOUNT; 46 | lockToken.transferFrom(msg.sender, address(this), REQUIRED_LOCK_AMOUNT); 47 | 48 | emit Lock(msg.sender, REQUIRED_LOCK_AMOUNT, block.timestamp); 49 | } 50 | 51 | function emergencyWithdraw(address _to) external { 52 | uint256 userBalance = balanceOf[msg.sender]; 53 | 54 | require(balanceOf[msg.sender] >= 0); 55 | balanceOf[_to] += userBalance; 56 | balanceOf[msg.sender] = 0; 57 | 58 | emit EmergencyWithdraw(msg.sender, _to, userBalance, block.timestamp); 59 | } 60 | 61 | function withdrawAndClaim() external nonReentrant { 62 | uint256 userBalance = balanceOf[msg.sender]; 63 | 64 | require(block.timestamp >= rewardApplicableAt, "LockPool: Not yet reach the withdraw and claim period"); 65 | require(userBalance >= REQUIRED_LOCK_AMOUNT, "LockPool: Inadequate amount for withdraw and claim"); 66 | 67 | lockToken.transfer(msg.sender, userBalance); 68 | uint256 tokenId = rewardToken.safeMint(msg.sender); 69 | 70 | balanceOf[msg.sender] = 0; 71 | 72 | emit WithdrawAndClaim(msg.sender, tokenId, block.timestamp); 73 | } 74 | } -------------------------------------------------------------------------------- /test/trick-or-thieve/trick-or-thieve.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 4 | import { NftMarketplace } from '../../typechain-types'; 5 | import { ValixPlaygroundNFT } from '../../typechain-types'; 6 | import { ValixPlaygroundTokenLite } from '../../typechain-types'; 7 | 8 | describe('Challenge - Trick or Thieve', function () { 9 | let deployer: SignerWithAddress; 10 | let player: SignerWithAddress; 11 | let seller: SignerWithAddress; 12 | let targetTokenId: BigInteger; 13 | 14 | let marketplace: NftMarketplace; 15 | let token: ValixPlaygroundTokenLite; 16 | let nft: ValixPlaygroundNFT; 17 | 18 | const PRICE_FOR_SALE: bigint = 999n * 10n ** 18n; 19 | const TOKEN_FOR_PLAYER: bigint = 0n * 10n ** 18n; 20 | 21 | before(async function () { 22 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 23 | [deployer, player, seller] = await ethers.getSigners(); 24 | 25 | const ValixPlaygroundTokenLite = await (await ethers.getContractFactory('ValixPlaygroundTokenLite', deployer)); 26 | token = await ValixPlaygroundTokenLite.deploy(); 27 | 28 | const ValixPlaygroundNFT = await (await ethers.getContractFactory('ValixPlaygroundNFT', deployer)); 29 | nft = await ValixPlaygroundNFT.deploy(); 30 | 31 | const NftMarketplace = await (await ethers.getContractFactory('NftMarketplace', deployer)); 32 | marketplace = await NftMarketplace.deploy(token.address, nft.address); 33 | 34 | // Mint NFT token to seller 35 | const mintNFTTx = await nft.safeMint(seller.address); 36 | const waitTx = await mintNFTTx.wait(); 37 | const event = waitTx.events?.find((event: any) => event.event === 'Transfer'); 38 | targetTokenId = event?.args?.tokenId; 39 | 40 | expect(await nft.ownerOf(targetTokenId)).to.eq(seller.address); 41 | 42 | // Seller approve and list NFT into the marketplace 43 | await nft.connect(seller).approve(marketplace.address, targetTokenId); 44 | await marketplace.connect(seller).listItem(0, PRICE_FOR_SALE); 45 | 46 | expect((await marketplace.getListing(targetTokenId)).price).to.eq(PRICE_FOR_SALE); 47 | expect((await marketplace.getListing(targetTokenId)).seller).to.eq(seller.address); 48 | }); 49 | 50 | it('Execution', async function () { 51 | /** CODE YOUR SOLUTION HERE */ 52 | 53 | }); 54 | 55 | after(async function () { 56 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 57 | 58 | //Player retrieve the NFT without purchase anything 59 | expect( 60 | await token.balanceOf(player.address) 61 | ).to.equal(TOKEN_FOR_PLAYER); 62 | expect( 63 | await nft.ownerOf(targetTokenId) 64 | ).to.equal(player.address); 65 | }); 66 | }); -------------------------------------------------------------------------------- /test/nft-whitelist/nft-whitelist.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | const { setBalance } = require('@nomicfoundation/hardhat-network-helpers'); 4 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 5 | import { ValixPlaygroundNFT } from '../../typechain-types'; 6 | import { NFTWhitelist } from '../../typechain-types'; 7 | import { NFTMarket } from '../../typechain-types'; 8 | 9 | describe('Challenge - NFT Whitelist', function () { 10 | let deployer: SignerWithAddress; 11 | let player: SignerWithAddress; 12 | let nft: ValixPlaygroundNFT; 13 | let nftMarket: NFTMarket; 14 | let nftWhitelist: NFTWhitelist; 15 | 16 | before(async function () { 17 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 18 | [deployer, player] = await ethers.getSigners(); 19 | const [,, alice, bob] = await ethers.getSigners(); 20 | 21 | // Initialize balance of Player account for paying the gas 22 | setBalance(player.address, ethers.utils.parseEther("0.1")); 23 | // Initialize balance of Bob account 24 | setBalance(bob.address, ethers.utils.parseEther("10.0")); 25 | 26 | const ValixPlaygroundNFT = await (await ethers.getContractFactory('ValixPlaygroundNFT', deployer)); 27 | nft = await ValixPlaygroundNFT.deploy(); 28 | 29 | // Deploy NFTWhitelist with the corresponding proxy 30 | const proxy = await upgrades.deployProxy( 31 | await ethers.getContractFactory('NFTWhitelist', deployer), 32 | [], 33 | { kind: 'uups', initializer: 'init', unsafeAllow: ["delegatecall"] } 34 | ); 35 | nftWhitelist = await ( 36 | await ethers.getContractFactory('NFTWhitelist') 37 | ).attach(proxy.address); 38 | 39 | // Deploy the NFTMarket 40 | const NFTMarket = await (await ethers.getContractFactory('NFTMarket', deployer)); 41 | nftMarket = await NFTMarket.deploy(nft.address, nftWhitelist.address); 42 | await nft.transferOwnership(nftMarket.address); 43 | 44 | // Add Alice to whitelist 45 | await nftWhitelist.add(alice.address); 46 | 47 | // Alice claims their NFT with the whitelisted 48 | await nftMarket.connect(alice).claim(); 49 | // Bob bought the NFT by spending the standard price 50 | await nftMarket.connect(bob).buy({value: ethers.utils.parseEther("1.0")}); 51 | 52 | expect(await nft.balanceOf(alice.address)).to.eq(1); 53 | expect(await nft.balanceOf(bob.address)).to.eq(1); 54 | expect(await nft.balanceOf(player.address)).to.eq(0); 55 | }); 56 | 57 | it('Execution', async function () { 58 | /** CODE YOUR SOLUTION HERE */ 59 | }); 60 | 61 | after(async function () { 62 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 63 | 64 | // Player must owned nft 65 | expect(await nft.balanceOf(player.address)).to.eq(1); 66 | }); 67 | }); -------------------------------------------------------------------------------- /test/bullied-boy/bullied-boy.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; 4 | const { setBalance } = require("@nomicfoundation/hardhat-network-helpers"); 5 | import { SchoolRunner } from '../../typechain-types'; 6 | 7 | describe("Challenge - Bullied Boy", function () { 8 | let teacher: SignerWithAddress; 9 | let player: SignerWithAddress; 10 | let alice: SignerWithAddress; 11 | let bob: SignerWithAddress; 12 | let carol: SignerWithAddress; 13 | let dan: SignerWithAddress; 14 | 15 | let schoolRunner: SchoolRunner; 16 | 17 | const TEACHER_INITIAL_ETH_BALANCE = 100n * 10n ** 18n; // 100 ETHER 18 | const STUDENT_INITIAL_ETH_BALANCE = 1n * 10n ** 17n; // 0.1 ETHER 19 | const TOTAL_REWARD = 15n * 10n ** 18n; // 15 ETHER 20 | 21 | before(async function () { 22 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 23 | [teacher, player, alice, bob, carol, dan] = await ethers.getSigners(); 24 | 25 | // Setup teacher and student balance 26 | setBalance(teacher.address, TEACHER_INITIAL_ETH_BALANCE); 27 | setBalance(player.address, STUDENT_INITIAL_ETH_BALANCE); 28 | setBalance(alice.address, STUDENT_INITIAL_ETH_BALANCE); 29 | setBalance(bob.address, STUDENT_INITIAL_ETH_BALANCE); 30 | setBalance(carol.address, STUDENT_INITIAL_ETH_BALANCE); 31 | setBalance(dan.address, STUDENT_INITIAL_ETH_BALANCE); 32 | 33 | // Setup SchoolRunner 34 | const SchoolRunner = await (await ethers.getContractFactory('SchoolRunner', teacher)); 35 | schoolRunner = await SchoolRunner.deploy(); 36 | 37 | // Teacher deposit reward 38 | await schoolRunner.depositReward({value: TOTAL_REWARD}); 39 | expect(await ethers.provider.getBalance(schoolRunner.address)).to.eq(TOTAL_REWARD); 40 | 41 | // Register Student 42 | await schoolRunner.connect(alice).register(); 43 | await schoolRunner.connect(bob).register(); 44 | await schoolRunner.connect(carol).register(); 45 | await schoolRunner.connect(dan).register(); 46 | expect(await schoolRunner.studentPositions(0)).to.eq(alice.address); 47 | expect(await schoolRunner.studentPositions(1)).to.eq(bob.address); 48 | expect(await schoolRunner.studentPositions(2)).to.eq(carol.address); 49 | expect(await schoolRunner.studentPositions(3)).to.eq(dan.address); 50 | }); 51 | 52 | it('Execution', async function () { 53 | /** CODE YOUR SOLUTION HERE */ 54 | 55 | }); 56 | 57 | after(async function () { 58 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 59 | 60 | // Ensure the transaction reverts when the teacher attempts to distribute rewards. 61 | await expect(schoolRunner.connect(teacher).distributeReward()).to.reverted; 62 | expect(await ethers.provider.getBalance(schoolRunner.address)).to.eq(TOTAL_REWARD); 63 | 64 | // Ensures that none of the students can receive a reward after distribution. 65 | expect(await ethers.provider.getBalance(alice.address)).to.lessThan(STUDENT_INITIAL_ETH_BALANCE); 66 | expect(await ethers.provider.getBalance(bob.address)).to.lessThan(STUDENT_INITIAL_ETH_BALANCE); 67 | expect(await ethers.provider.getBalance(carol.address)).to.lessThan(STUDENT_INITIAL_ETH_BALANCE); 68 | expect(await ethers.provider.getBalance(dan.address)).to.lessThan(STUDENT_INITIAL_ETH_BALANCE); 69 | expect(await ethers.provider.getBalance(player.address)).to.lessThan(STUDENT_INITIAL_ETH_BALANCE); 70 | }); 71 | }); -------------------------------------------------------------------------------- /contracts/trick-or-thieve/NftMarketplace.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 5 | 6 | import "./ValixPlaygroundTokenLite.sol"; 7 | import "../ValixPlaygroundNFT.sol"; 8 | 9 | // NFT Simple Marketplace adapted from: https://github.com/PatrickAlphaC/hardhat-nft-marketplace-fcc/blob/main/contracts/NftMarketplace.sol 10 | 11 | contract NftMarketplace is ReentrancyGuard { 12 | ValixPlaygroundTokenLite public immutable token; 13 | ValixPlaygroundNFT public immutable nft; 14 | 15 | struct Listing { 16 | uint256 price; 17 | address seller; 18 | } 19 | 20 | event ItemListed( 21 | address indexed seller, 22 | uint256 indexed tokenId, 23 | uint256 price 24 | ); 25 | event ItemCanceled(address indexed seller, uint256 indexed tokenId); 26 | event ItemBought( 27 | address indexed buyer, 28 | uint256 indexed tokenId, 29 | uint256 price 30 | ); 31 | 32 | mapping(uint256 => Listing) private listings; 33 | 34 | constructor(ValixPlaygroundTokenLite _token, ValixPlaygroundNFT _nft) { 35 | token = _token; 36 | nft = _nft; 37 | } 38 | 39 | modifier notListed(uint256 tokenId) { 40 | Listing memory listing = listings[tokenId]; 41 | require(listing.price == 0, "Already listed"); 42 | _; 43 | } 44 | 45 | modifier isListed(uint256 tokenId) { 46 | Listing memory listing = listings[tokenId]; 47 | require(listing.price > 0, "Not listed"); 48 | _; 49 | } 50 | 51 | modifier isOwner(uint256 tokenId) { 52 | require(msg.sender == nft.ownerOf(tokenId), "Permission denied"); 53 | _; 54 | } 55 | 56 | function listItem( 57 | uint256 tokenId, 58 | uint256 price 59 | ) external isOwner(tokenId) notListed(tokenId) { 60 | require(price > 0, "Listing price must above zero"); 61 | require( 62 | nft.getApproved(tokenId) == address(this), 63 | "No Approval For Marketplace" 64 | ); 65 | 66 | listings[tokenId] = Listing(price, msg.sender); 67 | emit ItemListed(msg.sender, tokenId, price); 68 | } 69 | 70 | function cancelListing( 71 | uint256 tokenId 72 | ) external isOwner(tokenId) isListed(tokenId) { 73 | delete (listings[tokenId]); 74 | emit ItemCanceled(msg.sender, tokenId); 75 | } 76 | 77 | function buyItem( 78 | uint256 tokenId 79 | ) external payable isListed(tokenId) nonReentrant { 80 | require( 81 | msg.sender != nft.ownerOf(tokenId), 82 | "Owner cannot buy their owned item" 83 | ); 84 | Listing memory listedItem = listings[tokenId]; 85 | 86 | delete (listings[tokenId]); 87 | token.transferFrom(msg.sender, listedItem.seller, listedItem.price); 88 | nft.safeTransferFrom(listedItem.seller, msg.sender, tokenId); 89 | 90 | emit ItemBought(msg.sender, tokenId, listedItem.price); 91 | } 92 | 93 | function updateListing( 94 | uint256 tokenId, 95 | uint256 newPrice 96 | ) external isOwner(tokenId) isListed(tokenId) nonReentrant { 97 | require(newPrice > 0, "Listing price must above zero"); 98 | listings[tokenId].price = newPrice; 99 | emit ItemListed(msg.sender, tokenId, newPrice); 100 | } 101 | 102 | function getListing( 103 | uint256 tokenId 104 | ) external view returns (Listing memory) { 105 | return listings[tokenId]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/extra-bank/extra-bank.challenge.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { expect } from 'chai'; 3 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 4 | 5 | import { ExtraBank } from '../../typechain-types'; 6 | import { AddOnFactory } from '../../typechain-types'; 7 | import { Factory } from '../../typechain-types'; 8 | 9 | import addOnJson from './utils/AddOn.json'; 10 | 11 | describe('Challenge - Extra Bank', function () { 12 | let deployer: SignerWithAddress; 13 | let player: SignerWithAddress; 14 | let newOperator: SignerWithAddress; 15 | let user1: SignerWithAddress; 16 | 17 | let bank: ExtraBank; 18 | let addOnFactory: AddOnFactory; 19 | let factory: Factory; 20 | 21 | before(async function () { 22 | /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ 23 | [deployer, player, newOperator, user1] = await ethers.getSigners(); 24 | 25 | // Deploy ExtraBank Contract 26 | const ExtraBank = await (await ethers.getContractFactory('ExtraBank', deployer)); 27 | bank = await ExtraBank.deploy(player.address); 28 | 29 | expect(await bank.owner()).to.eq(deployer.address); 30 | expect(await bank.operator()).to.eq(player.address); 31 | 32 | // Player Actions as an operator of the Extrabank Contract 33 | const Factory = await (await ethers.getContractFactory('Factory', player)); 34 | factory = await Factory.deploy(); 35 | 36 | const salt = "0x504c454153452053454355524520594f55522041535345540000000000000000" 37 | 38 | const createAddOnFactoryTx = await factory.connect(player).createFactory(salt); 39 | const waitAddOnFactory = await createAddOnFactoryTx.wait(); 40 | const eventFactory = waitAddOnFactory.events?.find((event: any) => event.event === 'CreateAddOnFactory'); 41 | const addOnFactoryAddress = eventFactory?.args?.addr; 42 | 43 | const AddOnFactory = await ethers.getContractFactory('AddOnFactory'); 44 | addOnFactory = await AddOnFactory.attach(addOnFactoryAddress) 45 | 46 | const createAddOnTx = await addOnFactory.connect(player).createAddOn(addOnJson.bytecode); 47 | const waitAddOn = await createAddOnTx.wait(); 48 | const eventAddOn = waitAddOn.events?.find((event: any) => event.event === 'CreateAddOn'); 49 | const addOnAddress = eventAddOn?.args?.addr; 50 | 51 | const AddOn = await ethers.getContractFactory(addOnJson.abi, addOnJson.bytecode); 52 | const addOn = await AddOn.attach(addOnAddress); 53 | 54 | await bank.connect(player).setAddOn(addOn.address); 55 | expect(await bank.addOnContract()).to.eq(addOn.address); 56 | await expect(bank.connect(deployer).executeAddOn()).not.to.be.reverted; 57 | 58 | // User interact with the ExtraBank contract; 59 | await bank.connect(user1).deposit({ value: ethers.utils.parseEther("100") }); 60 | expect(await bank.getUserBalance(user1.address)).to.eq(ethers.utils.parseEther("100")); 61 | expect(await bank.getBankBalance()).to.eq(ethers.utils.parseEther("100")); 62 | 63 | // Bank owner removes the player from the operator role, change to new operator 64 | await bank.connect(deployer).setOperator(newOperator.address); 65 | expect(await bank.operator()).to.eq(newOperator.address); 66 | }); 67 | 68 | it('Execution', async function () { 69 | /** CODE YOUR SOLUTION HERE */ 70 | 71 | }); 72 | 73 | after(async function () { 74 | /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */ 75 | 76 | // Player has taken ownership of the bank 77 | expect( 78 | await bank.owner() 79 | ).to.equal(player.address); 80 | // Player has taken all funds from the bank 81 | expect( 82 | await ethers.provider.getBalance(bank.address) 83 | ).to.equal(ethers.utils.parseEther("0")); 84 | }); 85 | }); --------------------------------------------------------------------------------