Gas Optimization In Solidity: Strategies For Cost-Effective Smart Contracts
Gas is the “fuel” that powers smart contract execution. This article offers practical strategies for Solidity gas optimization.
🇺🇦 Hacken stands with Ukraine!
Learn moreSmart 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…
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.
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.
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
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.
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?
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.
How to prevent Inconsistent Data?
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.
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.
# | Item | Type | Description |
1 | Default Visibility | SWC-100SWC-108 | Functions and state variables visibility should be set explicitly. Visibility levels should be specified consciously. |
2 | Integer Overflow and Underflow | SWC-101 | If unchecked math is used, all math operations should be safe from overflows and underflows. |
3 | Outdated Compiler Version | SWC-102 | It is recommended to use a recent version of the Solidity compiler. |
4 | Floating Pragma | SWC-103 | Contracts should be deployed with the same compiler version and flags that they have been tested thoroughly. |
5 | Unchecked Call Return Value | SWC-104 | The return value of a message call should be checked. |
6 | Access Control & Authorization | CWE-284 | Ownership takeover should not be possible. All crucial functions should be protected. Users could not affect data that belongs to other users. |
7 | SELFDESTRUCT Instruction | SWC-106 | The contract should not be self-destructible while it has funds belonging to users. |
8 | Check-Effect- Interaction | SWC-107 | Check-Effect-Interaction pattern should be followed if the code performs ANY external call. |
9 | Assert Violation | SWC-110 | Properly functioning code should never reach a failing assert statement. |
10 | Deprecated Solidity Functions | SWC-111 | Deprecated built-in functions should never be used. |
11 | Delegatecall to Untrusted Callee | SWC-112 | Delegatecalls should only be allowed to trusted addresses. |
12 | DoS (Denial of Service) | SWC-113SWC-128 | Execution of the code should never be blocked by a specific contract state unless it is required. |
13 | Race Conditions | SWC-114 | Race Conditions and Transactions Order Dependency should not be possible. |
14 | Authorization through tx.origin | SWC-115 | tx.origin should not be used for authorization. |
15 | Block values as a proxy for time | SWC-116 | Block numbers should not be used for time calculations. |
16 | Signature Unique Id | SWC-117SWC-121SWC-122EIP-155 | Signed messages should always have a unique id. A transaction hash should not be used as a unique id. Chain identifier should always be used. |
17 | Shadowing State Variable | SWC-119 | State variables should not be shadowed. |
18 | Weak Sources of Randomness | SWC-120 | Random values should never be generated from Chain Attributes or be predictable. |
19 | Incorrect Inheritance Order | SWC-125 | When inheriting multiple contracts, especially if they have identical functions, a developer should carefully specify inheritance in the correct order. |
20 | Calls Only to Trusted Addresses | EEA-Level-2 SWC-126 | All external calls should be performed only to trusted addresses. |
21 | Presence of unused variables | SWC-131 | The code should not contain unused variables if this is not justified by design. |
22 | EIP standards violation | EIP | EIP standards should not be violated. |
23 | Assets integrity | Custom | Funds are protected and cannot be withdrawn without proper permissions or be locked on the contract. |
24 | User Balances manipulation | Custom | Contract owners or any other third party should not be able to access funds belonging to users. |
25 | Data Consistency | Custom | Smart contract data should be consistent all over the data flow. |
26 | Flashloan Attack | Custom | When 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. |
27 | Token Supply manipulation | Custom | Tokens can be minted only according to rules specified in a whitepaper or any other documentation provided by the customer. |
28 | Gas Limit and Loops | Custom | 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. |
29 | Style guide violation | Custom | Style guides and best practices should be followed. |
30 | Requirements Compliance | Custom | The code should be compliant with the requirements provided by the Customer. |
31 | Environment Consistency | Custom | The project should contain a configured development environment with a comprehensive description of how to compile, build and deploy the code. |
32 | Secure Oracles Usage | Custom | The 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. |
33 | Tests Coverage | Custom | The 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. |
34 | Stable Imports | Custom | The code should not reference draft contracts, that may be changed in the future. |
# | Item | Description |
1 | Default Visibility | Functions and state variables visibility should be set explicitly. Visibility levels should be specified consciously. |
2 | Integer Overflow and Underflow | If unchecked math is used, all math operations should be safe from overflows and underflows. |
3 | Outdated Compiler Version | It is recommended to use a recent version of the Rust compiler. |
5 | Unchecked Call Return Value | The return value of a message call should be checked. |
6 | Access Control & Authorization | Ownership takeover should not be possible. All crucial functions should be protected. Users could not affect data that belongs to other users. |
9 | Assert Violation | Properly functioning code should never reach a failing assert statement. |
10 | Deprecated Rust Functions | Deprecated built-in functions should never be used. |
12 | DoS (Denial of Service) | Execution of the code should never be blocked by a specific contract state unless it is required. |
15 | Block values as a proxy for time | Block numbers should not be used for time calculations. |
16 | Signature Unique Id | Signed messages should always have a unique id. A transaction hash should not be used as a unique id. Chain identifier should always be used. |
17 | Shadowing State Variable | State variables should not be shadowed. |
18 | Weak Sources of Randomness | Random values should never be generated from Chain Attributes or be predictable. |
20 | Calls Only to Trusted Addresses | All external calls should be performed only to trusted addresses. |
21 | Presence of unused variables | The code should not contain unused variables if this is not justified by design. |
23 | Assets integrity | Funds are protected and cannot be withdrawn without proper permissions or be locked on the contract. |
24 | User Balances manipulation | Contract owners or any other third party should not be able to access funds belonging to users. |
25 | Data Consistency | Smart contract data should be consistent all over the data flow. |
26 | Flashloan Attack | When 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. |
27 | Token Supply manipulation | Tokens can be minted only according to rules specified in a whitepaper or any other documentation provided by the customer. |
28 | Gas 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. |
29 | Style guide violation | Style guides and best practices should be followed. |
30 | Requirements Compliance | The code should be compliant with the requirements provided by the Customer. |
31 | Environment Consistency | The project should contain a configured development environment with a comprehensive description of how to compile, build and deploy the code. |
32 | Secure Oracles Usage | The 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. |
33 | Tests Coverage | The 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. |
34 | Stable Imports | The code should not reference draft contracts, that may be changed in the future. |
35 | Unsafe Rust code | The 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 | ||
1 | As-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. |
2 | Near contract standards violation | FT or NFT smart contracts meet to Near standards |
3 | Missing Initializer Attribute | Init function marked with init attribute does not exist, or init attribute is missing on initializer function |
4 | Missing “private” macro in cross-contract callback | Usually, 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. |
5 | Missing “paybable” macro on payable functions | We 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. |
6 | Collection type is suitable for structure type | Check if a suitable collection is used for declared structure and contract logic. |
7 | Near-Sdk is up to date | Check is near-sdk is up to date |
Solana | ||
1 | Missing rent exemption checks | All Solana accounts holding an Account, Mint, or Multisig must contain enough SOL to be considered rent exempt. Otherwise the accounts may fail to load. |
2 | Signed invocation of unverified programs | The program does not verify the pubkey of any program called via the invoke_signed() API. |
3 | Solana account confusions | The program fails to ensure that the account data has the type it expects to have. |
4 | Redeployment with cross-instance confusion | The program fails to ensure that the wasm code has the code it expects to have |
5 | Missing freeze authority checks | When freezing is enabled, but the program does not verify that the freezing account call has been signed by the appropriate freeze_authority |
6 | Insufficient SPL-Token account verification | Finding 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. |
8 | As-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. |
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: