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!
AddressSet type will override existing values. So, if an attacker has a loan they could potentially override their collateral with 0, making it impossible to get any collateral out. Another case is providing a malicious oracle.array. Later on, when withdrawing USDC, it goes from an index of 0 to an infinite size. By making the array too big, users may be denied access from gas limitations. This can be solved by programming without controllable array, removing previously used values that are no longer needed or having indexed based calls in cases where the array is too long.nonReentrant modifiers on them. There existed a unstaking code path that hit a transfer function where BOTH had the nonReentrant modifier on it. Since this was the case, all calls to the unstake function with the case of there being vault rewards to fail. The solution the problem could be done in a few ways. In particular, having an external function with the modifier and an internal function without the modifier to access the functionality. liquidate() and endAuction() which call the users defined function. An attacker can revert all of these calls to make it impossible to end an auction or liquidate. balanceOf mapping is normally obvious. With deflationary tokens, this is dynamic and calculated based upon the supply._reflectFee() takes a small fee every time that a call to transfer() is made by sending it to charity and a few other places. The totalSupply() variable _tTotal is subtracted from and some internal accounting tracks the amount of funds now owned by the token. totalSupply or balanceOf is generally a bad idea. But why? Many locations calculate the price of a token in a pool based upon the amount of tokens available or the amount of tokens in a pool. By being able to burn() an arbitrary amount of tokens, we can manipulate the price of funds in a pool. Or can we?sync() on Pancake swap to update the price in the pool.advance() mints a new inflation according to the newly set parameters. This is a two step process within a TimelockConfig which is controlled by a multi-sig admin wallet.requestChange() with a waiting period of 7 days. This can be cancelled with cancelChange(). Both of these functions are only accessible to the administrators with the onlyAdmin modifier.confirmChange() function is used to enact the proposed change. This does not have an administrative modifier on top of it though. At first glance, this seems fine... the validation of the date works as expected. However, this does open up a new attack surface though!confirmChange() assumes that a change has been proposed for a given ID via this two step process. In reality, an external user can call this function without any proposals for a given ID. The only validation is that the block.timestamp is greater than the proposed time. advance() on the smart contract now. Overall, a bad developer assumption caused a major security flaw.MasterPlatypusV4 is the Masterchef-like orchestrator. The emergencyWithdraw() function allows for main contract to withdraw their LP tokens from a given pool without caring about rewards. The contract literal has "EMERGENCY ONLY" within the code lolz. (bool success, bytes memory returndata) = exchange.call(data); require(success, string(returndata));
approve() for an asset on the currency converter proxy. Then, the funds would be exchanged to the wrapper and eventually the exchange. This call() with the arbitrary data was being made from within the wrapper after some input had already been provided. Since the wrapper didn't have the approvals, there were not funds at risk.call() within the code. Since this contract had the approvals, this was bad.initialize() function. The initialization step must happen separately from the deployment. So, there is a race condition where the function could be called. If an attacker called this, or the function was forgotten about, an attacker could cause major havoc.delegateCall the implementation contract is using the storage of the proxy contract. If there is a collision between these two contracts for variables, then havoc can ensure. In the case of Audis, proxyadmin was stored in the initializable field for the contract. This allowed the contract to be reinitialized and steal the funds. delegateCall. Redirecting to an arbitrary contract allows for the contract to alter internal variables. The next issue is figuring out a selfdestruct call from the initial call in the proxy. By doing this, the address and variables are ruined forever.delegateCall not checking the result. By not checking the result, the function would have executed without anything happening. delegateCall doesn't revert on not calling a contract; it only returns a boolean to mention this.delegateCall to the implementation code. The delegateCall is used in order for the proxy to have the storage of the contract be decoupled from the code. The original proxy did not have the ability to upgrade though.constructor will only run on deployment. So, if we set a new implementation, we need a way to initialize or update the state of new variables.selfdestruct then redeploys the contract to the same address. Honestly, this pattern makes the most sense to me for user engagement.