Search code examples
node.jstypescriptethereumweb3jsganache

Using Web3 the value sent as payable (msg.value) is equal to 0


We have a smart contract that is supposed to receive payments in order to mint token; it follows the ERC1155 standards as it extends ERC1155URIStorage.

Here is a sample of the contract we deploy:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import '@openzeppelin/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol';
import '@openzeppelin/contracts/access/Ownable.sol';

contract TEST is ERC1155URIStorage, Ownable {
  constructor(string memory uri_, string memory baseUri_) ERC1155(uri_) {
    _setBaseURI(baseUri_);
  }

  function safeMintMarketplace(uint256 tokenId_) external payable {
    require(msg.value == 1000000000000000000000, Strings.toString(msg.value));
    _mint(msg.sender, tokenId_, 1, '');
  }
}

For the sake of simplicity I just want to be able to mint if the payable amount is 1eth or 10^18 wei as seen by the require statement that will revert with the msg.value attached.

We are using some web3 helper function to deploy our contract and call the functions, here is a sample of those helpers

import { AbiItem } from 'web3-utils';
import { TransactionReceipt, TransactionConfig } from 'web3-core';
import * as artifact from '@artifacts/TestContract.sol/TEST.json';
import Web3 from 'web3';

function getWeb3() {
  return new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:7545'));
}

export async function testDeploy(uri: string, token_uri: string, private_key: string): Promise<TransactionReceipt> {
  // get web3 instance
  const w3 = getWeb3();
  const { address } = w3.eth.accounts.privateKeyToAccount(private_key);

  // get contract to deploy
  const contract = new w3.eth.Contract(artifact.abi as AbiItem[]);

  // estimate gas
  const gas = await contract.deploy({ data: artifact.bytecode, arguments: [uri, token_uri] }).estimateGas({ from: address });

  // encoded data to deploy contract
  const data = contract.deploy({ data: artifact.bytecode, arguments: [uri, token_uri] }).encodeABI();

  // construct transaction (basic config + data)
  let transaction: TransactionConfig = {
    chainId: 5777,
    data: data,
    gas: gas + Math.ceil(gas * 0.1),
  };

  // sign transaction with private key
  const signature = await w3.eth.accounts.signTransaction(transaction, private_key);

  // send raw transaction
  const tx = await w3.eth.sendSignedTransaction(signature.rawTransaction || '');

  return tx;
}

export async function testMintMarketplace(contract_address: string, private_key: string, price: number, token_id: number): Promise<any> {
  // get web3
  const w3 = getWeb3();
  const { address } = w3.eth.accounts.privateKeyToAccount(private_key);

  // create web3 contract object
  const contract = new w3.eth.Contract(artifact.abi as AbiItem[], contract_address);

  // estimate gas
  const gas = await contract.methods.safeMintMarketplace(token_id).estimateGas({ from: address });

  // encoded data to represent the function call
  const data = contract.methods.safeMintMarketplace(token_id).encodeABI();

  // convert the price from eth to wei
  const value = Web3.utils.toWei(price.toString(), 'ether');

  //create the transaction
  let transaction: TransactionConfig = {
    chainId: 5777,
    to: contract_address,
    data: data,
    value: value,
    gas: gas + Math.ceil(gas * 0.1),
  };

  // sign transaction with private key
  const signature = await w3.eth.accounts.signTransaction(transaction, private_key);

  const tx = await w3.eth.sendSignedTransaction(signature.rawTransaction || '');

  return tx;
}

As you can see in the testMintMarketplace function we are passing the payable amount withing the value parameter of the TransactionConfig object.

Now if we run some test and try to call testMintMarketplace

import { testDeploy, testMintMarketplace } from '@utils/web3/test-web3';
import { TransactionReceipt } from 'web3-core';

//define test variables
//private key of owner
const pk = 'private_key_1_from_ganache';
//private key of user minting
const pk2 = 'private_key_2_from_ganache';
//token_id
const token_id = 123;
// price of token in eth
const price = 1;

async function main() {
  try {
    const tx: TransactionReceipt = await testDeploy('uri', 'token_uri', pk);
    console.log(`contract published: ${tx.contractAddress}`);

    let tx4 = await testMintMarketplace(tx.contractAddress as string, pk2, price, token_id);
    console.log(`token minted market: ${tx4.transaction_hash}`);

    return;
  } catch (error: any) {
    console.log(error);
    return;
  }
}

main();

The call to the function fails and revert with 0: we are returning msg.value as a String inside the require statement.

Now working on remix IDE for example this call passes normally, so the issue is most definitely in the way we are passing the payable amount.

Even if we pass a string in Wei such as '1000000000000000000', we still get 0 in the revert.

Also it it worth noting that if we remove the require statement the token is actually minted and id we look in the ganache transaction we can see the amount of 1eth being attached at the value.

The same issue happens when running on testnets such as Goerli and Sepolia.

Someone suggested we increase the gas fees, we did so but still same issue.

So how should we pass the payable amount using web3?


Solution

  • The issue was with the gas estimate that we were getting, as we did not include value for it so it was being too low.

    So by changing

    //estimate gas
    const gas = await contract.methods.safeMintMarketplace(token_id).estimateGas({ from: address });
    

    To:

    //estimate gas
    const gas = await contract.methods.safeMintMarketplace(token_id).estimateGas({ from: address, value:value });
    

    everything worked fine