diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml new file mode 100644 index 0000000..fa0f16a --- /dev/null +++ b/.github/workflows/foundry.yml @@ -0,0 +1,41 @@ +name: foundry + +on: + pull_request: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + defaults: + run: + working-directory: evm + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + # - name: Run Forge tests + # run: | + # forge test -vvv + # id: test diff --git a/.github/workflows/tilt.yml b/.github/workflows/tilt.yml index fb382f0..babaa49 100644 --- a/.github/workflows/tilt.yml +++ b/.github/workflows/tilt.yml @@ -7,6 +7,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: start minikube id: minikube uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # pinned to v0.0.19 commit diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9ff7e9d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "evm/lib/forge-std"] + path = evm/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "evm/lib/example-messaging-executor"] + path = evm/lib/example-messaging-executor + url = https://github.com/wormholelabs-xyz/example-messaging-executor +[submodule "evm/lib/wormhole-solidity-sdk"] + path = evm/lib/wormhole-solidity-sdk + url = https://github.com/wormhole-foundation/wormhole-solidity-sdk diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b681052 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +evm diff --git a/Dockerfile b/Dockerfile index 4c62713..5fce710 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,5 +17,5 @@ USER bun EXPOSE 3000/tcp ENTRYPOINT [ "bun", "start" ] -FROM release as test +FROM release AS test diff --git a/Dockerfile.e2e b/Dockerfile.e2e new file mode 100644 index 0000000..470f3d0 --- /dev/null +++ b/Dockerfile.e2e @@ -0,0 +1,9 @@ +FROM forge + +FROM executor + +WORKDIR /usr/src/app + +COPY --from=forge /app/out evm/out + +ENTRYPOINT [ "bun", "test" ] diff --git a/README.md b/README.md index 7780621..8189082 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,36 @@ implied. See the License for the specific language governing permissions and lim spoken - this is a very complex piece of software which targets a bleeding-edge, experimental smart contract runtime. Mistakes happen, and no matter how hard you try and whether you pay someone to audit it, it may eat your tokens, set your printer on fire or startle your cat. Cryptocurrencies are a high-risk investment, no matter how fancy. + +## Testing Flow + +> 🚧 This is a work in progress! + +First tilt up. + +```bash +tilt up +``` + +Next, deploy integration contracts to each chain. + +```bash +forge create ExecutorVAAv1Integration -r http://localhost:8545 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --constructor-args 0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78 0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B 200 +forge create ExecutorVAAv1Integration -r http://localhost:8546 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --constructor-args 0x79A1027a6A159502049F10906D333EC57E95F083 0x51B47D493CBA7aB97e3F8F163D6Ce07592CE4482 200 +``` + +Finally, bun test + +```bash +bun test +``` + +Click the link! It should be `pending` at first and then after a couple seconds, refresh and it should be `submitted`! + +Next steps + +- Support NTT v1 +- Publish the executor docker image +- Support passing the executor chain config from env or command line so that someone using the docker image can configure the chain info +- Deploy the contracts within the e2e test and actually confirm the messages send both ways and update the contract number. +- Trigger the e2e test in CI diff --git a/Tiltfile b/Tiltfile index d908616..fed2166 100644 --- a/Tiltfile +++ b/Tiltfile @@ -6,16 +6,12 @@ k8s_yaml("k8s/anvil-eth-sepolia.yaml") k8s_resource( "anvil-eth-sepolia", - port_forwards = [ - port_forward(8545, name = "RPC [:8545]"), - ], + port_forwards = 8545, labels = ["anvil"], ) k8s_resource( "anvil-base-sepolia", - port_forwards = [ - port_forward(8546, 8545, name = "RPC [:8546]"), - ], + port_forwards = '8546:8545', labels = ["anvil"], ) @@ -29,12 +25,30 @@ docker_build( ] ) +docker_build( + ref = "forge", + context = "evm", + dockerfile = "./evm/Dockerfile", + only=["foundry.toml", "lib", "src"] +) +docker_build( + ref = "e2e", + context = ".", + dockerfile = "./Dockerfile.e2e", + only=[] +) + k8s_yaml("k8s/executor.yaml") k8s_resource( "executor", - port_forwards = [ - port_forward(3000, name = "Executor [:3000]"), - ], + port_forwards = 3000, resource_deps = ["anvil-eth-sepolia", "anvil-base-sepolia"], labels = ["app"], ) + +k8s_yaml("k8s/e2e.yaml") +k8s_resource( + "e2e", + resource_deps=["anvil-eth-sepolia", "anvil-base-sepolia", "executor"], + labels=["tests"] +) diff --git a/bun.lock b/bun.lock index aa1fe9c..ebf5607 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@types/express": "^5.0.3", "@wormhole-foundation/sdk-base": "^2.4.0", "@wormhole-foundation/sdk-definitions": "^2.4.0", + "axios": "^1.10.0", "binary-layout": "^1.3.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -73,6 +74,10 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="], + "binary-layout": ["binary-layout@1.3.0", "", {}, "sha512-jDJ6rLgjjQ9q8NP5eIumdvsegbbMsNplJ7GHMuVnMWi0Qw59o8kIOw+ew4fLAryPL3LgIp5KrjfdAMoSpmpO8w=="], "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], @@ -87,6 +92,8 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -101,6 +108,8 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -115,6 +124,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -125,6 +136,10 @@ "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + + "form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -139,6 +154,8 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], @@ -187,6 +204,8 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -235,6 +254,10 @@ "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], } } diff --git a/evm/.gitignore b/evm/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/evm/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/evm/Dockerfile b/evm/Dockerfile new file mode 100644 index 0000000..a833bf6 --- /dev/null +++ b/evm/Dockerfile @@ -0,0 +1,8 @@ +FROM ghcr.io/foundry-rs/foundry:v1.2.3@sha256:d9133dae61c19383b72695dc7eeca29d1e7a89f1f1b5fdfd8900c660b46b4303 AS forge + +WORKDIR /app +COPY foundry.toml foundry.toml +COPY lib lib +COPY src src + +RUN forge build diff --git a/evm/README.md b/evm/README.md new file mode 100644 index 0000000..9265b45 --- /dev/null +++ b/evm/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/evm/foundry.toml b/evm/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/evm/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/evm/lib/example-messaging-executor b/evm/lib/example-messaging-executor new file mode 160000 index 0000000..a875ce6 --- /dev/null +++ b/evm/lib/example-messaging-executor @@ -0,0 +1 @@ +Subproject commit a875ce6afdc624477914988928092cc3102dc7fd diff --git a/evm/lib/forge-std b/evm/lib/forge-std new file mode 160000 index 0000000..77041d2 --- /dev/null +++ b/evm/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/evm/lib/wormhole-solidity-sdk b/evm/lib/wormhole-solidity-sdk new file mode 160000 index 0000000..b9e129e --- /dev/null +++ b/evm/lib/wormhole-solidity-sdk @@ -0,0 +1 @@ +Subproject commit b9e129e65d34827d92fceeed8c87d3ecdfc801d0 diff --git a/evm/src/ExecutorVAAv1Integration.sol b/evm/src/ExecutorVAAv1Integration.sol new file mode 100644 index 0000000..3228255 --- /dev/null +++ b/evm/src/ExecutorVAAv1Integration.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "example-messaging-executor/evm/src/interfaces/IExecutor.sol"; +import "example-messaging-executor/evm/src/interfaces/IVaaV1Receiver.sol"; +import "example-messaging-executor/evm/src/libraries/ExecutorMessages.sol"; +import {toWormholeFormat} from "wormhole-solidity-sdk/Utils.sol"; + +struct ExecutorArgs { + // The refund address used by the Executor. + address refundAddress; + // The signed quote to be passed into the Executor. + bytes signedQuote; + // The relay instructions to be passed into the Executor. + bytes instructions; +} + +/// This contract is for testing purposes only and is missing several security checks a real integration might do +/// Integrators are encouraged to follow the integration guide at +/// https://wormholelabs.notion.site/Executor-Integration-Notes-Public-1bd3029e88cb804e8281ec19e3264c3b +contract ExecutorVAAv1Integration is IVaaV1Receiver { + bytes32 public immutable emitterAddress; + uint16 public immutable ourChain; + uint8 public immutable wormholeFinality; + IWormhole public immutable wormhole; + IExecutor public immutable executor; + + uint256 public number; + + error InvalidVaa(string reason); + + constructor(address _wormhole, address _executor, uint8 _wormholeFinality) { + assert(_wormhole != address(0)); + assert(_executor != address(0)); + + wormhole = IWormhole(_wormhole); + executor = IExecutor(_executor); + + ourChain = wormhole.chainId(); + emitterAddress = toWormholeFormat(address(this)); + wormholeFinality = _wormholeFinality; + } + + function incrementAndSend(uint16 destinationChain, bytes32 destinationAddress, ExecutorArgs calldata executorArgs) + public + payable + { + number++; + uint256 wormholeFee = wormhole.messageFee(); + require(msg.value >= wormholeFee, "insufficient value"); + uint256 executionAmount = msg.value - wormholeFee; + + uint64 sequence = wormhole.publishMessage{value: wormholeFee}(0, abi.encodePacked(number), wormholeFinality); + + executor.requestExecution{value: executionAmount}( + destinationChain, + destinationAddress, // a real integration would likely send to its peer + executorArgs.refundAddress, + executorArgs.signedQuote, + ExecutorMessages.makeVAAv1Request(ourChain, emitterAddress, sequence), + executorArgs.instructions + ); + } + + function executeVAAv1(bytes memory vaa) public payable { + (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(vaa); + if (!valid) { + revert InvalidVaa(reason); + } + (number) = abi.decode(vm.payload, (uint256)); + } +} diff --git a/k8s/e2e.yaml b/k8s/e2e.yaml new file mode 100644 index 0000000..64132ee --- /dev/null +++ b/k8s/e2e.yaml @@ -0,0 +1,15 @@ +kind: Job +apiVersion: batch/v1 +metadata: + name: e2e +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: e2e + spec: + restartPolicy: Never + containers: + - name: e2e + image: e2e diff --git a/package.json b/package.json index 58336d4..8c4b0da 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@types/express": "^5.0.3", "@wormhole-foundation/sdk-base": "^2.4.0", "@wormhole-foundation/sdk-definitions": "^2.4.0", + "axios": "^1.10.0", "binary-layout": "^1.3.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/src/api/status.ts b/src/api/status.ts index d3169ec..0da62b3 100644 --- a/src/api/status.ts +++ b/src/api/status.ts @@ -201,9 +201,6 @@ async function verifySignedQuote(signedQuote: SignedQuote): Promise { `Bad quoterAddress. Expected: ${QUOTER_PUBLIC_KEY}, Received: ${signedQuote.quote.quoterAddress}`, ); } - if (!isHex(signedQuote.signature)) { - throw new Error(`Bad signature`); - } const recoveredPublicKey = await recoverAddress({ hash: keccak256(serialize(quoteLayout, signedQuote)), signature: signedQuote.signature, diff --git a/src/consts.ts b/src/consts.ts index ed267cc..59c3644 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,18 +1,18 @@ import { padHex, toHex } from "viem"; import { mnemonicToAccount } from "viem/accounts"; -const account = mnemonicToAccount( - "test test test test test test test test test test test junk", - { addressIndex: 9 }, -); +export const ANVIL_MNEMONIC = + "test test test test test test test test test test test junk"; + +const account9 = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 9 }); -export const PAYEE_PUBLIC_KEY = account.address; +export const PAYEE_PUBLIC_KEY = "0xf7122c001b3e07d7fafd8be3670545135859954a"; -export const EVM_PUBLIC_KEY = account.address; -export const EVM_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x"); +export const EVM_PUBLIC_KEY = account9.address; +export const EVM_PRIVATE_KEY = toHex(account9.getHdKey().privateKey || "0x"); -export const QUOTER_PUBLIC_KEY = account.address; -export const QUOTER_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x"); +export const QUOTER_PUBLIC_KEY = account9.address; +export const QUOTER_PRIVATE_KEY = toHex(account9.getHdKey().privateKey || "0x"); export const EMPTY_ADDRESS = padHex( "0x0000000000000000000000000000000000000000000000000000000000000000", diff --git a/src/e2e.test.ts b/src/e2e.test.ts new file mode 100644 index 0000000..11d8ec2 --- /dev/null +++ b/src/e2e.test.ts @@ -0,0 +1,267 @@ +import { serializeLayout } from "@wormhole-foundation/sdk-base"; +import { relayInstructionsLayout } from "@wormhole-foundation/sdk-definitions"; +import axios from "axios"; +import { sleep } from "bun"; +import { expect, test } from "bun:test"; +import { + createPublicClient, + createWalletClient, + getContract, + http, + isHex, + padHex, + toHex, + type Account, + type Chain, + type PublicClient, + type WalletClient, +} from "viem"; +import { mnemonicToAccount } from "viem/accounts"; +import forgeOutput from "../evm/out/ExecutorVAAv1Integration.sol/ExecutorVAAv1Integration.json"; +import { enabledChains } from "./chains"; +import { ANVIL_MNEMONIC } from "./consts"; +import { RelayStatus } from "./types"; + +const ABI = [ + { + type: "constructor", + inputs: [ + { name: "_wormhole", type: "address", internalType: "address" }, + { name: "_executor", type: "address", internalType: "address" }, + { + name: "_wormholeFinality", + type: "uint8", + internalType: "uint8", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "emitterAddress", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "executor", + inputs: [], + outputs: [ + { name: "", type: "address", internalType: "contract IExecutor" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "incrementAndSend", + inputs: [ + { + name: "destinationChain", + type: "uint16", + internalType: "uint16", + }, + { + name: "destinationAddress", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "executorArgs", + type: "tuple", + internalType: "struct ExecutorArgs", + components: [ + { + name: "refundAddress", + type: "address", + internalType: "address", + }, + { name: "signedQuote", type: "bytes", internalType: "bytes" }, + { name: "instructions", type: "bytes", internalType: "bytes" }, + ], + }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "number", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "ourChain", + inputs: [], + outputs: [{ name: "", type: "uint16", internalType: "uint16" }], + stateMutability: "view", + }, + { + type: "function", + name: "wormhole", + inputs: [], + outputs: [ + { name: "", type: "address", internalType: "contract IWormhole" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "wormholeFinality", + inputs: [], + outputs: [{ name: "", type: "uint8", internalType: "uint8" }], + stateMutability: "view", + }, +] as const; + +async function deployIntegrationContract( + publicClient: PublicClient, + client: WalletClient, + account: Account, + viemChain: Chain, + coreContractAddress: string, + executorAddress: string, +) { + if (!isHex(forgeOutput.bytecode.object)) { + throw new Error("invalid bytecode"); + } + if (!isHex(coreContractAddress)) { + throw new Error("invalid coreContractAddress"); + } + if (!isHex(executorAddress)) { + throw new Error("invalid executorAddress"); + } + const hash = await client.deployContract({ + account, + chain: viemChain, + abi: ABI, + bytecode: forgeOutput.bytecode.object, + args: [coreContractAddress, executorAddress, 200], + }); + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }); + if (!isHex(receipt.contractAddress)) { + throw new Error("invalid contractAddress"); + } + return receipt.contractAddress; +} + +test("it performs a VAA v1 relay", async () => { + const srcChain = enabledChains[10002]!; + const dstChain = enabledChains[10004]!; + const account = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 }); + if (!srcChain.viemChain || !dstChain.viemChain) { + throw new Error("invalid viem chain"); + } + const srcTransport = http(srcChain.rpc); + const srcPublicClient = createPublicClient({ + chain: srcChain.viemChain, + transport: srcTransport, + }); + const srcClient = createWalletClient({ + account, + chain: srcChain.viemChain, + transport: srcTransport, + }); + const dstTransport = http(dstChain.rpc); + const dstPublicClient = createPublicClient({ + chain: dstChain.viemChain, + transport: dstTransport, + }); + const dstClient = createWalletClient({ + account, + chain: dstChain.viemChain, + transport: dstTransport, + }); + const srcContract = await deployIntegrationContract( + srcPublicClient, + srcClient, + account, + srcChain.viemChain, + srcChain.coreContractAddress, + srcChain.executorAddress, + ); + console.log(`Deployed source contract: ${srcContract}`); + const dstContract = await deployIntegrationContract( + dstPublicClient, + dstClient, + account, + dstChain.viemChain, + dstChain.coreContractAddress, + dstChain.executorAddress, + ); + console.log(`Deployed destination contract: ${dstContract}`); + const dstTestContract = getContract({ + address: srcContract, + abi: ABI, + client: srcClient, + }); + expect(await dstTestContract.read.number()).toBe(0n); + const relayInstructions = toHex( + serializeLayout(relayInstructionsLayout, { + requests: [ + { + request: { + type: "GasInstruction", + gasLimit: 250000n, + msgValue: 0n, + }, + }, + ], + }), + ); + const response = await axios.post("http://executor:3000/v0/quote", { + srcChain: 10002, + dstChain: 10004, + relayInstructions, + }); + const srcTestContract = getContract({ + address: srcContract, + abi: ABI, + client: srcClient, + }); + const hash = await srcTestContract.write.incrementAndSend( + [ + 10004, + padHex(dstContract, { + dir: "left", + size: 32, + }), + { + instructions: relayInstructions, + refundAddress: account.address, + signedQuote: response.data.signedQuote, + }, + ], + { value: BigInt(response.data.estimatedCost) }, + ); + console.log( + `Request execution: https://wormholelabs-xyz.github.io/executor-explorer/#/chain/10002/tx/${hash}?endpoint=http%3A%2F%2Flocalhost%3A3000&env=Testnet`, + ); + await srcPublicClient.waitForTransactionReceipt({ + hash, + }); + let statusResult; + while ( + !statusResult || + statusResult.data?.[0].status === RelayStatus.Pending + ) { + console.log(`Statusing tx: ${hash}`); + if (statusResult) { + await sleep(1000); + } + statusResult = await axios.post("http://executor:3000/v0/status/tx", { + chainId: srcChain.wormholeChainId, + txHash: hash, + }); + if (statusResult.data.length !== 1) { + throw new Error(`unexpected status result length`); + } + } + expect(statusResult.data?.[0].status).toBe(RelayStatus.Submitted); + expect(await srcTestContract.read.number()).toBe(1n); + expect(await dstTestContract.read.number()).toBe(1n); +}, 60000); diff --git a/src/mockGuardian.ts b/src/mockGuardian.ts index 13be5a0..abdcd94 100644 --- a/src/mockGuardian.ts +++ b/src/mockGuardian.ts @@ -87,7 +87,9 @@ export async function mockWormhole( ): Promise { const vaa = await getWormholeMessage(rpc, txHash, coreContractAddress); if (vaa) { - const guardianSet = new mocks.MockGuardians(0, [EVM_PRIVATE_KEY]); + const guardianSet = new mocks.MockGuardians(0, [ + EVM_PRIVATE_KEY.substring(2), + ]); const signedVaa = guardianSet.addSignatures(vaa); const base64 = Buffer.from(serialize(signedVaa)).toString("base64"); return base64; diff --git a/src/relay/platform/evm.ts b/src/relay/platform/evm.ts index d1ea7a9..1c90454 100644 --- a/src/relay/platform/evm.ts +++ b/src/relay/platform/evm.ts @@ -1,3 +1,8 @@ +import { + relayInstructionsLayout, + type RelayInstructions, +} from "@wormhole-foundation/sdk-definitions"; +import { deserialize } from "binary-layout"; import { createPublicClient, createWalletClient, @@ -12,36 +17,24 @@ import { toHex, trim, type Hex, - type PublicClient, - type TransactionReceipt, } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { anvil } from "viem/chains"; +import { RequestForExecutionLogABI } from "../../abis/requestForExecutionLog"; +import { vaaV1ReceiveWithGasDropAbi } from "../../abis/vaaV1ReceiveWithGasDropoffAbi"; import type { ChainConfig } from "../../chains"; +import { EVM_PRIVATE_KEY } from "../../consts"; import type { RequestId } from "../../layouts/requestId"; import type { RelayRequestData, RequestForExecution, TxInfo, } from "../../types"; -import { - anvil, - type OpStackTransactionReceipt, - type ZksyncTransactionReceipt, -} from "viem/chains"; -import { RequestForExecutionLogABI } from "../../abis/requestForExecutionLog"; import { getFirstDropOffInstruction, getTotalGasLimitAndMsgValue, getTotalMsgValueFromGasInstructions, } from "../../utils"; -import { - relayInstructionsLayout, - type RelayInstructions, -} from "@wormhole-foundation/sdk-definitions"; -import { deserialize } from "binary-layout"; -import { EVM_PRIVATE_KEY } from "../../consts"; -import { privateKeyToAccount } from "viem/accounts"; -import { vaaV1ReceiveWithGasDropAbi } from "../../abis/vaaV1ReceiveWithGasDropoffAbi"; -import { mockWormhole } from "../../mockGuardian"; const REQUEST_FOR_EXECUTION_TOPIC = toEventHash( "RequestForExecution(address,uint256,uint16,bytes32,address,bytes,bytes,bytes)", @@ -157,14 +150,8 @@ export class EvmHandler { static async relayVAAv1( chainConfig: ChainConfig, relayRequest: RelayRequestData, + base64Vaa: string, ): Promise> { - if ( - !isHex(relayRequest.txHash) || - !isHex(chainConfig.coreContractAddress) - ) { - throw new Error(`TxHash not hex!`); - } - const transport = http(chainConfig.rpc); const publicClient = createPublicClient({ chain: anvil, @@ -199,12 +186,6 @@ export class EvmHandler { transport, }); - const base64Vaa = await mockWormhole( - chainConfig.rpc, - relayRequest.txHash, - chainConfig.coreContractAddress, - ); - const payloadHex = toHex(Buffer.from(base64Vaa, "base64")); const { request } = await publicClient.simulateContract({ diff --git a/src/relay/queue.ts b/src/relay/queue.ts index 6aaf17d..5ac3494 100644 --- a/src/relay/queue.ts +++ b/src/relay/queue.ts @@ -21,6 +21,7 @@ class InMemoryRelayRequestQueue { try { txInfos = await callback(relayData); } catch (e: unknown) { + console.error(e); relayData.status = RelayStatus.Failed; if (e instanceof UnsupportedRelayRequestError) { diff --git a/src/relay/relayer.ts b/src/relay/relayer.ts index a0f3594..e33602e 100644 --- a/src/relay/relayer.ts +++ b/src/relay/relayer.ts @@ -1,4 +1,6 @@ +import { isHex } from "viem"; import { enabledChains } from "../chains"; +import { mockWormhole } from "../mockGuardian"; import { RelayAbortedError, RequestPrefix, @@ -11,15 +13,23 @@ import { EvmHandler } from "./platform/evm"; export const processRelayRequests = async ( relayRequest: RelayRequestData, ): Promise> => { - const chainConfig = enabledChains[relayRequest.chainId]; + const srcChainConfig = enabledChains[relayRequest.chainId]; + const dstChainConfig = + enabledChains[relayRequest.requestForExecution.dstChain]; console.log( `Relaying ${relayRequest.id} of type ${relayRequest.instruction?.request.prefix}`, ); - if (!chainConfig) { + if (!srcChainConfig) { throw new RelayAbortedError( - `Error in chain configuration: Chain ID ${relayRequest.chainId} not configured.`, + `Error in chain configuration: Source Chain ID ${relayRequest.chainId} not configured.`, + ); + } + + if (!dstChainConfig) { + throw new RelayAbortedError( + `Error in chain configuration: Destination Chain ID ${relayRequest.chainId} not configured.`, ); } @@ -32,7 +42,7 @@ export const processRelayRequests = async ( const { request } = relayRequest.instruction; const { prefix } = request; - if (!chainConfig.capabilities.requestPrefixes.includes(prefix)) { + if (!dstChainConfig.capabilities.requestPrefixes.includes(prefix)) { throw new UnsupportedRelayRequestError( `Request type of ${relayRequest.instruction.request.prefix} not supported for Chain ID ${relayRequest.chainId}`, ); @@ -42,9 +52,21 @@ export const processRelayRequests = async ( switch (prefix) { case RequestPrefix.ERV1: + if (!isHex(relayRequest.txHash)) { + throw new Error(`TxHash not hex!`); + } + if (!isHex(srcChainConfig.coreContractAddress)) { + throw new Error(`Core contract not hex!`); + } + const base64Vaa = await mockWormhole( + srcChainConfig.rpc, + relayRequest.txHash, + srcChainConfig.coreContractAddress, + ); relayedTransactions = await EvmHandler.relayVAAv1( - chainConfig, + dstChainConfig, relayRequest, + base64Vaa, ); break; case RequestPrefix.ERC2: diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..4df190b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,6 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false - } + }, + "exclude": ["evm", "src/e2e.test.ts"] }