By Saylık Seher
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.
Vulnerability | Severity |
---|---|
External Calls to Arbitrary Addresses | High |
Checks-Effects-Interactions Pattern Violation / Reentrancy | High, Medium |
Missing Validation / Input Validation Violation | High, Medium |
Flashloan Attack | High |
Inconsistent Data | High, Medium |
Floating Pragma | Low |
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…
Developers must implement the necessary security mechanisms to guarantee that external calls are made only to trusted and intended addresses.
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
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.
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.
Transaction: 0xd8c1f7…. Github repo: EthCrossChainManager
Developers should add strong input validation checks to the code to guarantee that user input is structured correctly and is within allowable limitations.
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.
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.
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.
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;
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.
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)
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:
Subscribe to our newsletter
Enter your email address to subscribe to Hacken Reseach and receive notifications of new posts by email.