question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Hashing that matches Solidity `keccak256(abi.encode(...))`

See original GitHub issue

I spent a long time figuring out why I could not get matching hashes between Solidity and Ethers using the solidityKeccak256 function with dynamic types (i.e. string).

Example Solidity contract:

pragma solidity ^0.5.0;

contract TestingHashes {
    function hash() public pure returns (bytes32) {
        return keccak256(abi.encode("Hello", "world!"));
    }
    
    function hashPacked() public pure returns (bytes32) {
        return keccak256(abi.encodePacked("Hello", "world!"));
    }
}

In Ethers, if I use the hash function:

eth.utils.solidityKeccak256(["string", "string"], ["Hello", "world!"]);

The above matches the output of the Solidity function hashPacked. However, as the Solidity docs state, the packed encoding is ambiguous. Is there a way to mimic the behavior of abi.encode in Ethers?

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:17
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

27reactions
ricmoocommented, Jan 24, 2020

Right, the packed encoding is ambiguous. For example, imagine the following two cases:

// A
const a = solidityPack([ "bytes1", "bytes2" ], [ "0x12", "0x3456" ]);

//
const b = solidityPack([ "bytes2", "bytes1" ], [ "0x1234", "0x56" ]);

Both will have the same packed representation and hence the same hash. This can allow a malicious actor to trick your contract. So in general you need to use a distinguished encoding. The abi.encode in Solidity is mirrored in ethers, so they should both produce the same, non-ambiguous data; however abi encoding is not truly distinguished, as there are multiple ways to encode the same data, it’s just the most popular implementations all behave the same, so this point is moot (ish).

To get the result that matches hash(), you can use:

/home/ethers> ethers
homestead> ethers.utils.defaultAbiCoder.encode([ "string", "string" ], [ "Hello", "world" ])
'0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000'
homestead> ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode([ "string", "string" ], [ "Hello", "world" ]))
'0x5ac989f52ea4c399343f6c0cf5a4810fc1bdac5773de37ca0cd0a8287f75a5c6'

I am also working on an EIP for a scheme that provides an actual distinguished encoding too, but I’ve been quite busy and haven’t had a chance to follow up on some feedback for it. I’ll bump it up my todo list. 😃

Make sense? Feel free to ask follow-up questions. 😃

5reactions
mikoimcommented, Jun 6, 2022

I found different behavior. If struct have no array, ethers.utils.defaultAbiCoder.encode works expected. Otherwise, defaultAbiCoder adds array length before elements so bytes does not match abi.encode’s one .

test.sol

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

contract Test {
    struct Boo {
        uint256[] ids;
    }

    function foo(Boo memory data) external view returns (bytes memory) {
        return abi.encode(data);
    }
}

Test.test.ts

import { expect } from "chai";
import { ethers } from "hardhat";

describe("Test: ", function () {
  let Test: any;
  let test: any;

  before(async function () {
    Test = await ethers.getContractFactory("Test");
  });

  beforeEach(async function () {
    test = await Test.deploy();
  });

  it("Array in struct", async function () {
    {
      const data = ethers.utils.defaultAbiCoder.encode(["uint256[]"], [[0, 1, 2, 3, 4, 5]]);

      expect(await test.test([[0, 1, 2, 3, 4, 5]])).to.equal(data);
    }
  });
});
  1) Test: 
       Array in struct:

      AssertionError: expected '0x00000000000000000000000000000000000…' to equal '0x00000000000000000000000000000000000…'
      + expected - actual

      -0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005
      +0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005
      
      at Context.<anonymous> (test/Test.test.ts:20:56)

tuple works 😉

ethers.utils.defaultAbiCoder.encode(["tuple(uint256[])"], [[[0, 1, 2, 3, 4, 5]]]);
Read more comments on GitHub >

github_iconTop Results From Across the Web

Hashing with Keccak256 | Solidity by Example | 0.8.13
keccak256 computes the Keccak-256 hash of the input. Some use cases are: Creating a deterministic unique ID from a input; Commit-Reveal scheme ...
Read more >
Solidity's keccak256 hash doesn't match WEB3 keccak hash
The abi.encode function encode its parameters for a contract call using Solidity's ABI specs. For fixed size types like uintXXX, bool, ...
Read more >
Hashing Functions In Solidity Using Keccak256 - Medium
This means that dynamic types are encoded in-place without length while static types will not be padded if they are shorter than 32...
Read more >
Contract ABI Specification — Solidity 0.8.17 documentation
Indexed event parameters that are not value types, i.e. arrays and structs are not stored directly but instead a Keccak-256 hash of an...
Read more >
ethereum/web3.py - Gitter
There are examples for preparing a signature to be verified by a solidity contract, ... encodePacked(recipient , amount); bytes32 message = keccak256(abi.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found