Request an Audit
Back
  • 07/19/2024

Smart Contract Audit Series—-Solidity Smart Contract Risks

 1.Reentrancy Attack

Occur when a contract calls an external contract before it completes its execution, and the external contract calls back into the original contract, potentially leading to state being manipulated.

// Vulnerable contract that allows for reentrancy attacks

contract Reentrancy {

    mapping(address => uint) public balances;

    function withdraw() public {

        uint balance = balances[msg.sender];

        require(balance > 0);

        (bool success, ) = msg.sender.call{value: balance}("");

        require(success, "Transfer failed");

        balances[msg.sender] = 0;

    }

}

Description: The contract is easy to re-enter, as it updates user balances after sending ether, allowing for repeated withdrawals.

real case scenario: On August 30, 2021, Cream Finance suffered a reentrancy attack between multiple contracts, resulting in a loss of more than 10 million US dollars. The attacker called a lending contract of Cream Finance and conducted a reentrancy attack by exploiting the way the contract handles external calls and failing to properly manage the contract state (failed to update the internal state before external calls).

Reentrancy vulnerability protection:

1. Checks-Effects-Interactions Pattern

2. Use the contract ReentrancyGuard provided by OpenZeppelin

3. State lock prevents reentrancy by using state variables in the contract to identify whether it is being called.

2.Integer Overflow/Underflow

Happens when integer operations exceed the storage limit of the variable type, which can lead to logic errors or loss of funds.

// Vulnerable contract susceptible to integer overflow and underflow

contract IntegerOverflowUnderflow {

    uint public balance = 1;

    function decrement() public {

        balance -= 1; // Underflow if balance is zero

    }

    function increment() public {

        balance += 1; // Overflow if balance is at max uint256

    }

}

Description: This contract can suffer from underflow or overflow due to unchecked arithmetic operations.

real case scenario:On April 22, 2018, BEC suffered an integer overflow attack, and the attacker took out:

57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 BEC tokens then depreciated sharply, with a value of almost 0, the market collapsed instantly. The hacker passes in a very large value (here it is 2**255), and overflows upward through multiplication, so that the amount (the total number of coins to be transferred) overflows and becomes a very small number or 0 (here it becomes 0). , thus bypassing the check code of balances[msg.sender] >= amount, allowing malicious transfers with huge _value amounts to succeed.

Integer overflow vulnerability protection:

1. Use the SafeMath library (Solidity version is less than 0.8.0)

2. Utilize the built-in overflow check of Solidity 0.8.0 and above

3.Denial of Service

An attacker may exploit vulnerabilities to make a contract unusable, such as by using up gas or blocking key functionalities.

// Contract vulnerable to DoS by gas exhaustion

contract DenialOfService {

    address public richest;

    uint public highestBid;

    function bid() public payable {

        require(msg.value > highestBid);

        if (richest != address(0)) {

            (bool sent, ) = richest.call{value: highestBid}("");

            require(sent, "Failed to send Ether");

        }

        richest = msg.sender;

        highestBid = msg.value;

    }

}

Description: This contract can be made unusable by repeatedly calling bid() with values that exhaust all gas.

real case scenario: In the 2016 King of the Ether Throne game, a DoS attack caused the smart contract to stop responding, making game funds inaccessible. In the KotET event, the constructor that was not called contained important state initialization logic, which caused the key state variables in the contract to not be set correctly. When the new contract began to accept transactions, due to the misconfiguration of state variables, the contract logic went wrong, and all funds sent to the contract could not be retrieved. As a result, the funds were permanently locked.

Dos vulnerability protection:

1. Operations that limit mutable state changes

2.Set function call limits

3. Set Gas limits appropriately

4.Gas Limit Issues

You may encounter problems if the contract consumes all available Gas, resulting in incomplete transactions, or if the contract consumes a large amount of Gas, resulting in security risks.


// Contract with potential for gas limit issues

contract GasGuzzler {

    function complexCalculation() public {

        // Complex loop or operations that consume a large amount of gas

        for (uint i = 0; i < 10000; i++) {

            // some complex calculations here

        }

    }

}

Description: The contract is at risk of running out of gas, which willAs a result, its functions cannot be fully executed.

real case scenario: CryptoKitties caused network congestion at the end of 2017. Since smart contract operations consumed a large amount of Gas, transactions on the entire Ethereum network were congested and transaction fees surged.The popularity of CryptoKitties resulted in an influx of transaction requests into the Ethereum network, especially in the first few weeks of the game. These transactions mainly involve the purchase, breeding and trading of cats. Each operation needs to be confirmed through the Ethereum network and consumes Gas fees. As transaction volume surges, unprocessed transactions accumulate in the mempool (transaction pool), causing network processing speed to slow down and ordinary transactions and smart contract execution to encounter significant delays.

Vulnerability protection:

1. Simplify the logic and try to simplify the logic in the contract to avoid unnecessary complexity, especially in loops and recursive calls.

2. Gas optimization: state variable optimization, use smaller data types as much as possible, such as converting uint256 to uint8, if the data range allows, because using smaller data types in the EVM may save Gas; batch processing, for a large number of Data processing, designed to support batch processing, allows users or contracts to gradually complete operations in batches to avoid failure of a single transaction due to exceeding the Gas limit; inline functions, use inline functions to replace small external function calls, because functions The call will add additional Gas overhead.

3. Modular design: Decompose a large contract into multiple small contracts, and by calling simplified sub-contracts to perform complex operations, the Gas consumption of each part can be effectively managed.

5.Time Manipulation

The timestamp of a block can be manipulated by miners, which can affect contracts relying on specific timing.

// Contract vulnerable to time manipulation

contract TimeBased {

    uint public lastUpdated;

    function update() public {

        require(block.timestamp >= lastUpdated + 1 weeks);

        lastUpdated = block.timestamp;

    }

}

Description: The function relies on block.timestamp, which can be manipulated by miners, affecting the logic flow.

6.Front Running

Occurs when someone with knowledge of upcoming transactions in the mempool uses this information to their advantage before those transactions are processed.

// Example of a contract vulnerable to front running

contract Auction {

    uint public highestBid;

    address public highestBidder;

    function bid() public payable {

        require(msg.value > highestBid);

        highestBid = msg.value;

        highestBidder = msg.sender;

        // Issue: A higher bid can be seen and surpassed in the same block

    }

}

Description: Visible pending transactions can be exploited by malicious actors who place a higher bid before the first transaction is confirmed.

real case scenario: Commonly seen on various decentralized exchanges (DEX), attackers use front-running to earn profits from arbitrage transactions. For example, Uniswap or SushiSwap, front-loaded transactions are particularly common. Attackers monitor incoming transactions, especially large transactions, and then send their own transactions with higher gas fees before the original transactions are processed by miners.

Loss: This leaves the original trader at risk of increased slippage or failed trades, while the attacker earns arbitrage profits by manipulating trade prices.

Vulnerability protection:

1. Batch processing and time locking, batch processing of transactions, and use time lock technology to ensure that all transactions are executed in the same block in random order, so that front-loaded traders cannot know whether their transactions will be executed before other transactions.

2. Using a decentralized trading aggregator, the aggregator can disperse transactions among multiple DEXs and reduce the risk of being front-loaded on a single platform.

7.Randomness Attack

Reliance on pseudo-random functions can be exploited if the method of generating randomness is not secure.

// Contract using insecure randomness

contract GuessTheRandomNumber {

    uint public constant randomFactor = block.timestamp;

    function guess(uint _guess) public {

        require(_guess == randomFactor); // Insecure

    }

}

Description: The randomness source block.timestamp can be predicted or influenced, leading to exploitation.

real case scenario: Randomness attacks in smart contract games, such as Fomo3D, etc., the attacker predicts the game results by generating random numbers through research. An attacker can predict the outcome of the random number by listening to upcoming block information and analyzing historical data. If an attacker can predict the next random number, they may be able to purchase the key at a critical moment, increasing the probability of winning. Before the block is finally confirmed, the attacker may also take advantage of network delays by sending transactions with high gas fees to be packaged by miners first to ensure that they successfully purchase keys at the last minute.

Vulnerability protection:

1. Use a decentralized random number generator (VRF) to generate provably verifiable random numbers using a service like Chainlink VRF.

2. Use a mixture of multiple data sources to generate random numbers: combine block data, external data sources and participant input to generate random numbers through a complex hash function.

3. Introduce a time delay mechanism and introduce a random delay phase until a certain future block is confirmed before the result is determined.

4. An interactive proof system that requires users or nodes to submit preliminary hash values ​​(hidden choices) and then reveal their choices to complete random number generation.

8.Access Control Vulnerability


Improper restrictions on who can execute certain functions or access sensitive data within a contract.

// Contract with a flawed access control

contract AccessControl {

    address public owner = msg.sender;

    function restrictedAccess() public {

        require(msg.sender == owner, "Not authorized");

        // Critical functionality here

    }

}

Description: Incorrect or missing checks in access control can lead to unauthorized actions.

real case scenario: The Parity wallet freezing incident was a major vulnerability on November 7, 2017, which caused some users to be unable to withdraw Ethereum from the virtual wallet, and Ethereum funds worth more than 280 million US dollars were frozen. Due to an access control vulnerability, a novice user inadvertently triggered a self-destruct function in a smart contract, causing a wallet containing a large amount of ether to be permanently frozen.

Vulnerability protection:

1. Use inheritance-safe libraries, use OpenZeppelin’s audited and tested libraries.

2. Multi-Sig: For key operations, a multi-signature mechanism is used, which requires the joint confirmation of multiple accounts before execution.

3. Time locks (Timelocks), set time locks for sensitive operations, that is, there is a clear waiting period before executing the operation, during which all related parties can observe and respond to possible unauthorized operations.

4. Strict function modifiers. For all sensitive functions, use strict function modifiers to verify the caller’s permissions.

9.Replay Attack


Occurs when the same transaction is executed more than once, usually without the user’s knowledge, potentially leading to unauthorized actions.

// Contract susceptible to replay attacks

contract ReplayAttack {

    function transferOwnership(address _newOwner, bytes memory _signature) public {

        // Replay attack can occur if _signature can be reused

    }

}

Description: Without measures like nonce usage, transactions may be replayed on different contexts or multiple times.

real case scenario: NBA NFT is a digital collection trading platform based on the Flow blockchain. In 2021, hackers copied a transaction that had been confirmed on the chain by analyzing transaction data, and successfully replayed the transaction in different environments, resulting in unauthorized transfers of digital assets. Hackers used transaction replay to successfully transfer hundreds of thousands of dollars worth of NFTs (non-fungible tokens), causing heavy losses to the platform and users.

Vulnerability protection:

1. Using the Nonce mechanism, each account maintains a counter (Nonce), and the counter increases after each transaction. The network prevents transactions that have been broadcast from being executed again by checking the Nonce value.

2. Chain-specific transaction signatures add specific chain IDs to transactions in each chain to ensure that transactions in one chain cannot be executed on another chain.

3. Timestamp or time window limit, set the valid time window of the transaction. Beyond this time window, the transaction cannot be executed even if the signature is valid.

10.Storage

Occurs when the storage pointer is not properly initialized, resulting in undefined behavior or the storage being manipulated in an unexpected way. It also creates a security risk if a variable storage overwrite occurs.

// Contract with uninitialized storage pointers

contract UninitializedPointer {

    uint[] public data;

    function append(uint _value) public {

        uint[] storage ptr; // Uninitialized storage pointer

        ptr

Real case: On July 24, 2022, the Audius community vault of the Web3 music streaming service platform was hacked, resulting in the loss of 18.5 million AUDIO Tokens. There is an agency contract on the Audius platform. The Governance contract is called by the agency contract. When deployed, the Governance contract will call the modifier in the initialization contract. The result of the initialization is to determine the governanceAddress address, which exists in the first card slot, and in the modifier condition of this initialization, the Boolean value of the variable is also stored in the first card slot, resulting in a storage conflict. The result is that the initializing variable is always true. The initializer() modification can be called multiple times.

Vulnerability protection:

1. Ensure that all stored variables are initialized, especially in the constructor or initialization function, initialize all stored variables explicitly.

2. Use explicit storage layout, clearly declare the location of storage variables, and explicitly specify the storage location of variables in the contract. This avoids storage overwrite issues caused by uninitialized storage pointers.

3. Use a secure smart contract library, use a widely used and audited smart contract library like OpenZeppelin. Although the libraries provided by OpenZeppelin are safe, you also need to understand their working principles and potential risks when using them.

4. Avoid storage conflicts and ensure that during the contract development process, storage conflicts that may result from the logical combination of different contracts are avoided.

11.Logical Flaws in Design

Description: Logical flaws occur when the smart contract’s design does not correctly implement the intended logic or leaves room for unexpected behaviors, which can be exploited. This could lead to funds being locked forever, unauthorized actions, or other harmful effects.

Example:

contract LockedFunds {

    mapping(address => uint256) public balances;

    function deposit() public payable {

        balances[msg.sender] += msg.value;

    }

    function withdraw(uint256 _amount) public {

        require(balances[msg.sender] >= _amount, "Insufficient balance");

        // Logic flaw: balance is not updated before sending Ether

        (bool sent, ) = msg.sender.call{value: _amount}("");

        require(sent, "Failed to send Ether");

        balances[msg.sender] -= _amount; // Should be before sending Ether

    }

}

real case scenario: On August 12, 2020, YAM Finance officially announced that it had discovered a smart contract vulnerability, indicating that the vulnerability would generate more YAM tokens than the originally set number. In this case, a large number of reserved tokens would result in an excessive number of tokens required for governance operations. This means that the community will not have enough tokens to perform any governance operations in the future.

Protective measures

1. Code audit, conduct professional security audits of smart contract code regularly, especially after each important update.

2. Unit testing and integration testing. The development phase should include extensive unit testing and integration testing to ensure that all logic runs as expected.

3. Formal verification: Use formal verification tools for key smart contract logic to mathematically prove the correctness of the contract logic.

4. Multi-level security check: Implement multi-level security check in the contract, including permission verification, input verification and secondary confirmation of key operations.