nearcore icon indicating copy to clipboard operation
nearcore copied to clipboard

Difficulties Managing Gas Allocation for Chained Cross-Contract Calls

Open rm-Umar opened this issue 6 months ago • 2 comments

I’m currently developing a smart contract that requires multiple chained cross-contract calls. Specifically, I am using ft_transfer_call in a sequence of operations. The main issue I’m encountering is related to gas management, particularly when trying to allocate and efficiently utilize gas across these chained calls. I need to do multiple cross contract calls but the maximum gas limit is 300Tgas Problem Details:

1. Initial Gas Assignment:

  • I need to assign a minimum of 85 TGas upfront for ft_transfer_call to ensure it doesn't fail. However, the actual gas usage is around 20 TGas, leaving a significant amount of unused gas.

Example Code:

Promise::new(AccountId::from_str(&token_in.clone()).unwrap())
    .function_call(
        "ft_transfer_call".to_string(),
        json!({
            "receiver_id": receiver_id,
            "amount": "100",
            "msg": message
        })
        .to_string()
        .into_bytes(),
        NearToken::from_yoctonear(1),
        Gas::from_tgas(85), // Assigning 85 TGas upfront
    );

2. Real-world Example with Excessive Gas Allocation:

  • Here’s an actual call where I needed to assign 120 TGas to the ft_transfer_call, but it only consumed 25.2 TGas. This leaves a significant portion of gas unused, which could have been allocated to subsequent calls in the chain.
$ near contract call-function as-transaction eth.fakes.testnet ft_transfer_call \
  json-args '{
    "receiver_id": "ref-finance-101.testnet",
    "amount": "1000000000000000000",
    "msg": "{\"force\":0,\"actions\":[{\"pool_id\":410,\"token_in\":\"eth.fakes.testnet\",\"token_out\":\"dai.fakes.testnet\",\"amount_in\":\"1000000000000000000\",\"min_amount_out\":\"0\"},{\"pool_id\":849,\"token_in\":\"dai.fakes.testnet\",\"token_out\":\"wrap.testnet\",\"min_amount_out\":\"0\"},{\"pool_id\":12,\"token_in\":\"wrap.testnet\",\"token_out\":\"banana.ft-fin.testnet\",\"min_amount_out\":\"0\"}]}",
    "receiver_id": "ref-finance-101.testnet"
  }' \
  prepaid-gas '120.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as umar25.testnet \
  network-config testnet sign-with-legacy-keychain send

--- Logs ---------------------------
Logs [eth.fakes.testnet]:
  Transfer 1000000000000000000 from umar25.testnet to ref-finance-101.testnet
Logs [ref-finance-101.testnet]:
  Swapped 1000000000000000000 eth.fakes.testnet for 31934400827911866534 dai.fakes.testnet
  Swapped 31934400827911866534 dai.fakes.testnet for 4897188229574644179151 wrap.testnet
  Swapped 4897188229574644179151 wrap.testnet for 15 banana.ft-fin.testnet
Logs [umar25.testnet]:   No logs

--- Result -------------------------
"1000000000000000000"
------------------------------------

Gas burned: 25.2 Tgas
Transaction fee: 0.0024293414532766 NEAR
  • In this case, the actual gas consumed was only 25.2 TGas, even though I assigned 120 TGas upfront. This inefficiency makes it difficult to manage gas dynamically across a series of chained contract calls.

3. Chaining Calls with NEP-0264:

  • I tried using NEP-0264 to chain the calls and pass the remaining gas to subsequent functions using .then. However, the remaining gas is not passed directly; instead, gas is allocated based on weights, making it difficult to manage the gas efficiently across multiple chained calls.

Example Scenario:

let initial_gas = env::prepaid_gas() - env::used_gas(); // prepaidgas - used for basic operations
Promise::new(AccountId::from_str(&token_in.clone()).unwrap())
    .function_call_weight(
        "ft_transfer_call".to_string(),
        json!({
            "receiver_id": receiver_id,
            "amount": "100",
            "msg": message
        })
        .to_string()
        .into_bytes(),
        NearToken::from_yoctonear(1),
        Gas::from_tgas(initial_gas), // Trying to use the full gas allocation
        near_sdk::GasWeight(1),
    )
    .then(
        Promise::new(env::current_account_id())
            .function_call_weight(
                "callback_check_gas_1".to_string(),
                vec![],
                NearToken::from_near(0),
                Gas::from_gas(0), // Remaining gas should be passed here, but it's not direct
                near_sdk::GasWeight(1),
            )
    );

4. Callback Hell and Gas Allocation:

  • I attempted to create a "callback hell" where each callback after a successful ft_transfer_call initiates the next transfer. However, this approach led to issues where either the initial ft_transfer_call failed due to insufficient gas or the subsequent calls in the callbacks failed due to inadequate gas allocation.

Example of Callback Hell:

pub fn callback_check_gas_1(&self) {
    let remaining_gas = env::prepaid_gas() - env::used_gas(); // Remaining gas after first call
    env::log_str(&format!("Gas remaining after callback 1: {} TGas", Gas::from_gas(remaining_gas).as_tgas()));

    match env::promise_result(0) {
        PromiseResult::Successful(_) => {
            env::log_str("First call successful");
            self.call_second_promise(); // Call next function with remaining gas
        },
        _ => {
            env::panic_str("First swap operation failed. Withdrawal will not be processed.");
        }
    }
}

Problem Summary:

  • Gas Allocation Issue: The difficulty is in efficiently managing gas across multiple chained calls. With NEP-0264, the remaining gas is not passed directly, and managing it through callback chains is proving challenging.
  • Inefficiency: Due to the need to assign 85-120 TGas upfront but only using a fraction of it, the remaining gas is not effectively utilized in subsequent calls. -Failure of Subsequent Calls: Depending on how gas is allocated—either more to the initial call or the callback—some parts of the chain fail due to insufficient gas.

What I’m Looking For:

  • Optimal Solution: I am seeking a way to dynamically allocate and pass all remaining gas from one contract call to the next within a chained sequence.
  • Alternative Approaches: Suggestions or patterns that could help ensure each call in the sequence has sufficient gas without leaving significant unused gas or causing failures in subsequent operations.

Any guidance on how to better handle gas allocation in these scenarios, or enhancements to NEP-0264 to facilitate this, would be greatly appreciated.

rm-Umar avatar Aug 21 '24 06:08 rm-Umar