-
Notifications
You must be signed in to change notification settings - Fork 731
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
node: Add Transfer Verifier mechanism (#4169)
Adds a new package and command-line tool for Transfer Verification. This is a process of validating message publications from the core contracts. When a message is emitted, Transfer Verification will examine the corresponding receipt or other logs to ensure that funds were sent into the token bridge whenever this kind of message is emitted from the core bridge. This is a defense-in-depth mechanism to guard against a scenario where an attacker finds a way to spoof message publications.
- Loading branch information
1 parent
4f0e46f
commit 2615d55
Showing
30 changed files
with
6,406 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# There's nothing special about this version, it is simply the `latest` as of | ||
# the creation date of this file. | ||
FROM alpine:3.20.3@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a | ||
|
||
RUN apk add --no-cache inotify-tools | ||
|
||
COPY monitor.sh /monitor.sh | ||
RUN chmod +x /monitor.sh | ||
|
||
CMD ["/monitor.sh"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# These versions are pinned to match the Dockerfile in the `ethereum/` | ||
# directory. Otherwise, there is nothing special about them and they can be | ||
# updated alongside the other Dockerfile. | ||
FROM --platform=linux/amd64 ghcr.io/foundry-rs/foundry:nightly-55bf41564f605cae3ca4c95ac5d468b1f14447f9@sha256:8c15d322da81a6deaf827222e173f3f81c653136a3518d5eeb41250a0f2e17ea as foundry | ||
# node is required to install Foundry | ||
FROM node:19.6.1-slim@sha256:a1ba21bf0c92931d02a8416f0a54daad66cb36a85d2b73af9d73b044f5f57cfc | ||
|
||
COPY --from=foundry /usr/local/bin/cast /bin/cast | ||
|
||
COPY transfer-verifier-test.sh /transfer-verifier-test.sh | ||
RUN chmod +x /transfer-verifier-test.sh | ||
|
||
CMD ["/transfer-verifier-test.sh"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# Transfer Verifier -- Integration Tests | ||
|
||
## EVM Integration Tests | ||
|
||
### Overview | ||
|
||
The Transfer Verifier tests involve interacting with the local ethereum devnet defined by the Tilt set-up in this repository. | ||
|
||
The basic idea is as follows: | ||
* Interact with the local Ethereum testnet. This should already have important pieces such as the Token Bridge and Core Bridge deployed. | ||
* Use `cast` from the foundry tool set to simulate malicious interactions with the Token Bridge. | ||
* Transfer Verifier detects the malicious messages and emits errors about what went wrong. | ||
* The error messages are logged to a file | ||
* A "monitor" script is used to detect the expected error message, waiting until the file is written to | ||
* If the monitor script sees the expected error message in the error log, it terminates | ||
|
||
## Components | ||
|
||
### Scripts | ||
|
||
#### transfer-verifier-test.sh | ||
|
||
Contains the `cast` commands that simulate malicious interactions with the Token Bridge and Core Bridge. It is able to broadcast | ||
transactions to the `anvil` instance that powers the Ethereum testnet while being able to impersonate arbitrary senders. | ||
|
||
This lets us perform actions that otherwise should be impossible, like causing a Publish Message event to be emitted from the Core Bridge | ||
without a corresponding deposit or transfer into the Token Bridge. | ||
|
||
#### monitor.sh | ||
|
||
A bash script that monitors the error log file for a specific error pattern. It runs in an infinite loop so it will | ||
not exit until the error pattern is detected. | ||
|
||
The error pattern is defined in `wormhole/devnet/tx-verifier.yaml` and matches an error string in the Transfer Verifier package. | ||
|
||
Once the pattern is detected, a success message is logged to a status file. Currently this is unused but this set-up | ||
could be modified to detect that this script has written the success message to figure out whether the whole test completed successfully. | ||
|
||
### Pods | ||
|
||
The files detailed below each have a primary role and are responsible for running one of the main pieces of the test functionality: | ||
|
||
* The Transfer Verifier binary which monitors the state of the local Ethereum network | ||
* The integration test script that generates activity that the Transfer Verifier classifies as malicious | ||
* The monitor script which ensures that the Transfer Verifier successfully | ||
detected the error we expected, and signals to Tilt that the overall test has | ||
succeeded | ||
|
||
#### devnet/tx-verifier.yaml | ||
|
||
Runs the Transfer Verifier binary and redirects its STDERR to the error log file. This allows the output of the binary | ||
to be monitored by `monitor.sh`. | ||
|
||
#### devnet/tx-verifier-test.yaml | ||
|
||
Runs the `transfer-verifier-test.sh` script which simulates malicious Token Bridge activity. Defines the RPC URL used | ||
by that bash script, which corresponds to the `anvil` instance created in the Ethereum devnet. | ||
|
||
#### devnet/tx-verifier-monitor.yaml | ||
|
||
Defines the expected error string that should be emitted by the Transfer Verifier code assuming that it successfully recognizes | ||
the malicious Token Bridge activity simulated by the `cast` commands in `transfer-verifier-test.sh`. | ||
|
||
It also defines a path to the log file that contains this string. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
#!/bin/sh | ||
|
||
log_file="${ERROR_LOG_PATH:-/logs/error.log}" | ||
error_pattern="${ERROR_PATTERN:-ERROR}" | ||
status_file="/logs/status" | ||
|
||
# Wait for log file to exist and be non-empty | ||
while [ ! -s "${log_file}" ]; do | ||
echo "Waiting for ${log_file} to be created and contain data..." | ||
sleep 5 | ||
done | ||
|
||
# Initialize status | ||
echo "RUNNING" > "$status_file" | ||
echo "Monitoring file '${log_file}' for error pattern: '${error_pattern}'" | ||
|
||
# Watch for changes in the log file. If we find the error pattern that means we have | ||
# succeeded. (Transfer verifier should correctly detect errors. | ||
inotifywait -m -e modify "${log_file}" | while read -r directory events filename; do | ||
if grep -q "$error_pattern" "$log_file"; then | ||
echo "SUCCESS" > "$status_file" | ||
echo "Found error pattern. Exiting." | ||
exit 0 | ||
fi | ||
done |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
#!/usr/bin/env bash | ||
set -euo pipefail | ||
|
||
RPC="${RPC_URL:-ws://eth-devnet:8545}" | ||
|
||
# mainnet values | ||
# export CORE_CONTRACT="0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B" | ||
# export TOKEN_BRIDGE_CONTRACT="0x3ee18B2214AFF97000D974cf647E7C347E8fa585" | ||
|
||
# TODO these could be CLI params from the sh/devnet script | ||
CORE_BRIDGE_CONTRACT=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550 | ||
TOKEN_BRIDGE_CONTRACT=0x0290FB167208Af455bB137780163b7B7a9a10C16 | ||
|
||
MNEMONIC=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d | ||
|
||
ERC20_ADDR="0x47bdB2D7d6528C760b6f228b3B8F9F650169a10f" # Test token A | ||
|
||
VALUE="1000" # Wei value sent as msg.value | ||
TRANSFER_AMOUNT="10" | ||
|
||
# This account is generated by anvil and can be confirmed by running `anvil --accounts=13`. | ||
# The accounts at other indices are used by other tests in the test suite, so | ||
# account[13] is used here to help encapsulate the tests. | ||
ANVIL_USER="0x64E078A8Aa15A41B85890265648e965De686bAE6" | ||
ETH_WHALE="${ANVIL_USER}" | ||
FROM="${ETH_WHALE}" | ||
# Anvil user1 normalized to Wormhole size. (The value itself it unchecked but must have this format.) | ||
RECIPIENT="0x00000000000000000000000064E078A8Aa15A41B85890265648e965De686bAE6" | ||
NONCE="234" # arbitrary | ||
|
||
# Build the payload for token transfers. Declared on multiple lines to | ||
# be more legible. Data pulled from an arbitrary LogMessagePublished event | ||
# on etherscan. Metadata and fees commented out, leaving only the payload | ||
PAYLOAD="0x" | ||
declare -a SLOTS=( | ||
# "0000000000000000000000000000000000000000000000000000000000055baf" | ||
# "0000000000000000000000000000000000000000000000000000000000000000" | ||
# "0000000000000000000000000000000000000000000000000000000000000080" | ||
# "0000000000000000000000000000000000000000000000000000000000000001" | ||
# "00000000000000000000000000000000000000000000000000000000000000ae" | ||
"030000000000000000000000000000000000000000000000000000000005f5e1" | ||
"000000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5" | ||
"9900020000000000000000000000000000000000000000000000000000000000" | ||
"000816001000000000000000000000000044eca3f6295d6d559ca1d99a5ef5a8" | ||
"f72b4160f10001010200c91f01004554480044eca3f6295d6d559ca1d99a5ef5" | ||
"a8f72b4160f10000000000000000000000000000000000000000000000000000" | ||
) | ||
for i in "${SLOTS[@]}" | ||
do | ||
PAYLOAD="$PAYLOAD$i" | ||
done | ||
|
||
echo "DEBUG:" | ||
echo "- RPC=${RPC}" | ||
echo "- CORE_BRIDGE_CONTRACT=${CORE_BRIDGE_CONTRACT}" | ||
echo "- TOKEN_BRIDGE_CONTRACT=${TOKEN_BRIDGE_CONTRACT}" | ||
echo "- MNEMONIC=${MNEMONIC}" | ||
echo "- FROM=${FROM}" | ||
echo "- VALUE=${VALUE}" | ||
echo "- RECIPIENT=${RECIPIENT}" | ||
echo | ||
|
||
# Fund the token bridge from the user | ||
echo "Start impersonating Anvil key: ${ANVIL_USER}" | ||
cast rpc \ | ||
anvil_impersonateAccount "${ANVIL_USER}" \ | ||
--rpc-url "${RPC}" | ||
echo "Funding token bridge using the user's balance" | ||
cast send --unlocked \ | ||
--rpc-url "${RPC}" \ | ||
--from $ANVIL_USER \ | ||
--value 100000000000000 \ | ||
${TOKEN_BRIDGE_CONTRACT} | ||
echo "" | ||
echo "End impersonating User0" | ||
cast rpc \ | ||
anvil_stopImpersonatingAccount "${ANVIL_USER}" \ | ||
--rpc-url "${RPC}" | ||
|
||
BALANCE_CORE=$(cast balance --rpc-url "${RPC}" $CORE_BRIDGE_CONTRACT) | ||
BALANCE_TOKEN=$(cast balance --rpc-url "${RPC}" $TOKEN_BRIDGE_CONTRACT) | ||
BALANCE_USER=$(cast balance --rpc-url "${RPC}" $ANVIL_USER) | ||
echo "BALANCES:" | ||
echo "- CORE_BRIDGE_CONTRACT=${BALANCE_CORE}" | ||
echo "- TOKEN_BRIDGE_CONTRACT=${BALANCE_TOKEN}" | ||
echo "- ANVIL_USER=${BALANCE_USER}" | ||
echo | ||
|
||
# === Malicious call to transferTokensWithPayload() | ||
# This is the exploit scenario: the token bridge has called publishMessage() without a ERC20 Transfer or Deposit | ||
# being present in the same receipt. | ||
# This is done by impersonating the token bridge contract and sending a message directly to the core bridge. | ||
# Ensure that anvil is using `--auto-impersonate` or else that account impersonation is enabled in your local environment. | ||
# --private-key "$MNEMONIC" \ | ||
# --max-fee 500000 \ | ||
echo "Start impersonate token bridge" | ||
cast rpc \ | ||
--rpc-url "${RPC}" \ | ||
anvil_impersonateAccount "${TOKEN_BRIDGE_CONTRACT}" | ||
echo "Calling publishMessage as ${TOKEN_BRIDGE_CONTRACT}" | ||
cast send --unlocked \ | ||
--rpc-url "${RPC}" \ | ||
--json \ | ||
--gas-limit 10000000 \ | ||
--priority-gas-price 1 \ | ||
--from "${TOKEN_BRIDGE_CONTRACT}" \ | ||
--value "0" \ | ||
"${CORE_BRIDGE_CONTRACT}" \ | ||
"publishMessage(uint32,bytes,uint8)" \ | ||
0 "${PAYLOAD}" 1 | ||
echo "" | ||
cast rpc \ | ||
--rpc-url "${RPC}" \ | ||
anvil_stopImpersonatingAccount "${TOKEN_BRIDGE_CONTRACT}" | ||
echo "End impersonate token bridge" | ||
|
||
# TODO add the 'multicall' scenario encoded in the forge script | ||
|
||
echo "Done Transfer Verifier integration test." | ||
echo "Exiting." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
apiVersion: batch/v1 | ||
kind: Job | ||
metadata: | ||
name: tx-verifier-test | ||
spec: | ||
# Number of successful pod completions needed | ||
completions: 1 | ||
# Number of pods to run in parallel | ||
parallelism: 1 | ||
# Time limit after which the job is terminated (optional) | ||
# activeDeadlineSeconds: 100 | ||
# Number of retries before marking as failed | ||
backoffLimit: 4 | ||
template: | ||
metadata: | ||
labels: | ||
app: tx-verifier-test | ||
spec: | ||
restartPolicy: Never | ||
containers: | ||
- name: tx-verifier-test | ||
image: tx-verifier-test | ||
command: | ||
- /bin/bash | ||
- -c | ||
- "/transfer-verifier-test.sh" | ||
env: | ||
- name: RPC_URL | ||
value: "ws://eth-devnet:8545" | ||
volumes: | ||
- name: log-volume | ||
emptyDir: {} |
Oops, something went wrong.