Although in its infancy, Solidity has had widespread adoption and is used to compile the byte-code in many Ethereum smart contracts we see today. There have been a number of harsh lessons learned by developers and users alike in discovering the nuances of the language and the EVM. This post aims to be a relatively in-depth and up-to-date introductory post detailing the past mistakes that have been made by Solidity developers in an effort to prevent future devs from repeating history.
Here are 16 interesting hacks:
- Re-Entrancy One of the features of Ethereum smart contracts is the ability to call and utilise code of other external contracts. Contracts also typically handle ether, and as such often send ether to various external user addresses. The operation of calling external contracts, or sending ether to an address, requires the contract to submit an external call. These external calls can be hijacked by attackers whereby they force the contract to execute further code (i.e. through a fallback function) , including calls back into itself. Thus the code execution “re-enters” the contract. Attacks of this kind were used in the infamous DAO hack.
For further reading on re-entrancy attacks, see Reentrancy Attack On Smart Contracts and Consensus — Ethereum Smart Contract Best Practices.
The Vulnerability
This attack can occur when a contract sends ether to an unknown address. An attacker can carefully construct a contract at an external address which contains malicious code in the fallback function. Thus, when a contract sends ether to this address, it will invoke the malicious code. Typically the malicious code executes a function on the vulnerable contract, performing operations not expected by the developer. The name “re-entrancy” comes from the fact that the external malicious contract calls back a function on the vulnerable contract and “re-enters” code execution at an arbitrary location on the vulnerable contract.
To clarify this, consider the simple vulnerable contract, which acts as an Ethereum vault that allows depositors to only withdraw 1 ether per week.
EtherStore.sol
This contract has two public functions. depositFunds() and withdrawFunds(). The depositFunds() function simply increments the senders balances. The withdrawFunds() function allows the sender to specify the amount of wei to withdraw. It will only succeed if the requested amount to withdraw is less than 1 ether and a withdrawal hasn't occurred in the last week. Or does it?...
The vulnerability comes on line [17] where we send the user their requested amount of ether. Consider a malicious attacker creating the following contract,
Attack.sol
Let us see how this malicious contract can exploit our EtherStore contract. The attacker would create the above contract (let's say at the address 0x0...123) with the EtherStore's contract address as the constructor parameter. This will initialize and point the public variable etherStore to the contract we wish to attack.
The attacker would then call the pwnEtherStore() function, with some amount of ether (greater than or equal to 1), lets say 1 ether for this example. In this example we assume a number of other users have deposited ether into this contract, such that it's current balance is 10 ether. The following would then occur:
Attack.sol — Line [15] — The depositFunds() function of the EtherStore contract will be called with a msg.value of 1 ether (and a lot of gas). The sender (msg.sender) will be our malicious contract (0x0...123). Thus, balances[0x0..123] = 1 ether.
Attack.sol — Line [17] — The malicious contract will then call the withdrawFunds() function of the EtherStore contract with a parameter of 1 ether. This will pass all the requirements (Lines [12]-[16] of the EtherStore contract) as we have made no previous withdrawals.
EtherStore.sol — Line [17] — The contract will then send 1 ether back to the malicious contract.
Attack.sol — Line [25] — The ether sent to the malicious contract will then execute the fallback function.
Attack.sol — Line [26] — The total balance of the EtherStore contract was 10 ether and is now 9 ether so this if statement passes.
Attack.sol — Line [27] — The fallback function then calls the EtherStore withdrawFunds() function again and "re-enters" the EtherStore contract.
EtherStore.sol — Line [11] — In this second call to withdrawFunds(), our balance is still 1 ether as line [18] has not yet been executed. Thus, we still have balances[0x0..123] = 1 ether. This is also the case for the lastWithdrawTimevariable. Again, we pass all the requirements.
EtherStore.sol — Line [17] — We withdraw another 1 ether.
Steps 4–8 will repeat — until EtherStore.balance >= 1 as dictated by line [26] in Attack.sol.
Attack.sol — Line [26] — Once there less 1 (or less) ether left in the EtherStore contract, this if statement will fail. This will then allow lines [18] and [19] of the EtherStore contract to be executed (for each call to the withdrawFunds() function).
EtherStore.sol — Lines [18] and [19] — The balances and lastWithdrawTime mappings will be set and the execution will end.
The final result, is that the attacker has withdrawn all (bar 1) ether from the EtherStore contract, instantaneously with a single transaction.
Preventative Techniques
There are a number of common techniques which help avoid potential re-entrancy vulnerabilities in smart contracts. The first is to ( whenever possible) use the built-in transfer() function when sending ether to external contracts. The transfer function only sends 2300 gas which isn't enough for the destination address/contract to call another contract (i.e. re-enter the sending contract).
The second technique is to ensure that all logic that changes state variables happen before ether is sent out of the contract (or any external call). In the EtherStore example, lines [18] and [19] of EtherStore.sol should be put before line [17]. It is good practice to place any code that performs external calls to unknown addresses as the last operation in a localised function or piece of code execution. This is known as the checks-effects-interactions pattern.
A third technique is to introduce a mutex. That is, to add a state variable which locks the contract during code execution, preventing reentrancy calls.
Applying all of these techniques (all three are unnecessary, but is done for demonstrative purposes) to EtherStore.sol, gives the re-entrancy-free contract:
Real-World Example: The DAO
The DAO (Decentralized Autonomous Organization) was one of the major hacks that occurred in the early development of Ethereum. At the time, the contract held over $150 million USD. Re-entrancy played a major role in the attack which ultimately lead to the hard-fork that created Ethereum Classic (ETC). For a good analysis of the DAO exploit, see Phil Daian’s post.
- Arithmetic Over/Under Flows The Ethereum Virtual Machine (EVM) specifies fixed-size data types for integers. This means that an integer variable, only has a certain range of numbers it can represent. A uint8 for example, can only store numbers in the range [0,255]. Trying to store 256 into a uint8 will result in 0. If care is not taken, variables in Solidity can be exploited if user input is unchecked and calculations are performed which result in numbers that lie outside the range of the data type that stores them.
For further reading on arithmetic over/under flows, see How to Secure Your Smart Contracts, Ethereum Smart Contract Best Practices and Ethereum, Solidity and integer overflows: programming blockchains like 1970
The Vulnerability
An over/under flow occurs when an operation is performed that requires a fixed size variable to store a number (or piece of data) that is outside the range of the variable’s data type.
For example, subtracting 1 from a uint8 (unsigned integer of 8 bits, i.e. only positive) variable that stores 0 as it's value, will result in the number 255. This is an underflow. We have assigned a number below the range of the uint8, the result wraps around and gives the largest number a uint8 can store. Similarly, adding 2^8=256 to a uint8 will leave the variable unchanged as we have wrapped around the entire length of the uint (for the mathematicians, this is similar to adding $2\pi$ to the angle of a trigonometric function, $\sin(x) = \sin(x+2\pi)$). Adding numbers larger than the data type's range is called an overflow. For clarity, adding 257 to a uint8 that currently has a zero value will result in the number 1. It's sometimes instructive to think of fixed type variables being cyclic, where we start again from zero if we add numbers above the largest possible stored number, and vice-versa for zero (where we start counting down from the largest number the more we subtract from 0).
These kinds of vulnerabilities allow attackers to misuse code and create unexpected logic flows. For example, consider the time locking contract below.
TimeLock.sol
This contract is designed to act like a time vault, where users can deposit ether into the contract and it will be locked there for at least a week. The user may extend the time longer than 1 week if they choose, but once deposited, the user can be sure their ether is locked in safely for at least a week. Or can they?…
In the event a user is forced to hand over their private key (think hostage situation) a contract such as this may be handy to ensure ether is unobtainable in short periods of time. If a user had locked in 100 ether in this contract and handed their keys over to an attacker, an attacker could use an overflow to receive the ether, regardless of the lockTime.
The attacker could determine the current lockTime for the address they now hold the key for (its a public variable). Let's call this userLockTime. They could then call the increaseLockTime function and pass as an argument the number 2^256 - userLockTime. This number would be added to the current userLockTime and cause an overflow, resetting lockTime[msg.sender] to 0. The attacker could then simply call the withdraw function to obtain their reward.
Let’s look at another example, this one from the Ethernaut Challanges.
SPOILER ALERT: If you’ve not yet done the Ethernaut challenges, this gives a solution to one of the levels.
This is a simple token contract which employs a transfer() function, allowing participants to move their tokens around. Can you see the error in this contract?
The flaw comes in the transfer() function. The require statement on line [13] can be bypassed using an underflow. Consider a user that has no balance. They could call the transfer() function with any non-zero _value and pass the require statement on line [13]. This is because balances[msg.sender] is zero (and a uint256) so subtracting any positive amount (excluding 2^256) will result in a positive number due to the underflow we described above. This is also true for line [14], where our balance will be credited with a positive number. Thus, in this example, we have achieved free tokens due to an underflow vulnerability.
Preventative Techniques
The (currently) conventional technique to guard against under/overflow vulnerabilities is to use or build mathematical libraries which replace the standard math operators; addition, subtraction and multiplication (division is excluded as it doesn’t cause over/under flows and the EVM throws on division by 0).
OppenZepplin have done a great job in building and auditing secure libraries which can be leveraged by the Ethereum community. In particular, their Safe Math Library is a reference or library to use to avoid under/over flow vulnerabilities.
To demonstrate how these libraries are used in Solidity, let us correct the TimeLock contract, using Open Zepplin's SafeMathlibrary. The over flow-free contract would become:
Notice that all standard math operations have been replaced by the those defined in the SafeMath library. The TimeLockcontract no longer performs any operation which is capable of doing an under/over flow.
Real-World Examples: PoWHC and Batch Transfer Overflow (CVE-2018–10299)
A 4chan group decided it was a great idea to build a ponzi scheme on Ethereum, written in Solidity. They called it the Proof of Weak Hands Coin (PoWHC). Unfortunately it seems that the author(s) of the contract hadn’t seen over/under flows before and consequently, 866 ether was liberated from its contract. A good overview of how the underflow occurs (which is not too dissimilar to the Ethernaut challenge above) is given in Eric Banisadar’s post.
Some developers also implemented a batchTransfer() function into some ERC20 token contracts. The implementation contained an overflow. This post explains it, however I think the title is misleading, in that it has nothing to do with the ERC20 standard, rather some ERC20 token contracts have a vulnerable batchTransfer() function implemented.
- Unexpected Ether Typically when ether is sent to a contract, it must execute either the fallback function, or another function described in the contract. There are two exceptions to this, where ether can exist in a contract without having executed any code. Contracts which rely on code execution for every ether sent to the contract can be vulnerable to attacks where ether is forcibly sent to a contract.
For further reading on this, see How to Secure Your Smart Contracts: 6 and Solidity security patterns — forcing ether to a contract .
The Vulnerability
A common defensive programming technique that is useful in enforcing correct state transitions or validating operations is invariant-checking. This technique involves defining a set of invariants (metrics or parameters that should not change) and checking these invariants remain unchanged after a single (or many) operation(s). This is typically good design, provided the invariants being checked are in fact invariants. One example of an invariant is the totalSupply of a fixed issuance ERC20token. As no functions should modify this invariant, one could add a check to the transfer() function that ensures the totalSupply remains unmodified to ensure the function is working as expected.
There is one apparent “invariant”, in particular, that may tempt developers to use, but can in fact be manipulated by external users, regardless of the rules put in place in the smart contract. This is the current ether stored in the contract. Often, when developers first learn Solidity, they have the misconception that a contract can only accept or obtain ether via payable functions. This misconception can lead to contracts that have false assumptions about the ether balance within them which can lead to a range of vulnerabilities. The smoking gun for this vulnerability is the (incorrect) use of this.balance. As we will see, incorrect uses of this.balance can lead to serious vulnerabilities of this type.
There are two ways in which ether can (forcibly) be sent to a contract without using a payable function or executing any code on the contract. These are listed below.
Self Destruct / Suicide
Any contract is able to implement the selfdestruct(address) function, which removes all bytecode from the contract address and sends all ether stored there to the parameter-specified address. If this specified address is also a contract, no functions (including the fallback) get called. Therefore, the selfdestruct() function can be used to forcibly send ether to any contract regardless of any code that may exist in the contract. This is inclusive of contracts without any payable functions. This means, any attacker can create a contract with a selfdestruct() function, send ether to it, call selfdestruct(target) and force ether to be sent to a target contract. Martin Swende has an excellent blog post describing some quirks of the self-destruct opcode (Quirk #2) along with a description of how client nodes were checking incorrect invariants which could have lead to a rather catastrophic nuking of clients.
Pre-sent Ether
The second way a contract can obtain ether without using a selfdestruct() function or calling any payable functions is to pre-load the contract address with ether. Contract addresses are deterministic, in fact the address is calculated from the hash of the address creating the contract and the transaction nonce which creates the contract. i.e. of the form: address = sha3(rlp.encode([account_address,transaction_nonce])) (see Keyless Ether for some fun use cases of this). This means, anyone can calculate what a contract address will be before it is created and thus send ether to that address. When the contract does get created it will have a non-zero ether balance.
Let’s explore some pitfalls that can arise given the above knowledge.
Consider the overly-simple contract,
EtherGame.sol
This contract represents a simple game (which would naturally invoke race-conditions) whereby players send 0.5 etherquanta to the contract in hope to be the player that reaches one of three milestones first. Milestone's are denominated in ether. The first to reach the milestone may claim a portion of the ether when the game has ended. The game ends when the final milestone (10 ether) is reached and users can claim their rewards.
The issues with the EtherGame contract come from the poor use of this.balance in both lines 14 and [32]. A mischievous attacker could forcibly send a small amount of ether, let's say 0.1 ether via the selfdestruct()function (discussed above) to prevent any future players from reaching a milestone. As all legitimate players can only send 0.5 ether increments, this.balance would no longer be half integer numbers, as it would also have the 0.1 ethercontribution. This prevents all the if conditions on lines [18], [21] and [24] from being true.
Even worse, a vengeful attacker who missed a milestone, could forcibly send 10 ether (or an equivalent amount of ether that pushes the contract's balance above the finalMileStone) which would lock all rewards in the contract forever. This is because the claimReward() function will always revert, due to the require on line 32.
Preventative Techniques
This vulnerability typically arises from the misuse of this.balance. Contract logic, when possible, should avoid being dependent on exact values of the balance of the contract because it can be artificially manipulated. If applying logic based on this.balance, ensure to account for unexpected balances.
If exact values of deposited ether are required, a self-defined variable should be used that gets incremented in payable functions, to safely track the deposited ether. This variable will not be influenced by the forced ether sent via a selfdestruct() call.
With this in mind, a corrected version of the EtherGame contract could look like:
Here, we have just created a new variable, depositedEther which keeps track of the known ether deposited, and it is this variable to which we perform our requirements and tests. Notice, that we no longer have any reference to this.balance.
Real-World Example: Unknown
I’m yet to find and example of this that has been exploited in the wild. However, a few examples of exploitable contracts were given in the Underhanded Solidity Contest.
- Delegatecall The CALL and DELEGATECALL opcodes are useful in allowing Ethereum developers to modularise their code. Standard external message calls to contracts are handled by the CALL opcode whereby code is run in the context of the external contract/function. The DELEGATECALL opcode is identical to the standard message call, except that the code executed at the targeted address is run in the context of the calling contract along with the fact that msg.sender and msg.value remain unchanged. This feature enables the implementation of libraries whereby developers can create reusable code for future contracts.
Although the differences between these two opcodes are simple and intuitive, the use of DELEGATECALL can lead to unexpected code execution.
For further reading, see Ethereum Stack Exchange Question, Solidity Docs and How to Secure Your Smart Contracts: 6.
The Vulnerability
The context preserving nature of DELEGATECALL has proved that building vulnerability-free custom libraries is not as easy as one might think. The code in libraries themselves can be secure and vulnerability-free however when run in the context of another application new vulnerabilities can arise. Let's see a fairly complex example of this, using Fibonacci numbers.
Consider the following library which can generate the Fibonacci sequence and sequences of similar form.FibonacciLib.sol (This code was modified from web3j)
This library provides a function which can generate the n-th Fibonacci number in the sequence. It allows users to change the 0-th start number and calculate the n-th Fibonacci-like numbers in this new sequence.
Let’s now consider a contract that utilises this library.
FibonacciBalance.sol
This contract allows a participant to withdraw ether from the contract, with the amount of ether being equal to the Fibonacci number corresponding to the participants withdrawal order; i.e., the first participant gets 1 ether, the second also gets 1, the third gets 2, the forth gets 3, the fifth 5 and so on (until the balance of the contract is less than the Fibonacci number being withdrawn).
There are a number of elements in this contract that may require some explanation. Firstly, there is an interesting-looking variable, fibSig. This holds the first 4 bytes of the Keccak (SHA-3) hash of the string "fibonacci(uint256)". This is known as the function selector and is put into calldata to specify which function of a smart contract will be called. It is used in the delegatecall function on line [21] to specify that we wish to run the fibonacci(uint256) function. The second argument in delegatecall is the parameter we are passing to the function. Secondly, we assume that the address for the FibonacciLiblibrary is correctly referenced in the constructor (section Deployment Attack Vectors discuss some potential vulnerabilities relating to this kind if contract reference initialisation).
Can you spot any error(s) in this contract? If you put this into remix, fill it with ether and call withdraw(), it will likely revert.
You may have noticed that the state variable start is used in both the library and the main calling contract. In the library contract, start is used to specify the beginning of the Fibonacci sequence and is set to 0, whereas it is set to 3 in the FibonacciBalance contract. You may also have noticed that the fallback function in the FibonacciBalance contract allows all calls to be passed to the library contract, which allows for the setStart() function of the library contract to be called also. Recalling that we preserve the state of the contract, it may seem that this function would allow you to change the state of the start variable in the local FibonnacciBalance contract. If so, this would allow one to withdraw more ether, as the resulting calculatedFibNumber is dependent on the start variable (as seen in the library contract). In actual fact, the setStart()function does not (and cannot) modify the start variable in the FibonacciBalance contract. The underlying vulnerability in this contract is significantly worse than just modifying the start variable.
Before discussing the actual issue, we take a quick detour to understanding how state variables (storage variables) actually get stored in contracts. State or storage variables (variables that persist over individual transactions) are placed into slotssequentially as they are introduced in the contract. (There are some complexities here, and I encourage the reader to read Layout of State Variables in Storage for a more thorough understanding).
As an example, let’s look at the library contract. It has two state variables, start and calculatedFibNumber. The first variable is start, as such it gets stored into the contract's storage at slot0. The second variable, calculatedFibNumber, gets placed in the next available storage slot, slot[1]. If we look at the function setStart(), it takes an input and sets start to whatever the input was. This function is therefore setting slot[0] to whatever input we provide in the setStart() function. Similarly, the setFibonacci() function sets calculatedFibNumber to the result of fibonacci(n). Again, this is simply setting storage slot[1] to the value of fibonacci(n).
Now lets look at the FibonacciBalance contract. Storage slot[0] now corresponds to fibonacciLibrary address and slot[1] corresponds to calculatedFibNumber. It is here where the vulnerability appears. delegatecall preserves contract context. This means that code that is executed via delegatecall will act on the state (i.e. storage) of the calling contract.
Now notice that in withdraw() on line [21] we execute, fibonacciLibrary.delegatecall(fibSig,withdrawalCounter). This calls the setFibonacci() function, which as we discussed, modifies storage slot[1], which in our current context is calculatedFibNumber. This is as expected (i.e. after execution, calculatedFibNumber gets adjusted). However, recall that the start variable in the FibonacciLib contract is located in storage slot[0], which is the fibonacciLibrary address in the current contract. This means that the function fibonacci() will give an unexpected result. This is because it references start (slot[0]) which in the current calling context is the fibonacciLibrary address (which will often be quite large, when interpreted as a uint). Thus it is likely that the withdraw() function will revert as it will not contain uint(fibonacciLibrary) amount of ether, which is what calcultedFibNumber will return.
Even worse, the FibonacciBalance contract allows users to call all of the fibonacciLibrary functions via the fallback function on line [26]. As we discussed earlier, this includes the setStart() function. We discussed that this function allows anyone to modify or set storage slot[0]. In this case, storage slot[0] is the fibonacciLibrary address. Therefore, an attacker could create a malicious contract (an example of one is below), convert the address to a uint (this can be done in python easily using int('
',16)) and then call setStart(). This will change fibonacciLibrary to the address of the attack contract. Then, whenever a user calls withdraw() or the fallback function, the malicious contract will run (which can steal the entire balance of the contract) because we've modified the actual address for fibonacciLibrary. An example of such an attack contract would be,Notice that this attack contract modifies the calculatedFibNumber by changing storage slot[1]. In principle, an attacker could modify any other storage slots they choose to perform all kinds of attacks on this contract. I encourage all readers to put these contracts into Remix and experiment with different attack contracts and state changes through these delegatecallfunctions.
It is also important to notice that when we say that delegatecall is state-preserving, we are not talking about the variable names of the contract, rather the actual storage slots to which those names point. As you can see from this example, a simple mistake, can lead to an attacker hijacking the entire contract and its ether.
Preventative Techniques
Solidity provides the library keyword for implementing library contracts (see the Solidity Docs for further details). This ensures the library contract is stateless and non-self-destructable. Forcing libraries to be stateless mitigates the complexities of storage context demonstrated in this section. Stateless libraries also prevent attacks whereby attackers modify the state of the library directly in order to effect the contracts that depend on the library's code. As a general rule of thumb, when using DELEGATECALL pay careful attention to the possible calling context of both the library contract and the calling contract, and whenever possible, build state-less libraries.
Real-World Example: Parity Multisig Wallet (Second Hack)
The Second Parity Multisig Wallet hack is an example of how the context of well-written library code can be exploited if run in its non-intended context. There are a number of good explanations of this hack, such as this overview: Parity MultiSig Hacked. Again by Anthony Akentiev, this stack exchange question and An In-Depth Look at the Parity Multisig Bug.
To add to these references, let’s explore the contracts that were exploited. The library and wallet contract can be found on the parity github here.
Let’s look at the relevant aspects of this contract. There are two contracts of interest contained here, the library contract and the wallet contract.
The library contract,
and the wallet contract,
Notice that the Wallet contract essentially passes all calls to the WalletLibrary contract via a delegate call. The constant _walletLibrary address in this code snippet acts as a placeholder for the actually deployed WalletLibrary contract (which was at 0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4).
The intended operation of these contracts was to have a simple low-cost deployable Wallet contract whose code base and main functionality was in the WalletLibrary contract. Unfortunately, the WalletLibrary contract is itself a contract and maintains it's own state. Can you see why this might be an issue?
It is possible to send calls to the WalletLibrary contract itself. Specifically, the WalletLibrary contract could be initialised, and become owned. A user did this, by calling initWallet() function on the WalletLibrary contract, becoming an owner of the library contract. The same user, subsequently called the kill() function. Because the user was an owner of the Library contract, the modifier passed and the library contract suicided. As all Wallet contracts in existence refer to this library contract and contain no method to change this reference, all of their functionality, including the ability to withdraw ether is lost along with the WalletLibrary contract. More directly, all ether in all parity multi-sig wallets of this type instantly become lost or permanently unrecoverable.
- Default Visibilities Functions in Solidity have visibility specifiers which dictate how functions are allowed to be called. The visibility determines whether a function can be called externally by users, by other derived contracts, only internally or only externally. There are four visibility specifiers, which are described in detail in the Solidity Docs. Functions default to public allowing users to call them externally. Incorrect use of visibility specifiers can lead to some devestating vulernabilities in smart contracts as will be discussed in this section.
The Vulnerability
The default visibility for functions is public. Therefore functions that do not specify any visibility will be callable by external users. The issue comes when developers mistakenly ignore visibility specifiers on functions which should be private (or only callable within the contract itself).
Lets quickly explore a trivial example.
This simple contract is designed to act as an address guessing bounty game. To win the balance of the contract, a user must generate an Ethereum address whose last 8 hex characters are 0. Once obtained, they can call the WithdrawWinnings()function to obtain their bounty.
Unfortunately, the visibility of the functions have not been specified. In particular, the _sendWinnings() function is publicand thus any address can call this function to steal the bounty.
Preventative Techniques
It is good practice to always specify the visibility of all functions in a contract, even if they are intentionally public. Recent versions of Solidity will now show warnings during compilation for functions that have no explicit visibility set, to help encourage this practice.
Real-World Example: Parity MultiSig Wallet (First Hack)
In the first Parity multi-sig hack, about $31M worth of Ether was stolen from primarily three wallets. A good recap of exactly how this was done is given by Haseeb Qureshi in this post.
Essentially, the multi-sig wallet (which can be found here) is constructed from a base Wallet contract which calls a library contract containing the core functionality (as was described in Real-World Example: Parity Multisig (Second Hack)). The library contract contains the code to initialise the wallet as can be seen from the following snippet
Notice that neither of the functions have explicitly specified a visibility. Both functions default to public. The initWallet()function is called in the wallets constructor and sets the owners for the multi-sig wallet as can be seen in the initMultiowned() function. Because these functions were accidentally left public, an attacker was able to call these functions on deployed contracts, resetting the ownership to the attackers address. Being the owner, the attacker then drained the wallets of all their ether, to the tune of $31M.
we will share another 5 more common element in this area in another post
Top comments (1)
This article needs to be formatted better very hard to red