In this article, I will list and investigate a few relatively common albeit non-basic mistakes that people tend to introduce in their codebases without being aware of their consequences.
My plan is to release such tidbits from time to time to “immortalize” my security-related knowledge and make it easier for developers and auditors alike to become security-aware with readily available information. If there are any topics, in particular, you would like me to cover, feel free to let me know.
Many systems nowadays utilize hashes as a form of identifier within their smart contract system, namely due to the fact that hashing operations are readily available and “native” within the EVM. In short, a hash function is a function in which you provide input and a seemingly random output is generated.
The input-output pair is deterministic and the output is derived from the input using a set of irreversible instructions on the original data payload. In the case of
keccak256, the algorithm implemented within the EVM, the hash function is what is called a “sponge” function, effectively accepting an arbitrary length of data and “absorbing” it down to a pre-determined size, in this case, 256-bits (or 32-bytes).
Translating this into Solidity terms, the
keccak256 function accepts a
bytes memory array that is then hashed to a
bytes32 result. The
bytes memory data-type is not often utilized and is more of a low-level representation of data that is used in conducting raw external calls to contracts and other such actions.
For this purpose, conversion of the desired input arguments to the function is done via the ABI encoding methods:
abi.encodePacked. Both functions essentially encode the input variables based on their type signature into the ABI representation, however, the latter of the two methods performs an extra step.
It attempts to tight-pack variables using a similar mechanism to what we described in B009 and briefly talked about in B003; variable tight-packing. Variables are packed into the same 32-byte slot if they fit in one and, as an additional, advanced step, dynamic arrays are also packed together in sequence consuming as many 32-byte data slots as their entries demand.
As an array is no longer identifiable when tight-packed, the
abi.encodePacked mechanism can lead to two different arrays being packed into the same final
bytes memory result. A very small example can be found below:
The above segment showcases that using the same
bytes32 values, two different length array arguments can be passed into
abi.encodePacked and produce the same result.
While the above case is of course not exploitable and a showcase, the most common way to exploit this collision is when systems utilize the
abi.encodePacked with two dynamic arguments, such as for factories packing a token’s
symbol to calculate the “ID” of the generated copy, potentially overwriting storage space.
As a solution, whenever the hash generated is meant to be unique the usage of
abi.encodePacked is strongly encouraged and will prohibit any collision from occurring.
Spot Price Retrieval
With the boom of the DeFi ecosystem, multiple projects are rushing in to attempt to interface with the DeFi building blocks and build the next big thing in the blockchain space. To do so, most systems attempt to bring some traditional finance notions (i.e. put and call options) into the EVM.
In order to achieve this, the projects start looking at the available solutions for some mission-critical data points, more often than not the price of a particular asset. There are multiple solutions available to calculate a “secure” price of an asset but they are often ignored for the simplest one available, which is by using spot prices reported in DeFi exchanges like Uniswap V2.
Many hacks ranging from PancakeBunny to Fei were carried out on exploiting the false premise of security in spot prices. Taking into account the fact that flash-loans are a now widespread feature readily available to attackers, spot prices can temporarily be influenced allowing economic attacks to manifest.
To prevent abuse of the spot price exchanges, a strong countermeasure is to either utilize an on-chain price oracle, like Chainlink, or calculate a Time-Weighted-Price-Average (TWAP) of a DeFi exchange’s spot price with a comfortable time span.
Although both prices should be flash-loan resistant, we have seen attacks take place by manipulating the reported price by both Coinbase and Uniswap V2 as an example in a Compound liquidation attack. Bottom line is that price metrics can always be manipulated, be it by well-funded coordinated attacks or flash-loan-based temporary attacks.
Systems should take note of this inherent trait and perhaps enforce certain “emergency breaks”, such as utilizing a TWAP on top of a decentralized price oracle.
There are a lot of things to talk about when it comes to security; in this article, I noted down two important misconceptions that can lead to significant vulnerabilities in taking form.
Many more similar topics will be looked into the blog series I am currently on the 10th piece of to attempt and create a “database” of security-related data for the Ethereum ecosystem that anyone is free to browse and garner knowledge from.