Hats Finance is a decentralized bug bounty platform. One big difference that this platform has over bug bounty platforms for web3 is that the payments that are rewarded are firstly locked in vaults and also use the project’s/protocol’s token. Success of the project, which in some respects means a secure project, means the bounty is also increased.
Hats Finance also publishes Capture The Flag (CTF) challenges that are good practice for bounty hunters. They reward those with the top submissions and for CTF#2, the top 10 were able to claim 1000 DAI along with an NFT. I was a bit too late with getting started with the CTF so my submission didn’t make it in time, but I went ahead and used it as practice anyway.
https://github.com/hats-finance/vault-game
The contract Vault.sol is an ERC4626-like vault customized to be used with ETH. It allows anyone to deposit ETH in the vault and get shares corresponding to the amount deposited. The shares are an ERC20 which can be freely used by users, functioning effectively just like Wrapped ETH. The shares can also be redeemed at any time for the corresponding underlying amount of ETH.
The Vault.sol is deployed with the contract owning 1 ETH of the shares. Your mission is to capture the flag by emptying the vault, then calling captureTheFlag with an address you control to prove that you have succeeded in completing the challenge, so that vault.flagHolder returns your address.
It looks like the main contract only has two functions, the constructor and a function to capture the flag, nothing looks out of the ordinary here, but it does use a custom implementation of the ERC4626. With this, we can look for the differences in the contracts and gain a sense of what kind of vulnerabilities hide in the contract. The main areas we will look at are the _deposit and _withdraw functions.

The way this implementation differs from the proposed standard is that instead of shares, it uses actual ETH instead of shares like how the EIP states. Furthermore, if we dig into line 28 of _withdraw, we can see that it uses the balance of the contract in order to calculate the excessETH that should be sent back to the owner on line 33-34. This wouldn’t be a problem because Vault.sol doesn’t implement recieve or fallback so that means there is no way to send ETH to this contract…right? Wrong, you can use selfdestruct(target) in a contract to force the ETH and tokens into another address or contract. You can read more about it here:
https://solidity-by-example.org/hacks/self-destruct/
Now that we know a way of manipulating state in the contract, is there a way of continually doing this action? The answer is yes, with a re-entrancy attack! If we were to send ETH from a contract and have a receive function call Vault.sol again, we can repeat the action of sending excessETH to the owner because the variable is written above where the re-entracy happens.

Deploy a contract with 1 ETH that will
selfdestructto theVaultcontract.Deploy a contract that can call the
withdrawfunction in theVault. This contract should also have arecievefunction that will re-enter thewithdrawfunction in order to drain the vault.Withdraw using the deployed contract and drain the vault.
Capture the Flag
In order to help solidify the concept of re-entrancy attacks, I attempted to create some diagrams of how it works in this particular case.






After the re-entrancy is done the excess ETH gets sent back to the owner.



There are a couple ways of preventing this from happening:
Add a
non-reentrantmodifier to the withdraw function.Add a receive/fallback function to the vault contract and instead of using the contracts balance, a variable can be incremented and decremented when withdrawing or depositing into the vault.
Move the calculation for
excessETHunderneath the first transfer in the_withdrawfunction.
Below is a link to my solution to the CTF. I also included Foundry tests because I’m a foundry maxi.

