• Hacken
  • Blog
  • Discover
  • Smart Contract Vulnerabilities & How to Prevent Them

Smart Contract Vulnerabilities & How to Prevent Them

By Saylık Seher

Share via:

Smart contracts are self-executing programs that can be programmed to execute on their own when specific conditions are met. In the area of blockchain and decentralized finance, their popularity is rising. Smart contracts, however, are prone to flaws like all other software. This post covers the most typical smart contract vulnerabilities and their examples, along with solutions for avoiding them.

VulnerabilitySeverity
External Calls to Arbitrary AddressesHigh
Checks-Effects-Interactions Pattern Violation / ReentrancyHigh, Medium
Missing Validation / Input Validation ViolationHigh, Medium
Flashloan AttackHigh
Inconsistent DataHigh, Medium
Floating PragmaLow
Smart contract vulnerabilities and their severity level

Most Common Smart Contract Vulnerabilities And How To Prevent Them

1. External Calls to Arbitrary Addresses (High)

Malicious contracts may withdraw the contract’s balance in response to external calls to arbitrary addresses, resulting in a loss of funds. Due to this flaw, attackers can use the contract’s capabilities to their advantage and run malicious or unauthorized code that can extract assets from the contract or can break the working mechanism of the contract.

Example of the Dexible exploit (20 February 2023). Dexible is a decentralized exchange (DEX) aggregator and execution management system. One of their essential features was self swapping. The selfSwap function allows users to define a router contract which is the main problem that caused this hack.

Attention! This function can be called by anyone and the contract makes external calls to an arbitrary router address.

contract Dexible {
    function selfSwap(address tokenIn, address tokenOut, uint112 uint112 amount, address router, address spender, TokenAmount routeAmount, bytes routerData) external {
        IERC20(routeAmount.token).safeApprove(spender, routeAmount.amount);
        // Here an external call is made to the router
        (bool s, ) = router.call(routerData);
    }
}

Instead of a safe&valid DEX, a hacker made a contract to call a malicious ERC20 contract and drained $2M worth of tokens. The crucial part is the contracts were not audited.

Sample malicious contract

contract maliciousRouter {
    ...
    //Instead of a validated router contract, the hacker implements this tricky function and makes Dexible contract to transfer its assets to this malicious contract.
    function transfer() external {
        IERC20(USDC).transferFrom(msg.sender, address(this), IERC20(USDC).balanceOf(msg.sender));
    }
}

These code lines are simplified to demonstrate the hack easily. More info and the contract repo can be found here: Dexible. Transaction: 0x138daa4c…

How to prevent External Calls to Arbitrary Addresses? Developers must implement the necessary security mechanisms to guarantee that external calls are made only to trusted and intended addresses.

Protect your Web3 projects from potential threats and vulnerabilities with Hacken’s comprehensive smart contract auditing services.

2. Checks-Effects-Interactions Pattern Violation / Reentrancy (High)

Reentrancy attacks occur when an attacker exploits a vulnerability in a smart contract that allows them to repeatedly call a function before the previous function call has finished executing. This can occur when a smart contract has a function that calls another contract’s function, but fails to properly update the user’s balance or state before executing the function call.

Example of the Rari Capital hack (30 April 2022). Rari protocol is a decentralized platform that allows lending and borrowing. Protocol’s code was forked from Compound and their developers accidentally used one of their old commits which led them to get hacked.  

Their borrow function was lacking of proper checks-effects-interactions pattern. Seeing this, the exploiter (1) promptly got 150,000,000 USDC as a flashloan, (2) deposited it into fUSDC-127 contract, and (3) called the vulnerable borrow function to borrow some amount of assets.

As we can see, the function firstly transfers the borrowed amount and then updates the accountBorrows mapping. Since the function does not have any reentrancy guard, the hacker called the borrow function repetitively before it updated the mapping and drained the funds worth of $80 million.

function borrow() external {
    …
    doTransferOut(borrower, borrowAmount);
    // doTransferOut: function doTransferOut(borrower, amount) { 
    (bool success, ) = to.call.value(amount)("");
    require(success, "doTransferOut failed");
}
// !!State updates are made after the transfer
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;…
}

The hacker borrowed x amount of assets using a flashloan and ran the doTransferOut function five times in a loop. After paying back the flashloan, he/she took the remaining 4x amount and disappeared with it. Transaction: ​​0xab486012 

How to prevent Checks-Effects-Interactions Pattern Violation / Reentrancy? To prevent reentrancy attacks, there are several things to do according to the implementation. Developers must follow the “checks-effects-interactions” pattern, which involves executing all check statements and state updates before making any external calls. Additionally, it’s necessary to implement a guard to remove the ability to make multiple calls to the same function (see OpenZeppelin’s ReentrancyGuard). Alternatively, smart contract developers can restrict the gas amount available for the function to avoid infinite loops. However, the latter requires some attention since transaction gas limits of a network can be changed by the protocol.

3. Missing Validation/Input Validation Violation (High, Medium)

Example of the Poly Network Exploit (10 August 2021). The Poly Network is a decentralized finance (DeFi) platform enabling interoperability between different blockchains. On August 10, 2021, the platform suffered a massive hack, resulting in a loss of over $600 million worth of cryptocurrency. The attack exploited a vulnerability in the platform’s code, which allowed the attacker to steal assets from different blockchains supported by the platform.

The Poly network has a contract called EthCrossChainManager, which can trigger messages from another chain. Anybody may use the contract to execute a cross-chain transaction by calling the function verifyHeaderAndExecuteTx, which validates the block header and transaction inclusion. The contract’s weakness is the failure to stop users from accessing the EthCrossChainData contract, which manages the list of public keys used to authenticate data from the opposite chain. Users can cause a function to change the public keys by making changes to this list without needing to hack private keys. By delivering a cross-chain message directly to the EthCrossChainData contract, the attacker can force EthCrossChainManager to call the latter contract.

Exploiter used the _method parameter as an open gate to make the EthCrossChainManager contract call the right function since one of the parameters of hashed data can be specified by callers.

This method-ID is generated by keccak-hashing method’s name, including the signature, and then taking the first four bytes of the resulting hash:

   /* @notice Dynamically invoke the targeting contract, and trigger execution of cross chain tx on Ethereum side
    *  @param _toContract The targeting contract that will be invoked by the Ethereum Cross Chain Manager contract
    *  @param _method At which method will be invoked within the targeting contract
    *  @param _args The parameter that will be passed into the targeting contract
    *  @param _fromContractAddr From chain smart contract address
    *  @param _fromChainId Indicate from which chain current cross chain tx comes 
    *  @return true or false
    */
   function _executeCrossChainTx(address _toContract, bytes memory _method, bytes memory _args, bytes memory _fromContractAddr, uint64 _fromChainId) internal returns(bool) {
       ...(success, returnData) = _toContract.call(
abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_args, _fromContractAddr, _fromChainId)));...
   }
bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)")));

Since it converts the hashed data to a bytes4 variable, only 4 bytes will be validated as a signature. As we can guess, it will not be that hard to find the same output with malicious input data by brute-forcing.

  • 0xv14cts34rf44d…
  • 0xv14c2156789a…

Transaction: 0xd8c1f7…. Github repo: EthCrossChainManager 

How to prevent Missing Validation/Input Validation Violation? Developers should add strong input validation checks to the code to guarantee that user input is structured correctly and is within allowable limitations.

4. Flashloan Attack (High)

Flashloans lets users borrow massive amounts of crypto for just one quick transaction. Due to their adaptability and accessibility they don’t require collateral and may be used for a multitude of purposes. However, because they don’t require any collateral, flashloans can be used to manipulate the market or take advantage of smart contract weaknesses

Example of the Beanstalk DeFi exploit (17 April 2022). Beanstalk is a stablecoin decentralized protocol that rewards users for contributing funds to a central funding pool called “the silo.” The protocol keeps the “bean” token at around $1. The other thing, Beanstalk features a governance structure that allows token holders to vote to change the code, with voting power proportional to the number of tokens owned. This governance setup ultimately led to a vulnerability in the system that resulted in the project’s downfall.

The exploiter created two proposals. One was a request to send all the funds to their address. The other was donating 250000$ in BEAN tokens to Ukraine. And here comes the tricky part: the emergencyCommit function allows users with super majority of funds to approve a proposals.

function emergencyCommit(uint32 bip) external {
  require(isNominated(bip), "Governance: Not nominated.");
  // Requires 1 day to be passed (getGovernanceEmergencyPeriod=1day)
  require(
  block.timestamp >= timestamp(bip).add(C.getGovernanceEmergencyPeriod()),     "Governance: Too early.");
  require(isActive(bip), "Governance: Ended.");

  //Any vote can be executed if proposer has the super majority(getGovernanceEmergencyThreshold=67%)
  require(
  bipVotePercent (bip). greaterThanOrEqualTo(C.getGovernanceEmergencyThreshold()),   "Governance: Must have super majority." );
  _execute(msg.sender, bip, false, true);
}

The protocol forgot to get precautions for flashloans enabling the hack just one day after the malicious proposal. The exploiter got a massive flashloan: 350M DAI, 500M USDC, 150M USDT, 32M Bean and 11.6M LUSD. They converted them to BeanStalk DeFi’s governance BEAN3Crv-f and BEANLUSD-f tokens. Reaching the 78% funds majority, they called emergencyCommit to execute both proposals they had created. See the transaction 0xcd314668…, and the BeanStalk contract.

How to mitigate Flashloan Attack?

  1. Require collateral for loans to make it more expensive for hackers to borrow assets.
  2. Implement limits on the size of flash loans that can be taken out, which can help mitigate large-scale attacks.
  3. Add time locks to smart contracts to hinder the ability to repay flashloans too quickly, giving developers time to detect and prevent attacks.

Moreover, projects with governance voting systems must implement anti-flashloan mechanism to block exploiters from using flashloans to gain the majority of the voting power.

5. Inconsistent Data (High, Medium)

When smart contract functions use data without correct validation or verification, several issues may arise. Inconsistent data might result in improper contract execution, which may result in the loss of funds or other unfavorable effects. Inconsistent data can also damage the contract’s reputation and trustworthiness, which will lower adoption and cause a decline in user confidence.

How to prevent Inconsistent Data?

  • Validate and verify data: Data can be validated and verified using methods like hash functions, digital signatures, and encryption.
  • Configure strict access controls: Smart contracts should have strict access restrictions to stop unwanted data alteration.
  • Develop smart contracts thoroughly: Developers should thorough test their contracts, including functional and security testing, to find and correct any data discrepancies or inaccuracies.

6. Floating Pragma (Low)

Solidity pragmas are an important aspect of secure smart contract development, as they define the version of the Solidity compiler to be used for compiling the contract, and can have an impact on the security and functionality of the contract.

Using outdated or vulnerable Solidity versions can potentially lead to security vulnerabilities, as well as unexpected behavior in the contract’s execution. Additionally, certain pragma settings can impact the contract’s gas usage, which can have implications for the contract’s cost and efficiency.

Floating pragma
pragma solidity ^0.8.0;

Secure, locked pragma
pragma solidity 0.8.17;

How to prevent Floating Pragma? It’s important for smart contract developers to carefully choose and lock the Solidity pragma for their contracts, and to keep them up to date with the latest security patches and best practices.

Checked Items: List Of Vulnerabilities Hacken Looks For During Smart Contract Audit

While we’ve discussed six key vulnerabilities, it’s essential to know that there are numerous other potential pitfalls that may compromise the security of a smart contract. This is where the expertise of professional smart contract auditing companies becomes invaluable. Hacken’s team meticulously investigates all potential weak spots in your smart contracts, leaving no stone unturned.

In our smart contract auditing process, we examine a comprehensive list of common vulnerabilities, with the specific items on this list varying based on the language and platform in use.

Checklist For EVM-Based Smart Contracts (Solidity, Vyper, Yul)

#ItemTypeDescription
1Default VisibilitySWC-100SWC-108Functions and state variables visibility should be set explicitly. Visibility levels should be specified consciously.
2Integer Overflow and UnderflowSWC-101If unchecked math is used, all math operations should be safe from overflows and underflows.
3Outdated Compiler VersionSWC-102It is recommended to use a recent version of the Solidity compiler.
4Floating PragmaSWC-103Contracts should be deployed with the same compiler version and flags that they have been tested thoroughly.
5Unchecked Call Return ValueSWC-104The return value of a message call should be checked.
6Access Control & AuthorizationCWE-284Ownership takeover should not be possible. All crucial functions should be protected. Users could not affect data that belongs to other users.
7SELFDESTRUCT InstructionSWC-106The contract should not be self-destructible while it has funds belonging to users.
8Check-Effect- InteractionSWC-107Check-Effect-Interaction pattern should be followed if the code performs ANY external call.
9Assert ViolationSWC-110Properly functioning code should never reach a failing assert statement.
10Deprecated Solidity FunctionsSWC-111Deprecated built-in functions should never be used.
11Delegatecall to Untrusted CalleeSWC-112Delegatecalls should only be allowed to trusted addresses.
12DoS (Denial of Service)SWC-113SWC-128Execution of the code should never be blocked by a specific contract state unless it is required.
13Race ConditionsSWC-114Race Conditions and Transactions Order Dependency should not be possible.
14Authorization through tx.originSWC-115tx.origin should not be used for authorization.
15Block values as a proxy for timeSWC-116Block numbers should not be used for time calculations.
16Signature Unique IdSWC-117SWC-121SWC-122EIP-155Signed messages should always have a unique id. A transaction hash should not be used as a unique id. Chain identifier should always be used.
17Shadowing State VariableSWC-119State variables should not be shadowed.
18Weak Sources of RandomnessSWC-120Random values should never be generated from Chain Attributes or be predictable.
19Incorrect Inheritance OrderSWC-125When inheriting multiple contracts, especially if they have identical functions, a developer should carefully specify inheritance in the correct order.
20Calls Only to Trusted AddressesEEA-Level-2 SWC-126All external calls should be performed only to trusted addresses.
21Presence of unused variablesSWC-131The code should not contain unused variables if this is not justified by design.
22EIP standards violationEIPEIP standards should not be violated.
23Assets integrityCustomFunds are protected and cannot be withdrawn without proper permissions or be locked on the contract.
24User Balances manipulationCustomContract owners or any other third party should not be able to access funds belonging to users.
25Data ConsistencyCustomSmart contract data should be consistent all over the data flow.
26Flashloan AttackCustomWhen working with exchange rates, they should be received from a trusted source and not be vulnerable to short-term rate changes that can be achieved by using flash loans. Oracles should be used.
27Token Supply manipulationCustomTokens can be minted only according to rules specified in a whitepaper or any other documentation provided by the customer.
28Gas Limit and LoopsCustomTransaction execution costs should not depend dramatically on the amount of data stored on the contract. There should not be any cases when execution fails due to the block gas limit.
29Style guide violationCustomStyle guides and best practices should be followed.
30Requirements ComplianceCustomThe code should be compliant with the requirements provided by the Customer.
31Environment ConsistencyCustomThe project should contain a configured development environment with a comprehensive description of how to compile, build and deploy the code.
32Secure Oracles UsageCustomThe code should have the ability to pause specific data feeds that it relies on. This should be done to protect a contract from compromised oracles. 
33Tests CoverageCustomThe code should be covered with unit tests. Test coverage should be 100%, with both negative and positive cases covered. Usage of contracts by multiple users should be tested.
34Stable ImportsCustomThe code should not reference draft contracts, that may be changed in the future.

Checklist For Rust-Based Smart Contracts (Solala, Near, CosmWasm, and other platforms)

#ItemDescription
1Default VisibilityFunctions and state variables visibility should be set explicitly. Visibility levels should be specified consciously.
2Integer Overflow and UnderflowIf unchecked math is used, all math operations should be safe from overflows and underflows.
3Outdated Compiler VersionIt is recommended to use a recent version of the Rust compiler.
5Unchecked Call Return ValueThe return value of a message call should be checked.
6Access Control & AuthorizationOwnership takeover should not be possible. All crucial functions should be protected. Users could not affect data that belongs to other users.
9Assert ViolationProperly functioning code should never reach a failing assert statement.
10Deprecated Rust FunctionsDeprecated built-in functions should never be used.
12DoS (Denial of Service)
Execution of the code should never be blocked by a specific contract state unless it is required.
15Block values as a proxy for timeBlock numbers should not be used for time calculations.
16Signature Unique IdSigned messages should always have a unique id. A transaction hash should not be used as a unique id. Chain identifier should always be used.
17Shadowing State VariableState variables should not be shadowed.
18Weak Sources of RandomnessRandom values should never be generated from Chain Attributes or be predictable.
20Calls Only to Trusted AddressesAll external calls should be performed only to trusted addresses.
21Presence of unused variablesThe code should not contain unused variables if this is not justified by design.
23Assets integrityFunds are protected and cannot be withdrawn without proper permissions or be locked on the contract.
24User Balances manipulationContract owners or any other third party should not be able to access funds belonging to users.
25Data ConsistencySmart contract data should be consistent all over the data flow.
26Flashloan AttackWhen working with exchange rates, they should be received from a trusted source and not be vulnerable to short-term rate changes that can be achieved by using flash loans. Oracles should be used.
27Token Supply manipulationTokens can be minted only according to rules specified in a whitepaper or any other documentation provided by the customer.
28Gas Limit and
Loops
Transaction execution costs should not depend dramatically on the amount of data stored on the contract. There should not be any cases when execution fails due to the block gas limit.
29Style guide violationStyle guides and best practices should be followed.
30Requirements ComplianceThe code should be compliant with the requirements provided by the Customer.
31Environment ConsistencyThe project should contain a configured development environment with a comprehensive description of how to compile, build and deploy the code.
32Secure Oracles UsageThe code should have the ability to pause specific data feeds that it relies on. This should be done to protect a contract from compromised oracles. 
33Tests CoverageThe code should be covered with unit tests. Test coverage should be 100%, with both negative and positive cases covered. Usage of contracts by multiple users should be tested.
34Stable ImportsThe code should not reference draft contracts, that may be changed in the future.
35Unsafe Rust codeThe Rust type system does not check memory safety of unsafe Rust code. Thus, if a smart contract contains any unsafe Rust code, it may still suffer from memory corruptions such as buffer overflows, use after frees, uninitialized memory, etc.
Near
1As-of-yet Near blockchain unknown classes of vulnerabilities Checking for any other, as-of-yet unknown classes of vulnerabilities arising from the structure of the Near blockchain.
2Near contract standards violationFT or NFT smart contracts meet to Near standards 
3Missing Initializer AttributeInit function marked with init attribute does not exist, or init attribute is missing on initializer function
4Missing “private” macro in cross-contract callbackUsually, when a contract has to have a callback for a remote cross-contract call, this callback method should only be called by the contract itself. It’s to avoid someone else calling it and messing the state. 
5Missing “paybable” macro on payable functionsWe can allow methods to accept token transfer together with the function call. This is done so that contracts can define a fee in tokens that needs to be payed when they are used. By the default the methods are not payable and they will panic if someone will attempt to transfer tokens to them during the invocation. 
6Collection type is suitable for structure typeCheck if a suitable collection is used for declared structure and contract logic.
7Near-Sdk is up to dateCheck is near-sdk is up to date
Solana
1Missing rent exemption checksAll Solana accounts holding an Account, Mint, or Multisig must contain enough SOL to be considered rent exempt. Otherwise the accounts may fail to load.
2Signed invocation of unverified programsThe program does not verify the pubkey of any program called via the invoke_signed() API.
3Solana account confusionsThe program fails to ensure that the account data has the type it expects to have.
4Redeployment with cross-instance confusionThe program fails to ensure that the wasm code has the code it expects to have
5Missing freeze authority checksWhen freezing is enabled, but the program does not verify that the freezing account call has been signed by the appropriate freeze_authority
6Insufficient SPL-Token account verificationFinding extra checks that should not exist with the given type of accounts
7

Anti-pattern to transfer the ownership of an Associated Token Account
Note that it is an anti-pattern to transfer the ownership of an Associated Token Account:  
In that case, the best practice is to create an associated token account for the recipient’s wallet, transfer the tokens, and then close the first account.
8As-of-yet Solana blockchain unknown classes of vulnerabilities Checking for any other, as-of-yet unknown classes of vulnerabilities arising from the structure of the Solana blockchain.

Wrapping Up

Given the extensive array of potential vulnerabilities we’ve outlined – from the specific examples we’ve spotlighted to the over 30 additional issues identified across different platforms in our smart contract audit checklist – it’s evident that the landscape of threats in the blockchain domain is complex and multifaceted. The pivotal role of smart contract developers in this environment cannot be overstated. They bear the dual responsibility of developing cutting-edge blockchain applications, as well as establishing comprehensive security measures to protect these contracts from an ever-growing range of threats and vulnerabilities. Their task, challenging as it may be, is to remain ahead of these evolving risks, ensuring the security, integrity, and trustworthiness of their applications in a dynamic threat environment.

By adhering to these six pillars, developers can build a robust defense system that fortifies their smart contracts’ security:

  1. Best practices: Stick to the standards and best practices while creating and implementing smart contracts.
  2. Testing: Write unit tests before deploying smart contracts to ensure the system runs as intended.
  3. Auditing: Have smart contracts examined by qualified auditors to find any potential security flaws and recommend best practices to strengthen security.
  4. Code reviews: Do code reviews to detect problems, and in particular to respond to them quickly, before the implementation progresses too far
  5. Monitoring: Set up monitoring and alert systems to detect and respond to any unusual activity or suspicious behavior.
  6. Access controls: Put permissions and access controls to specify who can access funds and make changes on the smart contract data to mitigate asset loss due to data manipulation or outside intervention.
subscribe image
promotion image
IMPORTANT

Subscribe to our newsletter

Enter your email address to subscribe to Hacken Reseach and receive notifications of new posts by email.

Read next:

More related
  • Blog image
    DISCOVER
    Telegram Crypto Trading Bots: Convenience vs. Security Risks Hacken
  • Blog image
  • Blog image
    DISCOVER
    Fintoch Rug Pull Explained Hacker H.
  • Blog image
  • Blog image
  • Blog image
  • Blog image
  • Blog image
  • Blog image
  • Blog image
    DISCOVER
    51% Attack: The Concept, Risks & Prevention HackenBarwikowski B.

Get our latest updates and expert insights on Web3 security