Common Issues

To Share and +4 nLEARNs

Issue 1: Exceeded the prepaid gas failure

Let’s consider the following block of code:

// Gas needed for common operations.
pub const GAS_FOR_COMMON_OPERATIONS: Gas = Gas(30_000_000_000_000);

// Gas reserved for the current call.
pub const GAS_RESERVED_FOR_CURRENT_CALL: Gas = Gas(20_000_000_000_000);

#[ext_contract(ext_storage_management)]
pub trait StorageManagement {
    fn storage_balance_of(&self, account_id: AccountId) -> Option<StorageBalance>;
}

#[ext_contract(ext_self)]
pub trait ExtSelf {
    fn callback_after_storage_balance_of();
}

pub fn check_storage_balance(
    &mut self,
    token_id: &AccountId,
    account_id: &AccountId,
) -> PromiseOrValue<()> {
    // .................
    // {block of code 1}
    // .................

    let call = ext_storage_management::storage_balance_of(
        account_id.clone(),
        token_id.clone(),
        0,
        GAS_FOR_COMMON_OPERATIONS,
    );
    let REMAINING_GAS: Gas = env::prepaid_gas() - env::used_gas();
    let callback = ext_self::callback_after_storage_balance_of(
        env::current_account_id(),
        0,
        REMAINING_GAS
    );
    call.then(callback);

    // .................
    // {block of code 2}
    // .................
}

Legend:

  • check_storage_balance represents the current call.
  • storage_balance_of represents a cross-contract call made from the current call.
  • callback_after_storage_balance_of represents the callback that will get executed once a response is received from storage_balance_of.
  • env::prepaid_gas() represents the gas paid by the caller to execute the check_storage_balance call. If the caller doesn’t specify any value for the gas, a default value of 300_000_000_000_000 will automatically be deducted from his NEAR wallet.
  • env::used_gas() represents the gas used so far. It takes into account all the gas that got spent since the beginning of the check_storage_balance call.

At first glance, everything looks good, but if we are calling check_storage_balance with 300_000_000_000_000 gas, we are running into the following error:

status: Failure(Action #0: Exceeded the prepaid gas.)

The error seems to occur while executing callback_after_storage_balance_of. This is particularly odd, giving the fact that we allocate REMAINING_GAS to this callback which is a huge amount of gas that should cover all the costs.

So why is this happening? To find out, let’s check how much gas is consumed at different points in time:

pub fn check_storage_balance(
    &mut self,
    token_id: &AccountId,
    account_id: &AccountId,
) -> PromiseOrValue<()> {
    // .................
    // {block of code 1}
    // .................

    let gas_before_call = env::used_gas();
    let call = ext_storage_management::storage_balance_of(
        account_id.clone(),
        token_id.clone(),
        0,
        GAS_FOR_COMMON_OPERATIONS,
    );
    let gas_before_callback = env::used_gas();
    let REMAINING_GAS: Gas = env::prepaid_gas() - env::used_gas();
    let callback = ext_self::callback_after_storage_balance_of(
        env::current_account_id(),
        0,
        REMAINING_GAS
    );
    let gas_after_callback = env::used_gas();
    call.then(callback);
    panic!(
        "n{:?}n{:?}n{:?}n{:?}",
        env::prepaid_gas(),
        gas_before_call,
        gas_before_callback,
        gas_after_callback
    );

    // .................
    // {block of code 2}
    // .................
}

When panicking, the following result is displayed:

status: Failure(Action #0: Smart contract panicked: panicked at '
Gas(300000000000000)
Gas(1653585699141)
Gas(1680501847233)
Gas(1782487215252)')

where:

Gas(300000000000000) is the prepaid gas: 300_000_000_000_000 
Gas(1653585699141) is the gas before the first call: 1_653_585_699_141
Gas(1680501847233) is the gas after the first call and before the callback: 1_680_501_847_233
Gas(1782487215252) is the gas after the callback: 1_782_487_215_252')

To our surprise, neither the gas allocated for the first call (GAS_FOR_COMMON_OPERATIONS) nor the gas allocated for the callback (REMAINING_GAS) are taken into consideration when calling env::used_gas() in the current function. Otherwise, gas_before_callback would be equal to gas_before_call + GAS_FOR_COMMON_OPERATIONS and gas_after_callback would be 300_000_000_000_000 since we allocate all the remaining gas to the callback.
With this information in mind, we can explain why the error happens. When callback_after_storage_balance_of gets executed, we don’t have enough gas available because GAS_FOR_COMMON_OPERATIONS got consumed by the storage_balance_of call, but it was not tracked by env::used_gas().

To fix it, make the following change:

    let REMAINING_GAS: Gas =
        env::prepaid_gas() - env::used_gas() - GAS_FOR_COMMON_OPERATIONS;
    let callback = ext_self::callback_after_storage_balance_of(
        env::current_account_id(),
        0,
        REMAINING_GAS
    );

Now everything should be good, right? Wrong. If we call our check_storage_balance function, we still run into:

status: Failure(Action #0: Exceeded the prepaid gas.)

This happens because we allocated all the remaining gas to the callback, not taking into consideration the last part of the function which is {block of code 2}. Since the callback is async, {block of code 2} gets executed before the callback and it needs some gas for processing. When the time comes to execute the callback, we no longer have the promissed gas (env::prepaid_gas() - env::used_gas() - GAS_FOR_COMMON_OPERATIONS) because {block of code 2} also consumed some.

To address this issue, make the following change:

    let REMAINING_GAS: Gas = env::prepaid_gas()
        - env::used_gas()
        - GAS_FOR_COMMON_OPERATIONS
        - GAS_RESERVED_FOR_CURRENT_CALL;
    let callback = ext_self::callback_after_storage_balance_of(
        env::current_account_id(),
        0,
        REMAINING_GAS
    );

As you can see, we reserve some amount of gas (GAS_RESERVED_FOR_CURRENT_CALL) for any computations that might happen after creating the callback. As long as {block of code 2} does not consume more gas than GAS_RESERVED_FOR_CURRENT_CALL, we should have enough gas to execute the callback and avoid any Exceeded the prepaid gas errors.

Got a question?

Ask it on StackOverflow!

Scroll to Top