]> git.ardis.lu - function-extractor/commitdiff
Initial commit
authorArdis Lu <ardisglu@gmail.com>
Sat, 6 Apr 2024 23:39:54 +0000 (16:39 -0700)
committerArdis Lu <ardisglu@gmail.com>
Sat, 6 Apr 2024 23:39:54 +0000 (16:39 -0700)
README.md [new file with mode: 0644]
index.html [new file with mode: 0644]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..cd84f6f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# Function Extractor
+
+This is a minimal web app that extracts 4-byte function selectors from a smart contract's runtime bytecode and looks up the selectors on [openchain.xyz](https://openchain.xyz/).
+
+Source for the regex used to extract the 4-byte function selectors: [https://twitter.com/high_byte/status/1606634118808223747](https://twitter.com/high_byte/status/1606634118808223747)
+
+Similar projects:
+- [EVMole](https://github.com/cdump/evmole)
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..e60e75d
--- /dev/null
@@ -0,0 +1,239 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="icon" href="data:,">
+  <title>Function Extractor</title>
+  <style>
+    body {
+      background: hsl(216deg 50% 93%);
+      font-family: monospace;
+      inline-size: min(1440px, 100% - 16px);
+      margin-inline: auto;
+    }
+
+    header {
+      margin-block-end: 32px;
+    }
+
+    h1 {
+      margin: 0;
+      font-size: 2rem;
+    }
+
+    ul {
+      margin: 0;
+      padding: 0;
+      list-style-type: none;
+      display: flex;
+      flex-wrap: wrap;
+    }
+
+    li:not(:last-child)::after {
+      content: '|';
+      content: '|' / '';
+      padding: 8px;
+    }
+
+    a,
+    input,
+    textarea {
+      font-size: 1rem;
+    }
+
+    a {
+      border: 1px solid transparent;
+      border-radius: 4px;
+      transition: border 400ms;
+    }
+
+    form {
+      display: flex;
+      flex-direction: column;
+    }
+
+    label {
+      font-size: 1.25rem;
+      font-weight: 700;
+    }
+
+    label:not(:first-child) {
+      margin-block-start: 16px;
+    }
+
+    label[for="functions"],
+    #functions {
+      display: none;
+    }
+
+    textarea:not(:invalid)~:is(label[for="functions"], #functions) {
+      display: unset;
+    }
+
+    input,
+    textarea {
+      font-family: inherit;
+      padding: 6px;
+      border-radius: 4px;
+      border: 1px solid hsl(216deg 50% 80%);
+      transition: border 400ms;
+    }
+
+    textarea {
+      align-self: center;
+      min-inline-size: calc(100% - 16px);
+      max-inline-size: calc(100vi - 32px);
+      min-block-size: 10rem;
+    }
+
+    :is(a, input, textarea):is(:hover, :focus-within) {
+      border: 1px solid hsl(216deg 50% 65%);
+      transition: border 200ms;
+    }
+
+    :is(input, textarea):focus {
+      outline: 1px solid hsl(216deg 50% 50%);
+    }
+
+    output {
+      overflow: auto;
+    }
+
+    table {
+      border-collapse: collapse;
+      font-size: 1rem;
+    }
+
+    tr {
+      transition: background-color 400ms;
+    }
+
+    tr:hover,
+    tr:focus-within {
+      background: hsl(222deg 50% 90%);
+      transition: background-color 200ms;
+    }
+
+    th,
+    td {
+      line-height: 2;
+      padding-block: 8px;
+      padding-inline: 4px 16px;
+    }
+  </style>
+</head>
+
+<body>
+  <header>
+    <h1>Function Extractor</h1>
+    <ul>
+      <li><a href="https://github.com/ardislu/function-extractor">ardislu/function-extractor</a></li>
+      <li><a href="https://openchain.xyz/">openchain.xyz</a></li>
+      <li><a href="https://github.com/cdump/evmole">cdump/evmole</a></li>
+    </ul>
+  </header>
+
+  <main>
+    <form>
+      <label for="rpc">RPC</label>
+      <input type="url" name="rpc" id="rpc" placeholder="https://1rpc.io/eth" value="https://1rpc.io/eth">
+
+      <label for="contract">Contract address</label>
+      <input name="contract" id="contract" placeholder="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" value="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" autocomplete="off" data-1p-ignore data-lpignore="true" data-form-type="other" data-bwignore>
+
+      <label for="bytecode">Runtime bytecode</label>
+      <textarea name="bytecode" id="bytecode" placeholder="0x60606040..." required></textarea>
+
+      <label for="functions">Extracted functions</label>
+      <output name="functions" id="functions">
+        <table>
+          <thead>
+            <tr>
+              <th>Selector</th>
+              <th>Name</th>
+            </tr>
+          </thead>
+          <tbody></tbody>
+        </table>
+      </output>
+    </form>
+  </main>
+
+  <script type="module">
+    const form = document.querySelector('form');
+    const rpcInput = document.querySelector('#rpc');
+    const contractInput = document.querySelector('#contract');
+    const bytecodeInput = document.querySelector('#bytecode');
+    const tableBody = document.querySelector('tbody');
+
+    // https://twitter.com/high_byte/status/1606634118808223747
+    function getFunctionSelectors(bytecode) {
+      return [...bytecode.matchAll(/63([0-9a-f]{8})1461([0-9a-f]{4})57/ig)].map(g => `0x${g[1]}`);
+    }
+
+    async function getSelectorNames(selectors) {
+      const endpoint = 'https://api.openchain.xyz/signature-database/v1/lookup';
+      const params = new URLSearchParams({ function: selectors.join(',') });
+      const obj = await fetch(`${endpoint}?${params}`).then(r => r.json());
+
+      const out = {};
+      for (const [key, arr] of Object.entries(obj.result.function)) {
+        out[key] = arr?.[0].name ?? 'No name found.';
+      }
+      return out;
+    }
+
+    function debounce(fn, wait) {
+      let timeoutId;
+      return () => {
+        clearTimeout(timeoutId);
+        timeoutId = setTimeout(fn, wait);
+      }
+    }
+
+    contractInput.addEventListener('input', debounce(async () => {
+      // Do nothing if either RPC input or contract input are invalid
+      try { new URL(rpcInput.value); } catch { return; }
+      if (!/(0x)?[0-9a-f]{40}/i.test(contractInput.value)) { return; }
+
+      const bytecode = await fetch(rpcInput.value, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          jsonrpc: '2.0',
+          id: 1,
+          method: 'eth_getCode',
+          params: [contractInput.value, 'latest']
+        })
+      })
+        .then(r => r.json())
+        .then(obj => obj.result);
+
+      bytecodeInput.value = bytecode;
+      form.requestSubmit();
+    }, 250));
+
+    bytecodeInput.addEventListener('change', debounce(() => {
+      form.requestSubmit();
+    }, 250));
+
+    form.addEventListener('submit', async e => {
+      e.preventDefault();
+
+      const selectors = getFunctionSelectors(bytecodeInput.value);
+      const names = await getSelectorNames(selectors);
+
+      tableBody.innerHTML = '';
+      for (const [selector, name] of Object.entries(names)) {
+        tableBody.insertAdjacentHTML('beforeend', `<tr><td>${selector}</td><td>${name}</td></tr>`);
+      }
+    });
+
+    // Set initial bytecode from prepopulated contract
+    contractInput.dispatchEvent(new Event('input'));
+  </script>
+</body>
+
+</html>
\ No newline at end of file
This page took 0.050397 seconds and 4 git commands to generate.