We present the third part of the cycle devoted to typical vulnerabilities, attacks and problem areas inherent in smart contracts in the Solidity language, and the Ethereum platform as a whole. Here we will talk about what features Solidity has and what vulnerabilities they can turn into in the right hands.
In the first part, we discussed the front-running attack, various random number generation algorithms and network resiliency with the Proof-of-Authority consensus. The second talked about Integer overflow, ABI encoding / decoding, Uninitialized storage pointer, Type Confusion and how to make a backdoor. And in this part we will discuss several distinctive features of Solidity and look at the logical vulnerabilities that can occur in contracts.
To begin with, smart contracts exchange values and user addresses with each other. At the beginning, the broadcasts were transmitted by calling another contract:
msg.sender.call.value(42) // msg.sender.call.value(42)()
However, when calling a contract without specifying a signature, its fallback function will be called, in which there can be an arbitrary code. Such an unusual logic of work led to the famous reentrancy, with the help of which TheDAO was hacked.
After that, the send
function appeared, which is also just syntactic sugar, - under the hood it has the same call, only the amount of gas is limited, so reentrancy cannot be done.
msg.sender.send(42) // msg.sender.call.value(42).gas(2300)() - , ?
However, if something goes wrong and the broadcast cannot be sent, then send will not interrupt the flow of execution. This behavior can also be critical. For example, the broadcast was not sent, and the contract status has already changed. Someone will be left without ethers .
Therefore, transfer appeared, and it will raise an exception if something goes wrong.
msg.sender.transfer(42) // if (!msg.sender.send(42)) revert()
But she is not a silver bullet. Imagine that we have an array of addresses to which we need to broadcast, and if you use transfer
, the success of the whole operation will depend on each of the recipients - if one person does not accept the broadcast, then all changes will be rolled back completely.
And the last moment with sending the ether is the selfdestruct
function.
selfdestruct(where)
In fact, this is a function for the destruction of the contract, but all the air that remained on the contract will be sent to the address that is specified as an argument. And this can not be avoided - the air will go away, even if the receiving address is a contract, and the fallback function is not payable
(the fallback is simply not called). The air will be sent even to the not yet created contract!
In Solidity, to resolve multiple inheritance, the C3 linearization algorithm is used (the same as in Python, for example). And for those who had the good fortune not to step on the multiple inheritance rake, the final graph will most likely not seem obvious. Consider an example:
contract Grandfather { bool public grandfatherCalled; function pickUpFromKindergarten() internal { grandfatherCalled = true; } } contract Mom is Grandfather { bool public momCalled; function pickUpFromKindergarten() internal { momCalled = true; } } contract Dad is Grandfather { bool public dadCalled; function pickUpFromKindergarten() internal { dadCalled = true; super.pickUpFromKindergarten(); } } contract Son is Mom, Dad { function sonWannaHome() public { super.pickUpFromKindergarten(); } }
Continue the call graph starting from Son.sonWannaHome ().
Dad will be called, and then Mom. Total, inheritance is as follows.
Son -> Dad -> Mom -> Grandfather
An example of a more or less plausible contract with a bug regarding multiple inheritance was presented at the Underhanded Solidity Coding Contest .
Smart contracts are written by people, and people are often mistaken ... in the name of variables, constructors ; they forget to restrict access to some functions (as, for example, in Parity Multisig ), etc. Also, the developer should closely monitor the possible onset of a race condition, since any function of a smart contract can be called from any address at any time. It must itself implement the necessary synchronization primitives and access modifiers so that the smart contract can control the sequence of the call. In addition, there are things that no code analyzer can find — domain errors. Therefore, this section will address the author's vulnerabilities.
In the overwhelming majority of contracts that need to work with mathematics, for example, the SafeMath library is used to calculate how many tokens the user receives for sending air . However, the name may be deceptive - in fact, SafeMath only cares about overflows . We propose to consider the following piece of contract:
contract Crowdsale is Ownable { using SafeMath for uint; Token public token; address public beneficiary; uint public collectedWei; uint public tokensSold; uint public tokensForSale = 7000000000 * 1 ether; uint public priceTokenWei = 1 ether / 200; bool public crowdsaleFinished = false; function purchase() payable { require(!crowdsaleFinished); require(tokensSold < tokensForSale); require(msg.value >= 0.001 ether); uint sum = msg.value; uint amount = sum.div(priceTokenWei).mul(1 ether); uint retSum = 0; if(tokensSold.add(amount) > tokensForSale) { uint retAmount = tokensSold.add(amount).sub(tokensForSale); retSum = retAmount.mul(priceTokenWei).div(1 ether); amount = amount.sub(retAmount); sum = sum.sub(retSum); } tokensSold = tokensSold.add(amount); collectedWei = collectedWei.add(sum); beneficiary.transfer(sum); token.mint(msg.sender, amount); if(retSum > 0) { msg.sender.transfer(retSum); } LogNewContribution(msg.sender, amount, sum); } }
Noticed anything suspicious? Most likely not, and this is absolutely normal. Let's figure it out. Pay attention to the expression sum.div(priceTokenWei).mul(1 ether)
- from the point of view of logic, everything is very smooth: and then multiply by 1 ether to get the units you want. "
But there is a nuance. Each library call (and there are two of them here) will receive two uint and return also uint, and this, in turn, means that the fractional part of the first operation will be legitimately rejected completely.
// SafeMath function div(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a / b; return c; }
Thus, by sending not an integer number of airs to this crowdsale contract, the investor will lose tokens, and ICO can collect more than expected: D The full contract can be found in solidity_tricks .
Behind such a long name is a funny vulnerability discovered during the audit of the PoA network contracts. According to the rules of the network, it has 12 or more validators, which can hold various votes, including the change of key (and, accordingly, addresses) of the validator. In order that the validator could not change the key and vote twice, the smart contract keeps a history of all the keys. And when validating a vote, it checks that there is no ancestor among those who voted.
So, each time the key changes, it is placed in the mapping, where it refers to the previous key. Therefore, with each new change, the contract has the opportunity to go through the history of keys. However, in this configuration, without additional checks, the validator can loop the key history and thereby cut off the old keys:
1) A validator with key A registers the vote X, then requests a change of key. After that, he has B. in his hands. If he tries to vote with his new key right now, he will fail, because key A is in history B:
History(B): B => A => 0x
2) Therefore, the validator requests the change of the key again, gets the key C. Again, right now the trick will not work for the same reason:
History(C): C => B => A => 0x
3) Then the validator requests the change of key C to key B. After this, the history of the keys is fixed between B and C, and does not contain A:
History(B): B => C => B => C => B => ...
Now the validator can use the key B or C to vote in the voting X a second time. Fix and original report , as well as other vulnerabilities.
Right now you may reasonably have two questions:
function areOldMiningKeysVoted(uint256 _id, address _miningKey) public view returns(bool) { VotingData storage ballot = votingState[_id]; IKeysManager keysManager = IKeysManager(getKeysManager()); for (uint8 i = 0; i < maxOldMiningKeysDeepCheck; i++) { address oldMiningKey = keysManager.miningKeyHistory(_miningKey); if (oldMiningKey == address(0)) { return false; } if (ballot.voters[oldMiningKey]) { return true; } else { _miningKey = oldMiningKey; } } return false; }
In any case, the cycle size will be no more than 256 repetitions due to the fact that the variable i is defined as uint8.
The real possibility of exploitation of this vulnerability raises questions from the author, however, it will still be useful to those who are going to keep a unidirectional list in the mapping after it learns in the chatover on stackoverflow that arrays are expensive :)
The following vulnerability is more likely to ignorance / lack of understanding of the values of global variables. We propose to take a look at one of the possible implementations of the commit-reveal scheme:
pragma solidity ^0.4.4; import 'common/Object.sol'; import 'token/Recipient.sol'; /** * @title Random number generator contract */ contract Random is Object, Recipient { struct Seed { bytes32 seed; uint256 entropy; uint256 blockNum; } /** * @dev Random seed data */ Seed[] public randomSeed; /** * @dev Get length of random seed data */ function randomSeedLength() constant returns (uint256) { return randomSeed.length; } /** * @dev Minimal count of seed data parts */ uint256 public minEntropy; /** * @dev Set minimal count of seed data * @param _entropy Count of seed data parts */ function setMinEntropy(uint256 _entropy) onlyOwner { minEntropy = _entropy; } /** * @dev Put new seed data part * @param _hash Random hash */ function put(bytes32 _hash) { if (randomSeed.length == 0) randomSeed.push(Seed("", 0, 0)); var latest = randomSeed[randomSeed.length - 1]; if (latest.entropy < minEntropy) { latest.seed = sha3(latest.seed, _hash); latest.entropy += 1; latest.blockNum = block.number; } else { randomSeed.push(Seed(_hash, 1, block.number)); } // Refund transaction gas cost if (!msg.sender.send(msg.gas * tx.gasprice)) throw; } /** * @dev Get random number * @param _id Seed ident * @param _range Random number range value */ function get(uint256 _id, uint256 _range) constant returns (uint256) { var seed = randomSeed[_id]; if (seed.entropy < minEntropy) throw; return uint256(seed.seed) % _range; } }
Did you notice that the smart contract returns the spent gas when committing the next part of the seed (see the put function)? In itself, the desire to return the spent commission does not fit into the paradigm of the Ethereum platform, but this is not the worst. The vulnerability here is that the value of msg.gas is controlled by the sender and means the remaining gas. Thus, the attacker, by manipulating the gas of the transaction and its price, can withdraw all funds from the contract.
In this article, we examined only a few logical vulnerabilities in order to form the reader’s intuition about the places where you can make mistakes when writing smart contracts. In fact, such logical (copyright) vulnerabilities in contracts the most. They are associated primarily with business logic or subject area. It also suggests that most contract vulnerabilities cannot be detected by automatic means, at least until they begin to allow the user to describe the criteria for "misbehavior." By the way, in the next part we will look at what tools still exist, and what they are suitable for in their current state.
PS I am grateful to Raz0r for an example of Generous refund :)
Source: https://habr.com/ru/post/347110/