People often ask me "How did you learn how to hack?" The answer: by reading. This page is a collection of the blog posts and other articles that I have accumulated over the years of my journey. Enjoy!
harvest(). When this is triggered, the yields for the earned funds will be distributed pro-rata (based upon the amount of inserted funds. harvest() call. The final transaction has the the Uniswap pool go back to its undistorted state, taking some of the harvest funds. borrow can be used to specify information about the loan, such as the pool to take the collateral on and the loan id to use. Additionally, a different receiver that be sent as a parameter as well.loanid on the call, an attacker can take out loans for other users. According to the article, this is ONLY possible when the loanid has unused collateral. However, it may have been possible to race the removal of a loan from the contract as well.borrow() with the receiver that they control. Since they have a loan using someone else's collateral, there is no penalty for paying back the loan! They do not have to pay this back. marginTrade() function. A malicious user can put someone else in a bad position but specifying that user. In particular, the function call allowed for the setting of the trader on the call, when it should have been msg.sender.harvest() function to be public, meaning that anybody can decide when the yields can be harvested. Why is this a problem though? harvest(). The pool does the swap at the very bad prices for Asset A.harvest function can be called with extremely regularity to make the fees of the swap to expensive to perform this attack. Finally, check for slippage and reject to price if it has swayed too much. rebalance() is called, it takes the underlying assets of a user and buys VSP. By doing this, the price of VSP is increased; VSP holders are entitled to a percentage of the yield based upon the use of the underlying asset produces. This is done by a Vesper's Rebalancing bot in order to distribute yield to holders through inflating the price of Vesper. rebalance appears to have been callable by anyone and not just the bot. This is part of the where the problem lies. rebalance() manually. Now, the triggering of this call will perform a swap to get VSP. But, because of the inflated price from the flash loan, the swap gets a much smaller amount of VSP than it should. As a result, most of the WETH from the rebalance() goes back into the hands of the flash loaner even though they did not participate in the farming at all. rebalance() call. So, this was not the most likely attack to occur. Additionally, they claim this attack would have been discovered through monitoring... but, I think that relying on this for security is a bad precedent to set. transferFrom() to force the contract to transfer all LP tokens from any victim to the attacker. Since, at this point, a user has allowed the contract access to the coins. This allows for a cross account zap out, in terms of the service being used. /claim. Since this happens in web server code, there is a small window where multiple web requests can be made on the voucher. So, a claim can be made multiple times with something like Turbo Intruder. Nice! onlyOwner() modifier. Additionally, there was no check to ensure that a contact wasn't double initialized. Three of the pool contracts were vulnerable to this. mint() and burn() to add value to themselves. prove() and process(). The process() function is used for uses a transaction from one chain to another after proving it. require(acceptableRoot(messages[_messageHash]);
_messageHash is the keccak hash of the data being added to the blockchain.
acceptableRoot() is called to ensure that the root hash of this message is valid. If it is, the transaction continues. Otherwise, the contract reverts.
0x0 was made into a valid root! Since a Solidity map defaults to 0x0, any fake transaction will pass. Wait, how is this repeatable? acceptableRoot() had been updated to handle an ENUM in legacy code. Instead of having PROVEN and PROCESSED (which return true and false respectively) the legacy code checks to see if the time for the data passed in is 0. Since the structure will return a hash time that is not 0, this ALWAYS passes. Thanks to Zellic_io for tracing this down.messages[_messageHash], making it so the specific call cannot be used again. However, we can remake this call with slightly different values over and over again!unmintWithRewards() in order to claim the same share over and over again. By doing this, all of the funds from the contract could be stolen.