Description:
With liquidity pools, you can always trust that your investment is in good hands.
How to Submit a Solution:
Use the below submission form to submit the solution: https://quillaudits.typeform.com/QuillCTF
Objective of CTF
Your objective is to have a greater token balance than your initial balance.
You are a hacker, not the user. (see foundry test file)
Note: You can create POCs using Foundry/Hardhat. Without proper POC, your submissions will not be accepted.
Contract Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract PoolToken is ERC20("loan token", "lnt"), Ownable {
function mint(uint amount) external onlyOwner {
_mint(msg.sender, amount);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract InvestPool {
IERC20 token;
uint totalShares;
bool initialized;
mapping(address => uint) public balance;
modifier onlyInitializing() {
require(initialized, "Not initialized! You are so stupid!");
_;
}
constructor(address _token) {
token = IERC20(_token);
}
function initialize(string memory password) external {
// Password could be found in Goerli contract
// 0xA45aC53E355161f33fB00d3c9485C77be3c808ae
// Hint: Password length is more than 30 chars
require(!initialized, "Already initialized");
require(
keccak256(abi.encode(password)) ==
0x18617c163efe81229b8520efdba6384eb5c6d504047da674138c760e54c4e1fd,
"Wrong password"
);
initialized = true;
}
function deposit(uint amount) external onlyInitializing {
uint userShares = tokenToShares(amount);
balance[msg.sender] += userShares;
totalShares += userShares;
token.transferFrom(msg.sender, address(this), amount);
}
function tokenToShares(uint userAmount) public view returns (uint) {
uint tokenBalance = token.balanceOf(address(this));
if (tokenBalance == 0) return userAmount;
return (userAmount * totalShares) / tokenBalance;
}
function sharesToToken(uint amount) public view returns (uint) {
uint tokenBalance = token.balanceOf(address(this));
return (amount * tokenBalance) / totalShares;
}
function transferFromShare(uint amount, address from) public {
uint size;
assembly {
size := extcodesize(address())
}
require(size == 0, "code size is not 0");
require(balance[from] >= amount, "amount is too big");
balance[from] -= amount;
balance[msg.sender] += amount;
}
function withdrawAll() external onlyInitializing {
uint shares = balance[msg.sender];
uint toWithdraw = sharesToToken(shares);
balance[msg.sender] = 0;
totalShares -= shares;
token.transfer(msg.sender, toWithdraw);
}
}
Foundry setUp:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;
import "forge-std/Test.sol";
import "../src/poolToken.sol";
import "../src/investPool.sol";
contract Hack is Test {
PoolToken token;
InvestPool pool;
address user = vm.addr(1);
address hacker = vm.addr(2);
function setUp() external {
token = new PoolToken();
pool = new InvestPool(address(token));
token.mint(2000e18);
token.transfer(user, 1000e18);
token.transfer(hacker, 1000e18);
vm.prank(user);
token.approve(address(pool), type(uint).max);
vm.prank(hacker);
token.approve(address(pool), type(uint).max);
}
function userDeposit(uint amount) public {
vm.prank(user);
pool.deposit(amount);
vm.stopPrank();
}
function test_hack() public {
uint hackerBalanceBeforeHack = token.balanceOf(hacker);
vm.startPrank(hacker);
// solution
vm.stopPrank();
assertGt(token.balanceOf(hacker), hackerBalanceBeforeHack);
}
}