Transaction-ordering dependence, also known as TOD, is a type of vulnerability that can occur in smart contracts that rely on the order in which transactions are processed. These vulnerabilities can allow an attacker to manipulate the order of transactions and potentially exploit a contract’s logic. In this article, we’ll explore what transaction-ordering dependence is and how to prevent it in your smart contracts.
What is Transaction-Ordering Dependence?
Transaction-ordering dependence occurs when a contract’s behavior depends on the order in which transactions are processed. This can happen if a contract’s logic checks the state of the contract before and after a transaction is processed, and the behavior of the contract depends on the difference between these two states.
Here is an example of a contract that is vulnerable to transaction-ordering dependence:
pragma solidity ^0.6.0;
contract TODVulnerability {
uint public balance;
function deposit() public payable {
balance += msg.value;
}
function withdraw(uint amount) public {
require(balance >= amount);
balance -= amount;
}
}
In this contract, the deposit
function allows a caller to send ether to the contract and increments the balance
variable to track the total amount of ether received. The withdraw
function allows a caller to withdraw a specified amount of ether, as long as the balance
variable is greater than or equal to the amount being withdrawn.
However, this contract is vulnerable to transaction-ordering dependence because the withdraw
function does not check the current balance before processing the transaction. An attacker could potentially exploit this vulnerability by calling the deposit
function after calling the withdraw
function, causing the withdraw
function to incorrectly allow the withdrawal of more ether than the contract has on hand.
How to Prevent Transaction-Ordering Dependence
There are several ways to prevent transaction-ordering dependence in your smart contracts. One common method is to use a mutex, which is a mechanism that allows only one transaction to be processed at a time.
Here is an example of a contract that uses a mutex to prevent transaction-ordering dependence:
pragma solidity ^0.6.0;
contract TODSafe {
uint public balance;
bool mutex;
function deposit() public payable {
require(!mutex); // prevent TOD
mutex = true;
balance += msg.value;
mutex = false;
}
function withdraw(uint amount) public {
require(balance >= amount);
require(!mutex); // prevent TOD
mutex = true;
balance -= amount;
mutex = false;
}
}
In this contract, the mutex
variable acts as a flag that prevents multiple transactions from being processed at the same time. The require
statements at the beginning of the deposit
and withdraw
functions check the value of the mutex
variable and throw an exception if it is true
, which prevents transaction-ordering dependence.
Another method to prevent transaction-ordering dependence is to use the blockhash
function, which returns the hash of a block at a particular block height. By using the blockhash
function in your contract’s logic, you can ensure that the contract’s behavior is not dependent on the order of transactions.
Here is an example of a contract that uses the blockhash
function to prevent transaction-ordering dependence:
pragma solidity ^0.6.0;
contract TODSafe {
uint public balance;
function deposit() public payable {
balance += msg.value;
}
function withdraw(uint amount) public {
require(balance >= amount);
require(blockhash(block.number - 1) == 0); // prevent TOD
balance -= amount;
}
}
In this contract, the require
statement in the withdraw
function checks the hash of the previous block to ensure that it is not equal to zero. This ensures that the withdraw
function is not called until the deposit
function has completed, which prevents transaction-ordering dependence.
Conclusion
Transaction-ordering dependence can be a serious vulnerability in smart contracts if not properly addressed. By using a mutex or the blockhash
function, you can ensure that your contracts are not vulnerable to these attacks and maintain their security.
Exercises
To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.
Write a Solidity contract that defines a function named deposit
that allows a caller to send ether to the contract and increments a balance
variable to track the total amount of ether received. The contract should be vulnerable to transaction-ordering dependence.
pragma solidity ^0.6.0;
contract TODVulnerability {
uint public balance;
function deposit() public payable {
balance += msg.value;
}
}
Write a Solidity contract that defines a function named withdraw
that allows a caller to withdraw a specified amount of ether from the contract. The contract should be vulnerable to transaction-ordering dependence.
pragma solidity ^0.6.0;
contract TODVulnerability {
uint public balance;
function withdraw(uint amount) public {
require(balance >= amount);
balance -= amount;
}
}
Modify the TODVulnerability
contract from Exercise 1 to use a mutex to prevent transaction-ordering dependence.
pragma solidity ^0.6.0;
contract TODSafe {
uint public balance;
bool mutex;
function deposit() public payable {
require(!mutex); // prevent TOD
mutex = true;
balance += msg.value;
mutex = false;
}
}
Modify the TODVulnerability
contract from Exercise 2 to use the blockhash
function to prevent transaction-ordering dependence.
pragma solidity ^0.6.0;
contract TODSafe {
uint public balance;
function withdraw(uint amount) public {
require(balance >= amount);
require(blockhash(block.number - 1) == 0); // prevent TOD
balance -= amount;
}
}
Write a Solidity contract that defines a function named deposit
that allows a caller to send ether to the contract and increments a balance
variable to track the total amount of ether received. The contract should define a function named withdraw
that allows a caller to withdraw a specified amount of ether from the contract. The contract should use a mutex to prevent transaction-ordering dependence.
pragma solidity ^0.6.0;
contract TODSafe {
uint public balance;
bool mutex;
function deposit() public payable {
require(!mutex); // prevent TOD
mutex = true;
balance += msg.value;
mutex = false;
}
function withdraw(uint amount) public {
require(balance >= amount);
require(!mutex); // prevent TOD
mutex = true;
balance -= amount;
mutex = false;
}
}