Summary of Twenty-Three Types of DeFi Security Incidents: Risks and Prevention of Smart Contracts

AsymmetriesTechnologies
2022-05-11 10:27:18
Collection
The security issues of smart contracts have always been a key topic in the industry. Certain oversights by programmers have led to flaws in thinking and logic, providing hackers with opportunities to exploit.

Written by: Austin Zhang, Jon Li, Asymmetries Technologies

The security issues of smart contracts have always been a key topic in the industry. Certain oversights by programmers have led to logical vulnerabilities, providing hackers with opportunities. We have collected smart contracts in the DeFi space that have already experienced security incidents and conducted empirical analysis based on the example code we wrote, hoping to provide some insights for colleagues and peers.

(1) Reentrancy Attack

One of the main attack methods: a malicious external contract calls back the original contract function before the contract finishes calling the malicious external contract, exploiting related vulnerabilities.

(1) Example Code

image

(2) Case 1: On December 22, 2021, the Uniswap V3 liquidity management protocol Visor was hacked for 120 ETH.

image

image

Cause of Incident: The deposit function did not have a reentrancy lock and did not verify whether the from address was a legitimate Visor contract address. The attacker passed in the attack contract address, repeatedly called the deposit function, and bypassed the withdrawal amount check to withdraw multiple times.

(3) Case 2: On June 5, 2021, BurgerSwap was hacked for 7 million USD.

image

Cause of Incident: Similar to Uniswap's original DEX, it is divided into Platform and Pool contracts. The Platform is similar to Uniswap's Router, and Pair is similar to Uniswap's Pool. The developer mistakenly placed the K value check in the Platform calculation, allowing the attacker to perform a reentrancy attack in the Platform, repeatedly exchanging tokens with the old K value, resulting in losses for liquidity providers.

(4) Solution: Ensure all intermediate state variables are updated before calling external contracts and use a reentrancy lock (e.g., OpenZeppelin's ReentrancyGuard).

(2) Unchecked Function Return Values

When calling external contract functions, some function calls may fail without throwing an error to roll back the transaction but instead return false. Forgetting to check the function return value can lead to the misconception that the call was successful.

(1) Example Code

image

(2) Case: On April 4, 2021, ForceDao was attacked, resulting in a loss of 183 ETH.

image

Cause of Incident: The transferFrom function of the Force token returns false when the balance is insufficient instead of rolling back the transaction directly. The contract did not perform checks, leading to the failure of the transfer being considered successful, allowing the corresponding tokens to be exchanged.

(3) Solution: When using the call function to call external contracts, it is essential to check whether the call was successful. Note: If the call to the external contract does not match a function, it will call the external contract's fallback or receive function. If the external contract defines a receive function and the call function does not carry calldata, it will call the external contract's receive function; otherwise, it will call the fallback function.

(3) Incorrect Function Visibility Settings

In Solidity, functions are public by default and can be called externally. If key functions are not set to private, it can lead to security risks.

(1) Example Code

image

(2) Case 1: On January 22, 2022, Dex Crosswise was attacked, resulting in a loss of 800,000 USD.

image

Cause of Incident: Although Crosswise implemented the permission verification function onlyOwner, it forgot to set setTrustedForwarder to private, allowing the attacker to exploit this and set themselves as the pool's owner, transferring all tokens away.

(3) Case 2: On June 18, 2020, the cross-chain bridge Bancor Network was attacked, resulting in a loss of 140,000 USD.

image

Cause of Incident: The function used for transfers in the contract is public by default, allowing the attacker to directly call and transfer tokens from the contract.

(4) Solution: Withdrawal functions, which involve the transfer of contract assets, should be carefully controlled for permissions, ensuring that initialization functions can only run once.

(4) Unverified Key Absence in Mapping

In Solidity, when retrieving the Value corresponding to a Key in a Mapping, if the Key does not exist, it will return the default value of the corresponding type instead of throwing an error. For example, Mapping(int → int) will return the default value 0 if the corresponding int Key does not exist.

(1) Example Code

image

(2) Case: On July 11, 2021, the cross-chain bridge ChainSwap was attacked, resulting in a loss of 4 million USD.

image

Cause of Incident: ChainSwap relies on validators in its network for transfers. To limit the amount of tokens a validator can transfer at once, a quota was set. However, there was a vulnerability in the contract that allowed bypassing the quota limit. When the address variable signatory does not exist, authQuotes[signatory] and lasttimeUpdateQuoteOf[signatory] return 0, leading to incorrect quota calculations and unexpected large quotas.

(3) Solution: Always check if the key exists when using mappings.

(5) Transferring Before State Changes

Transferring funds can be subject to reentrancy, allowing attacks to exploit unchanged states.

(1) Case: On August 17, 2021, XSURGE was attacked, resulting in a loss of 5 million USD.

image

Cause of Incident: The totalSupply was modified only after the transfer, leading to a reentrancy attack on another function that did not have a reentrancy lock, resulting in a loss of 5 million USD.

(2) Solution: Even with a reentrancy lock, ensure that all state changes occur before transferring.

(6) Initialization Function Lacking Call and Permission Restrictions

Many contracts require initializing child contracts. For example, Uniswap needs to initialize Pool contracts through the Factory contract. If the initialization function for the child contract lacks permission and duplicate initialization restrictions, it may be maliciously initialized by an attacker.

(1) Case: On August 11, 2021, Punk Protocol was attacked, resulting in a loss of 4 million USD.

image

Cause of Incident: The pool's initialize function lacked permission and duplicate call restrictions, allowing the attacker to call this function, set themselves as the Forge administrator, and call withdrawToForge to send all funds from the pool to the attacker's address.

(2) Solution: The initialization function must be set to allow only one initialization.

(7) Incorrectly Checking Contract Function Implementations

Typically, when a function called on a smart contract does not exist, an error is thrown. However, if the contract implements a fallback function, it will automatically call the fallback function. Sometimes, the fallback function does not throw an error, leading the caller to mistakenly believe the call was successful.

(1) Case: On January 18, 2022, the cross-chain bridge Multichain was attacked, resulting in a loss of 450 ETH.

image

Cause of Incident: Typically, ERC20 contracts implement a permit function for signature checks and authorization operations (this function is similar to approve and can be called by other contracts using pre-generated signatures, saving users' gas fees). However, the contracts for six tokens, WETH, PERI, OMT, WBNB, MATIC, and AVAX, did not implement permit but did implement a fallback function. Multichain mistakenly believed that users had authorized the transfer to the attacker when checking the permissions for these tokens, resulting in stolen tokens.

(2) Solution: Different tokens have different implementation methods; carefully check their specific implementations before introducing new tokens.

(8) Incorrect Handling of Tokens with Transfer Fees

Some tokens burn a portion of the transfer fee during transfers, leading to a lower actual received token balance. If developers do not consider this, calculations based on the transfer value may lead to discrepancies.

(1) Case: On August 19, 2021, Pinecone was hacked for 200,000 USD.

Cause of Incident: Pinecone used its token PCT as the staking token for the liquidity pool, and PCT transfers incur a fee loss. The contract did not account for this loss, leading to discrepancies between user shares and the total staked PCT, which the attacker exploited to claim excess rewards.

(2) Solution: Remember that not all token transfer fees are native tokens.

(9) Signature Verification Vulnerabilities

Signatures may be reused, or the symmetry of the elliptic curve signature algorithm can be exploited to construct valid signatures based on existing signatures.

(1) Case: On July 12, 2021, AnySwap was hacked for 8 million USD.

Cause of Incident: In addition to the private key, a random number R is required for transaction signatures. However, AnySwap mistakenly deployed a new contract, leading to two transactions under the MPC account of the V3 router on BSC having the same R value signature. The attacker reverse-engineered the private key of this MPC account and stole the funds.

(2) Solution: Use the EIP-712 standard to verify signatures, referring to OpenZeppelin's implementation: https://docs.openzeppelin.com/contracts/3.x/api/drafts.

(10) Not Considering Possible Changes in Contract Balance

When miners mine blocks or when a smart contract calls the selfdestruct function to destroy itself, it can forcibly transfer tokens to any address, changing its native token balance. When using the return value of the balance function as a condition for judgment, the balance may be forcibly changed, leading to risks, and in extreme cases, even causing the contract to refuse service (DoS).

(1) Example Code

image

Even if the donation contract cannot accept token transfers, the contract balance may change after deployment. Strictly checking that the total airdrop amount plus the contract balance equals the total supply may lead to the donation contract refusing service (DoS).

(2) Solution: Avoid strict equality checks on contract balances within the contract.

(11) Using delegatecall to Call External Contracts

delegatecall can embed the function code of the corresponding contract into the current context for execution, similar to calling a built-in function. If a malicious contract is inadvertently called, it can easily lead to an attack.

(1) Example Code

image

When the attacker calls the forward function and passes the Attack contract address and the setOwner() function as parameters, the Proxy contract's owner will be changed to the attacker's address.

(2) Solution: It is not recommended to use delegatecall to call external contracts.

(12) Authorizing tx.origin

tx.origin is the address of the transaction initiator. If a contract uses tx.origin for permission checks, when the authorized user of the contract interacts with a malicious contract, the malicious contract can call the contract and pass the permission check.

(1) Example Code

image

When the owner of the MyWallet contract uses the transferTo function to transfer to the Attack contract, the Attack contract can re-enter the MyWallet contract and call the transferTo function. At this time, tx.origin is still the MyWallet owner, satisfying the require condition, and the MyWallet balance will be entirely transferred to the Attack contract.

(2) Solution: Do not use tx.origin for permission checks.

(13) Transaction Ordering Competition

Full node operators can obtain transaction information before the transaction is confirmed, allowing them to construct high-fee transactions based on the obtained transaction information, prompting miners to prioritize packaging their transactions to execute strategies favorable to themselves. For example, in a riddle contract that rewards the fastest user to find the answer, a malicious user can construct a high-fee transaction to submit the answer before the honest user submits it, thus obtaining the reward. Similarly, when a user updates the authorization limit, the authorized user can transfer the old authorization limit before the transaction updating the authorization limit is confirmed, resulting in the authorized user effectively receiving the sum of the two authorization limits.

Solution: For the riddle contract, the user who obtains the answer should first submit the hash of "random number + own address + answer." After the riddle contract stores this hash, the user can then submit random information and the answer. The contract checks the hash match before issuing rewards; when updating the authorization limit, set the authorization limit to zero first.

(14) Using block.timestamp or block.number as Contract Time Reference

block.timestamp and block.number cannot provide precise time, and using them as time references in smart contracts introduces potential risks.

Solution: Use an oracle to obtain time information.

(15) Denial-of-Service (DoS)

Calling external contracts may permanently fail, preventing the contract from accepting new instructions. For example, when a contract actively transfers to another contract that does not have a function to accept transfers, the transfer fails, and the contract may enter a denial-of-service state.

(1) Example Code

image

When the contract fails to transfer to one of the accounts, it causes all transfers to fail.

(2) Solution: The contract must include code to handle potential failures when calling external contracts to prevent entering a denial-of-service state.

(16) Using Chain Attributes as Random Sources

Chain attributes such as block.timestamp, blockhash, block.difficulty, and others can be manipulated by miners, posing risks.

Solution: Consider using RANDAO, oracles, or Bitcoin block hashes as random sources.

(17) Incorrect Inheritance Order

When multiple inherited contracts define the same function, the priority of the function call in the inheriting contract is determined by the inheritance order. An incorrect inheritance order can lead to erroneous function calls.

Solution: Refer to the official examples for inheritance order explanations: https://solidity-by-example.org/inheritance/.

(18) Insufficient Gas Attack

In multi-signature scenarios or when needing someone else to pay for gas, a user prepares a signed transaction and hands it to a relayer, who then submits the user's transaction to the executing contract. The relayer can review the user's transaction in advance. A malicious relayer, or one that finds the transaction content unfavorable, can restrict the gas supply, causing the transaction execution to fail and thus preventing the transaction from being executed.

(1) Example Code

image

When the relayer calls and restricts gas usage, causing a transaction to fail, that failed transaction can never be submitted again.

(2) Solution: Choose trusted relayers, or check whether the gas fees provided by the relayer are sufficient in the executing contract.

(19) Function Type Variable Jumping

Solidity supports function type variables. When function type variables are assigned using assembly instructions, they may be pointed to maliciously constructed functions.

Solution: Avoid using assembly instructions in smart contracts unless necessary.

(20) Gas Limit Denial-of-Service Attack (DoS)

Blocks have a gas usage limit. If a contract execution exceeds the block's gas usage limit, the contract can never be executed successfully.

(1) Example Code

image

When the number of iterations in an operation is too large, the gas required for executing the contract will exceed the block limit, causing the contract execution to fail.

(2) Solution: Be cautious when operating large arrays or loops in smart contracts.

(21) abi.encodePacked() Hash Collision

abi.encodePacked() uses non-padding serialization. When the serialized parameters include multiple variable-length arrays, attackers can change the elements of two variable-length arrays while keeping the order of all elements unchanged, resulting in the same serialized output.

(1) Example Code

image

By constructing the input for addUser, an attacker can add members of regularUsers to the admins, but the constructed input has the same signature as the original input.

(2) Solution: Use fixed-length arrays, or do not allow callers to pass parameters to abi.encodePacked(), or use abi.encode().

(22) Insufficient Gas in transfer() and send() Functions

The transfer() and send() functions use 2300 gas to prevent reentrancy attacks, and upgrades to public chains may lead to insufficient gas.

Solution: It is recommended to use the call() function, but ensure protection against reentrancy attacks.

(23) Unencrypted Privacy Data on Chain

On-chain data is completely transparent, and the private keyword in contracts cannot prevent the leakage of private data.

(1) Example Code

image

Although players are private, attackers can still read players by parsing on-chain data.

(2) Solution: Privacy data needs to be encrypted and stored on-chain.

The above is a summary of the twenty-three types of security incidents we have analyzed and compiled, hoping to provide you with some reference and insights.

ChainCatcher reminds readers to view blockchain rationally, enhance risk awareness, and be cautious of various virtual token issuances and speculations. All content on this site is solely market information or related party opinions, and does not constitute any form of investment advice. If you find sensitive information in the content, please click "Report", and we will handle it promptly.
ChainCatcher Building the Web3 world with innovators