Hats Finance Challenge #1 solution

Hats Finance Challenge #1

Capture the Flag

  • Anyone can join the game calling game.join()
  • On joining, a player receives a deck of 3 pseudo-random Mons
  • Each Mon is an NFT, and each Mon has powers: FIRE; WATER; AIR and SPEED, each with a value in a range from [0–9]
  • Users can try to improve their deck by swapping their Mons with other users, or by exchanging a Mon for a randomly generated new one
  • For a swap to succeed, another player must put one of their Mons for sale
  • At each moment, one single player holds the flag
  • Other players can try to capture the flag by challenging the flag holder
  • A fight between two Mons takes place with one of the 3 elements (FIRE, WATER or AIR). The Mon with the highest value in that element wins the fight. If the two Mons have the same strength, the Mon with the most SPEED wins. If the two Mons are exactly the same, the flag holder wins.
  • A fight between two decks consists of pairing the three Mons of the challenger with the 3 Mons of the flag holder, pseudo-randomly choosing 3 elements, and then having the 3 pairs fight on each of these elements

The Hats Challenge

How can we exploit the game?

  • Steal the flag holder Mons from his deck by swapping them, replacing them or stealing them in some way
  • Being able to fulfill the fight() winning condition that was checking if balanceOf(attacker) > balanceOf(opponent) where the opponent in this case is always the flagHolder.

randomGen() Pseduo-randomness generator can be exploited

function randomGen(uint256 i) private returns (uint8) {
uint8 x = uint8(uint256(keccak256(abi.encodePacked(block.number, msg.sender, nonce))) % i);
return x;

forSale is not resetted to false after a swap

  1. Bob want to sell Mon1 so it calls game.putUpForSale(mon1);
  2. Alice intends to swap Mon2 for Mon1 because it's much more powerful. Alice call game.swap(address(Bob), mon2, mon1) and the swap happen
  3. Paul, see the transaction and can swap again the Mon1 that Alice just purchased because the Mon is still “sellable”. Paul call game.swap(address(Alice), mon3, mon1) and swap the new Alice's Mon even if she did not intend to sell it
  4. At this point Mark could call game.swap(address(Paul), mon4, mon1) and steal the Mon from Paul and the cycle can go on again and again

Swaps don’t allow the seller to accept the transaction, favoring the buyer

indexInDeck function does not check if the _monId is present in the deck

function indexInDeck(address _owner, uint256 _monId) internal view returns(uint256 idx) {
for (uint256 i; i < DECK_SIZE; i++) {
if (decks[_owner][i] == _monId) {
idx = i;

swap function is vulnerable to reentrancy attack because of _safeTransfer. It does not follow Checks-Effects-Interactions Pattern best practice or use reentrancy guard

  1. implement any kind of Reentrancy Guard
  2. update the contract’s state variables after the external call, not following the checks-effects-interactions pattern described in the Solidity documentation
  1. Alice has mon1, mon2, mon3 in the deck
  2. Bob deploy an attacker contract that implement onERC721Received and join the game with his contract. The contract has mon4, mon5, mon6 in the deck
  3. Alice put for sale mon1
  4. Bob call attacker.game.swap(alice, mon4, mon1)
  • mon1 is already owned by Alice but
  • deck[alice][0] = mon1
  • deck[attacker][0] = mon4
  • deck[0] = mon7
  • deck[1] = mon8
  • deck[2] = mon9
// update the decks
uint256 idx1 = indexInDeck(swapper, _monId1);
uint256 idx2 = indexInDeck(_to, _monId2);
decks[swapper][idx1] = _monId2;
decks[_to][idx2] = _monId1;
  • indexInDeck(swapper, mon4); will return 0 because it will try to find a token with ID 4 inside the deck, but the user deck has been replaced because the user has lost the fight and now it has inside mon7, mon8, mon9. Because of the problem we have discussed before, instead of reverting when it does not find a token that should be in the deck, it will return the default value of idx return variable that is 0
  • indexInDeck(_to, mon1); will return 0 because deck[0] = mon1
  • decks[attacker][0] = mon1 (mon1 is still alive)
  • decks[alice][0] = mon4 (mon4 has been burned because attacker has lost the fight)

How to exploit Reentrancy to gain the flag holder title

  1. we create an Exploiter smart contract that is able to interact with the game. The smart contract is inheriting from IERC721Receiver and implementing the onERC721Received callback. This callback will be called automatically by the game during a swap operation.
    Inside the callback, we are just going to call game.fight();. If the callback has been called because of exploiter.swap(alice, monOfAttackInPos0, monOfAliceInPos0) it will make the attacker attack with monOfAttackInPos0 in position 0 so the game will burn the Mon that after the callback will be transferred to Alice.
    After the end of swap the Exploiter, contract will own 4 different Mons (3 new Mons after fight + 1 Mon from Alice)
  2. Use two different accounts to join the game and fill their decks. After that, put all the Mons for sale
  3. Make the exploiter join the game and fill its deck and put all the Mons for sale.
  4. Execute exploiter.swap(address(attacker2), 9, 3); to leverage the reentrancy exploit. At the end of each swap the exploiter contract will have the Mon balance increased by 1 (as explained before)
  5. Keep executing the exploit until you have at least 7 mon before starting the fight. This is important because after the fight the game will check if the attacker (you) have more tokens compared to the flag holder. If so, you (attacker) will become the flag holder! As soon as attacker2 has no more swappable Mons (that are burned each time the swap happen because of the fight) start using attacker3 Mons to keep swapping and exploiting the reentrancy attack.
  6. Profit and enjoy being the Flag Holder! (well not you but your exploit contract :D

GitHub repo & Links



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store