Developers Forum for XinFin XDC Network

Discussion on: Issue Report – Unexpected Gas Behavior on XDC Network

Collapse
vikash_kumar_5e685df6ec9c profile image
Vikash Kumar Author

Thank you for the calculation (12.5 GWei × 29,355,011 gas = 0.3669376375 XDC).
We understand how this value is derived.

However, our main concern is not the math, but the fact that the transaction cost has been continuously increasing over time, even though:

Our smart contract code has not changed

Our input data size is the same

Our function call parameters are identical

We are performing the exact same postCertificate() operation each time

What we observed

At the beginning:

Gas used per transaction was much lower

Total transaction fee was around ~0.003 XDC

As we continued sending transactions, the gas behavior changed:

Gas used kept increasing with each certificate

Transaction cost kept rising

Now each transaction costs ~0.36 XDC, which is more than 100× higher than before

Collapse
gzliudan profile image
Daniel Liu • Edited on

Let me look into your smart contract code on xdcscan explorer.

Collapse
gzliudan profile image
Daniel Liu • Edited on

Why gasUsed grows when calling postCertificate:

  • On-chain state growth (dynamic arrays): studentInfo is a mapping whose value (Student) contains dynamic arrays (uri, instituteName). As these arrays grow, any operation that reads/writes many elements costs more gas.
  • Whole-struct assignment triggers array copy: Code that does something like studentInfo[addr] = Student(..., studentInfo[addr].uri, ...) forces the compiler to copy storage arrays into memory and back. That copy cost scales linearly with array length, so per-tx gasUsed increases as arrays get longer.
  • Duplicated string storage (embedded struct): Cert embeds a full Institute (which contains strings). Each certificates[hash] = Cert(institutes[id], ...) duplicates those strings into storage for every certificate, multiplying SSTOREs and increasing gas usage.
  • More SSTOREs when state expands: Adding certificates/URIs creates new storage slots (0→non-zero) which are expensive SSTORE operations. As more slots are consumed over time, transactions that create or update them consume more gas.
  • Larger event/log payloads: Events like CertificatePosted(string hash, ...) include strings. If logged strings or number of logs grow, log-writing gas rises.
Collapse
gzliudan profile image
Daniel Liu • Edited on

A better version:

function postCertificate(
        string memory _studentname,
        address _studentAdd,
        string memory _uri,
        string memory _hash,
        string memory _type,
        string memory _issuerName
    ) external onlyInstitute {
        bytes32 byte_hash = stringToBytes32(_hash);
        require(
            certificates[byte_hash].timestamp == 0,
            "Certificate with this hash already exists"
        );

        uint256 id = institute_ID[msg.sender];

        // write certificate (keeps existing Cert layout)
        certificates[byte_hash] = Cert(
            institutes[id],
            _studentname,
            _studentAdd,
            _hash,
            _type,
            _uri,
            block.timestamp,
            msg.sender,
            _issuerName
        );

        // update student info in-place to avoid copying storage arrays to memory
        studentInstituteId[_studentAdd] = id;
        Student storage s = studentInfo[_studentAdd];
        s.uri.push(_uri);
        uint256 len = s.instituteName.length;
        if (
            len == 0 ||
            !compareStrings(
                s.instituteName[len - 1],
                institutes[id].instituteName
            )
        ) {
            s.instituteName.push(institutes[id].instituteName);
        }
        s.name = _studentname;
        s.studentAdd = _studentAdd;

        emit CertificatePosted(_hash, id, _studentname, _issuerName);
    }
Enter fullscreen mode Exit fullscreen mode

What I changed:

  • Replaced the whole-struct assignment that copied studentInfo[_studentAdd].instituteName and .uri into a new Student with an in-place storage update:
    • `Student storage s = studentInfo[_studentAdd];
    • s.uri.push(_uri);
    • s.name = _studentname;
    • s.studentAdd = _studentAdd;
  • Kept certificate storage logic intact (still assigns Cert to certificates[byte_hash]) to avoid larger structural changes in this patch.
  • This removes the expensive pattern of copying storage arrays into memory & back, preventing gasUsed from growing linearly with array length for this function.

Why this helps

  • Avoids copying dynamic arrays from storage to memory and then back on assignment — that copy cost grows with array length and was the main cause of per-tx gas inflation.
  • Now only the necessary storage writes are performed: push new URI, optionally push instituteName if new, set simple fields. This reduces both SLOAD/SSTORE counts and memory copy overhead.

Next recommendations (optional)

  • Convert Cert.institute to store only instituteId (uint256) instead of embedding Institute to avoid repeated string writes for every certificate.
  • Apply the same storage-in-place pattern to bulkUpload.
  • Consider changing URI storage to mapping(uint256 => string) + counter for very large histories.