Undergasing Attack

What is Undergasing Attack

In an undergasing attack, a dApp defines a private entry function for a user to:

  1. toss a coin (gas cost: 9), then
  2. get a reward (gas cost: 10) if coin=1, or get multiple punishments (gas cost: 100) otherwise.

A malicious user can control its account balance so it covers not more than 108 gas units (or set transaction parameter max_gas=108), then invoke this function, and it will never receive punishments because the publishment path will abort due to insufficient fund.

There are multiple ways to prevent an undergasing attack.

Solution 1: Deposit-execute-refund

If we know that the given any coin toss result, the function costs no more than X gas units (which should include execution gas, io gas and storage fee), here's a way to prevent undergasing attack at transaction execution time.

  1. Fetch transaction parameter max_gas and gas_unit_price.
  2. Ensure max_gas == X.
  3. Before function execution, hold X*gas_unit_price from user account balance.
    • Fail the transaction if user account balance is lower than the required deposit.
  4. After function execution, if the actual gas cost is Y, refund (X-Y)*gas_price to user account.

Aptos allows you to adopt this solution by simply annotating the entry function with #[randomness(max_gas=X)].

  • To guarantee contract safety, randomness API call is not allowed unless the outermost entry function has this annotation.
    • Calling randomness API without this annotation results in error E_API_USE_IS_BIASIBLE. Here is an example.
  • The property max_gas can be omitted and currently defaults to 10000.

module module_owner::game1 {
#[randomness(max_gas=999)]
entry fun make_random_move(player: &signer) {
let coin = randomness::u8_range(0, 2);
if (coin == 0) {
reward(player);
} else {
punish(player);
}
}
}

Solution 2: flip the coin in one transaction, apply the result in another

In the 1st transaction, the user commits the move, stores the random value. In the 2nd transaction, the user makes the move based on the random value stored in the 1st transaction. Commit and reveal must be done in order, the user cannot commit again without revealing the previous commit.


module aptogotchi::main {
struct RandomnessCommitmentExt has key {
revealed: bool,
value: u8,
}
// Throw error if RandomnessCommitmentExt does not exist or is not committed
fun check_randomness_commitment_exist_and_not_revealed(
aptogotchi_address: address
) acquires RandomnessCommitmentExt {
let exist_randomness_commitment_ext = exists<RandomnessCommitmentExt>(aptogotchi_address);
assert!(exist_randomness_commitment_ext, ERANDOMNESS_COMMITMENT_NOT_EXIST);
let random_commitment_ext = borrow_global<RandomnessCommitmentExt>(aptogotchi_address);
assert!(!random_commitment_ext.revealed, EALREADY_REVEALED)
}
// This prevents undergasing attack by committing it first.
entry fun make_random_move_commit(
aptogotchi_address: address
) acquires Aptogotchi, RandomnessCommitmentExt {
check_aptogotchi_exist_and_live(aptogotchi_address);
let exist_randomness_commitment_ext =
exists<RandomnessCommitmentExt>(aptogotchi_address);
if (exist_randomness_commitment_ext) {
let random_commitment_ext =
borrow_global_mut<RandomnessCommitmentExt>(aptogotchi_address);
// Randomness should already be revealed now so it can be committed again
// Throw error if it's already committed but not revealed
assert!(random_commitment_ext.revealed, EALREADY_COMMITTED);
let random_value = randomness::u8_range(0, 2);
// Commit a new random value now, flip the revealed flag to false
random_commitment_ext.revealed = false;
random_commitment_ext.value = random_value;
} else {
let random_value = randomness::u8_range(0, 2);
let aptogotchi_signer_ref = &get_aptogotchi_signer(aptogotchi_address);
move_to(aptogotchi_signer_ref, RandomnessCommitmentExt {
revealed: false,
value: random_value,
});
}
}
// If user doesn't reveal cause it doesn't like the result
// It cannot enter the next round of game
// In our case cannot make another move without revealing the previous move
entry fun make_random_move_reveal(
aptogotchi_address: address,
) acquires Aptogotchi, RandomnessCommitmentExt {
check_aptogotchi_exist_and_live(aptogotchi_address);
let aptogotchi = borrow_global_mut<Aptogotchi>(aptogotchi_address);
check_randomness_commitment_exist_and_not_revealed(aptogotchi_address);
let random_commitment_ext =
borrow_global_mut<RandomnessCommitmentExt>(aptogotchi_address);
if (random_commitment_ext.value == 0) {
aptogotchi.health = aptogotchi.health + 1;
} else {
aptogotchi.health = aptogotchi.health - 1;
if (aptogotchi.health == 0) {
aptogotchi.live = false;
}
};
random_commitment_ext.revealed = true
}
}