docs/content/guides/developer/on-chain-primitives/randomness-onchain.mdx
A Move function can create a new instance of RandomGenerator and use it for generating random values of different types. For example, generate_u128(&mut generator), generate_u8_in_range(&mut generator, 1, 6) or the following example:
entry fun roll_dice(r: &Random, ctx: &mut TxContext): Dice {
let mut generator = new_generator(r, ctx); // generator is a PRG
Dice { value: random::generate_u8_in_range(&mut generator, 1, 6) }
}
Random has a reserved address 0x8. See <UnsafeLink href="/references/framework/sui-framework/sui_sui/random">random.move</UnsafeLink> for the Move APIs that access randomness on Sui.
:::info
Although Random is a shared object, it is inaccessible for mutable operations. Any transaction attempting to modify it fails.
:::
Random dependent flowsBe aware that some resources that are available to transactions are limited. If you are not careful, an attacker can break or exploit your application by deliberately controlling the point where your function runs out of resources. Concretely, gas is such a resource.
The Random API does not automatically prevent this kind of attack, and you must be aware of this subtlety when designing your contracts.
Other limited resources per transaction that you should consider are:
The number of new objects.
The number of objects that can be used, including dynamic fields.
Number of events emitted.
Number of UIDs generated, deleted, or transferred.
Complete list of ProtocolConfig limits.
For many use cases, like when selecting a raffle winner or lottery numbers, this attack is not an issue as the code running is independent of the randomness. However, in other use cases where this attack can be problematic, consider using one of the following approaches.
Split the logic into 2 functions that different transactions must call.
The first transaction, tx1, calls the first function, which fetches a random value and stores it in an object that is unreadable by other commands in tx1. For example, the function might transfer the object to the caller or store the transaction digest and check that it is different on read.
The second transaction, tx2, calls a second function, which reads the stored value and completes the operation. tx2 might fail, but now the random value is fixed and cannot be modified using repeated calls.
It is important that the inputs to the second function are fixed and cannot be modified after tx1, otherwise an attacker can modify them after seeing the randomness committed by tx1.
Gracefully handle the case in which the second step is never completed. For example, you could accomplish this by charging a fee in the first step.
<details> <summary>Example
</summary> <ImportContent source="examples/move/random/random_nft/sources/example.move" mode="code" fun="reveal_alternative2_step1,reveal_alternative2_step2" /> </details>Write the function such that the main processing path does the heavy work, while early-exit paths return quickly. Keep the following in mind:
Both external or native functions can change in the future, potentially resulting in different costs compared to when you conducted your tests.
Profile the transaction to benchmark the costs of a transaction.
UIDs generated and deleted on the same transaction do not count towards the limit on generated or deleted UIDs.
entry functionsWhile composition is very powerful for smart contracts, it opens the door to attacks on functions that use randomness.
Consider for example a betting game that uses randomness for rolling dice:
module games::dice {
...
public enum Ticket has drop {
Lost,
Won,
}
public fun is_winner(t: &Ticket): bool {
match (t) {
Ticket::Won => true,
Ticket::Lost => false,
}
}
/// If you guess correctly the output, then you get a GuessedCorrectly object.
/// Otherwise you get nothing.
public fun play_dice(guess: u8, fee: Coin<SUI>, r: &Random, ctx: &mut TxContext): Ticket {
// Pay for the turn
assert!(coin::value(&fee) == 1000000, EInvalidAmount);
transfer::public_transfer(fee, CREATOR_ADDRESS);
// Roll the dice
let mut generator = new_generator(r, ctx);
if (guess == generator.generate_u8_in_range(1, 6)) {
Ticket::Won
} else {
Ticket::Lost
}
}
...
}
An attacker can deploy the next function:
public fun attack(guess: u8, r: &Random, ctx: &mut TxContext): Ticket {
let t = dice::play_dice(guess, r, ctx);
// revert the transaction if play_dice lost
assert!(!dice::is_winner(&t), 0);
t
}
The attacker can now call attack with a guess and always revert the fee transfer if the guess is incorrect. To protect against composition attacks, define your function as a private entry function so functions from other modules cannot call it.
The Move compiler enforces this behavior by rejecting public functions with Random as an argument.
A similar attack to the one previously described involves PTBs even when play_dice is defined as a private entry function. For example, consider the entry play_dice(guess: u8, fee: Coin<SUI>, r: &Random, ctx: &mut TxContext): Ticket { … } function defined earlier, the attacker can publish the following function and send a PTB with commands play_dice(...), attack(Result(0)) where Result(0) is the output of the first command:
public fun attack(t: Ticket): Ticket {
assert!(!dice::is_winner(&t), 0);
t
}
The attack takes advantage of the atomic nature of PTBs and always reverts the entire transaction if the guess was incorrect without paying the fee. Sending multiple transactions can repeat the attack, each one executed with different randomness and reverted if the guess is incorrect.
:::tip
To protect against PTB-based composition attacks, Sui rejects PTBs that have commands that are not TransferObjects or MergeCoins following a MoveCall command that uses Random as an input.
:::
RandomGeneratorRandomGenerator is secure as long as it is created by the consuming module. If passed as an argument, the caller might be able to predict the outputs of that RandomGenerator instance, for example, by calling bcs::to_bytes(&generator) and parsing its internal state.
The Move compiler enforces this behavior by rejecting public functions with RandomGenerator as an argument.
Random from TypeScriptIf you want to call roll_dice(r: &Random, ctx: &mut TxContext) in module example, use the following code:
const tx = new Transaction();
tx.moveCall({
target: "${PACKAGE_ID}::example::roll_dice",
arguments: [tx.object.random()]
});
...
Having access to random numbers is only one part of designing secure applications. You should also pay careful attention to how you use that randomness.
To securely access randomness:
Define your function as (private) entry.
Prefer generating randomness using function-local RandomGenerator.