A smart contract is an automated transaction protocol that executes the terms of a contract. They are one of the most exciting areas of blockchain technology implementation.
The audit of a Smart Contract is technically the same as auditing a regular code. It entails meticulously investigating code to find security flaws and vulnerabilities before publicly deploying the code. In addition, it involves developers scrutinizing the code that is used to underwrite the terms of the smart contract.
An effective Smart Contract audit should focus on the following areas;
- Common errors such as stack problems, compilation, and reentrance mistakes.
- Any known errors or security flaws of the Smart contract’s host platform.
- Simulating attacks on the Smart Contract.
Bad randomness
Pseudorandom number generation on the blockchain is generally unsafe. There are a number of reasons for this, including:
- The blockchain does not provide any cryptographically secure source of randomness. Block hashes in isolation are cryptographically random, however, a malicious miner can modify block headers, introduce additional transactions, and choose not to publish blocks in order to influence the resulting hashes. Therefore, miner-influenced values like block hashes and timestamps should never be used as a source of randomness.
Attack Scenarios
- A lottery where people bet on whether the hash of the current block is even or odd. A miner that bets on even can throw out blocks whose hash are even.
- A commit-reveal scheme where users don’t necessarily have to reveal their secret (to prevent DoS). A user has money riding on the outcome of the PRG and submits a large number of commits, allowing them to choose the one they want to reveal at the end.
Mitigations
There are currently not any recommended mitigations for this issue. Do not build applications that require on-chain randomness. In the future, however, these approaches show promise
- Verifiable delay functions: functions which produce a pseudorandom number and take a fixed amount of sequential time to evaluate
- Randao: A commit reveal scheme where users must stake wei to participate
Vulnerable Code
The random function in theRun was vulnerable to this attack. It used the blockhash, timestamp and block number to generate numbers in a range to determine winners of the lottery. To exploit this, an attacker could set up a smart contract that generates numbers in the same way and submits entries when it would win. As well, the miner of the block has some control over the blockhash and timestamp and would also be able to influence the lottery in their favor.
function random(uint Max) constant private returns (uint256 result){ //get the best seed for randomness uint256 x = salt * 100 / Max; uint256 y = salt * block.number / (salt % 5) ; uint256 seed = block.number/3 + (salt % 300) + Last_Payout +y; uint256 h = uint256(block.blockhash(seed)); return uint256((h / x)) % Max + 1; //random number between 1 and Max } |
Denial of Service
A malicious contract can permanently stall another contract by failing in a strategic way. In particular, contracts that bulk perform transactions or updates using a for loop can be DoS’d if a call to another contract or transfer fails during the loop.
Attack Scenarios
- Auction contract where frontrunner must be reimbursed when they are outbid. If the call refunding the frontrunner continuously fails, the auction is stalled and they become the de facto winner.
- Contract iterates through an array to pay back its users. If one transfer fails in the middle of a for loop all reimbursements fail.
- Attacker spams contract, causing some array to become large. Then for loops iterating through the array might run out of gas and revert.
Mitigations
- Favor pull over push for external calls
- If iterating over a dynamically sized data structure, be able to handle the case where the function takes multiple blocks to execute. One strategy for this is storing iterator in a private variable and using while loop that exists when gas drops below certain threshold.
Vulnerable Code
Bulk refund functionality that is suceptible to DoS
address[] private refundAddresses; mapping(address => uint) public refundAmount; function refundDos() public { for(uint i; i < refundAddresses.length; i++) { require(refundAddresses[i].transfer(refundAmount[refundAddresses[i]])); } }} contract CrowdFundPull { address[] private refundAddresses; mapping(address => uint) public refundAmount; function withdraw() external { uint refund = refundAmount[msg.sender]; refundAmount[msg.sender] = 0; msg.sender.transfer(refund); }} |
Contracts can be forced to receive ether
In certain circunstances, contracts can be forced to receive ether without triggering any code. This should be considered by the contract developers in order to avoid breaking important invariants in their code.
Attack Scenarios
- An attacker can use a specially crafted contract to forceful send ether using suicide / selfdestruct:
contract Sender { function receive_and_suicide(address target) payable { suicide(target); }} |
Mitigations
- There is no way to block the reception of ether. The only mitigation is to avoid assuming how the balance of the contract increases and implement checks to handle this type of edge cases.
Vulnerable Code
The MyAdvancedToken contract is vulnerable to this attack. It will stop the owner to perform the migration of the contract.
mapping (address => bool) public frozenAccount; /* This generates a public event on the blockchain that will notify clients */ event FrozenFunds(address target, bool frozen); /* Initializes contract with initial supply tokens to the creator of the contract */ function MyAdvancedToken( string tokenName, string tokenSymbol ) TokenERC20(tokenName, tokenSymbol) public {} /* Internal transfer, only can be called by this contract */ function _transfer(address _from, address _to, uint _value) internal { require (_to != 0x0); // Prevent transfer to 0x0 address. require (balanceOf[_from] >= _value); // Check if the sender has enough require (balanceOf[_to] + _value >= balanceOf[_to]); // Check for overflows require(!frozenAccount[_from]); // Check if sender is frozen require(!frozenAccount[_to]); // Check if recipient is frozen balanceOf[_from] -= _value; // Subtract from the sender balanceOf[_to] += _value; // Add the same to the recipient emit Transfer(_from, _to, _value); } /// @notice Buy tokens from contract by sending ether function buy() payable public { uint amount = msg.value; // calculates the amount balanceOf[msg.sender] += amount; // updates the balance totalSupply += amount; // updates the total supply _transfer(address(0x0), msg.sender, amount); // makes the transfer } /* Migration function */ function migrate_and_destroy() onlyOwner { assert(this.balance == totalSupply); // consistency check suicide(owner); // transfer the ether to the owner and kill the contract } |
Incorrect interface
A contract interface defines functions with a different type signature than the implementation, causing two different method id’s to be created. As a result, when the interface is called, the fallback method will be executed.
Attack Scenarios
- The interface is incorrectly defined. Alice.set(uint) takes an uint in Bob.sol but Alice.set(int) a int in Alice.sol. The two interfaces will produce two differents method IDs. As a result, Bob will call the fallback function of Alice rather than of set.
Mitigations
- Verify that type signatures are identical between interfaces and implementations.
Vulnerable Code
// Wait to mine the block containing the transaction var alice = contractAlice.at(contractPartialInstanceAlice.address); var contractBob = eth.contract(abiBob); var txDeployBob = {from:eth.coinbase, data: bytecodeBob, gas: 1000000}; var contractPartialInstanceBob = contractBob.new(txDeployBob); // Wait to mine the block containing the transaction var bob = contractBob.at(contractPartialInstanceBob.address); // From now, wait for each transaction to be mined before calling// the others transactions // print the default value of val: 0alice.val() // call bob.set, as the interface is wrong, it will call// the fallback function of alicebob.set(alice.address, {from: eth.accounts[0]} )// print val: 1alice.val() // call the fixed version of the interfacebob.set_fixed(alice.address, {from: eth.accounts[0]} )// print val: 42alice.val() |
Integer Overflow
It is possible to cause add and sub to overflow (or underflow) on any type of integer in Solidity.
Attack Scenarios
- Attacker has 5 of some ERC20 token. They spend 6, but because the token doesn’t check for underflows, they wind up with 2^256 tokens.
- A contract contains a dynamic array and an unsafe pop method. An attacker can underflow the length of the array and alter other variables in the contract.
Mitigations
- Use openZeppelin’s safeMath library
- Validate all arithmetic
Vulnerable Code
// SPDX-License-Identifier: GPL-3.0pragma solidity 0.8.0; contract RolloverExample2 { uint8 public myUint8; function decrement() public { myUint8–; } function increment() public { myUint8++; }} |
Race Condition
There is a gap between the creation of a transaction and the moment it is accepted in the blockchain. Therefore, an attacker can take advantage of this gap to put a contract in a state that advantages them.
Attack Scenarios
- Bob creates RaceCondition(100, token). Alice trusts RaceCondition with all its tokens. Alice calls buy(150) Bob sees the transaction, and calls changePrice(300). The transaction of Bob is mined before the one of Alice and as a result, Bob received 300 tokens.
- The ERC20 standard’s approve and transferFrom functions are vulnerable to a race condition. Suppose Alice has approved Bob to spend 100 tokens on her behalf. She then decides to only approve him for 50 tokens and sends a second approve transaction. However, Bob sees that he’s about to be downgraded and quickly submits a transferFrom for the original 100 tokens he was approved for. If this transaction gets mined before Alice’s second approve, Bob will be able to spend 150 of Alice’s tokens.
Mitigations
- For the ERC20 bug, insist that Alice only be able to approve Bob when he is approved for 0 tokens.
- Keep in mind that all transactions may be front-run
Vulnerable Code
// If the owner sees someone calls buy // he can call changePrice to set a new price // If his transaction is mined first, he can // receive more tokens than excepted by the new buyer function buy(uint new_price) payable public { require(msg.value >= price); // we assume that the RaceCondition contract // has enough allowance token.transferFrom(msg.sender, owner, price); price = new_price; owner = msg.sender; } function changePrice(uint new_price){ require(msg.sender == owner); price = new_price; } |
Re-entrancy
A state variable is changed after a contract uses call.value. The attacker uses a fallback function—which is automatically executed after Ether is transferred from the targeted contract—to execute the vulnerable function again, before the state variable is changed.
Attack Scenarios
- A contract that holds a map of account balances allows users to call a withdraw function. However, withdraw calls send which transfers control to the calling contract, but doesn’t decrease their balance until after send has finished executing. The attacker can then repeatedly withdraw money that they do not have.
Mitigations
- Avoid use of call.value
- Update all bookkeeping state variables before transferring execution to an external contract.
Vulnerable Code
function withdrawRewardFor(address _account) noEther internal returns (bool _success) { if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account]) throw; uint reward = (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply – paidOut[_account]; reward = rewardAccount.balance < reward ? rewardAccount.balance : reward; paidOut[_account] += reward; // XXXXX if (!rewardAccount.payOut(_account, reward)) // XXXXX throw; return true;} |
Unchecked External Call
Certain Solidity operations known as “external calls”, require the developer to manually ensure that the operation succeeded. This is in contrast to operations which throw an exception on failure. If an external call fails, but is not checked, the contract will continue execution as if the call succeeded. This will likely result in buggy and potentially exploitable behavior from the contract.
Attack Scenarios
- A contract uses an unchecked address.send() external call to transfer Ether.
- If it transfers Ether to an attacker contract, the attacker contract can reliably cause the external call to fail, for example, with a fallback function which intentionally runs out of gas.
- The consequences of this external call failing will be contract specific.
- In the case of the King of the Ether contract, this resulted in accidental loss of Ether for some contract users, due to refunds not being sent.
Mitigations
- Manually perform validation when making external calls
- Use address.transfer()
Vulnerable Code
// If they paid too little, reject claim and refund their money. if (valuePaid < currentClaimPrice) { msg.sender.send(valuePaid); return; } // If they paid too much, continue with claim but refund the excess. if (valuePaid > currentClaimPrice) { uint excessPaid = valuePaid – currentClaimPrice; msg.sender.send(excessPaid); valuePaid = valuePaid – excessPaid; } |
Unprotected function
Missing (or incorrectly used) modifier on a function allows an attacker to use sensitive functionality in the contract.
Attack Scenarios
- A contract with a changeOwner function does not label it as private and therefore allows anyone to become the contract owner.
Mitigations
- Always specify a modifier for functions.
Vulnerable Code
An onlyOwner modifier is defined but not used, allowing anyone to become the owner
pragma solidity ^0.4.15; contract Unprotected{ address private owner; modifier onlyowner { require(msg.sender==owner); _; } function Unprotected() public { owner = msg.sender; } // This function should be protected function changeOwner(address _newOwner) public { owner = _newOwner; } function changeOwner_fixed(address _newOwner) public onlyowner { owner = _newOwner; }} |
Variable Shadowing
Variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope.
Attack Scenarios
- This depends a lot on the code of the contract itself.
Mitigations
- The solidity compiler has some checks to emit warnings when it detects this kind of issue.
Vulnerable Code
variable shadowing prevents the owner of contract C from performing self destruct
contract Suicidal { address owner; function suicide() public returns (address) { require(owner == msg.sender); selfdestruct(owner); }}contract C is Suicidal { address owner; function C() { owner = msg.sender; }} function changeOwner_fixed(address _newOwner) public onlyowner { owner = _newOwner; }} |
Wrong Constructor Name
A function intended to be a constructor is named incorrectly, which causes it to end up in the runtime bytecode instead of being a constructor.
Attack Scenarios
- Anyone can call the function that was supposed to be the constructor. As a result anyone can change the state variables initialized in this function.
Mitigations
- Use constructor instead of a named constructor
Vulnerable Code
pragma solidity ^0.4.15; contract Missing{ address private owner; modifier onlyowner { require(msg.sender==owner); _; } // The name of the constructor should be Missing // Anyone can call the IamMissing once the contract is deployed function IamMissing() public { owner = msg.sender; } function withdraw() public onlyowner { owner.transfer(this.balance); }} |
Top 8 Smart Contract Security Tools
Mythril
Security analysis tool for EVM bytecode. Supports smart contracts built for Ethereum, Hedera, Quorum, Vechain, Roostock, Tron and other EVM-compatible blockchains.
For example:
myth a killbilly.sol -t 3
sabre
Security analyzer for Solidity smart contracts. Uses the MythX smart contract security service.
For example:
sabre check <solidity-file> [contract-name]
smartbugs
SmartBugs is an execution framework aiming at simplifying the execution of analysis tools on datasets of smart contracts.
For example:
python3 smartBugs.py –tool all –file dataset/reentrancy/simple_dao.sol
truffle
Truffle is a development environment, testing framework and asset pipeline for Ethereum, aiming to make life as an Ethereum developer easier. With Truffle, you get:
- Built-in smart contract compilation, linking, deployment and binary management.
- Automated contract testing with Mocha and Chai.
- Configurable build pipeline with support for custom build processes.
- Scriptable deployment & migrations framework.
- Network management for deploying to many public & private networks.
- Interactive console for direct contract communication.
- Instant rebuilding of assets during development.
- External script runner that executes scripts within a Truffle environment.
For example:
truffle test ./test/TestMetaCoin.sol
solhint
Solhint is an open source project created by https://protofire.io. Its goal is to provide a linting utility for Solidity code.
For example:
solhint contracts/MyToken.sol
Ethlint
Ethlint (Formerly Solium) analyzes your Solidity code for style & security issues and fixes them.
For example:
solium -f foobar.sol
echidna
Ethereum smart contract fuzzer
For example:
echidna-test myContract.sol
securify
Security Scanner for Ethereum Smart Contracts
For example:
java -jar build/libs/securify.jar -fs src/test/resources/solidity/transaction-reordering.sol
Top Resource for Learning
Fundamentals of Smart Contract Security
- Explore smart contract fundamentals, including the Ethereum protocol, Solidity programming language, and the Ethereum Virtual Machine
- Dive into smart contract development using Solidity and gain experience with Truffle framework tools for deploying and testing your contracts
- Use Web3 to connect your smart contracts to an applicationso users can easily interact with the blockchain
- Examine smart contract security along with free online resources for smart contract security auditing
Truffle Quick Start Guide
- Build your first Ethereum Dapp with Truffle: the most popular Ethereum development framework
- Build, compile, and deploy smart contracts in your development environment
- Embrace Blockchains and utilize it to create new generation of secured and scalable apps
Security Checklist
Security Checklist
Resources
- https://blog.csdn.net/ityouknow/article/details/121896509
- https://blog.csdn.net/smellycat000/article/details/120213401
- https://blog.csdn.net/smellycat000/article/details/119814296
- https://github.com/nomi-sec/PoC-in-GitHub