All skills

Prevent Ethereum hash bugs in JavaScript and TypeScript. Node SHA3-256 is not Ethereum Keccak-256. Using it can break selectors, signatures, storage slots, and address creation with no warning.

  • 1 file
  • 6.6 KB
  • Updated 2 weeks ago
  • GitHub

Use this Skill: https://skilld.dev/gh/agenticluke/ethereum-keccak-guard-plus/skill

This session only. Nothing lands on disk.

SKILL.md

≈50 tokens always: the name and description. ≈1.6k when used: this file.

Node.js Keccak-256

Ethereum uses Keccak-256.

Node's crypto.createHash('sha3-256') uses NIST SHA3-256. It is a different hash. The same input gives a different result.

When to Use This Skill

Use this skill when you:

  • Make Ethereum function selectors.
  • Make event topics.
  • Build EIP-712 data.
  • Build Merkle trees.
  • Find Solidity storage slots.
  • Make an address from a public key.
  • Review Ethereum code that uses Node crypto.

Main Rule

Never use this for Ethereum hashing:

crypto.createHash('sha3-256')

Use a clear Keccak-256 helper from ethers, viem, web3.js, or another trusted package.

Why This Matters

Node gives no warning when the wrong hash is used.

import crypto from 'node:crypto';
import { keccak256, toUtf8Bytes } from 'ethers';

const data = 'hello';

const sha3 = crypto.createHash('sha3-256').update(data).digest('hex');
const keccak = keccak256(toUtf8Bytes(data)).slice(2);

console.log(sha3 === keccak); // false

Pick the Right Input Form

Hash bytes, not a value that only looks like bytes.

  • Use UTF-8 bytes for plain text.
  • Use hex bytes for data that starts with 0x.
  • Use ABI encoding when Solidity uses abi.encode.
  • Use packed encoding only when Solidity uses abi.encodePacked.
  • Keep the exact type list and value order used by Solidity.

Do not hash the text "0x1234" when you mean the bytes 0x12, 0x34.

import { getBytes, keccak256, toUtf8Bytes } from 'ethers';

const textHash = keccak256(toUtf8Bytes('0x1234'));
const byteHash = keccak256(getBytes('0x1234'));

console.log(textHash === byteHash); // false

ethers v6

import {
  id,
  keccak256,
  solidityPackedKeccak256,
  toUtf8Bytes,
} from 'ethers';

const byteHash = keccak256(new Uint8Array([0x01, 0x02]));
const textHash = keccak256(toUtf8Bytes('hello'));

const topic = id('Transfer(address,address,uint256)');

const packedHash = solidityPackedKeccak256(
  ['address', 'uint256'],
  ['0x742d35Cc6634C0532925a3b8D4C9B569890FaC1c', 100n],
);

viem

import { keccak256, stringToBytes } from 'viem';

const hash = keccak256(stringToBytes('hello'));

web3.js

const hash = web3.utils.keccak256('hello');

const packedHash = web3.utils.soliditySha3(
  {
    type: 'address',
    value: '0x742d35Cc6634C0532925a3b8D4C9B569890FaC1c',
  },
  { type: 'uint256', value: '100' },
);

Check your web3.js version before hashing hex strings. Some helpers may treat a 0x value as bytes instead of text.

Concrete Example: Function Selector

The selector for transfer(address,uint256) is the first four bytes of its Keccak-256 hash.

import { id } from 'ethers';

const signature = 'transfer(address,uint256)';
const selector = id(signature).slice(0, 10);

console.log(selector); // 0xa9059cbb

The signature must use exact ABI types. Do not add argument names or spaces.

id('transfer(address,uint256)');          // correct
id('transfer(address to,uint256 amount)'); // wrong

Event Topic and Type Hash

import { id, keccak256, toUtf8Bytes } from 'ethers';

const eventTopic = id('Transfer(address,address,uint256)');

const typeHash = keccak256(
  toUtf8Bytes('Transfer(address from,address to,uint256 value)'),
);

Names matter in an EIP-712 type string. Field order and spacing must match the type you mean to use.

For full EIP-712 messages, use the package's EIP-712 helper. Do not join fields by hand.

Solidity Mapping Slot

Solidity mapping slots use normal ABI encoding, not packed encoding.

import { AbiCoder, keccak256 } from 'ethers';

function getAddressMappingSlot(
  key: string,
  mappingSlot: bigint,
): string {
  const encoded = AbiCoder.defaultAbiCoder().encode(
    ['address', 'uint256'],
    [key, mappingSlot],
  );

  return keccak256(encoded);
}

Nested mappings and dynamic keys use more steps. Follow Solidity's storage rules for the exact field type.

Public Key to Address

Ethereum uses the 64-byte x || y part of an uncompressed secp256k1 public key. Drop the first 0x04 byte, hash the rest, and keep the last 20 bytes.

import { getBytes, keccak256 } from 'ethers';

function pubkeyToAddress(publicKey: string): string {
  const bytes = getBytes(publicKey);

  if (bytes.length !== 65 || bytes[0] !== 0x04) {
    throw new Error('Expected a 65-byte uncompressed public key');
  }

  const hash = keccak256(bytes.slice(1));
  return `0x${hash.slice(-40)}`;
}

Do not slice a compressed 33-byte public key. Decompress it first with a trusted secp256k1 package.

Use a checksum helper such as getAddress if you need a checksummed address.

Merkle Tree Checks

A Merkle proof only works when every part matches:

  • Leaf encoding
  • Pair order
  • Pair sorting
  • Byte joining
  • Hash function

Do not guess these rules. Copy them from the smart contract or its clear spec.

Packed encoding can be unsafe when two or more dynamic values are joined. Different inputs can make the same packed bytes. Use normal ABI encoding unless the contract calls for packed encoding.

Code Review

Use rg when it is installed:

rg -n "createHash\s*\(\s*['\"]sha3-256['\"]" \
  --glob '*.js' --glob '*.ts' --glob '!node_modules/**' .

rg -n "keccak256|soliditySha3|solidityPackedKeccak256" \
  --glob '*.js' --glob '*.ts' --glob '!node_modules/**' .

Also check aliases and wrappers around createHash.

For each hash call, confirm:

  1. The code uses Keccak-256.
  2. Text and hex data are turned into the right bytes.
  3. ABI types and value order match Solidity.
  4. Normal and packed ABI encoding are not mixed up.
  5. The output size is cut only when the rule calls for it.
  6. A known test value is checked.

Test Known Values

Keep at least one known result in your tests.

import { id, keccak256, toUtf8Bytes } from 'ethers';
import { expect, test } from 'vitest';

test('uses Ethereum Keccak-256', () => {
  expect(keccak256(toUtf8Bytes(''))).toBe(
    '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470',
  );

  expect(id('transfer(address,uint256)').slice(0, 10)).toBe(
    '0xa9059cbb',
  );
});

Final Rule

In Ethereum code, use an explicit Keccak-256 helper. Never replace it with Node SHA3-256. Then check the input bytes and encoding, since the right hash with the wrong input is still wrong.

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at dfdbee1. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 weeks ago
origin
ECC direct-port adaptation
version
1.0.0

README badge

README badge for agenticluke/ethereum-keccak-guard-plus