defi-wonderland / smock

The Solidity mocking library
MIT License
319 stars 40 forks source link

Transaction reverted: function returned an unexpected amount of data from faked contract struct. #95

Closed richard-massless closed 2 years ago

richard-massless commented 2 years ago

Describe the bug When getting a faked struct from inside a real solidity contract this error is returned from faked struct. Transaction reverted: function returned an unexpected amount of data

Reproduction steps I wrote out the minimal test example. Let me know if you need more. DebugMapping.sol

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

import "./IDebugMappingDeployed.sol";

contract DebugMapping {
    IDebugMappingDeployed private remoteDeployed;

    constructor(address _remoteDeployedAddress) {
        remoteDeployed = IDebugMappingDeployed(_remoteDeployedAddress);
    }

    function getStructFromDeployed(uint256 _item)
        public
        view
        returns (IDebugMappingDeployed.SomeStruct memory)
    {
        return (remoteDeployed.getStruct(_item));
    }
}

DebugMappingDeployed.sol

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

contract DebugMappingDeployed {
    struct SomeStruct {
        uint256 id;
        string name;
    }

    mapping(uint256 => SomeStruct) public getStruct;

    constructor() {
        getStruct[1] = SomeStruct({id: 3, name: "one"});
        getStruct[2] = SomeStruct({id: 2, name: "two"});
        getStruct[3] = SomeStruct({id: 1, name: "three"});
    }
}

IDebugMappingDeployed.sol

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

interface IDebugMappingDeployed {
    struct SomeStruct {
        uint256 id;
        string name;
    }

    function getStruct(uint256) external view returns (SomeStruct memory);
}

DebugMapping.ts (test)

import * as dotenv from "dotenv";

import { expect } from "chai";
import { ethers } from "hardhat";
import { FakeContract, smock } from "@defi-wonderland/smock";

import { Contract, ContractFactory } from "ethers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { DebugMappingDeployed } from "../typechain";

dotenv.config();

let contractFactory: ContractFactory;
let contract: Contract;
let debugMappingDeployed: FakeContract<DebugMappingDeployed>;
let owner: SignerWithAddress;

describe("DebugMapping", function () {
  before(async () => {
    [owner] = await ethers.getSigners();

    // Fake Contract
    debugMappingDeployed = await smock.fake<DebugMappingDeployed>(
      "DebugMappingDeployed"
    );

    // Set up this test contract
    contractFactory = await ethers.getContractFactory("DebugMapping", owner);
  });

  beforeEach(async () => {
    contract = await contractFactory.deploy(debugMappingDeployed.address);
    await contract.deployed();
  });

  it("Should return faked struct", async () => {
    debugMappingDeployed.getStruct.returns({ id: 1, name: "one" });

    const struct = await contract.getStructFromDeployed(1);
    console.log(struct);

    expect(await contract.getStructFromDeployed(1).id).to.equal(1);
  });
});

Expected behavior The unit test "Should return faked struct" should successful execute await contract.getStructFromDeployed(1); and return {id: 1, name: "one" }.

System Specs:

Additional context When running with Mocha Test Explorer plugin for vscode. Stack trace

Error: Transaction reverted: function returned an unexpected amount of data
    at DebugMapping.getStructFromDeployed (contracts/DebugMapping.sol:18)
    at HardhatNode._gatherTraces (node_modules\hardhat\src\internal\hardhat-network\provider\node.ts:1484:30)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at runNextTicks (internal/process/task_queues.js:62:3)
    at listOnTimeout (internal/timers.js:523:9)
    at processTimers (internal/timers.js:497:7)
    at async HardhatNode.runCall (node_modules\hardhat\src\internal\hardhat-network\provider\node.ts:503:20)
    at async EthModule._callAction (node_modules\hardhat\src\internal\hardhat-network\provider\modules\eth.ts:353:9)

When running with hardhat test Stack trace

UnhandledPromiseRejectionWarning: Error: Failed to encode return value for getStruct
    at SafeProgrammableContract.encodeValue (C:\test\smart-contract\node_modules\@defi-wonderland\smock\src\logic\programmable-function-logic.ts:128:17)
    at SafeProgrammableContract.modifyAnswer (C:\test\smart-contract\node_modules\@defi-wonderland\smock\src\logic\programmable-function-logic.ts:97:52)
    at C:\test\smart-contract\node_modules\@defi-wonderland\smock\src\logic\programmable-function-logic.ts:42:18
    at Object.next (C:\test\smart-contract\node_modules\rxjs\src\internal\Subscriber.ts:194:14)
    at SafeSubscriber.Subscriber._next (C:\test\smart-contract\node_modules\rxjs\src\internal\Subscriber.ts:119:22)
    at SafeSubscriber.Subscriber.next (C:\test\smart-contract\node_modules\rxjs\src\internal\Subscriber.ts:75:12)
    at C:\test\smart-contract\node_modules\rxjs\src\internal\operators\withLatestFrom.ts:104:22
    at OperatorSubscriber._this._next (C:\test\smart-contract\node_modules\rxjs\src\internal\operators\OperatorSubscriber.ts:43:13)       
    at OperatorSubscriber.Subscriber.next (C:\test\smart-contract\node_modules\rxjs\src\internal\Subscriber.ts:75:12)
    at C:\test\smart-contract\node_modules\rxjs\src\internal\operators\map.ts:56:20
richard-massless commented 2 years ago

I have discovered that in solidity 0.8.4 one can not return a struct from a mapping in the way I am attempting to do. The solution is to destructure the result when calling a public mapping with struct.

Once again the bug is not with your library. 🤘

The solution when using the above test scenario.

IDebugMappingDeployed.sol

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

interface IDebugMappingDeployed {
    struct SomeStruct {
        uint256 id;
        string name;
    }

    function getStruct(uint256) external view returns (uint256 id, string memory name); // Destructured the return value
}

DebugMapping.sol

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

import "./IDebugMappingDeployed.sol";

contract DebugMapping {
    IDebugMappingDeployed private remoteDeployed;

    constructor(address _remoteDeployedAddress) {
        remoteDeployed = IDebugMappingDeployed(_remoteDeployedAddress);
    }

    function getStructFromDeployed(uint256 _item)
        public
        view
        returns (uint256 id, string memory name) // Returns the destructured return value
    {
        return (remoteDeployed.getStruct(_item));
    }
}

DebugMapping.ts (test)

import * as dotenv from "dotenv";

import { expect } from "chai";
import { ethers } from "hardhat";
import { FakeContract, smock } from "@defi-wonderland/smock";

import { Contract, ContractFactory } from "ethers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { DebugMappingDeployed } from "../typechain";

dotenv.config();

let contractFactory: ContractFactory;
let contract: Contract;
let debugMappingDeployed: FakeContract<DebugMappingDeployed>;
let owner: SignerWithAddress;

describe("DebugMapping", function () {
  before(async () => {
    [owner] = await ethers.getSigners();

    // Fake Contract
    debugMappingDeployed = await smock.fake<DebugMappingDeployed>(
      "DebugMappingDeployed"
    );

    // Set up this test contract
    contractFactory = await ethers.getContractFactory("DebugMapping", owner);
  });

  beforeEach(async () => {
    contract = await contractFactory.deploy(debugMappingDeployed.address);
    await contract.deployed();
  });

  it("Should return faked struct", async () => {
    // debugMappingDeployed.getStruct.returns({ id: 1, name: "one" }); 
   debugMappingDeployed.getStruct.returns([1, "one" ]); // Workaround (https://github.com/defi-wonderland/smock/issues/94)

    const struct = await contract.getStructFromDeployed(1);
    console.log(struct);

    expect(await contract.getStructFromDeployed(1).id).to.equal(1);
  });
});