Skip to content

Commit

Permalink
Merge pull request #11 from wormhole-foundation/monitoring+replay-pro…
Browse files Browse the repository at this point in the history
…tection

Monitoring+replay protection
  • Loading branch information
derpy-duck authored Aug 8, 2023
2 parents 5dc3387 + 3568d16 commit 5cf4b0c
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 118 deletions.
76 changes: 44 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

This tutorial contains a solidity contract (`HelloWormhole.sol`) that can be deployed onto many EVM chains to form a fully functioning cross-chain application.

Specifically, we will write and deploy a contract onto many chains that allows users to request, from one contract, that a `GreetingReceived` event be emitted from a contracts on a _different chain_.
Specifically, we will write and deploy a contract onto many chains that allows users to request, from one contract, that a `GreetingReceived` event be emitted from a contracts on a _different chain_.

This also allows users to pay for their custom greeting to be emitted on a chain that they do not have any gas funds for!
This also allows users to pay for their custom greeting to be emitted on a chain that they do not have any gas funds for!

## Getting Started

Expand Down Expand Up @@ -41,7 +41,7 @@ Test result: ok. 1 passed; 0 failed; finished in 3.98s

### Deploying to Testnet

You will need a wallet with at least 0.05 Testnet AVAX and 0.01 Testnet CELO.
You will need a wallet with at least 0.05 Testnet AVAX and 0.01 Testnet CELO.

- [Obtain testnet AVAX here](https://core.app/tools/testnet-faucet/?token=C)
- [Obtain testnet CELO here](https://faucet.celo.org/alfajores)
Expand Down Expand Up @@ -78,14 +78,14 @@ contract HelloWorld {
event GreetingReceived(string greeting, address sender);
string[] public greetings;
/**
* @notice Returns the cost (in wei) of a greeting
*/
function quoteGreeting() public view returns (uint256 cost) {
return 0;
}
/**
* @notice Updates the list of 'greetings'
* and emits a 'GreetingReceived' event with 'greeting'
Expand All @@ -103,17 +103,17 @@ contract HelloWorld {

### Taking HelloWorld cross-chain using Wormhole Automatic Relayers

Suppose we want users to be able to request, through their Ethereum wallet, that a greeting be sent to Avalanche, and vice versa.
Suppose we want users to be able to request, through their Ethereum wallet, that a greeting be sent to Avalanche, and vice versa.

Let us begin writing a contract that we can deploy onto Ethereum, Avalanche, or any number of other chains, to enable greetings be sent freely between each contract, irrespective of chain.
Let us begin writing a contract that we can deploy onto Ethereum, Avalanche, or any number of other chains, to enable greetings be sent freely between each contract, irrespective of chain.

We'll want to implement the following function:
We'll want to implement the following function:

```solidity
/**
* @notice Updates the list of 'greetings'
* @notice Updates the list of 'greetings'
* and emits a 'GreetingReceived' event with 'greeting'
* on the HelloWormhole contract at
* on the HelloWormhole contract at
* chain 'targetChain' and address 'targetAddress'
*/
function sendCrossChainGreeting(
Expand All @@ -128,18 +128,18 @@ The Wormhole Relayer contract lets us do exactly this! Let’s take a look at th
```solidity
/**
* @notice Publishes an instruction for the default delivery provider
* to relay a payload to the address `targetAddress` on chain `targetChain`
* to relay a payload to the address `targetAddress` on chain `targetChain`
* with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
*
*
* `targetAddress` must implement the IWormholeReceiver interface
*
*
* This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
*
* Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function
*
* Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function
* with `refundChain` and `refundAddress` as parameters
*
*
* @param targetChain in Wormhole Chain ID format
* @param targetAddress address to call on targetChain (that implements IWormholeReceiver)
* @param targetAddress address to call on targetChain (that implements IWormholeReceiver)
* @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
* @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
* @param gasLimit gas limit with which to call `targetAddress`.
Expand All @@ -154,7 +154,7 @@ The Wormhole Relayer contract lets us do exactly this! Let’s take a look at th
) external payable returns (uint64 sequence);
```

The Wormhole Relayer network is powered by **Delivery Providers**, who perform the service of watching for Wormhole Relayer delivery requests and performing the delivery to the intended target chain as instructed.
The Wormhole Relayer network is powered by **Delivery Providers**, who perform the service of watching for Wormhole Relayer delivery requests and performing the delivery to the intended target chain as instructed.

In exchange for calling your contract at `targetAddress` on `targetChain` and paying the gas fees that your contract consumes, they charge a source chain fee. The fee charged will depend on the conditions of the target network and the fee can be requested from the delivery provider:

Expand All @@ -166,7 +166,7 @@ So, following this interface, we can implement `sendCrossChainGreeting` by simpl

```solidity
uint256 constant GAS_LIMIT = 50_000;
IWormholeRelayer public immutable wormholeRelayer;
/**
Expand All @@ -185,9 +185,9 @@ So, following this interface, we can implement `sendCrossChainGreeting` by simpl
}
/**
* @notice Updates the list of 'greetings'
* @notice Updates the list of 'greetings'
* and emits a 'GreetingReceived' event with 'greeting'
* on the HelloWormhole contract at
* on the HelloWormhole contract at
* chain 'targetChain' and address 'targetAddress'
*/
function sendCrossChainGreeting(
Expand All @@ -209,9 +209,9 @@ So, following this interface, we can implement `sendCrossChainGreeting` by simpl
```

A key part of this system, though, is that the contract at the `targetAddress` must implement the `IWormholeReceiver` interface.
A key part of this system, though, is that the contract at the `targetAddress` must implement the `IWormholeReceiver` interface.

Since we want to allow sending and receiving messages by the `HelloWormhole` contract, we must implement this interface.
Since we want to allow sending and receiving messages by the `HelloWormhole` contract, we must implement this interface.

```solidity
// SPDX-License-Identifier: Apache 2
Expand All @@ -238,7 +238,7 @@ interface IWormholeReceiver {
* The invocation of this function corresponding to the `send` request will have msg.value equal
* to the receiverValue specified in the send request.
*
* If the invocation of this function reverts or exceeds the gas limit
* If the invocation of this function reverts or exceeds the gas limit
* specified by the send requester, this delivery will result in a `ReceiverFailure`.
*
* @param payload - an arbitrary message which was included in the delivery by the
Expand All @@ -265,18 +265,28 @@ interface IWormholeReceiver {
}
```

After `sendPayloadToEvm` is called on the source chain, the off-chain Delivery Provider will pick up the VAA corresponding to the message. It will then call the `receiveWormholeMessages` method on the `targetChain` and `targetAddress` specified.
After `sendPayloadToEvm` is called on the source chain, the off-chain Delivery Provider will pick up the VAA corresponding to the message. It will then call the `receiveWormholeMessages` method on the `targetChain` and `targetAddress` specified.

So, in receiveWormholeMessages, we want to:

1) Update the latest greeting
2) Emit a 'GreetingReceived' event with the 'greeting' and sender of the greeting
1. Update the latest greeting
2. Emit a 'GreetingReceived' event with the 'greeting' and sender of the greeting

> Note: It is crucial that only the Wormhole Relayer contract can call receiveWormholeMessages
To provide certainty about the validity of the payload, we must restrict the msg.sender of this function to only be the Wormhole Relayer contract. Otherwise, anyone could call this receiveWormholeMessages endpoint with fake greetings, source chains, and source senders.

### One more step

You should store each delivery hash in a mapping from delivery hashes to booleans, to prevent duplicate processing of deliveries! This also gives you a way of tracking the completion of sent deliveries

```solidity
event GreetingReceived(string greeting, uint16 senderChain, address sender);
string public latestGreeting;
mapping(bytes32 => bool) public seenDeliveryVaaHashes;
/**
* @notice Endpoint that the Wormhole Relayer contract will call
* to deliver the greeting
Expand All @@ -290,6 +300,12 @@ So, in receiveWormholeMessages, we want to:
) public payable override {
require(msg.sender == address(wormholeRelayer), "Only relayer allowed");
// Ensure no duplicate deliveries
require(!seenDeliveryVaaHashes[deliveryHash], "Message already processed");
seenDeliveryVaaHashes[deliveryHash] = true;
// Parse the payload and do the corresponding actions!
(string memory greeting, address sender) = abi.decode(payload, (string, address));
latestGreeting = greeting;
Expand All @@ -302,13 +318,9 @@ So, in receiveWormholeMessages, we want to:
}
```

> Note: It is crucial that only the Wormhole Relayer contract can call receiveWormholeMessages
To provide certainty about the validity of the payload, we must restrict the msg.sender of this function to only be the Wormhole Relayer contract. Otherwise, anyone could call this receiveWormholeMessages endpoint with fake greetings, source chains, and source senders.

And voila, we have a full contract that can be deployed to many EVM chains, and in totality would form a full cross-chain application powered by Wormhole!

Users with any wallet can request greetings to be emitted on any chain that is part of the system.
Users with any wallet can request greetings to be emitted on any chain that is part of the system.

### How does it work?

Expand Down
6 changes: 3 additions & 3 deletions beyond-hello-wormhole.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ Often, it is desirable that all of the requests go through your own source contr

### Problem 2 - The greetings can be relayed multiple times

Anyone can fetch the delivery VAA corresponding to a sent greeting, and have it delivered again to the target HelloWormhole contract! This causes another `GreetingReceived` event to be emitted from the same `senderChain` and `sender`, even though the sender only intended on sending this greeting once.
As mentioned in the first article, without having the mapping of delivery hashes to boolean, anyone can fetch the delivery VAA corresponding to a sent greeting, and have it delivered again to the target HelloWormhole contract! This causes another `GreetingReceived` event to be emitted from the same `senderChain` and `sender`, even though the sender only intended on sending this greeting once.

**Solution:** In our implementation of receiveWormholeMessages, we can store each delivery hash in a mapping from delivery hashes to booleans, to indicate that the delivery has already been processed. Then, at the beginning we can check to see if the delivery has already been processed, and revert if it has.
**Solution:** In our implementation of receiveWormholeMessages, we store each delivery hash in a mapping from delivery hashes to booleans, to indicate that the delivery has already been processed. Then, at the beginning we can check to see if the delivery has already been processed, and revert if it has.

```solidity
mapping(bytes32 => bool) seenDeliveryVaaHashes;
mapping(bytes32 => bool) public seenDeliveryVaaHashes;
modifier replayProtect(bytes32 deliveryHash) {
require(!seenDeliveryVaaHashes[deliveryHash], "Message already processed");
Expand Down
11 changes: 10 additions & 1 deletion src/HelloWormhole.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,25 @@ contract HelloWormhole is IWormholeReceiver {
);
}

mapping(bytes32 => bool) public seenDeliveryVaaHashes;

function receiveWormholeMessages(
bytes memory payload,
bytes[] memory, // additionalVaas
bytes32, // address that called 'sendPayloadToEvm' (HelloWormhole contract address)
uint16 sourceChain,
bytes32 // deliveryHash - this can be stored in a mapping deliveryHash => bool to prevent duplicate deliveries
bytes32 deliveryHash // this can be stored in a mapping deliveryHash => bool to prevent duplicate deliveries
) public payable override {
require(msg.sender == address(wormholeRelayer), "Only relayer allowed");

// Ensure no duplicate deliveries
require(!seenDeliveryVaaHashes[deliveryHash], "Message already processed");
seenDeliveryVaaHashes[deliveryHash] = true;

// Parse the payload and do the corresponding actions!
(string memory greeting, address sender) = abi.decode(payload, (string, address));
latestGreeting = greeting;
emit GreetingReceived(latestGreeting, sourceChain, sender);
}

}
84 changes: 54 additions & 30 deletions ts-scripts/hello_wormhole.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,64 @@
import {describe, expect, test} from "@jest/globals";
import { describe, expect, test } from "@jest/globals";
import { ethers } from "ethers";
import {
getHelloWormhole,
getChain
} from "./utils"
import {
getStatus
} from "./getStatus"
import {
CHAIN_ID_TO_NAME
} from "@certusone/wormhole-sdk"
import { getHelloWormhole, getWallet, getDeliveryHash } from "./utils";
import { CHAIN_ID_TO_NAME } from "@certusone/wormhole-sdk";

const sourceChain = 6;
const targetChain = 14;

describe("Hello Wormhole Integration Tests on Testnet", () => {
test("Tests the sending of a random greeting", async () => {
const arbitraryGreeting = `Hello Wormhole ${new Date().getTime()}`;
const sourceHelloWormholeContract = getHelloWormhole(sourceChain);
const targetHelloWormholeContract = getHelloWormhole(targetChain);
test(
"Tests the sending of a random greeting",
async () => {
const arbitraryGreeting = `Hello Wormhole ${new Date().getTime()}`;
const sourceHelloWormholeContract = getHelloWormhole(sourceChain);
const targetHelloWormholeContract = getHelloWormhole(targetChain);

const cost = await sourceHelloWormholeContract.quoteCrossChainGreeting(targetChain);
console.log(`Cost of sending the greeting: ${ethers.utils.formatEther(cost)} testnet AVAX`);
const cost = await sourceHelloWormholeContract.quoteCrossChainGreeting(
targetChain
);
console.log(
`Cost of sending the greeting: ${ethers.utils.formatEther(
cost
)} testnet AVAX`
);

console.log(`Sending greeting: ${arbitraryGreeting}`);
const tx = await sourceHelloWormholeContract.sendCrossChainGreeting(targetChain, targetHelloWormholeContract.address, arbitraryGreeting, {value: cost});
console.log(`Transaction hash: ${tx.hash}`);
await tx.wait();
console.log(`See transaction at: https://testnet.snowtrace.io/tx/${tx.hash}`);
console.log(`Sending greeting: ${arbitraryGreeting}`);
const tx = await sourceHelloWormholeContract.sendCrossChainGreeting(
targetChain,
targetHelloWormholeContract.address,
arbitraryGreeting,
{ value: cost }
);
console.log(`Transaction hash: ${tx.hash}`);
const rx = await tx.wait();

await new Promise(resolve => setTimeout(resolve, 1000*15));
const deliveryHash = await getDeliveryHash(
rx,
CHAIN_ID_TO_NAME[sourceChain],
{ network: "TESTNET" }
);
console.log("Waiting for delivery...");
await new Promise((resolve) => {
let seconds = 0;
const interval = setInterval(async () => {
seconds += 1;
const completed =
await targetHelloWormholeContract.seenDeliveryVaaHashes(
deliveryHash
);
if (completed) {
resolve("done");
clearInterval(interval);
}
}, 1000);
});

console.log(`Reading greeting`);
const readGreeting = await targetHelloWormholeContract.latestGreeting();
console.log(`Latest greeting: ${readGreeting}`);
expect(readGreeting).toBe(arbitraryGreeting);

}, 60*1000) // timeout
})
console.log(`Reading greeting`);
const readGreeting = await targetHelloWormholeContract.latestGreeting();
console.log(`Latest greeting: ${readGreeting}`);
expect(readGreeting).toBe(arbitraryGreeting);
},
60 * 1000
); // timeout
});
4 changes: 2 additions & 2 deletions ts-scripts/testnet/deployedAddresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
null,
null,
null,
"0x63633CBf891a45A7BdCb320DfC5079EE5E20621d",
"0x156Bd8cc44a9eE24BEA225e704c906F627a276b7",
null,
null,
null,
null,
null,
null,
null,
"0x4a4c821E1fd10C6B2093A66aacEb3A629Edc9ebA"
"0xfB42a01f3E4e55740CdaDeC061cE24FE40589662"
]
}
Loading

0 comments on commit 5cf4b0c

Please sign in to comment.