Complicated register() payable method with loops throw error: contract call run out of gas

Hello everyone! I try to explain my trouble...
I have a contract that has payable register() method. In that register() method when it called going on loop for already registered users and calculates based on that loop - amount of deposit (it's payable method). Because of it, I cannot calculate gas fee for advance. Because between 2 calls (calculate gas and send bnb with that gas) can be registered another users...
But if I call register method with gas, without in advance calculation gas, I've got errors:
if I send gas more than 30000000:

MetaMask - RPC Error: [ethjs-query] while formatting outputs from RPC '{"value":{"code":-32603,"data":{"code":-32000,"message":"Transaction gas limit is 31000000 and exceeds block gas limit of 30000000"}}}' 

If I send gas 30000000 and less:

MetaMask - RPC Error: [ethjs-query] while formatting outputs from RPC '{"value":{"code":-32603,"data":{"code":-32603,"message":"Error: Transaction reverted: contract call run out of gas and made the transaction revert","data":{"txHash":"0x3b6dd13bac2*****************************************308a05c0d589"}}}}' 

How can I solve this case? Help, please...

Please share all the relevant code (and only the relevant code).
It is rather impossible to address your question without it.

Hey @adminoid,

The problem I see is not only related to gas estimations but also to scalability.

Sounds like your implementation is O(n) in complexity, where the n is the number of registered users, so you'll only be able to scale up to the block gas limit (30_000_000 gas units).

So, to solve your problem, you may need to design your contract with a cached amount of deposit so you don't look every time.

Not only it becomes hard to predict the gas to be used within the call, but a transaction can be frontrun, increasing the loop size previously and failing even if the previous estimation was correct.

If you can share you contract details or script in which you're doing your call, it'd be helpful

Best

@ernestognw @barakman

This is simplified code of contract Core with register() method. And behind the scene is one more contract MatrixTemplate:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "./MatrixTemplate.sol";

contract Core {
    uint payUnit = 0.01 * (10 ** 18); // first number is bnb amount

    uint maxLevel = 19; // 0..19 (total 20)

    // array of matrices (addresses)
    address[20] Matrices;

    struct UserGlobal {
        uint gifts;
        uint level;
        address whose; // whose referral is user
        bool isValue;
    }

    mapping(address => UserGlobal) AddressesGlobal;

    constructor() payable {
        uint256 startGas = gasleft();
        zeroWallet = msg.sender;

        console.log("constructor(), msg.sender is", msg.sender);

        // initialize 20 matrices
        uint i;
        for (i = 0; i <= maxLevel; i++) {
            MatrixTemplate matrixInstance = new MatrixTemplate(msg.sender, i, address(this));
            Matrices[i] = address(matrixInstance);
        }

        AddressesGlobal[msg.sender] = UserGlobal(0, 0, maxLevel, zeroWallet, true);
    }

    function register(address whose) external payable {
        require(!AddressesGlobal[msg.sender].isValue, "user already registered");

        if (AddressesGlobal[whose].gifts >= payUnit) {
            AddressesGlobal[whose].gifts = AddressesGlobal[whose].gifts.sub(payUnit);
        } else {
            payable(this).transfer(payUnit);
        }

        AddressesGlobal[msg.sender] = UserGlobal(0, 0, 0, whose, true);
        MatrixTemplate(Matrices[0]).register(msg.sender);
    }
}

What does MatrixTemplate do?

It's also complicated function with loop

for (uint i = 2; i <= 5; i++) { ... }

Core contract process all hight level, MatrixTemplate process only one level of 20 (0..19) levels
Exist one Core contract, than process whole things, and 20 MatrixTemplate instances for 20 levels

I've got line, that throw this error. I found this line by in turn commented lines and recompile contracts.

payable(this).transfer(payUnit); // if I just comment this line all working fine

I want with this line top up deposit via transaction from user to the contract

Another question, what is the proper way to withdraw from user wallet to contract address (eth, bnb and same, in my case - bnb) by calling by user the method of a contract through metamask?

There are two problems here:

  1. This line is completely futile, as it transfers ETH from the contract to itself!
  2. If the contract doesn't have a fallback function or a receive function, then any attempt to transfer ETH to it (from any address whatsoever), would cause the transaction to revert. So obviously, as you attempt to execute the (futile) transfer of ETH from the contract to itself, your transaction reverts.

I can not find any example for call contract method and transfer to contract with that calling eth from user wallet to contract. I already have receive method that work correctly, but I need also with money transfer pass additional parameters to the contract.

Can you get me any example for my case?

If you could start by rephrasing this obfuscated description into something that makes sense...


If you are looking for a way to "make" the caller of the register method transfer payUnit ETH to your contract, then you only need to verify that the caller has done so (inside your contract's method):

require(msg.value == payUnit, "did not pay the required amount");

Or alternatively:

if (msg.value > payUnit)
    payable(msg.sender).transfer(msg.value - payUnit);
else
    require(msg.value == payUnit, "did not pay the required amount");
1 Like

Hello again! One another question:
I don't know in advance - register is a paid registration or not.
Can I get money from wallet, then if registration is free - return eth back to user wallet?

Can you help me with example of contract method, that return eth/bnb/... from contract address to the user?

Yeah man, look at the code right above your message:

payable(msg.sender).transfer(msg.value);

For any token, unlike ETH, the user isn't supposed to send it in advance, only to approve your contract to take it (transferFrom) the user's wallet. So there's nothing special that you need to do in this case.

1 Like

Thank you very much for your help!

I would want make conclusion...

I understand correctly - call contract method register() must be with a value that will transferred from user wallet to contract address (in my case bnb for bsc network, like eth in ethereum network).
No other ways to transfer bnb from client through contract method calling?

If that is true, then I must call my register() method with value equal registration amount, but if available free registration I make money back with - payable(msg.sender).transfer(msg.value);

That's it?

Yeah, sounds like that's it, though with regards to with value equal registration amount, I'd say that you could also allow the value to be higher than the required amount, and then send back the diff.
See the two options that I have described in this message (above).

1 Like