SECURING SMART CONTRACTS: A PHISHY BUSINESS

Lruedalaje
NicaSource
Published in
18 min readNov 3, 2022

In a previous article, we discussed what Smart Contracts are, which are their use cases and which challenges are tackled with blockchain technology. As we stated, a smart contract is a piece of executable code that automatically runs on the blockchain to enforce an agreement between parties involved in the transaction. In practice, many security issues have been reported when using these contracts. This poses a new challenge, which is avoiding potential financial losses and users’ distrust of this perfectible tool.

In this article, we will go through security pitfalls when drafting a smart contract and the possible ways to mitigate them.

GLOBAL VARIABLES VULNERABILITIES

Contracts using ‘tx.origin’ for authorization are vulnerable to phishing

Phishing is a cyber-attack by which an attacker disguises himself as a trustworthy entity and deceives the user into doing something they would not want to do otherwise. For example, an attacker can scam a user into sending all of his ether to the attacker’s address.

tx.origin is a global variable in Solidity that is vulnerable to phishing. It is well-known for storing the address where a transaction originates. Let’s see it in an example.

Alice calls contract A, and contract A calls contract B. Inside contract B, ‘msg.sender’ will be equal to A, but ‘tx.origin’ will be equal to Alice since it is equal to the address where the transaction originated. In other words, since Alice created the transaction, tx.origin will always be equal to Alice.

Let’s see this variable in the code. Imagine you had the following smart contract:

Anyone can deposit ethers into this contract, but only the owner can transfer from it.

As you can see, the transferTo function checks that tx.origin is equal to the owner state variable, and this variable is set to msg.sender in the constructor.

Let’s say that someone named Alex deploys the TxUserWallet contract, so the owner state variable will be equal to Alex. He will be able to withdraw ethers from this contract by calling the function transferTo. When he does call this function, it will first check that tx.origin is equal to Alex. In this case, he will be able to transfer ethers.

But what happens if Eve calls this function? tx.origin will be equal to Eve, but the owner will be equal to Alex, so Eve won’t be able to withdraw ethers from this contract. Therefore, if Eve can trick Alex into calling this function, tx.origin will be equal to Alex, the owner will be equal to Alex, and Eve will be able to trick Alex into withdrawing ether from this contract.

So, if Eve can trick Alex into calling her malicious contract (TxAttackWallet, as shown below), as a consequence, the transfer function from the wallet contract will be called, and the tx.origin will still be equal to Alex. By doing this, Eve will be able to withdraw ethers from Alex’s wallet.

Let’s take a more careful look at the code that allows Eve, or anyone, to trick you into sending ethers to their address:

The function transferTo from TxUserWallet contract is called in the TxAttackWallet contract, and the address of the owner’s attack contract is passed as a parameter, as well as all the balance from the wallet contract to steal all the ether found inside that contract.

In this case, if TxUserWallet contract had checked msg.sender for authorization, it would get the address of the attack wallet, instead of the owner’s address. But by checking tx.origin, it gets the original address that kicked off the transaction, which is still the owner’s address. The attack wallet instantly drains all your funds!

How to prevent it?

Instead of using tx.origin for authentication purposes, it is advisable to use msg.sender as shown below.

This will prevent the phishing attack because msg.sender always points to the contract or the account that calls the contract.

In the end, that’s what we should not forget, using tx.origin might make your contract vulnerable to phishing attacks!

You should watch out for timestamps and miners!

Another global variable worth mentioning is the block.timestamp.

To address this issue, we first need to know that the timestamp provides information about the date and time in which the block is mined.

For example, if you go to the etherscan you can find the timestamp of each block that was mined.

Often, the timestamp of a current block is used as input for a critical operation, such as determining the winner of a toss.

Block timestamps can be manipulated by miners and then used to their advantage to attack a smart contract since they can know in advance which will be the input number.

Let’s see how in an example in code:

This contract named Roulette has ten ethers stored in it. It will reward the user with all the ethers saved in the contract if he can send a transaction at the right time. The right time will be calculated with the use of the block.timestamp.

As you can see in this contract, for a user to play, he has to call the function spin and send two ethers. Then, if the value of the block.timestamp is divisible by seven, then the user wins all of the ether stored in the contract. Everyone has one out of seven chances of winning.

Everyone but miners.

What can a miner do to improve his chances?

The miner could send a transaction to play this game by calling the function spin and then try to submit a timestamp divisible by seven for the next block. If the miner is lucky enough to mine the next block, then he will win the reward. Miners have more control over the value in the block.timestamp than regular users.

Furthermore, if a powerful miner is the one attempting to attack the contract, say its mining power is 30% of the whole network, this will mean that his chances of winning this game would be at least 30%, which is a better chance than one out of seven.

How to prevent it?

Do not use block.timestamp!

If you MUST use it, follow the 15-second rule. It states that if the scale of your time-dependent event can vary by 15 seconds and still maintain integrity, it is safe to use block.timestamp. This means that if the contract’s code does not rely on a time interval of fewer than 15 seconds then it is probably safe to use block.timestamp.

STORAGE VARIABLES VULNERABILITIES

OVERFLOW & UNDERFLOW

The uint overflow/underflow, also known as uint wrapping around, is an arithmetic operation that produces a result that is larger than the maximum above for an N-bit integer or produces a result that is smaller than the minimum below for an N-bit integer.

Let’s see it in an example so as to make it clearer.

In the previous contract, we have a function that counts kilometers and a function that subtracts kilometers.

We have an unsigned integer that can only have 16 bits. This means that the largest number that can be stored is the decimal number 65535.

Let’s compile and deploy the KilometersCount contract. As you can see, kilometers are first initialized at 0.

If we subtract kilometers, they should be decremented by one. Let’s see what happens:

Kilometers are now 65535, which is the maximum value possible that the variable can store. This is an underflow example. If we now add a kilometer to the maximum value (65535), instead of being incremented by one, kilometers will be 0. This is an example of overflow.

This can represent a major flaw in balances.

How to prevent it?

The easiest way is to use at least a 0.8 version of the Solidity compiler. The compiler will automatically check for overflow and underflow. Let’s see it in code.

This is the same smart contract compiled with version 0.8.0. If we try to subtract kilometers initially, we will get the following error in the Remix console:

Bear in mind that there are many contracts out there that were implemented in older versions of the compiler and will unfortunately still have this problem.

The solution would be to:

  • Update the compiler to the latest versions.
  • Implement libraries to operate safely.

PRIVATE VARIABLES

Have you ever wondered if there is still something that can be private? If you haven’t, it is time to do so.

As you know, everything in a smart contract is accessible for anyone to see, which means that sensitive information can not be hidden.

You might be wondering, what about PRIVATE variables? Spoiler alert: they are not REALLY private.

In order to explain this, some basis of Solidity programming language must be recalled. In essence, there are four types of visibility for functions and state variables:

Therefore, EVERYTHING that is inside a contract is visible to ALL external observers.

Making something private only prevents other contracts from accessing and modifying the information, but it will still be visible to the whole world outside of the blockchain.

But how can these private variables be hacked?

Variables in a contract will be stored as follows:

  • All state variables in Solidity are stored in storage in order of declaration.
  • Local variables of structs and arrays are always stored in storage by default. Variables declared as constants or as immutable are directly compiled and do not occupy a slot.
  • Each slot can store up to 256 bits (32 bytes).
  • Multiple variables can be stored in the same slot, if they fit.
  • Booleans only need one byte. Addresses take 20 bytes.
  • If the size of the variable exceeds the remaining size of the slot it will be passed to a new slot.
  • Struct creates a new slot, and the struct elements are put in the slot as described above.
  • A fixed-size array creates a new slot, struct elements are put into the slot, and the same procedure is followed again.
  • A dynamic size array creates a new slot. This slot only stores the length of the array, while the values in the array will be stored at other locations.
  • Mapping always creates a new slot to hold a place, yet the values ​​in the array will be stored in other locations.
  • String creates a new slot that stores both data and data length.

Now let’s see in code how the hacking would work…

Below we have a contract named Privado, which stores, in slot 0, a variable called “numeroSecreto”. In the constructor, we define the variable as equal to block.timestamp. We can see in the terminal the block timestamp is 1639681587.

Then, we can access the value of the variable by typing web3.eth.getStorageAt(“”). The first input inside the parentheses is the contract address and the second input is the slot number where the variable whose value we are trying to find out is stored.

As you can see below, the output is ‘0x61bb8e33’, which is 1639681587 when converted from hexadecimal to decimal.

That’s it. The private variable has been unveiled.

INTERACTING WITH OTHER CONTRACTS

AVOID DELEGATECALL

To begin with this topic, we will first explain what a call is. A call is a low-level function used to interact with other contracts. Let’s see how to use it in an example that is going to call the contract TestCall written below:

As you can see below, we have coded a contract named Call that will call the TestCall contract function foo by executing the callFoo function.

We will deploy both contracts, TestCall and Call. When we call the function callFoo we will pass the address of TestCall. That is why we have included a variable named _test of type address as an input of the function.

To call the function foo using the low-level function call we type _test.call(). Inside the parentheses, we need to encode the function that we will be calling followed by the inputs that we are going to be passing. This is why we type “abi.encodeWithSignature()”. The first input will be the function that we are going to be calling, “foo(string,uint256)”. Next, we need to pass the function inputs. In this case, the first input needs to be a message of type string, ‘“call foo”, and the second one a uint, 123.

Using call will return two outputs. The first one is a boolean that tells whether the call was successful or not, “bool success”, and the second one is any output that was returned by calling the function foo,” bytes memory_data”.

Next, we will check if the call was successful by typing require(success, “call failed”). If the call was not successful we will define a message saying ”call failed”.

When we use call to call other functions we can specify the amount of gas and the amount of ethers that we are going to be sending. For that, after _test.call, inside curly braces, we have specified the number of ethers, 111.

We will also create the variable data and store the output returned once the function finishes its execution.

After that, we will deploy both contracts and will call the callFoo function adding the TestCall contract address, as shown below.

Then, if we successfully call the callFoo function and transfer our ethers, we’ll see something as shown in the Remix terminal:

So, what are DELEGATECALLS, and why should we avoid them?

delegatecall is a low-level function similar to call. The difference is that it executes code in another contract in the context of the contract that has called it.

This means that when contract A executes the delegatecall to contract B, B’s code is executed with contract A’s storage, msg.sender, and msg.value.

What does this mean?

A calls B and sends 100 weis. B delegateCall to C. In this last case, msg.sender will be A. This is because when we query msg.sender inside B, it will be equal to A. A called B and inside B thus msg.sender is A. Delegatecall preserves the context, so when B delegateCalls to C, msg.sender remains and is equal to A. The same happens to msg.value, which will be equal to 100.

Let’s see an example in code. First, we coded the contract TestDelegateCall with the function setVars that gives values to the variables num, sender, and value.

Then we have the DelegateCall contract, from where we will delegateCall the function setVars.

We then compile and deploy both contracts. We will call the function setVars from DelegateCall contract, and this will delegatecall to TestDelegateCall and execute the code inside the setVars function inside the TestDelegateCall contract. Nevertheless, since we are executing delegateCall, the state variables that you see in the contract TestDelegateCall will not be updated. Instead, the state variables in the contract that made the call, DelegateCall, are the ones that will be updated.

So, why do we need to avoid it?

This low-level function has been very useful as it’s the backbone for implementing libraries and modularizing code. However, it opens up the doors to vulnerabilities as, essentially, your contract is allowing anyone to do whatever they want with their state.

Here is a contract named HackMe. It has a state variable called owner, which is set inside the constructor when the contract is deployed.

We will try to hijack the contract by changing the variable owner even though the contract does not have any functions that update the value of the variable owner.

As you can see above, the only function that the contract has is a fallback function that doesn’t update the owner state variable. Inside this function, the delegatecall function is used. And who does it delegatecall to? To the state variable lib, that is another contract inside the constructor.

Let’s take a look at contract Lib:

It declares a single state variable called owner, and has a single function called pwn, and when this function is called, it sets the single declared variable to msg.sender.

We are now going to change the value of the owner variable from the HackMe contract.

We will store the address of the hackMe contract in a state variable called hackMe. The actual address will be set when we deploy the contract. This is why we pass the address into the constructor and then set it to the state variable hackMe.

We create a function called attack which is the one we will be calling to update the state owner variable from the hackMe contract. Our goal here is to call the “pwn” function inside the lib contract, which we can do by calling the fallback function inside the hackMe contract and then passing in the function signature of pwn as msg.data (we know that fallback functions get triggered when a function that does not exist inside the contract is called).

But how did the hack work?

When we call the attack function, the HackMe contract is called, and the function that tries to be called inside the HackMe contract is “pwn”. Since the “pwn” function does not exist inside the HackMe contract, the fallback function will be called instead. The fallback function will delegatecall to the Lib contract and forward the msg.data.

Delegatecall calls the Lib contract, and since we sent msg.data to match the pwn function, the function pwn inside lib contract will be executed and consequently will run the code to update the owner state variable.

Since delegatecall runs code using the storage of hackMe contract, it will actually update the owner state variable inside the hackMe contract.

Alice (0x5B38Da6a701c568545dCfcB03FcB875f56beddC4) deployed the Lib contract. Next, she took the address of the Lib contract and deployed the HackMe contract.

Eve (0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2) deployed the Attack contract with the address of the HackMe contract.

As can be seen, the owner of the contract changed. Eve hijacked Alice’s contract.

This is why we recommend avoiding the use of delegatecall. In case you MUST use it, then you should never forget that delegatecall preserves context.

TRANSFER’S ATTACKS

FORCEFULLY SENDING ETHER

selfdestruct is a Solidity function that transfers the funds from a contract to an account and then removes a contract from the network.

Let’s see how a contract can be hacked when using this function.

etherGame is a contract in which the seventh person to deposit one ether becomes the winner and can claim the seven ethers stored in this contract.

To play this game, the players will call the function deposit. If the current balance stored in this contract is greater than seven ethers, the game is over. After seven ethers have been achieved, no one should be able to send more ethers.

If the current balance is equal to seven ethers, then we set the winner to msg.sender. This means that if msg.sender is the seventh person to deposit one ether, then that person becomes the winner.

The winner can claim its reward by calling the function claimReward.

Let’s now write a contract that can tamper with this game. The basic idea is to use selfdestruct so that the balance exceeds the target amount of seven ethers. By doing so, the balance will never equal the target amount, and the winner will never be set. No one will be able to call the function claimReward and all of the ethers from the players will be stuck in this contract.

Let’s see it in code.

The contract Attack has a single function named attack that will be called to break the contract above. For the input, we pass the target address and then declare it as payable. The reason that it is payable is that the address that is passed to selfdestruct must be a payable address. We will send ether to this function and have that ether forcefully sent to the contract above.

Inside the function, we will call selfdestruct passing our target address.

We deploy the two contracts, etherGame and Attack contract. We will say that account 1 is Alice’s, account 2 is Bob’s, and account 3 is Eve’s.

Eve is going to play this game and deposit one ether. Bob also joins in and deposits the same amount. At this point, the contract has two ethers. Alice and Bob are both trying to become the seventh person to deposit one ethe, become the winner, and finally, claim all the ethers stored.

Eve decides to break this game. She knows there are two ethers stored in this contract and that if she forcefully sends five ether at once, she can break the game. Every user who then sends one ether will get the message ‘Game is Over’, since the balance will always be greater than the target amount. The winner won’t be set.

As we can see, after Eve sends five ethers, the balance of the contract is seven and the winner has not been set. All rewards are then locked in this contract, with no winner.

In this specific case, this could have been avoided. The value of the balance can be manipulated, so the decision of who is the winner should not rely on it. If the contract was coded as shown below, attackers might still forcefully send ethers to the contract, but they won’t be able to update the variable state balance.

This is why we recommend knowing how selfdestruct works and if you must implement it, we advise you to do it in a very careful way.

REENTRANCY

What is a reentrancy attack?

Over here we have contract A and contract B.

Let’s imagine that contract A calls contract B. The very basic idea of reentrancy is that contract B can call back into contract A while contract A is still being executed.

Now imagine that contract A has ten ethers and contract B has none. Inside contract A, there is also a record of how many ethers are owed to other contracts and addresses. Imagine there is also a function called withdraw that checks if your address is included in the record, that is to say, if you have some ethers stored in that contract, so that you are able to withdraw them.

Contract B can use the reentrancy to exploit the withdraw function. It will have two functions. The fallback function and the attack function, in both functions contract B will call the withdraw function from contract A.

First, Eve calls the attack function. This will call the withdraw function inside contract A. Inside contract A, since contract B is the caller, it will check that the balance of contract B is greater than 0. In this case, it is, since the balance is equal to one. Therefore it sends the ether back to contract B.

Inside contract B, the fallback function is triggered. Now, contract B has one ether, and contract A has nine ethers. The fallback function inside contract B calls the withdraw function from contract A, and checks the balance of B.

As you can see, the balance of contract B is still one, so it will once more send one ether to contract B. Now, contract B has two ethers while the balance of B inside contract A is still one ether. This could keep on going because the state variable balance is not updated until the last line of code inside the withdraw function, and it is not reached because it keeps on looping between the fallback function and the withdraw function. Therefore, contract B can keep on withdrawing ether from contract A.

How can we prevent the reentrancy attack?

  • Update state variables before you make any external calls to other contracts. In the example below, the state variable balance inside the withdraw function is updated before calling another contract.
  • Use a modifier to lock the contract while a function is being executed. In this way, only one function will be executed per time, causing attacks to fail.

CONCLUSION

In this article, we have addressed security risks that exist when coding a smart contract. Additionally, we have discussed variable and storage vulnerabilities and some concerns that may arise when interacting with other contracts or transferring ether. To enrich our experience, we have also coded and hacked every contract to demonstrate the security issues we have mentioned.

Evidently, our aim was not to encourage hacking. We believe spreading information about these vulnerabilities might contribute to improving developers’ coding skills and eventually help in decreasing hacking. So, if you have any questions or comments, don’t hesitate in contacting us.

BIBLIOGRAPHY

Yuan He, Security Researcher from HaloBlock.io

https://solidity-by-example.org/delegatecall/

--

--

NicaSource
NicaSource

Published in NicaSource

Nicasource is a specialized staffing firm offering clients high-quality dedicated teams & staff augmentations solutions. You will find us here sharing our best practices, tips & tricks on the technologies we love and some other trends.