evm: scaffold
This commit is contained in:
parent
cf109ecfb2
commit
8b17cdc1c5
|
@ -0,0 +1,7 @@
|
|||
anvil.log
|
||||
cache
|
||||
lib
|
||||
node_modules
|
||||
out
|
||||
broadcast
|
||||
forge-scripts/deploy.out
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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_
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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");
|
||||
_;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,510 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
/*
|
||||
* @title Solidity Bytes Arrays Utils
|
||||
* @author Gonçalo Sá <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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)",
|
||||
];
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue