Safe Practice of TRON Solidity Smart Contracts: Implement Random Numbers in the Contracts

TRON Core Devs
TRON
Published in
7 min readMar 27, 2020

--

Introduction

In blockchain, Solidity is a widely-used language to write many smart contracts that we see today (such as TRON smart contracts). Much has thus been learned by developers and users.

Developing DApps is somehow an experiment requiring a new way of thinking that differs from other languages. Code errors, often disastrous and barely fixable, make the programming more like hardware programming or financial service programming than Web or mobile development. Here we provide you a few safety practices that will help you develop error-free smart contracts.

This series of articles will focus on the basics of safe Solidity development on TRON blockchain and offer specific cases and codes for a detailed explanation.

Random numbers on blockchains

As a popular segment of TRON DApps, games usually require random numbers to support their inner logic. Dice games, for example, generate random numbers through a random number algorithm. In theory, a good algorithm guarantees the absolute fairness of the game.

Random numbers are important, but to implement a basic one in TRON virtual machine is not as easy as one might imagine. In traditional Internet, random numbers are the cornerstones of cryptography and privacy control. While a traditional pseudorandom number generator depends more or less on the single machine’s physical or computing state, it is not viable on blockchain. Since most programming languages are able to generate random numbers, this might not be intuitive enough for people unfamiliar with blockchain. Blockchain is a distributed system where each node must have its computing results verified and confirmed. This naturally raises a barrier for generating random numbers on the blockchain.

Difficulties to implement random numbers on a blockchain

Predictability

Since everyone can check the source code of smart contracts on a blockchain, the algorithm to generate random numbers is, in fact, accessible to anyone. It only takes connecting a FullNode for a potential attacker to sync the environment that generates the random number. Under appropriate conditions, the attacker will be able to know the number before a transaction is recorded on chain, rendering the number useless.

Verifiability

On TRON blockchain, for any transaction with a random number, there is always an SR who packs it into a block. That is to say, the SR might be tempted to take malicious action if the return is big enough. Therefore, it is necessary for other SRs to be able to verify the random number without knowing it beforehand.

Use public on chain data as random numbers

TVM provides the following special functions to access public on chain data:

  • block.number (uint): current block number
  • block.timestamp (uint): timestamp of the current block (measured in seconds) since unix epoch
  • block.coinbase (address): address of the SR that produces the current block

Given the randomness of this basic information, many contract developers use the three functions above to generate random numbers. For instance, the well-known Fomo3D uses the following function to generate a core random number.

function airdrop()
private
view
returns(bool)
{
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number)

)));
if((seed - ((seed / 1000) * 1000)) < airDropTracker_)
return(true);
else
return(false);

}

We can see that in Fomo3D, the seed is calculated based on timestamp, difficulty, coinbase, gasLimit, number and other block information. These values are fixed once the transaction is packed into the block, except for msg.sender, which is the only variable here. Therefore, attackers can create a contract as demonstrated below to calculate and predict the result, and call Fomo3D's target contract once the correct result is found.

Interface IFomo3d {
function airDropTracker_() external returns (uint256);
function airDropPot_() external returns (uint256);
function withdraw() external;
}
Contract Hack3dExample {
constructor() public payable {
// link to f3d contract instance
IFomo3d fomo3d = IFomo3d (0xa);
// Calculate seed
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp) +
(block.difficulty) +
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)) +
(block.gaslimit) +
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)) +
(block.number)
)));

uint256 tracker = fomo3d.airDropTracker_();
if((seed - ((seed / 1000) * 1000)) >= tracker) {
revert();
}
//do something to break fomo3d
selfdestruct(msg.sender);
}
}

This attacking contract adopts the same random number generation algorithm as in Fomo3D to generate the seed. Meanwhile, it intercepts calls that are unable to claim airdrops. In this way, attackers successfully crack Fomo3D.

In practice, there are multiple ways to prevent smart contracts from calling and predicting contracts, which will be explained in detail in further articles. Even if inter-contract calls are prohibited, the on-chain data remains predictable as long as attackers choose to broadcast transactions that are favorable to them. Contracts with open-source random numbers will be exposed to substantial risk.

function pickWinner() private {
address entropy1 = contestants[uint(block.coinbase) % totalTickets].addr;
address entropy2 = contestants[uint(msg.sender) % totalTickets].addr;
uint256 entropy3 = block.time;
bytes32 randHash = keccak256(entropy1 , entropy2 , entropy3 );

uint winningNumber = uint(randHash) % totalTickets;
address winningAddress = contestants[winningNumber].addr;
RaffleResult(raffleId, winningNumber, winningAddress, entropy1 , entropy2 , entropy3 , randHash);

// Start next raffle
raffleId++;
nextTicket = 0;
blockNumber = block.number;

// gaps.length = 0 isn't necessary here,
// because buyTickets() eventually clears
// the gaps array in the loop itself.

// Distribute prize and fee
winningAddress.transfer(prize);
feeAddress.transfer(fee);
}

This function defines three random number seeds.

  • The first one is block.coinbase = contestants [uint(block.coinbase) % totalTickets].addr
  • The second is msg.sender = contestants [uint(msg.sender) % totalTickets].addr
  • The third is block.time.

However, no matter how many random number sources are defined, the three random number seeds are still predictable locally:

  • block.coinbase represents the address of the SR that packs the current block, which can be derived from the previous block's SR.
  • msg.sender represents the address of the contract caller.
  • block.time can also be derived from that of the previous block, as the time period for block production on TRON is fixed.

In other words, participants can extract and predict the three variables for malicious purposes.

Since totalTickets is fixed by contracts, entropy 1, entropy 2, entropy3 can be calculated by attackers in advance, and the execution results by contracts can even be predicted before they are packed by SRs. In this sense, such an approach to generating random numbers is highly unsafe.

BlockHash as a random number

As BlockHash is unknown before a block is generated, can it be used as the source of random numbers then? Let’s take a look at the following contract codes for generating random numbers:

function rand() public returns(uint256) {

uint256 random = uint256(keccak256(block.blockhash(block.number)));

return random%10;

}

The current block height is available through the variable block.number, but the current block's blockhash remains unknown at execution as the current block is a future block. Contracts can only access the bock's block hash when an SR packs the transaction called by this contract, turning the future block into a current one. Such a calling method will lead to a permanent zero as its result, so some contract developers misinterpret block.blockhash(block.number) - believing the current block's blockhash is known during execution - and use it as a source of random numbers. Unfortunately, the above rand function will always return 0 as a result, which will trigger severe security threats.

Now the question is: can we use the block previous to the current one? ie.

uint256 random = uint256(keccak256(block.blockhash(block.number - 1)));

Although it is theoretically possible to generate a random number in this way, the number would be highly unsafe. Attackers can execute this transaction on the revised FullNode, and selectively broadcast transactions that are favorable to their expectations after acquiring the results, so as to manipulate the execution results of transactions.

Therefore, the better option is to use the blockhash of a future block, ie. generating random numbers through a two-step transaction.

  1. The first transaction triggers contracts, which store the height of a certain future block.
  2. In the second transaction, contracts retrieve the current block’s height. Shall it exceeds the stored future block’s height, a pseudo-random number will be generated through the block’s blockhash.

However, this approach has its limitations: In the TVM, blockhash only acquires data of the latest 256 block heights, so the above method will be rendered invalid when two transactions happen 12.8 minutes (256* 3 seconds) apart.

Random number generation strategy based on hash-commit-reveal

hash-commit-reveal is considered by many contract developers the best implementation method for random numbers and has been widely applied to a large number of DApps. Let’s take a look at how it works.

The essence of hash-commit-reveal is that the contract caller and the random number provider (usually some external oracles) generate random numbers through a series of contracts on the TRON blockchain platform. The process looks like this:

  1. External oracles (secret Signer) randomly generate a random number reveal while calculating the function commit=keccak256(reveal) and providing a commitment to the said reveal. Then, according to the current block height, the oracles will set a last block height commitLastBlock used by the commitment, sign the combination of commitLastBlock and commit to achieve sig, and send (commit, commitLastBlock, sig) together to the contract callers.
  2. After the contract caller receives (commit, commitLastBlock, sig), send the specific transactions (as annotated below) to the smart contract.
  3. After the contract caller submits the transaction, the external oracle (secretSigner) sends reveal, the public commit value of the transaction, to the blockchain. The contract calculates random numbers like this: random_number=keccak256(reveal,block1.hash).

This manner of random number generation, although relies on external oracles, guarantees fairness to another extent, because oracles do not know the action plans of the contract caller even though they can control the random numbers.

Take Dice2-win contract as an example.

https://github.com/dice2-win/contracts/blob/master/Dice2Win.sol

The drawback of such a method also has its obvious flaws: it highly relies on the callback of the contracts by oracles (secretSigner). Therefore, oracles could choose to maliciously callback.

Summary

Blockchain is a decentralized system. Theoretically, the random numbers generated are fairer and more transparent than a centralized system but are more prone to hackers because of its decentralized environment and public algorithm. In addition, the remaining balance of the contract is also open. When the balance reaches a certain threshold, potential malicious behaviors from SR should be taken into consideration. Therefore, it is necessary to select a random number solution based on the scenario. In blockchains, there are a large number of random number scenarios, that are essential to the mass adoption of blockchain applications. It is safe to say that more developers and teams will partake in building and perfecting the random number generation mechanism of blockchains in the future.

References

https://www.freebuf.com/vuls/179173.html
http://www.jouypub.com/2018/7665609b0126606e2ae90ad7cfcdce8b/

For more information

Github: https://github.com/tronprotocol

Telegram: https://t.me/troncoredevscommunity

--

--