foundry icon indicating copy to clipboard operation
foundry copied to clipboard

Cannot keys or values from json object using stdJson.readStringArray

Open lnist opened this issue 2 years ago • 5 comments

Component

Forge

Have you ensured that all of these are up to date?

  • [X] Foundry
  • [X] Foundryup

What version of Foundry are you on?

forge 0.2.0 (8f246e0 2023-04-28T00:03:59.802167368Z)

What command(s) is the bug in?

forge test

Operating System

Linux

Describe the bug

Due to https://github.com/foundry-rs/foundry/pull/4833 we tried to update lib/forge-std from 4a79aca83f8075f8b1b4fe9153945fef08375630 to b971f66b8416e3b09f240f6ee7230ad3bdb13c19 in order to set the number of expected calls.

However, that breaks our usage of readStringArray . We try to read the methodIdentifiers from the ABI output in the "out" files. We have previously been able to use code similar to the following:

// SPDX-License-Identifier:	AGPL-3.0
pragma solidity ^0.8.17;

import {stdJson} from "forge-std/StdJson.sol";
import {Test, console2} from "forge-std/Test.sol";

contract JsonTest is Test {
  function test_readStringArray() public view {
    string memory json = "{ \"abi\": [ ], \"methodIdentifiers\": { \"function1()\": \"abcdabcd\", \"function2()\": \"def0def0\" } } ";
    string[] memory methodIdentifiers = stdJson.readStringArray(json, ".methodIdentifiers[*]~");
    bytes[] memory selectors = new bytes[](methodIdentifiers.length);
    for (uint i = 0; i < methodIdentifiers.length; i++) {
      selectors[i] = vm.parseBytes(methodIdentifiers[i]);
    }
    console2.log(vm.toString(selectors[0]));
    console2.log(vm.toString(selectors[1]));
  }
}

Which outputs the following on the old version of the lib

[PASS] test_readStringArray() (gas: 15952)
Logs:
  0xabcdabcd
  0xdef0def0

But with the updated lib/forge-std we get:

 [FAIL. Reason: EvmError: Revert] test_readStringArray() (gas: 4281)
Traces:
  [4281] JsonTest::test_readStringArray() 
    ├─ [0] VM::parseJsonStringArray({ "abi": [ ], "methodIdentifiers": { "function1()": "abcdabcd", "function2()": "def0def0" } } , .methodIdentifiers[*]~) 
    │   └─ ← 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000086162636461626364000000000000000000000000000000000000000000000000
    └─ ← "EvmError: Revert"

Note, the ~ at the end of .methodIdentifiers[*]~ should actually make it output the keys and not the values, but that is a separate and not important issue for us. Without it it still does not work.

Looking at https://github.com/foundry-rs/foundry/blob/8f246e07c89129b6effa89f0d71c4ac67758a155/testdata/cheats/Json.t.sol it seems reading all the keys or values of objects is not in the test suite, so could it be broken or are we using it wrong?

lnist avatar Apr 28 '23 08:04 lnist

Hmm interesting, I don't recall anything touching this functionality recently that would have broken it. Are you certain it's the forge-std version bump that causes the issue, as opposed to a forge version bump? cc'ing @odyslam here too

mds1 avatar Apr 28 '23 13:04 mds1

It works before "forge update" but not after. Isn't that only the libs that get updated?

lnist avatar Apr 28 '23 14:04 lnist

I've the same error. Let me investigate..

0xMelkor avatar May 01 '23 14:05 0xMelkor

I believe #294 introduced some issue. Need to investigate more..

@lnist this workaround can help for the moment:

// SPDX-License-Identifier:	AGPL-3.0
pragma solidity ^0.8.17;

import {stdJson} from "forge-std/StdJson.sol";
import {Test, console2} from "forge-std/Test.sol";

contract JsonTest is Test {
  function test_readStringArray() public view {
    string memory json = "{ \"abi\": [ ], \"methodIdentifiers\": { \"function1()\": \"abcdabcd\", \"function2()\": \"def0def0\" } } ";
    string memory key = ".methodIdentifiers[*]~";

    bytes memory encoded = vm.parseJson(json, key);
    string[] memory methodIdentifiers = abi.decode(encoded, (string[]));

    bytes[] memory selectors = new bytes[](methodIdentifiers.length);
    for (uint i = 0; i < methodIdentifiers.length; i++) {
      selectors[i] = vm.parseBytes(methodIdentifiers[i]);
    }
    console2.log(vm.toString(selectors[0]));
    console2.log(vm.toString(selectors[1]));
  }
}

0xMelkor avatar May 01 '23 17:05 0xMelkor

Thanks! the workaround fixes the problem for us. Will switch back to the other implementation when it works for this scenario :)

lnist avatar May 02 '23 11:05 lnist

Initial repro now yields:

[FAIL: vm.parseJsonStringArray: path ".methodIdentifiers[*]~" must return exactly one JSON value] test_readStringArray() (gas: 3899)

readStringArray only operates on string arrays, not JSON arrays of strings

If you have data that looks like this or use the workaround you specified it does work:

import {stdJson} from "forge-std/StdJson.sol";
import {Test, console2} from "forge-std/Test.sol";

contract JsonTest is Test {
    function test_readStringArray() public view {
        // Updated JSON: methodIdentifiers is now an array
        string memory json = '{ "abi": [], "methodIdentifiers": ["abcdabcd", "def0def0"] }';

        // Read the array of strings
        string[] memory methodIdentifiers = stdJson.readStringArray(json, ".methodIdentifiers");

        // Convert each string into bytes
        bytes[] memory selectors = new bytes[](methodIdentifiers.length);
        for (uint256 i = 0; i < methodIdentifiers.length; i++) {
            selectors[i] = vm.parseBytes(methodIdentifiers[i]);
        }

        // Log the selectors
        console2.log(vm.toString(selectors[0]));
        console2.log(vm.toString(selectors[1]));
    }
}

it does yield

[PASS] test_readStringArray() (gas: 19224)
Logs:
  0xabcdabcd
  0xdef0def0

Traces:
  [19224] JsonTest::test_readStringArray()
    ├─ [0] VM::parseJsonStringArray("<stringified JSON>", ".methodIdentifiers") [staticcall]
    │   └─ ← [Return] ["abcdabcd", "def0def0"]
    ├─ [0] VM::parseBytes("abcdabcd") [staticcall]
    │   └─ ← [Return] 0xabcdabcd
    ├─ [0] VM::parseBytes("def0def0") [staticcall]
    │   └─ ← [Return] 0xdef0def0
    ├─ [0] VM::toString(0xabcdabcd) [staticcall]
    │   └─ ← [Return] "0xabcdabcd"
    ├─ [0] console::log("0xabcdabcd") [staticcall]
    │   └─ ← [Stop]
    ├─ [0] VM::toString(0xdef0def0) [staticcall]
    │   └─ ← [Return] "0xdef0def0"
    ├─ [0] console::log("0xdef0def0") [staticcall]
    │   └─ ← [Stop]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 408.73µs (171.37µs CPU time)

For now marking as won't fix

zerosnacks avatar Mar 25 '25 15:03 zerosnacks