660 likes | 692 Views
Learn about common pitfalls like Denial of Service in Ethereum contracts, and how to mitigate risks. Explore the importance of contract security strategies to protect against vulnerabilities.
E N D
Solidity Pitfalls and Hazards (Part Four) CS1951 L Spring 2019 5 March 2019 Maurice Herlihy Brown University
Today’s Pitfalls: Denial of Service Block Timestamp Manipulation Constructor Names Uninitialized Storage References Floating Point Precision Tx.Origin and Phishing
Denial of Service Attacks Parties leave a contract inoperable for short period … or sometimes a long period … trapping ether in contract
function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); // 5 times the wei sent } function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // here transferToken(to,amount) transfers "amount" of tokens to the address "to" transferToken(investors[i],investorTokens[i]); } } function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); // 5 times the wei sent } function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // here transferToken(to,amount) transfers "amount" of tokens to the address "to" transferToken(investors[i],investorTokens[i]); } } } Inflatable Data Structures functioninvest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); } functiondistribute() public{ require(msg.sender == owner); for(uinti=0;i<investors.length;i++) { transferToken(investors[i], investorTokens[i]); } } Load with ether
function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); // 5 times the wei sent } function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // here transferToken(to,amount) transfers "amount" of tokens to the address "to" transferToken(investors[i],investorTokens[i]); } } function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); // 5 times the wei sent } function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // here transferToken(to,amount) transfers "amount" of tokens to the address "to" transferToken(investors[i],investorTokens[i]); } } } Inflatable Data Structures functioninvest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); } function distribute() public { require(msg.sender == owner); for(uinti=0;i<investors.length;i++) { transferToken(investors[i], investorTokens[i]); } } accumulate investments
function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); // 5 times the wei sent } function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // here transferToken(to,amount) transfers "amount" of tokens to the address "to" transferToken(investors[i],investorTokens[i]); } } function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); // 5 times the wei sent } function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // here transferToken(to,amount) transfers "amount" of tokens to the address "to" transferToken(investors[i],investorTokens[i]); } } } Inflatable Data Structures function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); } function distribute() public { require(msg.sender == owner); for(uinti=0;i<investors.length;i++) { transferToken(investors[i], investorTokens[i]); } } attacker can inflate array size until gas required by loop exceeds block gas limit locking up tokens forever
function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); // 5 times the wei sent } function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // here transferToken(to,amount) transfers "amount" of tokens to the address "to" transferToken(investors[i],investorTokens[i]); } } function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); // 5 times the wei sent } function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // here transferToken(to,amount) transfers "amount" of tokens to the address "to" transferToken(investors[i],investorTokens[i]); } } } Inflatable Data Structures function invest() public payable { investors.push(msg.sender); investorTokens.push(msg.value * 5); } functiondistribute() public{ require(msg.sender == owner); for(uinti=0;i<investors.length;i++) { transferToken(investors[i], investorTokens[i]); } } distribute tokens to investors
External Call Progression Progression to next state requires call sending ether to investor? Investor’s contract doesn’t accept ether? Input from external source? Source malicious, or DDOS victim
Prevention Use individual withdrawal pattern Use multisigs to avoid single point of failure Or use timelock to finalize
This Happened GovernMental was a Ponzi scheme Accumulated lots of ether Payoff function deletes a mapping so large … Gas cost exceeds max block limit. 1100 ether stuck
Fomo3D Satirizes ICO and Ponzi schemes, while maybe being those things. “socioeconomic performance art” more traffic than CryptoKitties
Goal: Purchase last key before the timer goes to zero Purchsing key increases timer, key price If timer reach zero, winner gets half the rest is complicated: dividends, etc.
Classical “dollar auction” paradox Say, pot has $100 bid $1, pot is $101, and you stand to win it. Bargain! Someone else bids $1, now $102 and you won’t win for only $1 above your sunk costs, you win again.. That counter should never go to zero!
Forensic Evidence Block containing winner txn is normal: 92 txns But next 11 blocks had many fewer txns! All with unusually high txn fees all to same contract
Issues lots of transactions where High gas price means … transaction likely to be chosen first for block High gas limit means … Transaction uses up block gas limit … and squeezes out other transactions
Contract also called getCurrentRoundInfo() to check timer state to check whether attacker’s key is last If good, revert() to use up gas, squeeze out later transactions If bad, wait and don’t use gas
In Summary winner launched many txns high gas price, high gas limit squeezes out competing transactions Consumes gas limit only when promising many contracts, different addresses, limits … Spent about $11K fees to win $3M
Block Timestamp Manipulation Block timestamps used for randomness (bad idea) escrowing funds time-dependent state changes Danger: miners can manipulate slightly
contract Roulette { uintpublicpastBlockTime; constructor() public payable {} function() public payable { require(msg.value== 10 ether); require(now != pastBlockTime); pastBlockTime= now; if (now % 15 == 0) { msg.sender.transfer(this.balance); }}}
contract Roulette { uintpublicpastBlockTime; constructor() public payable {} function () public payable { require(msg.value== 10 ether); require(now != pastBlockTime); pastBlockTime= now; if (now % 15 == 0) { msg.sender.transfer(this.balance); }}} forces one bet per block
contract Roulette { uint public pastBlockTime; constructor() public payable {} function () public payable { require(msg.value== 10 ether); require(now != pastBlockTime); pastBlockTime= now; if (now % 15 == 0) { msg.sender.transfer(this.balance); }}} initially fund contract
contract Roulette { uint public pastBlockTime; constructor() public payable {} function() public payable { require(msg.value== 10 ether); require(now != pastBlockTime); pastBlockTime= now; if (now % 15 == 0) { msg.sender.transfer(this.balance); }}} bet via fallback function
contract Roulette { uint public pastBlockTime; constructor() public payable {} function () public payable { require(msg.value== 10 ether); require(now != pastBlockTime); pastBlockTime= now; if (now % 15 == 0) { msg.sender.transfer(this.balance); }}} must bet 10 ether to play
contract Roulette { uint public pastBlockTime; constructor() public payable {} function () public payable { require(msg.value== 10 ether); require(now != pastBlockTime); pastBlockTime= now; if (now % 15 == 0) { msg.sender.transfer(this.balance); }}} only one transaction per block
contract Roulette { uint public pastBlockTime; constructor() public payable {} function () public payable { require(msg.value== 10 ether); require(now != pastBlockTime); pastBlockTime= now; if (now % 15 == 0) { msg.sender.transfer(this.balance); }}} synonym for block.timestamp
contract Roulette { uint public pastBlockTime; constructor() public payable {} function () public payable { require(msg.value== 10 ether); require(now != pastBlockTime); pastBlockTime= now; if (now % 15 == 0) { msg.sender.transfer(this.balance); }}} Winner? What’s wrong with this?
Miners can adjust timestamp within limits increasing not “too far” in future Miner can ensure any bet loses … … Collecting block reward, increasing contract pool (Also front-running attack)
Prevention Never use timestamps for entropy Avoid sensitive decisions based on small timestamp differences For time-sensitive logic, maybe use (block.number X average block time)
This Happened GovernMental Ponzi again Pays back one player per round Last to join round for at least 1 min wins Easy for miners to manipulate winner
Fun With Constructors In early Solidity, a constructor was syntactically just a function with same name as the contract. Semantically, constructors not just another function If contract name changes, but constructor doesn’t, then constructor becomes normal, callable function This was fixed after the predictable disasters
contractOwnerWallet{ address public owner; functionownerWallet(address _owner) public { owner = _owner; } function() payable{} function withdraw() public{ require(msg.sender== owner); msg.sender.transfer(this.balance); }}
contract OwnerWallet{ address public owner; function ownerWallet(address _owner) public { owner = _owner; } function() payable{} function withdraw() public { require(msg.sender== owner); msg.sender.transfer(this.balance); }} collect ether via fallback function
contract OwnerWallet{ address public owner; function ownerWallet(address _owner) public { owner = _owner; } function () payable {} function withdraw() public{ require(msg.sender== owner); msg.sender.transfer(this.balance); }} only owner can collect ether
contract OwnerWallet{ address public owner; functionownerWallet(address _owner) public { owner = _owner; } function () payable {} function withdraw() public { require(msg.sender== owner); msg.sender.transfer(this.balance); }} constructor initializes owner
contract OwnerWallet{ address public owner; function ownerWallet(address _owner) public { owner = _owner; } function () payable {} function withdraw() public { require(msg.sender== owner); msg.sender.transfer(this.balance); }} Anyone can call ownerWallet, become the owner and take all the ether. BOOM!
Prevention Latest compiler requires constructor syntax Included here as language design lesson “Do not put a stumbling block before the blind”
This Happened Rubixi: worst Ethereum Ponzi ever? Originally named DynamicPyramid They forgot to change the constructor name before deploying Code is law! Predators repeatedly called DynamicPyramid() to confiscate investments by gullable public
Uninitialized Storage Pointers Early versions of Solidity allowed unitialized storage variables These variables could secretly point to other storage variables Leading to (intentional or not) vulnerabilities This is no longer allowed by recent compilers
contractHodlFraud { … constructor() public payable { … } functionpayIn(uintholdTime) publicpayable { … } functionwithdraw () public { … } functionownerWithdrawal () public{ … }} HODL contract … Users pay ether … Locked up for some duration Avoid temptation to sell in volatile times And, of course, it steals your money
contractHodlFraud { uintownerAmount; uintnumberOfPayouts; address owner; structHoldRecord { uintamount; uintunlockTime; } mapping (address => HoldRecord) … }
contract HodlFraud { uintownerAmount; uintnumberOfPayouts; address owner; structHoldRecord { uintamount; uintunlockTime; } mapping (address => HoldRecord) … } balance information
contract HodlFraud{ uintownerAmount; uintnumberOfPayouts; address owner; … function payIn(uintholdTime) public payable { require(msg.value> 0); HoldRecord storage newRecord; newRecord.amount+= msg.value; newRecord.unlockTime= now + holdTime; balance[msg.sender] = newRecord; } … } unintialized reference to storage (actually location 0)
contract HodlFraud{ uintownerAmount; uintnumberOfPayouts; address owner; … function payIn(uintholdTime) public payable { require(msg.value> 0); HoldRecord storage newRecord; newRecord.amount+= msg.value; newRecord.unlockTime= now + holdTime; balance[msg.sender] = newRecord; } … } Increments value at location 0 !?
contract HodlFraud{ uintownerAmount; uintnumberOfPayouts; address owner; … function payIn(uintholdTime) public payable { require(msg.value> 0); HoldRecord storage newRecord; newRecord.amount+= msg.value; newRecord.unlockTime= now + holdTime; balance[msg.sender] = newRecord; } … } Contract owner actually credits own account!
Prevention Recent Solidity compilers no longer permit unitialized references to storage
This Happened Honeypot lotteries … With easily-predictable randomness … Pathetically vulnerable … Except for unitialized storage vars … That overwrite random values! Compiler fixes will not affect honeypots!
Floating Point Precision Solidity does not support fixed or floating point numbers Solidity does not support fixed or floating point numbers Beware of rolling your own Beware of rolling your own Round-off errors cause vulnerabilities Round-off errors cause vulnerabilities
contract HodlFraud{ uintownerAmount; uintnumberOfPayouts; address owner; … function payIn(uintholdTime) public payable { require(msg.value> 0); HoldRecord storage newRecord; newRecord.amount+= msg.value; newRecord.unlockTime= now + holdTime; balance[msg.sender] = newRecord; } … }
functionbuyTokens() public payable { uinttokens = msg.value /weiPerEth * tokensPerEth; balances[msg.sender] += tokens; } The math is correct … But if value is less than 1 ether … The number of tokens is 0
functionsellTokens(uint tokens) public { require( balances[msg.sender] >= tokens); uint eth = tokens/tokensPerEth; balances[msg.sender] -= tokens; msg.sender.transfer(eth*weiPerEth); } The math is correct … Any tokens less than 10 will result in 0 ether Rounding is always down