Ethers.js: Contract with function overloading

Created on 28 Jan 2019  Β·  21Comments  Β·  Source: ethers-io/ethers.js

Hi,

A contract that I'm testing has different interfaces for same function name:

// Order that appers on ABI
initialize(string _name, string _symbol);
initialize(string _name, string _symbol, address _registry);
initialize(string _sender);
initialize();

I'm getting the error: Error: incorrect number of arguments when I'm trying call the second function.
Seems that when Contract is reading the ABI and it's only considering the first interface and ignoring the others.

https://github.com/ethers-io/ethers.js/blob/c2ce59f95e2b53cb4121c9e941a312913f4fc279/src.ts/utils/interface.ts#L328
https://github.com/ethers-io/ethers.js/blob/442553620a5eb466ca20c2f81a8e5685f14b7bcd/src.ts/contract.ts#L431

Notes:

  • I'm receving "WARNING: Multiple definitions" as well.
  • With web3js I can call all of interfaces.

Thanks!

discussion

Most helpful comment

If you are using function overloading, the first definition gets the "bare" name. You will need to specify the specific function you wish to call, for example:

// initialize(string _name, string _symbol);
contract.initialize(name, symbol).then( ... );
contract["initialize(string,string)"](name, symbol).then( ... );

// initialize(string _name, string _symbol, address _registry);
contract["initialize(string,string,address)"](name, symbol, address).then( ... );

// initialize(string _sender);
contract["initialize(string)"](sender).then( ... );

// initialize();
contract["initialize()"]().then( ... );

You should be able to print contract.functions as well to get a list of all available functions, overloaded or otherwise.

This is more of a limitation of un-typed languages (well, any language which doesn't have at least the subset of types that Solidity has). Make sense? :)

All 21 comments

If you are using function overloading, the first definition gets the "bare" name. You will need to specify the specific function you wish to call, for example:

// initialize(string _name, string _symbol);
contract.initialize(name, symbol).then( ... );
contract["initialize(string,string)"](name, symbol).then( ... );

// initialize(string _name, string _symbol, address _registry);
contract["initialize(string,string,address)"](name, symbol, address).then( ... );

// initialize(string _sender);
contract["initialize(string)"](sender).then( ... );

// initialize();
contract["initialize()"]().then( ... );

You should be able to print contract.functions as well to get a list of all available functions, overloaded or otherwise.

This is more of a limitation of un-typed languages (well, any language which doesn't have at least the subset of types that Solidity has). Make sense? :)

Hi @ricmoo thanks for response,

  1. contract.functions works and using your code I've was able to call them. Maybe it'll solve my specific problem. Thanks.

  2. I'm not sure if is a javascript limitation because web3js works on the same scenario. In the case where we have two functions with the same name, the same number of params and different types, I agree with you, but itsn't the case.

I've written a sample contract with some test cases:

SampleContract.sol

pragma solidity ^0.5.0;

contract SampleContract {

  function overloading() public pure returns(uint) {
    return 1;
  }

  function overloading(string memory a) public pure returns(uint) {
    return 2;
  }

  function overloading(string memory a, string memory b) public pure returns(uint) {
    return 3;
  }
}

SampleContrat.test.js

describe.only("", () => {
    // OK
    it("should call overloading functions - web3js", async function() {
      const sampleContractWeb3 = new web3.eth.Contract(abi, address);

      const f1 = await sampleContractWeb3.methods.overloading().call();
      const f2 = await sampleContractWeb3.methods.overloading("a").call();
      const f3 = await sampleContractWeb3.methods.overloading("a", "b").call();

      expect(f1).to.equal("1");
      expect(f2).to.equal("2");
      expect(f3).to.equal("3");
    });

    // OK
    it("should call overloading functions - ethers", async function() {
      const provider = new ethers.providers.JsonRpcProvider();
      const sampleContractEthers = new ethers.Contract(address, abi, provider);

      const f1 = await sampleContractEthers["overloading()"]();
      const f2 = await sampleContractEthers["overloading(string)"]("a");
      const f3 = await sampleContractEthers["overloading(string,string)"](
        "a",
        "b"
      );

      expect(f1.toNumber()).to.equal(1);
      expect(f2.toNumber()).to.equal(2);
      expect(f3.toNumber()).to.equal(3);
    });

    // FAIL
    it("should call overloading functions - ethers", async function() {
      const provider = new ethers.providers.JsonRpcProvider();
      const sampleContractEthers = new ethers.Contract(address, abi, provider);

      const f1 = await sampleContractEthers.overloading();  // Error: incorrect number of arguments
      const f2 = await sampleContractEthers.overloading("a");
      const f3 = await sampleContractEthers.overloading("a", "b");

      expect(f1.toNumber()).to.equal(1);
      expect(f2.toNumber()).to.equal(2);
      expect(f3.toNumber()).to.equal(3);
    });
  });

Tests Output

> npx truffle test

Using network 'development'.



  Contract: SampleContract

      βœ“ should call overloading functions - web3js (79ms)
WARNING: Multiple definitions for overloading
WARNING: Multiple definitions for overloading
      βœ“ should call overloading functions - ethers (84ms)
WARNING: Multiple definitions for overloading
WARNING: Multiple definitions for overloading
      1) should call overloading functions - ethers
    > No events were emitted


  2 passing (240ms)
  1 failing

  1) Contract: SampleContract
       should call overloading functions - ethers:
     Error: incorrect number of arguments
      at Contract.overloading (node_modules/ethers/contract.js:130:19)
      at Context.<anonymous> (test/SampleContract.js:85:45)

"ethers": "^4.0.23",

So, there are certain cases which can be unambiguous, but that is mores the exception than the rule. A bigger problem comes when you wish to start passing overrides in, for example:

let overrides = {
    gasLimit: 100000
};
let response = contract.overloading(someValue, overrides);

In this case, it becomes more difficult to detect whether this is the 2-parameter signature with no overrides, or the 1-parameter signature with overrides. Type inspection can give you some additional non-ambiguous abilities, however, in the case that the second parameter to overloading were a tuple, you would be back in the same boat, since a struct Thing { uint256 gasLimit } could be a valid input to a contract.

So, rather than trying to guess, and sometimes being able to and other times not being able to, which would confuse the developer even more, I error on the side of non-ambiguity. In v5, overloaded operations will not be exposed. (correction: this only applies to ambiguous bare named overloaded operations; see below).

You can imagine things get more confusing when you have:

contract Foo {
    // In JavaScript a BigNumberish is the same regardless
    function bar(uint256 a) { }
    function bar(uint8 a) { }

    // In JavaScript, an address is a string...
    function bar(string a) { }
    function bar(address a) { }

    // In JavaScipt "0x12" could be a 4 character string, or a 1 byte array
    function bar(string a) { }
    function bar(bytes a) { }    
}

This is another area Web3 and I have disagreed on though. Guessing has often led to either significantly more code, or unexpected dapp security exploits though. :)

Oh, thanks for the explanation!
Got it. I think that isn't an issue from your side. I'll need to think more about my implementation.
Thanks.

You can imagine things get more confusing when you have...

@ricmoo I don't really feel as though the example you give in contract Foo is realistic. I have never seen a contract attempt to overload a function in this way (where you simple change the type of a param).

The example that @marcelomorgado gives in contract SampleContract where the number of params change is far more common. We have similar situations where we quite often offer 2 different versions of the same function, depending on who is calling owner or admin (admin will add the extra address _owner param).

FYI.. For a while now, I've noticed problems with the way web3js handles contracts in apps built with react(-native) and vue. Ethers.js has always "saved the day" (so really very grateful), but I'm worried about what this means In v5, overloaded operations will **not** be exposed. Will there still be a way to call overloaded function in v5?

Thanks!

Just want to add...

So, there are _certain cases_ which can be unambiguous, but that is mores the exception than the rule.

as I mentioned above, I actually believe this to be the opposite, based on all the open-source solidity code I've read.

A bigger problem comes when you wish to start passing overrides in...

__This seems to be the issue.__ Is there anyway to solve this with type casting, ie with TypeScript? I'm fairly new to TS, so I'm really not sure how that would work.

@d14na I have seen a few contracts that use overloading to accept different types of parameters during code audits. I agree it isn't common, but it is done, and would need to be handled. It is very confusing why something works in a bunch of cases, but in others it "just doesn't", and its because of an unrelated method to what you are dealing with having a name collision.

Oh! Totally sorry, I should have used a semi-colon in that sentence , it should read: "I error on the side of non-ambiguity; in v5, [ambiguous bare named] overloaded operations will not be exposed", i.e. only in the case of ambiguity. If there is only foobar(address), you can use contract.foobar(addr) or contract["foobar(address)"](addr). If there is foobar(address) and foobar(uint256), you would have to use contract["foobar(address)"](addr), there would be no contract.foobar (i.e. access by name).

Make sense? Sorry for the confusion. :)

There are ways to solve it, but they are all terrible, and some would fail for people using the JavaScript library directly without using TypeScript. I think this is the least terrible solution.

One suggestion being put forward is to expose "type indicator wrappers"... You could for example use contract.foobar(Uint256(value), Overrides({ gasPrice: 9000000000 })) vs contract.foobar(Address(addr)) for example, but that to me (at this point), seems alike a lot of extra typing and room for error for very little gained.

I'm always open to discussion though. :)

There are ways to solve it, but they are all terrible, and some would fail for people using the JavaScript library directly without using TypeScript. I think this is the least terrible solution.

__I agree.__ I'm always annoyed when software fails "silently", because it leads to inconsistencies that are extremely hard to diagnose for the uninitiated.

I have 4 WARNINGS that appear each time I load a new (rather complex) contract i'm working on (which is what brought me here); but they couldn't bother me any less, I just wanted to make sure that Ethers would remain a viable option for such uses past v4, which you've confirmed. Thanks so much for creating an amazing alternative to Web3js.

__Keep up the great work!__

Thanks! Glad you enjoy it. :)

As a quick note, ethers.errors.setLogLevel(β€œerror”) will silence those warnings if they do start to get on your nerves. :)

ethers.errors.setLogLevel(β€œerror”) doesn't seem to be working in the beta version anymore. Using 5.0.0-beta.162.

Oh yes. Each library has its own logger in v5 and I haven’t added the global flag yet.

I’ll be adding it soon.

How would one use encodeFunctionData (part of Contract.interface) for an overloaded function? I'm getting this error:

Error: multiple matching functions

You must pass in the fully qualified signature. e.g. "setAddr(bytes32,uint256,bytes)" or "setAddr(bytes32,address)". You cannot use "setAddr" in this case.

If using the contract, the fully qualified signature must be normalized, but if using the Interface.encodeFunctionData, you can pass in and valid Fragment parameter (i.e. you may include memory modifiers, or other superfluous data like variable names).

Any chance that a TypeScript approach may provide a better solution ?

@SvenMeyer Can you tell what version of @typechain/ethers-v5 you're using? 5.0.0 is the latest and I just confirmed it doesn't have the problem (I don't remember when/which version this was fixed).

Anyways you can still be able to use following in your codebase.

await this.stake.connect(this.signers.admin)['getStakedAmount()']();

@zemse Actually I just created my project 7 days ago from Paul Bergs' template
https://github.com/paulrberg/solidity-template

$ yarn list --depth 0 | grep ethers 
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @nomiclabs/[email protected]
β”œβ”€ @typechain/[email protected]
β”œβ”€ [email protected]

I tried the "alternative function call", it works, but ideally I would not confuse front end dev with another notation ...

You can try upgrading @typechain/ethers-v5 and typechain packages to the latest ones in your project. There is just a one line change (PR under review) in the template.

Also, do you get type suggestions in like this? The name collision functions do not have a short name entry (getStakedAmount), only having function signature named functions.

Screenshot 2020-12-25 at 4 32 05 PM

@zemse I created a new project based on @paulrberg template https://github.com/paulrberg/solidity-template

https://github.com/SvenMeyer/paulrberg-solidity_template

created a second version of greet function with a different signature

    function greet() public view returns (string memory) {
        return greeting;
    }

    function greet(string memory message) public pure returns (string memory) {
        return (message);
    }

Test looks like this

    expect(await this.greeter.greet()).to.equal("Hello, world!");
    expect(await this.greeter.greet("New Message")).to.equal("New Message");

... and I still I get an error

$ yarn test
yarn run v1.22.10
$ hardhat test
Creating Typechain artifacts in directory typechain for target ethers-v5
Successfully generated Typechain artifacts!


  Unit tests
    Greeter
Deploying a Greeter with greeting: Hello, world!
      1) should return the new greeting once it's changed


  0 passing (602ms)
  1 failing

  1) Unit tests
       Greeter
         should return the new greeting once it's changed:
     TypeError: this.greeter.greet is not a function
      at Context.<anonymous> (test/Greeter.behavior.ts:5:31)
      at step (test/Greeter.behavior.ts:33:23)
      at Object.next (test/Greeter.behavior.ts:14:53)
      at /home/sum/DEV/ETH/PaulRBerg/paulrberg-solidity_template/test/Greeter.behavior.ts:8:71
      at new Promise (<anonymous>)
      at __awaiter (test/Greeter.behavior.ts:4:12)
      at Context.<anonymous> (test/Greeter.behavior.ts:43:16)

error Command failed with exit code 1.


$ yarn list --depth 0 | grep ethers
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @ethersproject/[email protected]
β”œβ”€ @nomiclabs/[email protected]
β”œβ”€ @typechain/[email protected]
β”œβ”€ [email protected]

Ethers.js does not create a greet function for you (like web3js does maybe), so you have to explicitly use the fully qualified signature (e.g. this.greet['greet()']) for the function you are intending to pick.

The reason is available in an earlier comment.

@zemse thanks for clarifying (again). I thought that with the update, ethers would just provide all the "right" functions in the standard call format.

No worries.. I thought you were looking for a typescript solution which does tells you that a this.greeter.greet() is incorrect, right when you are typing it.

I thought that with the update, ethers would just provide all the "right" functions in the standard call format.

The problem is there is no β€œright” function; either could be what you wanted. :s

Was this page helpful?
0 / 5 - 0 ratings