Common Vulnerabilities
Re-Entrancy
One of the major dangers of calling external contracts is that they can take over the control flow, and make unexpected changes to your data.
Reentrancy on a Single Functions
Re-entrancy involves functions that could be called repeatedly, before the first invocation of the function is finished. This may cause the different invocations of the function to interact in destructive ways.
// Example of potential re-entrancy
mapping (address => uint) private userBalances;
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
// At this point, the caller's code is executed, and can call withdrawBalance again
(bool success, ) = msg.sender.call.value(amountToWithdraw)("");
require(success);
userBalances[msg.sender] = 0;
}Since the user's balance is not set to 0 until the very end of the function, the second (and later) invocations will still succeed and will withdraw the balance over and over again.
Cross-Function Re-entrancy
An attacker may also be able to do a similar attack using two different functions that share the same state.
mapping (address => uint) private userBalances;
function transfer(address to, uint amount) {
if (userBalances[msg.sender] >= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
(bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // At this point, the caller's code is executed, and can call transfer()
require(success);
userBalances[msg.sender] = 0;
}The attacker can call transfer() while their code is still being executed on the external call in withdrawBalance. Since their balance has not yet been set to 0, they are able to transfer the tokens even though they already received the withdrawal.
Solution: Checks-Effects-Interactions pattern
Check if execution is valid
Make the neccesary state changes required
Interact with external contracts or functions that call external functions
Oracle Manipulation
Spot Price Manipulation
Trusting the spot price of a decentralized exchange is risky. If the spot price of an asset is inflated to a value way larger than the true value, an attacker could take out a flash loan to drain one side of the Uniswap pool. An arbitrage trade on top of the newly created price difference or an advantageous position in the system can also be gained.
The issue arises because:
The use of a single price feed source smart contract allows for easy on-chain manipulation using flash loans.
Despite a notable anomaly, the smart contracts consuming the price information continue to operate on the manipulated data.
Possible attack vector:
An attacker can take out a flash loan on the incoming asset A and on the relevant Uniswap pool, swapping a large volume of asset A for asset B.
This trade will increase the price of asset B (increased demand) and reduce the cost of asset A (increased supply) in the pool.
When asset B is deposited into the above function, its price is still pumped up by the flash loan.
Consequentially, asset B gives the attacker an over-proportional amount of shares.
These shares can be withdrawn, giving the attacker equal parts of asset A and asset B from the pool.
Repeating this process will drain the vulnerable pool of all funds.
With the money gained from the withdrawal of their shares, the attacker can repay the flash loan.
Off-Chain Infrastructure
Attacks on access control, cryptographic implementation, transport, and database security, among others, can be performed.
E.g. Synthetix sKRW incident - Synthetix (at the time) relied on a custom off-chain price feed implementation where an aggregate price calculated from a secret set of price feeds was posted on-chain at a fixed interval. These prices then allowed users to take long or short positions against supported assets.
One of the price feeds that Synthetix relied on mis-reported the price of the Korean Won to be 1000x higher than the true rate. A trading bot quickly traded in and out of the sKRW market and was able to earn a profit of over 1B USD.
Decentralized Oracle Security
Decentralized oracles aim to diversify the group of data collectors to a point where disrupting a quorum of participants becomes unfeasible for an attacker. In a decentralized scenario, further security considerations stem from how participants are incentivized and what sort of misbehavior if left unpunished. Participants providing (valid) data to the oracle system are economically rewarded. Aiming to maximize their profit, the participants are incentivized to provide the cheapest version of their service possible.
Freeloading - A node can leverage another oracle or off-chain component (such as an API) and simply copy the values without validation. Freeloading attacks can be easily prevented for more complex data feeds by implementing a commit-reveal scheme. This security measure will prevent oracle system participants from peeking into each other's data.
Mirroring - misbehaving nodes aim to save work by reading from a centralized data source, which results in increased weight on a single data point that reduces data quality. A commit-reveal scheme is ineffective to mitigate (purposeful) mirroring attacks as it does not consider private data transfers between Sybil nodes. Due to the lack of transparency in Sybil communications, mirroring attacks can be very hard to detect in practice.
Solutions
Chainlink is the largest decentralized oracle provider, and the Chainlink network can be leveraged to bring decentralized data on-chain.
Tellor is an oracle that provides censorship-resistant data, secured by economic incentives, ensuring data can be provided by anyone, anytime, and checked by everyone.
Witnet leverages state-of-the-art cryptographic and economic incentives to provide smart contracts with off-chain data.
Another standard solution is to use a time-weighted average price feed so that price is averaged out over X periods and multiple sources. Not only does this prevent oracle manipulation, but it also reduces the chance you can be front-run, as an order executed right before cannot have as drastic of an impact on the price
Frontrunning
Since all transactions are visible in the mempool for a short while before being executed, observers of the network can see and react to an action before it is included in a block.
We define the following categories of front-running attacks:
Displacement - an attacker is able to submit a larger gasPrice to copy & override certain transactions in the mempool.
Insertion - an attacker will insert a function call that changes the state of the network, and wants the user’s original function call to run on this modified state
Suppression - spamming transactions to fill up a block’s gas limit to prevent other transactions from going through
Preventive Techniques
Remove the benefit of front-running in your application, mainly by removing the importance of transaction ordering or time.
Mitigate the cost of front-running by specifying a maximum or minimum acceptable price range on a trade, thereby limiting price slippage.
Use a commit-reveal scheme - a cryptographic algorithm used to allow someone to commit to a value while keeping it hidden from others with the ability to reveal it later. The values in a commitment scheme are binding, meaning that no one can change them once committed. The scheme has two phases: a commit phase in which a value is chosen and specified, and a reveal phase in which the value is revealed and checked.
Arithmetic Overflow and Underflow
If a balance reaches the maximum uint value (2^256) it will circle back to zero.
If a uint is made to be less than zero, it will cause an underflow and get set to its maximum value.
Griefing
This attack may be possible on a contract which accepts generic data and uses it to make a call another contract (a 'sub-call') via the low level address.call() function, as is often the case with multisignature and transaction relayer contracts.
If the call fails, the contract has two options:
revert the whole transaction
continue execution
Force Feeding
Forcing a smart contract to hold an Ether balance can influence its internal accounting and security assumptions. There are multiple ways a smart contract can receive Ether. The hierarchy is as follows:
Check whether a payable external
receivefunction is defined.If not, check whether a payable external
fallbackfunction is defined.Revert.
Self Destruct - When the
SELFDESTRUCTopcode is called, funds of the calling address are sent to the address on the stack, and execution is immediately halted. Since this opcode works on the EVM-level, Solidity-level functions that might block the receipt of Ether will not be executed. A malicious contract can useselfdestructto force sending Ether to any contract.Pre-calculated Deployments - The target address of newly deployed smart contracts is generated in a deterministic fashion. An attacker can send funds to this address before the deployment has happened.
The above effects illustrate that relying on exact comparisons to the contract's Ether balance is unreliable. The smart contract's business logic must consider that the actual balance associated with it can be higher than the internal accounting's value.
Preventative Techniques
Don't rely on address(this).balance
Delegatecall
delegatecall is tricky to use and wrong usage or incorrect understanding can lead to devastating results.
Two things to keep in mind when using delegatecall:
delegatecallpreserves context (storage, caller, etc...)storage layout must be the same for the contract calling
delegatecalland the contract getting called
Preventative Techniques
Only use stateless
Librarywhen executing delegatecalls
Denial of Service
There are many ways to attack a smart contract to make it unusable.
Looping through arrays may result in the transactions exceeding block gas limits, and eventually revert all previous state changes. Solution - use pull over push pattern.
Another exploit could be making the function to send Ether fail.
Preventative Techniques
One way to prevent this is to allow the users to withdraw their Ether instead of sending it.
Phishing with tx.origin
What's the difference between msg.sender and tx.origin?
If contract A calls B, and B calls C, in C msg.sender is B and tx.origin is A.
Vulnerability - A malicious contract can deceive the owner of a contract into calling a function that only the owner should be able to call.
Preventative Techniques
Use msg.sender instead of tx.origin
Hiding Malicious Code with External Contract
In Solidity any address can be casted into specific contract, even if the contract at the address is not the one being casted. This can be exploited to hide malicious code.
Preventative Techniques
Initialize a new contract inside the constructor
Make the address of external contract
publicso that the code of the external contract can be reviewed
Honeypot
A honeypot is a trap to catch hackers.
Vulnerability
Combining two exploits, reentrancy and hiding malicious code, we can build a contract that will catch malicious users.
Block Timestamp Manipulation
block.timestamp can be manipulated by miners with the following constraints:
it cannot be stamped with an earlier time than its parent
it cannot be too far in the future
Preventative Techniques
Don't use
block.timestampfor a source of entropy and random number
Signature Replay
Signing messages off-chain and having a contract that requires that signature before executing a function is a useful technique.
For example this technique is used to:
reduce number of transaction on chain
gas-less transaction, called
meta transaction
Vulnerability
Same signature can be used multiple times to execute a function. This can be harmful if the signer's intention was to approve a transaction once.
Preventative Techniques
Sign messages with nonce and address of the contract.
Bypass Contract Size Check
If an address is a contract then the size of code stored at the address should be greater than 0.
A contract can be created with code size returned by extcodesize equal to 0.
Last updated