evm: scaffold

This commit is contained in:
A5 Pickle 2022-10-14 15:12:30 +00:00
parent cf109ecfb2
commit 8b17cdc1c5
No known key found for this signature in database
GPG Key ID: DAE5ED38828A9F35
26 changed files with 2988 additions and 0 deletions

7
evm/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
anvil.log
cache
lib
node_modules
out
broadcast
forge-scripts/deploy.out

36
evm/Makefile Normal file
View File

@ -0,0 +1,36 @@
include testing.env
.PHONY: dependencies unit-test forge-test integration-test clean all
all: build
.PHONY: clean
clean:
rm -rf anvil.log node_modules lib out
.PHONY: dependencies
dependencies: node_modules lib/forge-std
node_modules:
yarn
lib/forge-std:
forge install foundry-rs/forge-std --no-git --no-commit
build: dependencies
forge build
.PHONY: unit-test
unit-test: forge-test
.PHONY: forge-test
forge-test: dependencies
forge test --fork-url ${TESTING_FORK_RPC} -vv
.PHONY: integration-test
integration-test: dependencies build
bash shell-scripts/run_integration_tests.sh
.PHONY: test
test: forge-test integration-test

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import {IWormhole} from "../src/interfaces/IWormhole.sol";
import {HelloWorld} from "../src/01_hello_world/HelloWorld.sol";
import "forge-std/console.sol";
contract ContractScript is Script {
IWormhole wormhole;
HelloWorld helloWorld;
function setUp() public {
wormhole = IWormhole(vm.envAddress("TESTING_WORMHOLE_ADDRESS"));
}
function deployHelloWorld() public {
// deploy the HelloWorld contract
helloWorld = new HelloWorld(
address(wormhole),
wormhole.chainId(),
1 // wormholeFinality
);
}
function run() public {
// begin sending transactions
vm.startBroadcast();
// HelloWorld.sol
deployHelloWorld();
// finished
vm.stopBroadcast();
}
}

View File

@ -0,0 +1,263 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../../src/01_hello_world/HelloWorld.sol";
import "../../src/01_hello_world/HelloWorldStructs.sol";
import {WormholeSimulator} from "wormhole-solidity/WormholeSimulator.sol";
import "forge-std/console.sol";
contract HelloWorldTest is Test {
IWormhole wormhole;
uint256 guardianSigner;
// contract instances
WormholeSimulator public wormholeSimulator;
HelloWorld public helloWorldSource;
HelloWorld public helloWorldTarget;
function setUp() public {
// verify that we're using the correct fork (AVAX mainnet in this case)
require(block.chainid == vm.envUint("TESTING_FORK_CHAINID"), "wrong evm");
// this will be used to sign wormhole messages
guardianSigner = uint256(vm.envBytes32("TESTING_DEVNET_GUARDIAN"));
// set up Wormhole using Wormhole existing on AVAX mainnet
wormholeSimulator = new WormholeSimulator(vm.envAddress("TESTING_WORMHOLE_ADDRESS"), guardianSigner);
// we may need to interact with Wormhole throughout the test
wormhole = wormholeSimulator.wormhole();
// verify Wormhole state from fork
require(wormhole.chainId() == uint16(vm.envUint("TESTING_WORMHOLE_CHAINID")), "wrong chainId");
require(wormhole.messageFee() == vm.envUint("TESTING_WORMHOLE_MESSAGE_FEE"), "wrong messageFee");
require(
wormhole.getCurrentGuardianSetIndex() == uint32(vm.envUint("TESTING_WORMHOLE_GUARDIAN_SET_INDEX")),
"wrong guardian set index"
);
// initialize "source chain" HelloWorld contract
uint8 wormholeFinality = 15;
helloWorldSource = new HelloWorld(address(wormhole), wormhole.chainId(), wormholeFinality);
// Initialize "target chain" HelloWorld contract. This contract will share the same
// chainID as the source contract, but (for testing purposes) will be treated like a contract living on
// a different blockchain.
helloWorldTarget = new HelloWorld(address(wormhole), wormhole.chainId(), wormholeFinality);
// confirm that the source and target contract addresses are different
assertTrue(address(helloWorldSource) != address(helloWorldTarget));
}
// This test confirms that the contracts are able to serialize and deserialize
// the HelloWorld message correctly.
function testMessageDeserialization(
string memory messageToSend
) public {
// encode the message by calling the encodeMessage method
bytes memory encodedMessage = helloWorldSource.encodeMessage(
HelloWorldStructs.HelloWorldMessage({
payloadID: uint8(1),
message: messageToSend
})
);
// decode the message by calling the decodeMessage method
HelloWorldStructs.HelloWorldMessage memory results = helloWorldSource.decodeMessage(encodedMessage);
// verify the parsed output
assertEq(results.payloadID, 1);
assertEq(results.message, messageToSend);
}
// This test confirms that decodeMessage reverts when a message
// has an unexpected payloadID.
function testIncorrectMessagePayload() public {
// encode the message by calling the encodeMessage method
bytes memory encodedMessage = helloWorldSource.encodeMessage(
HelloWorldStructs.HelloWorldMessage({
payloadID: uint8(2),
message: "HelloSolana"
})
);
// expect a revert when trying to decode a message with payloadID 2
vm.expectRevert("invalid payloadID");
helloWorldSource.decodeMessage(encodedMessage);
}
// This test confirms that decodeMessage reverts when a message
// is an unexpected length.
function testIncorrectMessageLength() public {
// encode the message by calling the encodeMessage method
bytes memory encodedMessage = helloWorldSource.encodeMessage(
HelloWorldStructs.HelloWorldMessage({
payloadID: uint8(1),
message: "HelloSolana"
})
);
// add some bytes to the encodedMessage
encodedMessage = abi.encodePacked(
encodedMessage,
uint256(42000)
);
// expect a revert when trying to decode a message with payloadID 2
vm.expectRevert("invalid message length");
helloWorldSource.decodeMessage(encodedMessage);
}
// This test confirms that the owner can correctly register a trusted emitter
// with the HelloWorld contracts. It also tests that an emitter chainId can
// only be registered once.
function testRegisterEmitter() public {
// cache the new emitter info
uint16 newEmitterChainId = helloWorldTarget.chainId();
bytes32 newEmitterAddress = bytes32(uint256(uint160(address(helloWorldTarget))));
// register the emitter with the owners wallet
helloWorldSource.registerEmitter(newEmitterChainId, newEmitterAddress);
// verify that the contract state was updated correctly
bytes32 emitterInContractState = helloWorldSource.getRegisteredEmitter(
helloWorldTarget.chainId()
);
assertEq(emitterInContractState, newEmitterAddress);
// confirm that the target chain emitter can only be registered once
vm.expectRevert("emitterChainId already registered");
helloWorldSource.registerEmitter(newEmitterChainId, newEmitterAddress);
}
// This test confirms that only the owner can register a trusted emitter
// with the HelloWorld contracts.
function testRegisterEmitterNotOwner() public {
// cache the new emitter info
uint16 newEmitterChainId = helloWorldTarget.chainId();
bytes32 newEmitterAddress = bytes32(uint256(uint160(address(helloWorldTarget))));
// prank the caller address to something different than the owner address
vm.prank(address(wormholeSimulator));
// expect the registerEmitter call to revert
vm.expectRevert("caller not the owner");
helloWorldSource.registerEmitter(newEmitterChainId, newEmitterAddress);
}
// This test confirms that the `sendMessage` method correctly sends the
// HelloWorld message.
function testSendMessage() public {
// start listening to events
vm.recordLogs();
// call the source HelloWorld contract and emit the HelloWorld message
uint64 sequence = helloWorldSource.sendMessage();
// record the emitted wormhole message
Vm.Log[] memory entries = vm.getRecordedLogs();
// simulate signing the wormhole message
// NOTE: in the wormhole-sdk, signed wormhole messages are referred to as signed VAAs
bytes memory encodedMessage = wormholeSimulator.fetchSignedMessageFromLogs(entries[0]);
// parse and verify the message
(
IWormhole.VM memory wormholeMessage,
bool valid,
string memory reason
) = wormhole.parseAndVerifyVM(encodedMessage);
require(valid, reason);
// verify the message payload
HelloWorldStructs.HelloWorldMessage memory results = helloWorldSource.decodeMessage(wormholeMessage.payload);
// verify the parsed output
assertEq(results.payloadID, 1);
assertEq(results.message, "HelloSolana");
assertEq(wormholeMessage.sequence, sequence);
assertEq(wormholeMessage.nonce, 42000); // message ID
assertEq(wormholeMessage.consistencyLevel, helloWorldSource.wormholeFinality());
}
// This test confirms that the `receiveMessage` method correctly consumes
// a HelloWorld messsage from the registered HelloWorld emitter. It also confirms
// that message replay protection works.
function testReceiveMessage() public {
// start listening to events
vm.recordLogs();
// call the target HelloWorld contract and emit the HelloWorld message
helloWorldTarget.sendMessage();
// record the emitted wormhole message
Vm.Log[] memory entries = vm.getRecordedLogs();
// simulate signing the wormhole message
// NOTE: in the wormhole-sdk, signed wormhole messages are referred to as signed VAAs
bytes memory encodedMessage = wormholeSimulator.fetchSignedMessageFromLogs(entries[0]);
// register the emitter on the source contract
helloWorldSource.registerEmitter(
helloWorldTarget.chainId(),
bytes32(uint256(uint160(address(helloWorldTarget))))
);
// invoke the source HelloWorld contract and pass the encoded wormhole message
helloWorldSource.receiveMessage(encodedMessage);
// Parse the encodedMessage to retrieve the hash. This is a safe operation
// since the source HelloWorld contract already verfied the message in the
// previous call.
IWormhole.VM memory parsedMessage = wormhole.parseVM(encodedMessage);
// Verify that the message was consumed and the payload was saved
// in the contract state.
bool messageWasConsumed = helloWorldSource.isMessageConsumed(parsedMessage.hash);
string memory savedMessage = helloWorldSource.getReceivedMessage(parsedMessage.hash);
assertTrue(messageWasConsumed);
assertEq(savedMessage, "HelloSolana");
// Confirm that message replay protection works by trying to call receiveMessage
// with the same wormhole message again.
vm.expectRevert("message already consumed");
helloWorldSource.receiveMessage(encodedMessage);
}
// This test confirms that the `receiveMessage` method correclty verifies the wormhole
// message emitter.
function testReceiveMessageEmitterVerification() public {
// start listening to events
vm.recordLogs();
// publish the HelloWorld message from an untrusted emitter
bytes memory helloWorldMessage = helloWorldSource.encodeMessage(
HelloWorldStructs.HelloWorldMessage({
payloadID: uint8(1),
message: "HelloSolana"
})
);
wormhole.publishMessage(42000, helloWorldMessage, helloWorldSource.wormholeFinality());
// record the emitted wormhole message
Vm.Log[] memory entries = vm.getRecordedLogs();
// simulate signing the wormhole message
// NOTE: in the wormhole-sdk, signed wormhole messages are referred to as signed VAAs
bytes memory encodedMessage = wormholeSimulator.fetchSignedMessageFromLogs(entries[0]);
// register the emitter on the source contract
helloWorldSource.registerEmitter(
helloWorldTarget.chainId(),
bytes32(uint256(uint160(address(helloWorldTarget))))
);
// Expect the receiveMessage call to revert, since the message was generated
// by an untrusted emitter.
vm.expectRevert("unknown emitter");
helloWorldSource.receiveMessage(encodedMessage);
}
}

20
evm/foundry.toml Normal file
View File

@ -0,0 +1,20 @@
[profile.default]
solc_version = "0.8.13"
optimizer = true
optimizer_runs = 200
test = "forge-test"
libs = [
"lib",
"node_modules",
]
remappings = [
"@openzeppelin/=node_modules/@openzeppelin/",
"@solidity-parser/=node_modules/@solidity-parser/",
"ds-test/=lib/forge-std/lib/ds-test/src/",
"forge-std/=lib/forge-std/src/",
"wormhole-solidity/=modules/src",
]
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

11
evm/modules/README.md Normal file
View File

@ -0,0 +1,11 @@
# Solidity Modules
The intent of this directory is to warehouse common Solidity helpers that we can
one day put into its own package (so we can import it via package.json) as opposed
to free-floating for integrators to have to port around.
### Contents
- src
- _WormholeSimulator.sol_

View File

@ -0,0 +1,155 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {IWormhole} from "../../src/interfaces/IWormhole.sol";
import "../../src/libraries/BytesLib.sol";
import "forge-std/Vm.sol";
import "forge-std/console.sol";
contract WormholeSimulator {
using BytesLib for bytes;
// Taken from forge-std/Script.sol
address private constant VM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code")))));
Vm public constant vm = Vm(VM_ADDRESS);
// Allow access to Wormhole
IWormhole public wormhole;
// Save the guardian PK to sign messages with
uint256 private devnetGuardianPK;
constructor(address wormhole_, uint256 devnetGuardian) {
wormhole = IWormhole(wormhole_);
devnetGuardianPK = devnetGuardian;
overrideToDevnetGuardian(vm.addr(devnetGuardian));
}
function overrideToDevnetGuardian(address devnetGuardian) internal {
{
bytes32 data = vm.load(address(this), bytes32(uint256(2)));
require(data == bytes32(0), "incorrect slot");
// Get slot for Guardian Set at the current index
uint32 guardianSetIndex = wormhole.getCurrentGuardianSetIndex();
bytes32 guardianSetSlot = keccak256(abi.encode(guardianSetIndex, 2));
// Overwrite all but first guardian set to zero address. This isn't
// necessary, but just in case we inadvertently access these slots
// for any reason.
uint256 numGuardians = uint256(vm.load(address(wormhole), guardianSetSlot));
for (uint256 i = 1; i < numGuardians;) {
vm.store(
address(wormhole), bytes32(uint256(keccak256(abi.encodePacked(guardianSetSlot))) + i), bytes32(0)
);
unchecked {
i += 1;
}
}
// Now overwrite the first guardian key with the devnet key specified
// in the function argument.
vm.store(
address(wormhole),
bytes32(uint256(keccak256(abi.encodePacked(guardianSetSlot))) + 0), // just explicit w/ index 0
bytes32(uint256(uint160(devnetGuardian)))
);
// Change the length to 1 guardian
vm.store(
address(wormhole),
guardianSetSlot,
bytes32(uint256(1)) // length == 1
);
// Confirm guardian set override
address[] memory guardians = wormhole.getGuardianSet(guardianSetIndex).keys;
require(guardians.length == 1, "guardians.length != 1");
require(guardians[0] == devnetGuardian, "incorrect guardian set override");
}
}
function doubleKeccak256(bytes memory body) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(keccak256(body)));
}
function parseVMFromLogs(Vm.Log memory log) internal pure returns (IWormhole.VM memory vm_) {
uint256 index = 0;
// emitterAddress
vm_.emitterAddress = bytes32(log.topics[1]);
// sequence
vm_.sequence = log.data.toUint64(index + 32 - 8);
index += 32;
// nonce
vm_.nonce = log.data.toUint32(index + 32 - 4);
index += 32;
// skip random bytes
index += 32;
// consistency level
vm_.consistencyLevel = log.data.toUint8(index + 32 - 1);
index += 32;
// length of payload
uint256 payloadLen = log.data.toUint256(index);
index += 32;
vm_.payload = log.data.slice(index, payloadLen);
index += payloadLen;
// trailing bytes (due to 32 byte slot overlap)
index += log.data.length - index;
require(index == log.data.length, "failed to parse wormhole message");
}
function encodeObservation(IWormhole.VM memory vm_) public pure returns (bytes memory) {
return abi.encodePacked(
vm_.timestamp,
vm_.nonce,
vm_.emitterChainId,
vm_.emitterAddress,
vm_.sequence,
vm_.consistencyLevel,
vm_.payload
);
}
function fetchSignedMessageFromLogs(Vm.Log memory log) public returns (bytes memory) {
// Create message instance
IWormhole.VM memory vm_;
// Parse wormhole message from ethereum logs
vm_ = parseVMFromLogs(log);
// Set empty body values before computing the hash
vm_.version = uint8(1);
vm_.timestamp = uint32(block.timestamp);
vm_.emitterChainId = wormhole.chainId();
// Compute the hash of the body
bytes memory body = encodeObservation(vm_);
vm_.hash = doubleKeccak256(body);
// Sign the hash with the devnet guardian private key
IWormhole.Signature[] memory sigs = new IWormhole.Signature[](1);
(sigs[0].v, sigs[0].r, sigs[0].s) = vm.sign(devnetGuardianPK, vm_.hash);
sigs[0].guardianIndex = 0;
return abi.encodePacked(
vm_.version,
wormhole.getCurrentGuardianSetIndex(),
uint8(sigs.length),
sigs[0].guardianIndex,
sigs[0].r,
sigs[0].s,
sigs[0].v - 27,
body
);
}
}

20
evm/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "wormhole-circle-integration-evm",
"version": "0.1.0",
"main": "index.js",
"license": "ISC",
"devDependencies": {
"@types/node": "^18.8.4",
"chai": "^4.3.6",
"ethers": "^5.7.1"
},
"dependencies": {
"@openzeppelin/contracts": "^4.7.3",
"@types/chai": "^4.3.3",
"@types/mocha": "^9.1.1",
"elliptic": "^6.5.4",
"mocha": "^10.0.0",
"ts-mocha": "^10.0.0",
"typescript": "^4.8.3"
}
}

View File

@ -0,0 +1,33 @@
#/bin/bash
pgrep anvil > /dev/null
if [ $? -eq 0 ]; then
echo "anvil already running"
exit 1;
fi
anvil \
-m "myth like bonus scare over problem client lizard pioneer submit female collect" \
--fork-url $TESTING_FORK_RPC \
--timestamp 0 \
--chain-id $TESTING_FORK_CHAINID > anvil.log &
sleep 2
## anvil's rpc
RPC="http://localhost:8545"
## first key from mnemonic above
PRIVATE_KEY="0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
echo "deploy contracts"
forge script forge-scripts/deploy_contracts.sol \
--rpc-url $RPC \
--private-key $PRIVATE_KEY \
--broadcast --slow > forge-scripts/deploy.out 2>&1
## run tests here
npx ts-mocha -t 1000000 ts-test/*.ts
# nuke
pkill anvil

View File

@ -0,0 +1,113 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {IWormhole} from "../interfaces/IWormhole.sol";
import "../libraries/BytesLib.sol";
import "./HelloWorldGetters.sol";
import "./HelloWorldMessages.sol";
contract HelloWorld is HelloWorldGetters, HelloWorldMessages {
using BytesLib for bytes;
constructor(address wormhole_, uint16 chainId_, uint8 wormholeFinality_) {
// sanity check input values
require(wormhole_ != address(0), "invalid wormhole address");
require(chainId_ > 0, "invalid chainId");
require(wormholeFinality_ > 0, "invalid wormholeFinality");
// set constructor state values
setOwner(msg.sender);
setWormhole(wormhole_);
setChainId(chainId_);
setWormholeFinality(wormholeFinality_);
}
function sendMessage() public payable returns (uint64 messageSequence) {
// cache wormhole instance and fees to save on gas
IWormhole wormhole = wormhole();
uint256 wormholeFee = wormhole.messageFee();
// Confirm that the caller has sent enough ether to pay for the wormhole
// message fee.
require(msg.value == wormholeFee, "insufficient value");
// create the HelloWorldMessage struct
HelloWorldMessage memory parsedMessage = HelloWorldMessage({
payloadID: uint8(1),
message: "HelloSolana"
});
// encode the message
bytes memory encodedMessage = encodeMessage(parsedMessage);
// Send the HelloWorld message by calling publishMessage on the
// wormhole core contract.
messageSequence = wormhole.publishMessage{value: wormholeFee}(
42000, // user specified message ID
encodedMessage,
wormholeFinality()
);
}
function receiveMessage(bytes memory encodedMessage) public {
// call the wormhole core contract to parse and verify the encodedMessage
(
IWormhole.VM memory wormholeMessage,
bool valid,
string memory reason
) = wormhole().parseAndVerifyVM(encodedMessage);
// confirm that the core layer verified the message
require(valid, reason);
// verify that this message was emitted by a trusted contract
require(verifyEmitter(wormholeMessage), "unknown emitter");
// decode the message payload into the HelloWorldStruct
HelloWorldMessage memory parsedMessage = decodeMessage(wormholeMessage.payload);
/**
Check to see if this message has been consumed already. If not,
save the parsed message in the receivedMessages mapping.
This check can protect against replay attacks in xDapps where messages are
only meant to be consumed once.
*/
require(!isMessageConsumed(wormholeMessage.hash), "message already consumed");
consumeMessage(wormholeMessage.hash, parsedMessage.message);
}
function registerEmitter(
uint16 emitterChainId,
bytes32 emitterAddress
) public onlyOwner {
// sanity check both input arguments
require(
emitterAddress != bytes32(0),
"emitterAddress cannot equal bytes32(0)"
);
require(
getRegisteredEmitter(emitterChainId) == bytes32(0),
"emitterChainId already registered"
);
// update the registeredEmitters state variable
setEmitter(emitterChainId, emitterAddress);
}
function verifyEmitter(IWormhole.VM memory vm) internal view returns (bool) {
// Verify that the sender of the wormhole message is a trusted
// HelloWorld contract.
if (getRegisteredEmitter(vm.emitterChainId) == vm.emitterAddress) {
return true;
}
return false;
}
modifier onlyOwner() {
require(owner() == msg.sender, "caller not the owner");
_;
}
}

View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {IWormhole} from "../interfaces/IWormhole.sol";
import "./HelloWorldSetters.sol";
contract HelloWorldGetters is HelloWorldSetters {
function owner() public view returns (address) {
return _state.owner;
}
function wormhole() public view returns (IWormhole) {
return IWormhole(_state.wormhole);
}
function chainId() public view returns (uint16) {
return _state.chainId;
}
function wormholeFinality() public view returns (uint8) {
return _state.wormholeFinality;
}
function getRegisteredEmitter(uint16 emitterChainId) public view returns (bytes32) {
return _state.registeredEmitters[emitterChainId];
}
function getReceivedMessage(bytes32 hash) public view returns (string memory) {
return _state.receivedMessages[hash];
}
function isMessageConsumed(bytes32 hash) public view returns (bool) {
return _state.consumedMessages[hash];
}
}

View File

@ -0,0 +1,48 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "../libraries/BytesLib.sol";
import "./HelloWorldStructs.sol";
contract HelloWorldMessages is HelloWorldStructs {
using BytesLib for bytes;
function encodeMessage(
HelloWorldMessage memory parsedMessage
) public pure returns (bytes memory) {
// convert message string to bytes so that we can use the .length attribute
bytes memory encodedMessagePayload = abi.encodePacked(parsedMessage.message);
// return the encoded message
return abi.encodePacked(
parsedMessage.payloadID,
encodedMessagePayload.length,
encodedMessagePayload
);
}
function decodeMessage(
bytes memory encodedMessage
) public pure returns (HelloWorldMessage memory parsedMessage) {
// starting index for byte parsing
uint256 index = 0;
// parse and verify the payloadID
parsedMessage.payloadID = encodedMessage.toUint8(index);
require(parsedMessage.payloadID == 1, "invalid payloadID");
index += 1;
// parse the message string length
uint256 messageLength = encodedMessage.toUint256(index);
index += 32;
// parse the message string
bytes memory messageBytes = encodedMessage.slice(index, messageLength);
parsedMessage.message = string(messageBytes);
index += messageLength;
// confirm that the message was the expected length
require(index == encodedMessage.length, "invalid message length");
}
}

View File

@ -0,0 +1,31 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./HelloWorldState.sol";
contract HelloWorldSetters is HelloWorldState {
function setOwner(address owner_) internal {
_state.owner = owner_;
}
function setWormhole(address wormhole_) internal {
_state.wormhole = payable(wormhole_);
}
function setChainId(uint16 chainId_) internal {
_state.chainId = chainId_;
}
function setWormholeFinality(uint8 finality) internal {
_state.wormholeFinality = finality;
}
function setEmitter(uint16 chainId, bytes32 emitter) internal {
_state.registeredEmitters[chainId] = emitter;
}
function consumeMessage(bytes32 hash, string memory message) internal {
_state.receivedMessages[hash] = message;
_state.consumedMessages[hash] = true;
}
}

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {IWormhole} from "../interfaces/IWormhole.sol";
contract HelloWorldStorage {
struct State {
// owner of this contract
address owner;
// address of the Wormhole contract on this chain
address wormhole;
// Wormhole chain ID of this contract
uint16 chainId;
// The number of block confirmations needed before the wormhole network
// will attest a message.
uint8 wormholeFinality;
// Wormhole chain ID to known emitter address mapping. Xapps using
// Wormhole should register all deployed contracts on each chain to
// verify that messages being consumed are from trusted contracts.
mapping(uint16 => bytes32) registeredEmitters;
// verified message hash to received message mapping
mapping(bytes32 => string) receivedMessages;
// verified message hash to boolean
mapping(bytes32 => bool) consumedMessages;
}
}
contract HelloWorldState {
HelloWorldStorage.State _state;
}

View File

@ -0,0 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract HelloWorldStructs {
struct HelloWorldMessage {
// unique identifier
uint8 payloadID;
// message payload (max size uint256)
string message;
}
}

View File

@ -0,0 +1,72 @@
// contracts/Messages.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
interface IWormhole {
struct GuardianSet {
address[] keys;
uint32 expirationTime;
}
struct Signature {
bytes32 r;
bytes32 s;
uint8 v;
uint8 guardianIndex;
}
struct VM {
uint8 version;
uint32 timestamp;
uint32 nonce;
uint16 emitterChainId;
bytes32 emitterAddress;
uint64 sequence;
uint8 consistencyLevel;
bytes payload;
uint32 guardianSetIndex;
Signature[] signatures;
bytes32 hash;
}
event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);
function publishMessage(
uint32 nonce,
bytes memory payload,
uint8 consistencyLevel
) external payable returns (uint64 sequence);
function parseAndVerifyVM(bytes calldata encodedVM) external view returns (VM memory vm, bool valid, string memory reason);
function verifyVM(VM memory vm) external view returns (bool valid, string memory reason);
function verifySignatures(bytes32 hash, Signature[] memory signatures, GuardianSet memory guardianSet) external pure returns (bool valid, string memory reason);
function parseVM(bytes memory encodedVM) external pure returns (VM memory vm);
function getGuardianSet(uint32 index) external view returns (GuardianSet memory);
function getCurrentGuardianSetIndex() external view returns (uint32);
function getGuardianSetExpiry() external view returns (uint32);
function governanceActionIsConsumed(bytes32 hash) external view returns (bool);
function isInitialized(address impl) external view returns (bool);
function chainId() external view returns (uint16);
function governanceChainId() external view returns (uint16);
function governanceContract() external view returns (bytes32);
function messageFee() external view returns (uint256);
function evmChainId() external view returns (uint256);
function nextSequence(address emitter) external view returns (uint64);
}

View File

@ -0,0 +1,56 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
interface ICircleBridge {
event MessageSent(bytes message);
/**
* @notice Deposits and burns tokens from sender to be minted on destination domain.
* Emits a `DepositForBurn` event.
* @dev reverts if:
* - given burnToken is not supported
* - given destinationDomain has no CircleBridge registered
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
* to this contract is less than `amount`.
* - burn() reverts. For example, if `amount` is 0.
* - MessageTransmitter returns false or reverts.
* @param _amount amount of tokens to burn
* @param _destinationDomain destination domain (ETH = 0, AVAX = 1)
* @param _mintRecipient address of mint recipient on destination domain
* @param _burnToken address of contract to burn deposited tokens, on local domain
* @return _nonce unique nonce reserved by message
*/
function depositForBurn(uint256 _amount, uint32 _destinationDomain, bytes32 _mintRecipient, address _burnToken)
external
returns (uint64 _nonce);
/**
* @notice Deposits and burns tokens from sender to be minted on destination domain. The mint
* on the destination domain must be called by `_destinationCaller`.
* WARNING: if the `_destinationCaller` does not represent a valid address as bytes32, then it will not be possible
* to broadcast the message on the destination domain. This is an advanced feature, and the standard
* depositForBurn() should be preferred for use cases where a specific destination caller is not required.
* Emits a `DepositForBurn` event.
* @dev reverts if:
* - given destinationCaller is zero address
* - given burnToken is not supported
* - given destinationDomain has no CircleBridge registered
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
* to this contract is less than `amount`.
* - burn() reverts. For example, if `amount` is 0.
* - MessageTransmitter returns false or reverts.
* @param _amount amount of tokens to burn
* @param _destinationDomain destination domain
* @param _mintRecipient address of mint recipient on destination domain
* @param _burnToken address of contract to burn deposited tokens, on local domain
* @param _destinationCaller caller on the destination domain, as bytes32
* @return _nonce unique nonce reserved by message
*/
function depositForBurnWithCaller(
uint256 _amount,
uint32 _destinationDomain,
bytes32 _mintRecipient,
address _burnToken,
bytes32 _destinationCaller
) external returns (uint64 _nonce);
}

View File

@ -0,0 +1,42 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
interface IMessageTransmitter {
/**
* @notice Emitted when tokens are minted
* @param _mintRecipient recipient address of minted tokens
* @param _amount amount of minted tokens
* @param _mintToken contract address of minted token
*/
event MintAndWithdraw(address _mintRecipient, uint256 _amount, address _mintToken);
/**
* @notice Receive a message. Messages with a given nonce
* can only be broadcast once for a (sourceDomain, destinationDomain)
* pair. The message body of a valid message is passed to the
* specified recipient for further processing.
*
* @dev Attestation format:
* A valid attestation is the concatenated 65-byte signature(s) of exactly
* `thresholdSignature` signatures, in increasing order of attester address.
* ***If the attester addresses recovered from signatures are not in
* increasing order, signature verification will fail.***
* If incorrect number of signatures or duplicate signatures are supplied,
* signature verification will fail.
*
* Message format:
* Field Bytes Type Index
* version 4 uint32 0
* sourceDomain 4 uint32 4
* destinationDomain 4 uint32 8
* nonce 8 uint64 12
* sender 32 bytes32 20
* recipient 32 bytes32 52
* messageBody dynamic bytes 84
* @param _message Message bytes
* @param _attestation Concatenated 65-byte signature(s) of `_message`, in increasing order
* of the attester address recovered from signatures.
* @return success bool, true if successful
*/
function receiveMessage(bytes memory _message, bytes calldata _attestation) external returns (bool success);
}

View File

@ -0,0 +1,510 @@
// SPDX-License-Identifier: Unlicense
/*
* @title Solidity Bytes Arrays Utils
* @author Gonçalo <goncalo.sa@consensys.net>
*
* @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity.
* The library lets you concatenate, slice and type cast bytes arrays both in memory and storage.
*/
pragma solidity >=0.8.0 <0.9.0;
library BytesLib {
function concat(
bytes memory _preBytes,
bytes memory _postBytes
)
internal
pure
returns (bytes memory)
{
bytes memory tempBytes;
assembly {
// Get a location of some free memory and store it in tempBytes as
// Solidity does for memory variables.
tempBytes := mload(0x40)
// Store the length of the first bytes array at the beginning of
// the memory for tempBytes.
let length := mload(_preBytes)
mstore(tempBytes, length)
// Maintain a memory counter for the current write location in the
// temp bytes array by adding the 32 bytes for the array length to
// the starting location.
let mc := add(tempBytes, 0x20)
// Stop copying when the memory counter reaches the length of the
// first bytes array.
let end := add(mc, length)
for {
// Initialize a copy counter to the start of the _preBytes data,
// 32 bytes into its memory.
let cc := add(_preBytes, 0x20)
} lt(mc, end) {
// Increase both counters by 32 bytes each iteration.
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
// Write the _preBytes data into the tempBytes memory 32 bytes
// at a time.
mstore(mc, mload(cc))
}
// Add the length of _postBytes to the current length of tempBytes
// and store it as the new length in the first 32 bytes of the
// tempBytes memory.
length := mload(_postBytes)
mstore(tempBytes, add(length, mload(tempBytes)))
// Move the memory counter back from a multiple of 0x20 to the
// actual end of the _preBytes data.
mc := end
// Stop copying when the memory counter reaches the new combined
// length of the arrays.
end := add(mc, length)
for {
let cc := add(_postBytes, 0x20)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
mstore(mc, mload(cc))
}
// Update the free-memory pointer by padding our last write location
// to 32 bytes: add 31 bytes to the end of tempBytes to move to the
// next 32 byte block, then round down to the nearest multiple of
// 32. If the sum of the length of the two arrays is zero then add
// one before rounding down to leave a blank 32 bytes (the length block with 0).
mstore(0x40, and(
add(add(end, iszero(add(length, mload(_preBytes)))), 31),
not(31) // Round down to the nearest 32 bytes.
))
}
return tempBytes;
}
function concatStorage(bytes storage _preBytes, bytes memory _postBytes) internal {
assembly {
// Read the first 32 bytes of _preBytes storage, which is the length
// of the array. (We don't need to use the offset into the slot
// because arrays use the entire slot.)
let fslot := sload(_preBytes.slot)
// Arrays of 31 bytes or less have an even value in their slot,
// while longer arrays have an odd value. The actual length is
// the slot divided by two for odd values, and the lowest order
// byte divided by two for even values.
// If the slot is even, bitwise and the slot with 255 and divide by
// two to get the length. If the slot is odd, bitwise and the slot
// with -1 and divide by two.
let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2)
let mlength := mload(_postBytes)
let newlength := add(slength, mlength)
// slength can contain both the length and contents of the array
// if length < 32 bytes so let's prepare for that
// v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage
switch add(lt(slength, 32), lt(newlength, 32))
case 2 {
// Since the new array still fits in the slot, we just need to
// update the contents of the slot.
// uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length
sstore(
_preBytes.slot,
// all the modifications to the slot are inside this
// next block
add(
// we can just add to the slot contents because the
// bytes we want to change are the LSBs
fslot,
add(
mul(
div(
// load the bytes from memory
mload(add(_postBytes, 0x20)),
// zero all bytes to the right
exp(0x100, sub(32, mlength))
),
// and now shift left the number of bytes to
// leave space for the length in the slot
exp(0x100, sub(32, newlength))
),
// increase length by the double of the memory
// bytes length
mul(mlength, 2)
)
)
)
}
case 1 {
// The stored value fits in the slot, but the combined value
// will exceed it.
// get the keccak hash to get the contents of the array
mstore(0x0, _preBytes.slot)
let sc := add(keccak256(0x0, 0x20), div(slength, 32))
// save new length
sstore(_preBytes.slot, add(mul(newlength, 2), 1))
// The contents of the _postBytes array start 32 bytes into
// the structure. Our first read should obtain the `submod`
// bytes that can fit into the unused space in the last word
// of the stored array. To get this, we read 32 bytes starting
// from `submod`, so the data we read overlaps with the array
// contents by `submod` bytes. Masking the lowest-order
// `submod` bytes allows us to add that value directly to the
// stored value.
let submod := sub(32, slength)
let mc := add(_postBytes, submod)
let end := add(_postBytes, mlength)
let mask := sub(exp(0x100, submod), 1)
sstore(
sc,
add(
and(
fslot,
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00
),
and(mload(mc), mask)
)
)
for {
mc := add(mc, 0x20)
sc := add(sc, 1)
} lt(mc, end) {
sc := add(sc, 1)
mc := add(mc, 0x20)
} {
sstore(sc, mload(mc))
}
mask := exp(0x100, sub(mc, end))
sstore(sc, mul(div(mload(mc), mask), mask))
}
default {
// get the keccak hash to get the contents of the array
mstore(0x0, _preBytes.slot)
// Start copying to the last used word of the stored array.
let sc := add(keccak256(0x0, 0x20), div(slength, 32))
// save new length
sstore(_preBytes.slot, add(mul(newlength, 2), 1))
// Copy over the first `submod` bytes of the new data as in
// case 1 above.
let slengthmod := mod(slength, 32)
let mlengthmod := mod(mlength, 32)
let submod := sub(32, slengthmod)
let mc := add(_postBytes, submod)
let end := add(_postBytes, mlength)
let mask := sub(exp(0x100, submod), 1)
sstore(sc, add(sload(sc), and(mload(mc), mask)))
for {
sc := add(sc, 1)
mc := add(mc, 0x20)
} lt(mc, end) {
sc := add(sc, 1)
mc := add(mc, 0x20)
} {
sstore(sc, mload(mc))
}
mask := exp(0x100, sub(mc, end))
sstore(sc, mul(div(mload(mc), mask), mask))
}
}
}
function slice(
bytes memory _bytes,
uint256 _start,
uint256 _length
)
internal
pure
returns (bytes memory)
{
require(_length + 31 >= _length, "slice_overflow");
require(_bytes.length >= _start + _length, "slice_outOfBounds");
bytes memory tempBytes;
assembly {
switch iszero(_length)
case 0 {
// Get a location of some free memory and store it in tempBytes as
// Solidity does for memory variables.
tempBytes := mload(0x40)
// The first word of the slice result is potentially a partial
// word read from the original array. To read it, we calculate
// the length of that partial word and start copying that many
// bytes into the array. The first word we copy will start with
// data we don't care about, but the last `lengthmod` bytes will
// land at the beginning of the contents of the new array. When
// we're done copying, we overwrite the full first word with
// the actual length of the slice.
let lengthmod := and(_length, 31)
// The multiplication in the next line is necessary
// because when slicing multiples of 32 bytes (lengthmod == 0)
// the following copy loop was copying the origin's length
// and then ending prematurely not copying everything it should.
let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
let end := add(mc, _length)
for {
// The multiplication in the next line has the same exact purpose
// as the one above.
let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
mstore(mc, mload(cc))
}
mstore(tempBytes, _length)
//update free-memory pointer
//allocating the array padded to 32 bytes like the compiler does now
mstore(0x40, and(add(mc, 31), not(31)))
}
//if we want a zero-length slice let's just return a zero-length array
default {
tempBytes := mload(0x40)
//zero out the 32 bytes slice we are about to return
//we need to do it because Solidity does not garbage collect
mstore(tempBytes, 0)
mstore(0x40, add(tempBytes, 0x20))
}
}
return tempBytes;
}
function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
require(_bytes.length >= _start + 20, "toAddress_outOfBounds");
address tempAddress;
assembly {
tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
}
return tempAddress;
}
function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) {
require(_bytes.length >= _start + 1 , "toUint8_outOfBounds");
uint8 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x1), _start))
}
return tempUint;
}
function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16) {
require(_bytes.length >= _start + 2, "toUint16_outOfBounds");
uint16 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x2), _start))
}
return tempUint;
}
function toUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32) {
require(_bytes.length >= _start + 4, "toUint32_outOfBounds");
uint32 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x4), _start))
}
return tempUint;
}
function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) {
require(_bytes.length >= _start + 8, "toUint64_outOfBounds");
uint64 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x8), _start))
}
return tempUint;
}
function toUint96(bytes memory _bytes, uint256 _start) internal pure returns (uint96) {
require(_bytes.length >= _start + 12, "toUint96_outOfBounds");
uint96 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0xc), _start))
}
return tempUint;
}
function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) {
require(_bytes.length >= _start + 16, "toUint128_outOfBounds");
uint128 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x10), _start))
}
return tempUint;
}
function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256) {
require(_bytes.length >= _start + 32, "toUint256_outOfBounds");
uint256 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x20), _start))
}
return tempUint;
}
function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) {
require(_bytes.length >= _start + 32, "toBytes32_outOfBounds");
bytes32 tempBytes32;
assembly {
tempBytes32 := mload(add(add(_bytes, 0x20), _start))
}
return tempBytes32;
}
function equal(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) {
bool success = true;
assembly {
let length := mload(_preBytes)
// if lengths don't match the arrays are not equal
switch eq(length, mload(_postBytes))
case 1 {
// cb is a circuit breaker in the for loop since there's
// no said feature for inline assembly loops
// cb = 1 - don't breaker
// cb = 0 - break
let cb := 1
let mc := add(_preBytes, 0x20)
let end := add(mc, length)
for {
let cc := add(_postBytes, 0x20)
// the next line is the loop condition:
// while(uint256(mc < end) + cb == 2)
} eq(add(lt(mc, end), cb), 2) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
// if any of these checks fails then arrays are not equal
if iszero(eq(mload(mc), mload(cc))) {
// unsuccess:
success := 0
cb := 0
}
}
}
default {
// unsuccess:
success := 0
}
}
return success;
}
function equalStorage(
bytes storage _preBytes,
bytes memory _postBytes
)
internal
view
returns (bool)
{
bool success = true;
assembly {
// we know _preBytes_offset is 0
let fslot := sload(_preBytes.slot)
// Decode the length of the stored array like in concatStorage().
let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2)
let mlength := mload(_postBytes)
// if lengths don't match the arrays are not equal
switch eq(slength, mlength)
case 1 {
// slength can contain both the length and contents of the array
// if length < 32 bytes so let's prepare for that
// v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage
if iszero(iszero(slength)) {
switch lt(slength, 32)
case 1 {
// blank the last byte which is the length
fslot := mul(div(fslot, 0x100), 0x100)
if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) {
// unsuccess:
success := 0
}
}
default {
// cb is a circuit breaker in the for loop since there's
// no said feature for inline assembly loops
// cb = 1 - don't breaker
// cb = 0 - break
let cb := 1
// get the keccak hash to get the contents of the array
mstore(0x0, _preBytes.slot)
let sc := keccak256(0x0, 0x20)
let mc := add(_postBytes, 0x20)
let end := add(mc, mlength)
// the next line is the loop condition:
// while(uint256(mc < end) + cb == 2)
for {} eq(add(lt(mc, end), cb), 2) {
sc := add(sc, 1)
mc := add(mc, 0x20)
} {
if iszero(eq(sload(sc), mload(mc))) {
// unsuccess:
success := 0
cb := 0
}
}
}
}
}
default {
// unsuccess:
success := 0
}
}
return success;
}
}

44
evm/testing.env Normal file
View File

@ -0,0 +1,44 @@
###############################################################################
#
# Private key of devnet (Tilt) guardian
#
###############################################################################
export TESTING_DEVNET_GUARDIAN=cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
###############################################################################
#
# Private key of anvil wallet
#
###############################################################################
export WALLET_PRIVATE_KEY=4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
###############################################################################
#
# Wormhole (Core Bridge) Contract Address on AVAX Mainnet
#
###############################################################################
export TESTING_WORMHOLE_ADDRESS=0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c
###############################################################################
#
# Fork specification (in this case, AVAX mainnet)
#
###############################################################################
export TESTING_FORK_RPC=https://api.avax.network/ext/bc/C/rpc
export TESTING_FORK_CHAINID=43114
###############################################################################
#
# Expected values for Wormhole contract
#
###############################################################################
export TESTING_WORMHOLE_CHAINID=6
export TESTING_WORMHOLE_MESSAGE_FEE=0
export TESTING_WORMHOLE_GUARDIAN_SET_INDEX=2
###############################################################################
#
# HelloWorld (01_hello_world) Contract Address on AVAX Mainnet Fork
#
###############################################################################
export TESTING_HELLO_WORLD_ADDRESS=0xe982E462b094850F12AF94d21D470e21bE9D0E9C

113
evm/ts-test/00_wormhole.ts Normal file
View File

@ -0,0 +1,113 @@
import { expect } from "chai";
import { ethers } from "ethers";
import { tryNativeToHexString } from "@certusone/wormhole-sdk";
import {
FORK_CHAIN_ID,
GUARDIAN_PRIVATE_KEY,
LOCALHOST,
WORMHOLE_ADDRESS,
WORMHOLE_CHAIN_ID,
WORMHOLE_GUARDIAN_SET_INDEX,
WORMHOLE_MESSAGE_FEE,
} from "./helpers/consts";
import { makeContract } from "./helpers/io";
describe("Fork Test", () => {
const provider = new ethers.providers.StaticJsonRpcProvider(LOCALHOST);
const wormholeAbiPath = `${__dirname}/../out/IWormhole.sol/IWormhole.json`;
const wormhole = makeContract(provider, WORMHOLE_ADDRESS, wormholeAbiPath);
describe("Verify AVAX Mainnet Fork", () => {
it("Chain ID", async () => {
const network = await provider.getNetwork();
expect(network.chainId).to.equal(FORK_CHAIN_ID);
});
});
describe("Verify Wormhole Contract", () => {
it("Chain ID", async () => {
const chainId = await wormhole.chainId();
expect(chainId).to.equal(WORMHOLE_CHAIN_ID);
});
it("Message Fee", async () => {
const messageFee: ethers.BigNumber = await wormhole.messageFee();
expect(messageFee.eq(WORMHOLE_MESSAGE_FEE)).to.be.true;
});
it("Guardian Set", async () => {
// Check guardian set index
const guardianSetIndex = await wormhole.getCurrentGuardianSetIndex();
expect(guardianSetIndex).to.equal(WORMHOLE_GUARDIAN_SET_INDEX);
// Override guardian set
const abiCoder = ethers.utils.defaultAbiCoder;
// Get slot for Guardian Set at the current index
const guardianSetSlot = ethers.utils.keccak256(
abiCoder.encode(["uint32", "uint256"], [guardianSetIndex, 2])
);
// Overwrite all but first guardian set to zero address. This isn't
// necessary, but just in case we inadvertently access these slots
// for any reason.
const numGuardians = await provider
.getStorageAt(WORMHOLE_ADDRESS, guardianSetSlot)
.then((value) => ethers.BigNumber.from(value).toBigInt());
for (let i = 1; i < numGuardians; ++i) {
await provider.send("anvil_setStorageAt", [
WORMHOLE_ADDRESS,
abiCoder.encode(
["uint256"],
[
ethers.BigNumber.from(
ethers.utils.keccak256(guardianSetSlot)
).add(i),
]
),
ethers.utils.hexZeroPad("0x0", 32),
]);
}
// Now overwrite the first guardian key with the devnet key specified
// in the function argument.
const devnetGuardian = new ethers.Wallet(GUARDIAN_PRIVATE_KEY).address;
await provider.send("anvil_setStorageAt", [
WORMHOLE_ADDRESS,
abiCoder.encode(
["uint256"],
[
ethers.BigNumber.from(ethers.utils.keccak256(guardianSetSlot)).add(
0 // just explicit w/ index 0
),
]
),
ethers.utils.hexZeroPad(devnetGuardian, 32),
]);
// Change the length to 1 guardian
await provider.send("anvil_setStorageAt", [
WORMHOLE_ADDRESS,
guardianSetSlot,
ethers.utils.hexZeroPad("0x1", 32),
]);
// Confirm guardian set override
const guardians = await wormhole.getGuardianSet(guardianSetIndex).then(
(guardianSet: any) => guardianSet[0] // first element is array of keys
);
expect(guardians.length).to.equal(1);
expect(guardians[0]).to.equal(devnetGuardian);
});
});
describe("Check wormhole-sdk", () => {
it("tryNativeToHexString", async () => {
const accounts = await provider.listAccounts();
expect(tryNativeToHexString(accounts[0], "ethereum")).to.equal(
"00000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1"
);
});
});
});

View File

@ -0,0 +1,65 @@
import {expect} from "chai";
import {ethers} from "ethers";
import {MockGuardians} from "@certusone/wormhole-sdk/mock";
import {ChainId, CHAIN_ID_AVAX, tryNativeToHexString} from "@certusone/wormhole-sdk";
import {
LOCALHOST,
WORMHOLE_ADDRESS,
HELLO_WORLD_ADDRESS,
WALLET_PRIVATE_KEY,
GUARDIAN_SET_INDEX,
GUARDIAN_PRIVATE_KEY,
} from "./helpers/consts";
import {makeContract} from "./helpers/io";
describe("Hello World Test", () => {
// create signer
const provider = new ethers.providers.StaticJsonRpcProvider(LOCALHOST);
const wallet = new ethers.Wallet(WALLET_PRIVATE_KEY, provider);
// contracts
const wormholeAbiPath = `${__dirname}/../out/IWormhole.sol/IWormhole.json`;
const wormhole = makeContract(wallet, WORMHOLE_ADDRESS, wormholeAbiPath);
const helloWorldAbiPath = `${__dirname}/../out/HelloWorld.sol/HelloWorld.json`;
const helloWorld = makeContract(wallet, HELLO_WORLD_ADDRESS, helloWorldAbiPath);
describe("Test Hello World Interface", () => {
// Create dummy variables for target contract info. This is to show that
// the HelloWorld contracts should be registered with contracts on a different chain.
const targetContractAddress = helloWorld.address;
const targetContractChainId = CHAIN_ID_AVAX as ChainId;
// for signing wormhole messages
const guardians = new MockGuardians(GUARDIAN_SET_INDEX, [GUARDIAN_PRIVATE_KEY]);
it("Verify Contract Deployment", async () => {
expect(helloWorld.address).to.equal(HELLO_WORLD_ADDRESS);
// confirm chainId
const deployedChainId = await helloWorld.chainId();
expect(deployedChainId).to.equal(CHAIN_ID_AVAX);
});
it("Should Register HelloWorld Contract Emitter", async () => {
// convert the target contract address to bytes32
const targetContractAddressHex = "0x" + tryNativeToHexString(targetContractAddress, targetContractChainId);
// register the emitter
await helloWorld
.registerEmitter(targetContractChainId, targetContractAddressHex)
.then((tx: ethers.ContractTransaction) => tx.wait());
// query the contract and confirm that the emitter is set in contract storage
const emitterInContractState = await helloWorld.getRegisteredEmitter(targetContractChainId);
expect(emitterInContractState).to.equal(targetContractAddressHex);
});
it("Should Send HelloWorld Message", async () => {
// invoke the HelloWorld contract to emit the HelloWorld wormhole message
const receipt: ethers.ContractReceipt = await helloWorld
.sendMessage()
.then((tx: ethers.ContractTransaction) => tx.wait());
});
});
});

View File

@ -0,0 +1,28 @@
import {ethers} from "ethers";
// rpc
export const LOCALHOST = "http://localhost:8545";
// fork
export const FORK_CHAIN_ID = Number(process.env.TESTING_FORK_CHAINID!);
// wormhole
export const WORMHOLE_ADDRESS = process.env.TESTING_WORMHOLE_ADDRESS!;
export const WORMHOLE_CHAIN_ID = Number(process.env.TESTING_WORMHOLE_CHAINID!);
export const WORMHOLE_MESSAGE_FEE = ethers.BigNumber.from(process.env.TESTING_WORMHOLE_MESSAGE_FEE!);
export const WORMHOLE_GUARDIAN_SET_INDEX = Number(process.env.TESTING_WORMHOLE_GUARDIAN_SET_INDEX!);
// HelloWorld
export const HELLO_WORLD_ADDRESS = process.env.TESTING_HELLO_WORLD_ADDRESS!;
// signer
export const GUARDIAN_PRIVATE_KEY = process.env.TESTING_DEVNET_GUARDIAN!;
export const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY!;
// mock guardian
export const GUARDIAN_SET_INDEX = 2;
// wormhole event ABIs
export const WORMHOLE_MESSAGE_EVENT_ABI = [
"event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
];

18
evm/ts-test/helpers/io.ts Normal file
View File

@ -0,0 +1,18 @@
import { ethers } from "ethers";
import fs from "fs";
export function makeContract(
signerOrProvider: ethers.Signer | ethers.providers.Provider,
contractAddress: string,
abiPath: string
): ethers.Contract {
return new ethers.Contract(contractAddress, readAbi(abiPath), signerOrProvider);
}
function readAbi(abiPath: string): any {
const compiled = JSON.parse(fs.readFileSync(abiPath, "utf8"));
if (compiled.abi === undefined) {
throw new Error("compiled.abi === undefined");
}
return compiled.abi;
}

13
evm/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2020"],
"module": "CommonJS",
"target": "es2020",
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true,
"moduleResolution": "node"
}
}

1167
evm/yarn.lock Normal file

File diff suppressed because it is too large Load Diff