The timestamp_randomness rule detects use of env.ledger().timestamp() as a source of randomness entropy. Block timestamps are not secret and can be nudged by validators within a small window, making any randomness derived solely from them manipulable.
High — exploitable on-chain; a validator or a well-timed transaction can influence the outcome.
The rule fires when all of the following hold:
env.ledger().timestamp()is called inside a function or expression.- The context is randomness-related, identified by name:
- Function name contains
rand,seed,pick, orwinner(case-insensitive), OR - A variable binding on the left-hand side of an assignment contains one of those keywords.
- Function name contains
Non-sensitive uses of env.ledger().timestamp() (deadline checks, expiry guards, audit logs) are not flagged.
pub fn pick_winner(env: Env, participants: Vec<Address>) -> Address {
// UNSAFE: timestamp is predictable entropy
let idx = env.ledger().timestamp() % participants.len() as u64;
participants.get(idx as u32).unwrap()
}pub fn initialize_game(env: Env) {
// UNSAFE: 'seed' bound to timestamp
let seed = env.ledger().timestamp();
env.storage().persistent().set(&symbol_short!("seed"), &seed);
}pub fn roll_dice(env: Env) -> u64 {
let rand = env.ledger().timestamp() % 6 + 1;
rand
}pub fn check_expiry(env: Env, deadline: u64) -> bool {
// SAFE: not randomness — pure time comparison
env.ledger().timestamp() > deadline
}pub fn record_action(env: Env) {
let ts = env.ledger().timestamp();
env.storage().persistent().set(&symbol_short!("LAST_TS"), &ts);
}Soroban ledger timestamps represent the UNIX timestamp of the ledger close. Validators have limited but real influence over this value:
- A validator can delay or advance a ledger close within protocol bounds.
- An attacker who can predict or influence the timestamp can reverse-engineer the outcome of any computation based solely on it.
- For lottery/NFT draw/reward distribution use cases this translates to direct financial manipulation.
pub fn pick_winner(
env: Env,
participants: Vec<Address>,
vrf_proof: BytesN<64>,
) -> Address {
// Verify the VRF proof, then derive index from it
let idx = derive_index_from_vrf(&vrf_proof, participants.len() as u64);
participants.get(idx as u32).unwrap()
}pub fn pick_winner(env: Env, participants: Vec<Address>) -> Address {
let mut prng = env.prng();
// Combine timestamp with ledger sequence and contract address hash
let entropy = env.ledger().timestamp()
^ env.ledger().sequence() as u64
^ env.current_contract_address().to_string().len() as u64;
prng.reseed(entropy);
let idx = prng.gen_range(0..participants.len() as u64);
participants.get(idx as u32).unwrap()
}Have participants commit a secret hash off-chain; reveal it on-chain before the draw. The XOR of all revealed secrets becomes the seed. This eliminates any single party's ability to manipulate the outcome.
- S018 — Unsafe PRNG (
unsafe_prng): flagsenv.prng()used in state-critical code without reseeding. See unsafe-prng.md. - S006 — Unsafe Pattern (
UNSAFE_PATTERN): generic unsafe runtime patterns.
- Soroban Ledger API
- OWASP: Insufficient Randomness
- CWE-338: Use of Cryptographically Weak PRNG
- SWC-120: Weak Sources of Randomness
# Run the rule unit tests
cargo test -p sanctifier-core timestamp_randomness
# Test against the fixture contract
cargo test -p sanctifier-core -- timestamp_randomnessThe rule is enabled by default. To disable it selectively in .sanctify.toml:
[rules]
timestamp_randomness = "off"