Add evm integration tests (#9)

* evm: rename 00_environment.ts

* evm: add IUSDC.sol

* evm: add two anvil procs

* evm: add eth to avax test

* evm: add test failure cases

* evm: add mock contract to test redemptions

* evm: test clean up

* Add testnet governance upgrade script

* Remove typo in deploy_mock_contracts.sol

* evm: fix foundry.toml

* Remove null check in test

Co-authored-by: A5 Pickle <a5-pickle@users.noreply.github.com>
Co-authored-by: gator-boi <gator-boi@users.noreply.github.com>
This commit is contained in:
Reptile 2022-11-22 10:15:45 -06:00 committed by GitHub
parent 5c6479d370
commit dbc7d7d10f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2207 additions and 836 deletions

3
evm/env/testing.env vendored
View File

@ -19,6 +19,8 @@ export AVAX_FORK_RPC=https://api.avax-test.network/ext/bc/C/rpc
export AVAX_FORK_CHAIN_ID=43113
export AVAX_FORK_BLOCK_NUMBER=15405470
export AVAX_USDC_TOKEN_ADDRESS=0x5425890298aed601595a70AB815c96711a31Bc65
export AVAX_CIRCLE_BRIDGE_ADDRESS=0x0fC1103927AF27aF808D03135214718bCEDbE9ad
export AVAX_WORMHOLE_ADDRESS=0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C
###############################################################################
#
@ -44,3 +46,4 @@ export TESTING_LAST_NONCE=94802
#
###############################################################################
export WALLET_PRIVATE_KEY=4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
export WALLET_PRIVATE_KEY_TWO=92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

View File

@ -3,6 +3,7 @@
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {IWormhole} from "wormhole/interfaces/IWormhole.sol";
@ -15,8 +16,6 @@ import {CircleIntegrationProxy} from "../src/circle_integration/CircleIntegratio
import {WormholeSimulator} from "wormhole-forge-sdk/WormholeSimulator.sol";
import "forge-std/console.sol";
contract ContractScript is Script {
// Wormhole
WormholeSimulator wormholeSimulator;
@ -64,7 +63,7 @@ contract ContractScript is Script {
// begin sending transactions
vm.startBroadcast();
// HelloWorld.sol
// deploy Circle Integration proxy
deployCircleIntegration();
// finished

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {MockIntegration} from "../src/mock/MockIntegration.sol";
contract ContractScript is Script {
function deployMockIntegration() public {
// first Setup
new MockIntegration();
}
function run() public {
// begin sending transactions
vm.startBroadcast();
// MockIntegration.sol
deployMockIntegration();
// finished
vm.stopBroadcast();
}
}

View File

@ -9,6 +9,7 @@ import {BytesLib} from "wormhole/libraries/external/BytesLib.sol";
import {IWormhole} from "wormhole/interfaces/IWormhole.sol";
import {ICircleIntegration} from "../src/interfaces/ICircleIntegration.sol";
import {IUSDC} from "../src/interfaces/circle/IUSDC.sol";
import {CircleIntegrationStructs} from "../src/circle_integration/CircleIntegrationStructs.sol";
import {CircleIntegrationSetup} from "../src/circle_integration/CircleIntegrationSetup.sol";
@ -17,14 +18,6 @@ import {CircleIntegrationProxy} from "../src/circle_integration/CircleIntegratio
import {WormholeSimulator} from "wormhole-forge-sdk/WormholeSimulator.sol";
interface IUSDC is IERC20 {
function mint(address to, uint256 amount) external;
function configureMinter(address minter, uint256 minterAllowedAmount) external;
function masterMinter() external view returns (address);
function owner() external view returns (address);
function blacklister() external view returns (address);
}
contract CircleIntegrationTest is Test {
using BytesLib for bytes;
@ -169,7 +162,7 @@ contract CircleIntegrationTest is Test {
bytes32 fromAddress,
bytes32 mintRecipient,
bytes memory payload
) public {
) public view {
vm.assume(token != bytes32(0));
vm.assume(amount > 0);
vm.assume(targetDomain != sourceDomain);
@ -277,7 +270,7 @@ contract CircleIntegrationTest is Test {
bytes32 fromAddress,
bytes32 mintRecipient,
bytes memory payload
) public {
) public view {
vm.assume(token != bytes32(0));
vm.assume(amount > 0);
vm.assume(targetDomain != sourceDomain);

View File

@ -133,7 +133,11 @@ contract WormholeSimulator {
);
}
function signObservation(uint256 guardian, IWormhole.VM memory wormholeMessage) public returns (bytes memory) {
function signObservation(uint256 guardian, IWormhole.VM memory wormholeMessage)
public
view
returns (bytes memory)
{
require(guardian != 0, "devnetGuardian is zero address");
bytes memory body = encodeObservation(wormholeMessage);
@ -155,7 +159,7 @@ contract WormholeSimulator {
);
}
function signDevnetObservation(IWormhole.VM memory wormholeMessage) public returns (bytes memory) {
function signDevnetObservation(IWormhole.VM memory wormholeMessage) public view returns (bytes memory) {
return signObservation(devnetGuardianPK, wormholeMessage);
}
@ -178,6 +182,7 @@ contract WormholeSimulator {
function fetchSignedMessageFromLogs(Vm.Log memory log, uint16 emitterChainId, bytes32 emitterAddress)
public
view
returns (bytes memory)
{
// Create message instance

View File

@ -9,6 +9,7 @@ fi
# ethereum goerli testnet
anvil \
-m "myth like bonus scare over problem client lizard pioneer submit female collect" \
--port 8545 \
--fork-url $ETH_FORK_RPC > anvil_eth.log &
# avalanche fuji testnet
@ -22,18 +23,29 @@ sleep 2
## first key from mnemonic above
export PRIVATE_KEY="0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
## anvil's rpc (eth)
export RPC="http://localhost:8545"
export RELEASE_WORMHOLE_ADDRESS=$TESTING_WORMHOLE_ADDRESS
export RELEASE_CIRCLE_BRIDGE_ADDRESS=$TESTING_CIRCLE_BRIDGE_ADDRESS
mkdir -p cache
cp -v foundry.toml cache/foundry.toml
cp -v foundry-test.toml foundry.toml
echo "deploy contracts"
bash $(dirname $0)/deploy_circle_integration.sh > deploy.out 2>&1
RELEASE_WORMHOLE_ADDRESS=$ETH_WORMHOLE_ADDRESS \
RELEASE_CIRCLE_BRIDGE_ADDRESS=$ETH_CIRCLE_BRIDGE_ADDRESS \
forge script forge-scripts/deploy_contracts.sol \
--rpc-url http://localhost:8545 \
--private-key $PRIVATE_KEY \
--broadcast --slow > deploy.out 2>&1
RELEASE_WORMHOLE_ADDRESS=$AVAX_WORMHOLE_ADDRESS \
RELEASE_CIRCLE_BRIDGE_ADDRESS=$AVAX_CIRCLE_BRIDGE_ADDRESS \
forge script forge-scripts/deploy_contracts.sol \
--rpc-url http://localhost:8546 \
--private-key $PRIVATE_KEY \
--broadcast --slow >> deploy.out 2>&1
forge script forge-scripts/deploy_mock_contracts.sol \
--rpc-url http://localhost:8546 \
--private-key $PRIVATE_KEY \
--broadcast --slow >> deploy.out 2>&1
echo "overriding foundry.toml"
mv -v cache/foundry.toml foundry.toml

View File

@ -132,7 +132,7 @@ contract CircleIntegration is CircleIntegrationMessages, CircleIntegrationGovern
// call the circle bridge to mint tokens to the recipient
bool success = circleTransmitter().receiveMessage(params.circleBridgeMessage, params.circleAttestation);
require(success, "failed to mint USDC");
require(success, "CIRCLE_INTEGRATION: failed to mint tokens");
}
function verifyWormholeRedeemMessage(bytes memory encodedMessage) internal returns (IWormhole.VM memory) {

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
interface IUSDC {
function mint(address to, uint256 amount) external;
function configureMinter(address minter, uint256 minterAllowedAmount) external;
function masterMinter() external view returns (address);
function owner() external view returns (address);
function blacklister() external view returns (address);
// IERC20
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import { ICircleIntegration } from "../ICircleIntegration.sol";
interface IMockIntegration {
function owner() external view returns (address);
function trustedSender() external view returns (address);
function trustedChainId() external view returns (uint16);
function redemptionSequence() external view returns (uint256);
function circleIntegration() external view returns (ICircleIntegration);
function redeemTokensWithPayload(
ICircleIntegration.RedeemParameters memory redeemParams,
address transferRecipient
) external returns (uint256);
function getPayload(uint256 redemptionSequence_) external view returns (bytes memory);
function setup(
address circleIntegrationAddress,
address trustedRecipient_,
uint16 trustedChainId_
) external;
}

View File

@ -0,0 +1,84 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import {IWormhole} from "wormhole/interfaces/IWormhole.sol";
import {ICircleIntegration} from "../interfaces/ICircleIntegration.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract MockIntegration {
// owner address
address public owner;
// trusted sender address
address public trustedSender;
// trusted Wormhole chainId
uint16 public trustedChainId;
// redemption sequence
uint256 public redemptionSequence;
// payload mapping
mapping(uint256 => bytes) payloadMap;
// Wormhole's CircleIntegration instance
ICircleIntegration public circleIntegration;
// save the deployer's address in the `owner` state variable
constructor() {
owner = msg.sender;
}
function redeemTokensWithPayload(
ICircleIntegration.RedeemParameters memory redeemParams,
address transferRecipient
) public returns (uint256) {
// mint USDC to this contract
ICircleIntegration.DepositWithPayload memory deposit =
circleIntegration.redeemTokensWithPayload(redeemParams);
// verify that the sender is the trustedSender
require(
msg.sender == trustedSender &&
circleIntegration.getChainIdFromDomain(deposit.sourceDomain) == trustedChainId,
"invalid sender"
);
// uptick sequence
redemptionSequence += 1;
// save the payload
payloadMap[redemptionSequence] = deposit.payload;
// send the tokens to the transferRecipient address
SafeERC20.safeTransfer(
IERC20(address(uint160(uint256(deposit.token)))),
transferRecipient,
deposit.amount
);
return redemptionSequence;
}
function getPayload(uint256 redemptionSequence_) public view returns (bytes memory) {
return payloadMap[redemptionSequence_];
}
function setup(
address circleIntegrationAddress,
address trustedSender_,
uint16 trustedChainId_
) public onlyOwner {
// create contract interfaces and store `trustedSender` address
circleIntegration = ICircleIntegration(circleIntegrationAddress);
trustedSender = trustedSender_;
trustedChainId = trustedChainId_;
}
modifier onlyOwner() {
require(owner == msg.sender, "caller not the owner");
_;
}
}

View File

@ -1,113 +0,0 @@
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

@ -1,28 +0,0 @@
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)",
];

View File

@ -1,18 +0,0 @@
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;
}

View File

@ -1,412 +0,0 @@
require("dotenv").config({ path: ".env" });
import { ethers } from "ethers";
import axios, { AxiosResponse } from "axios";
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_AVAX,
tryNativeToHexString,
getEmitterAddressEth,
getSignedVAAWithRetry,
} from "@certusone/wormhole-sdk";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { abi as USDC_INTEGRATION_ABI } from "../../out/CircleIntegration.sol/CircleIntegration.json";
import { abi as IERC20_ABI } from "../../out/IERC20.sol/IERC20.json";
import { abi as WORMHOLE_ABI } from "../../out/IWormhole.sol/IWormhole.json";
// consts fuji
const AVAX_PROVIDER = new ethers.providers.JsonRpcProvider(
process.env.FUJI_PROVIDER
);
const AVAX_SIGNER = new ethers.Wallet(
process.env.ETH_PRIVATE_KEY!,
AVAX_PROVIDER
);
const AVAX_CONTRACT_ADDRESS = "0x3e6a4543165aaecbf7ffc81e54a1c7939cb12cb8";
const AVAX_TRANSMITTER_ADDRESS = "0x52FfFb3EE8Fa7838e9858A2D5e454007b9027c3C";
const WORMHOLE_AVAX = "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C";
const AVAX_DOMAIN: number = 1;
// create the USDC integration contract
const ETH_PROVIDER = new ethers.providers.JsonRpcProvider(
process.env.GOERLI_PROVIDER
);
const ETH_SIGNER = new ethers.Wallet(
process.env.ETH_PRIVATE_KEY!,
ETH_PROVIDER
);
const ETH_CONTRACT_ADDRESS = "0xdbedb4ebd098e9f1777af9f8088e794d381309d1";
const ETH_DOMAIN: number = 0;
// wormhole
export const WORMHOLE_RPC_HOSTS = [
"https://wormhole-v2-testnet-api.certus.one",
];
let AVAX_WORMHOLE_CONTRACT = new ethers.Contract(
WORMHOLE_AVAX,
WORMHOLE_ABI,
AVAX_PROVIDER
);
AVAX_WORMHOLE_CONTRACT = AVAX_WORMHOLE_CONTRACT.connect(AVAX_SIGNER);
// avax contracts
let USDC_INTEGRATION_SOURCE = new ethers.Contract(
AVAX_CONTRACT_ADDRESS,
USDC_INTEGRATION_ABI,
AVAX_PROVIDER
);
USDC_INTEGRATION_SOURCE = USDC_INTEGRATION_SOURCE.connect(AVAX_SIGNER);
// eth contracts
let USDC_INTEGRATION_TARGET = new ethers.Contract(
ETH_CONTRACT_ADDRESS,
USDC_INTEGRATION_ABI,
AVAX_PROVIDER
);
USDC_INTEGRATION_TARGET = USDC_INTEGRATION_TARGET.connect(ETH_SIGNER);
// create USDC contract to approve
const USDC_ADDRESS = "0x5425890298aed601595a70AB815c96711a31Bc65";
let USDC_CONTRACT = new ethers.Contract(
USDC_ADDRESS,
IERC20_ABI,
AVAX_PROVIDER
);
USDC_CONTRACT = USDC_CONTRACT.connect(AVAX_SIGNER);
// wormhole event ABIs
export const WORMHOLE_MESSAGE_EVENT_ABI = [
"event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
];
// circle event ABIS
export const CIRCLE_MESSAGE_SENT_ABI = ["event MessageSent(bytes message)"];
export async function parseEventFromAbi(
log_: ethers.providers.Log,
eventAbi: string[]
): Promise<ethers.utils.LogDescription> {
// create the wormhole message interface
const interface_ = new ethers.utils.Interface(eventAbi);
return interface_.parseLog(log_);
}
export async function parseCircleMessageEvent(
receipt: ethers.ContractReceipt,
circleTransmitter: string
): Promise<ethers.utils.LogDescription> {
let circleMessageEvent: ethers.utils.LogDescription = null!;
for (const log of receipt.logs) {
if (log.address == circleTransmitter) {
circleMessageEvent = await parseEventFromAbi(
log,
CIRCLE_MESSAGE_SENT_ABI
);
}
}
return circleMessageEvent;
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getCircleAttestation(
messageHash: ethers.BytesLike
): Promise<ethers.BytesLike> {
while (true) {
// get the post
let response: AxiosResponse = await axios.get(
`https://iris-api-sandbox.circle.com/attestations/${messageHash}`
);
if (response.status != 200) {
console.log(
"Failed to get attestation from circle, sleeping for 5 seconds"
);
await sleep(5000);
}
if (response.data.status == "pending_confirmations") {
console.log(
"Waiting for confirmations from circle, sleeping for 5 seconds."
);
await sleep(5000);
}
if (response.data.status == "complete") {
return response.data.attestation;
}
}
}
export async function parseWormholeEventsFromReceipt(
receipt: ethers.ContractReceipt,
wormhole: ethers.BytesLike
): Promise<ethers.utils.LogDescription[]> {
let wormholeEvents: ethers.utils.LogDescription[] = [];
for (const log of receipt.logs) {
if (log.address == wormhole) {
wormholeEvents.push(
await parseEventFromAbi(log, WORMHOLE_MESSAGE_EVENT_ABI)
);
}
}
return wormholeEvents;
}
export async function getSignedVaaFromReceiptOnEth(
receipt: ethers.ContractReceipt,
emitterChainId: ChainId,
contractAddress: ethers.BytesLike,
wormholeAddress: ethers.BytesLike
): Promise<Uint8Array> {
const messageEvents = await parseWormholeEventsFromReceipt(
receipt,
wormholeAddress
);
// grab the sequence from the parsed message log
if (messageEvents.length !== 1) {
throw Error("more than one message found in log");
}
const sequence = messageEvents[0].args.sequence;
// fetch the signed VAA
const result = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
emitterChainId,
getEmitterAddressEth(contractAddress),
sequence.toString(),
{
transport: NodeHttpTransport(),
}
);
return result.vaaBytes;
}
async function registerEmitter(
contract: ethers.Contract,
targetChainId: ChainId,
targetContractAddress: ethers.utils.BytesLike,
targetContractDomain: number
) {
// register the target contract
console.log("Registering chain.");
const tx = await contract.registerEmitter(
targetChainId,
targetContractAddress
);
await tx.wait();
// register the target domain
console.log("Registering domain.");
const tx2 = await contract.registerChainDomain(
targetChainId,
targetContractDomain
);
await tx2.wait();
}
async function updateFinality(
contract: ethers.Contract,
chainId: ChainId,
newFinality: number
) {
console.log("Updating finality");
const tx = await contract.updateWormholeFinality(chainId, newFinality);
await tx.wait();
}
export interface RedeemParameters {
encodedWormholeMessage: ethers.BytesLike;
circleBridgeMessage: ethers.BytesLike;
circleAttestation: ethers.BytesLike;
}
async function registerEverything() {
// make sure the USDC integration contracts have been registered, domains have been set
await registerEmitter(
USDC_INTEGRATION_SOURCE,
CHAIN_ID_ETH,
"0x" + tryNativeToHexString(ETH_CONTRACT_ADDRESS, CHAIN_ID_ETH),
ETH_DOMAIN
);
await registerEmitter(
USDC_INTEGRATION_TARGET,
CHAIN_ID_AVAX,
"0x" + tryNativeToHexString(AVAX_CONTRACT_ADDRESS, CHAIN_ID_AVAX),
AVAX_DOMAIN
);
await registerEmitter(
USDC_INTEGRATION_SOURCE,
CHAIN_ID_AVAX,
"0x" + tryNativeToHexString(AVAX_CONTRACT_ADDRESS, CHAIN_ID_AVAX),
AVAX_DOMAIN
);
await registerEmitter(
USDC_INTEGRATION_TARGET,
CHAIN_ID_ETH,
"0x" + tryNativeToHexString(ETH_CONTRACT_ADDRESS, CHAIN_ID_ETH),
ETH_DOMAIN
);
}
async function transferTokens() {
// struct to call target chain `redeemTokens` method with
const redeemParams = {} as RedeemParameters;
// input params to transferTokens
const amount: ethers.BigNumber = ethers.utils.parseUnits("0.000001", 6);
const toChain = CHAIN_ID_ETH;
// create signing key and derive public key
const mintRecipient =
"0x" + tryNativeToHexString(AVAX_SIGNER.address, CHAIN_ID_ETH);
// approve the contract to spend USDC
const tx = await USDC_CONTRACT.approve(
USDC_INTEGRATION_SOURCE.address,
amount
);
await tx.wait();
// depositForBurn (transferTokens)
const tx2 = await USDC_INTEGRATION_SOURCE.transferTokens(
USDC_ADDRESS,
amount,
toChain,
mintRecipient
);
const receipt: ethers.ContractReceipt = await tx2.wait();
console.log(
`Deposit for burn transaction on Avax: ${receipt.transactionHash}`
);
// fetch the wormhole VAA
redeemParams.encodedWormholeMessage = await getSignedVaaFromReceiptOnEth(
receipt,
CHAIN_ID_AVAX,
USDC_INTEGRATION_SOURCE.address,
WORMHOLE_AVAX
);
// parse the circle message event from the MessageTransmitter contract
const circleEvent = await parseCircleMessageEvent(
receipt,
AVAX_TRANSMITTER_ADDRESS
);
// hash the circleEvent message field from the event
const circleEventHash = ethers.utils.keccak256(circleEvent.args.message);
// sleep for 10 seconds, then fetch the attestation from circle
console.log(`Searching for attestation: ${circleEventHash}`);
await sleep(10000);
const circleAttestation = await getCircleAttestation(circleEventHash);
// set cricle values in redeemParams
redeemParams.circleBridgeMessage = circleEvent.args.message;
redeemParams.circleAttestation = circleAttestation;
// redeem the tokens on the target chain
const tx3 = await USDC_INTEGRATION_TARGET.redeemTokens(redeemParams);
const receipt2: ethers.ContractReceipt = await tx3.wait();
console.log(`Mint transaction on Eth: ${receipt2.transactionHash}`);
}
async function transferTokensWithPayload() {
// struct to call target chain `redeemTokens` method with
const redeemParams = {} as RedeemParameters;
// input params to transferTokens
const amount: ethers.BigNumber = ethers.utils.parseUnits("0.000001", 6);
const toChain = CHAIN_ID_ETH;
// create signing key and derive public key
const mintRecipient =
"0x" + tryNativeToHexString(ETH_SIGNER.address, CHAIN_ID_ETH);
// approve the contract to spend USDC
const tx = await USDC_CONTRACT.approve(
USDC_INTEGRATION_SOURCE.address,
amount
);
await tx.wait();
// create an arbitrary payload to test with
const arbitraryPayload = ethers.utils.hexlify(
ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff0")
);
// depositForBurn (transferTokens)
const tx2 = await USDC_INTEGRATION_SOURCE.transferTokensWithPayload(
USDC_ADDRESS,
amount,
toChain,
mintRecipient,
arbitraryPayload
);
const receipt: ethers.ContractReceipt = await tx2.wait();
console.log(
`Deposit for burn transaction on Avax: ${receipt.transactionHash}`
);
// fetch the wormhole VAA
redeemParams.encodedWormholeMessage = await getSignedVaaFromReceiptOnEth(
receipt,
CHAIN_ID_AVAX,
USDC_INTEGRATION_SOURCE.address,
WORMHOLE_AVAX
);
// parse the wormhole message to verify that the payload is correct
const parsedWormholeMessage = await AVAX_WORMHOLE_CONTRACT.parseVM(
redeemParams.encodedWormholeMessage
);
const parsedPayload = await USDC_INTEGRATION_TARGET.decodeDepositWithPayload(
parsedWormholeMessage.payload
);
console.log(parsedPayload);
// parse the circle message event from the MessageTransmitter contract
const circleEvent = await parseCircleMessageEvent(
receipt,
AVAX_TRANSMITTER_ADDRESS
);
// hash the circleEvent message field from the event
const circleEventHash = ethers.utils.keccak256(circleEvent.args.message);
// sleep for 10 seconds, then fetch the attestation from circle
console.log(`Searching for attestation: ${circleEventHash}`);
await sleep(10000);
const circleAttestation = await getCircleAttestation(circleEventHash);
// set cricle values in redeemParams
redeemParams.circleBridgeMessage = circleEvent.args.message;
redeemParams.circleAttestation = circleAttestation;
// redeem the tokens on the target chain
const tx3 = await USDC_INTEGRATION_TARGET.redeemTokensWithPayload(
redeemParams
);
const receipt2: ethers.ContractReceipt = await tx3.wait();
console.log(`Mint transaction on Eth: ${receipt2.transactionHash}`);
}
async function main() {
//await registerEverything();
//await updateFinality(USDC_INTEGRATION_TARGET, CHAIN_ID_ETH, 200);
//transferTokens();
transferTokensWithPayload();
}
main();

View File

@ -0,0 +1,164 @@
import {ethers} from "ethers";
import {
CHAIN_ID_ALGORAND,
CHAIN_ID_AVAX,
CHAIN_ID_ETH,
tryNativeToUint8Array,
ChainId,
} from "@certusone/wormhole-sdk";
import {MockGuardians} from "@certusone/wormhole-sdk/lib/cjs/mock";
import {CircleGovernanceEmitter} from "../test/helpers/mock";
import {abi as WORMHOLE_ABI} from "../../out/IWormhole.sol/IWormhole.json";
import {abi as CIRCLE_INTEGRATION_ABI} from "../../out/CircleIntegration.sol/CircleIntegration.json";
import {getTimeNow} from "../test/helpers/utils";
import {expect} from "chai";
require("dotenv").config({path: process.argv.slice(2)[0]});
// ethereum wallet, CircleIntegration contract and USDC contract
const provider = new ethers.providers.StaticJsonRpcProvider(
process.env.SOURCE_PROVIDER!
);
const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY!, provider);
// set up Wormhole instance
let wormhole = new ethers.Contract(
process.env.SOURCE_WORMHOLE!,
WORMHOLE_ABI,
provider
);
wormhole = wormhole.connect(wallet);
// set up circleIntegration contract
let circleIntegration = new ethers.Contract(
process.env.SOURCE_CIRCLE_INTEGRATION_ADDRESS!,
CIRCLE_INTEGRATION_ABI,
provider
);
circleIntegration = circleIntegration.connect(wallet);
// produces governance VAAs for CircleAttestation contract
const governance = new CircleGovernanceEmitter();
async function registerEmitterAndDomain() {
// MockGuardians and MockCircleAttester objects
const guardians = new MockGuardians(
await wormhole.getCurrentGuardianSetIndex(),
[process.env.TESTNET_GUARDIAN_KEY!]
);
// put together VAA
const timestamp = getTimeNow();
const chainId = Number(process.env.SOURCE_CHAIN_ID!);
const emitterChain = Number(process.env.TARGET_CHAIN_ID!);
const emitterAddress = Buffer.from(
tryNativeToUint8Array(
process.env.TARGET_CIRCLE_INTEGRATION_ADDRESS!,
"avalanche"
)
);
const domain = Number(process.env.TARGET_DOMAIN!);
// create unsigned registerEmitterAndDomain governance message
const published = governance.publishCircleIntegrationRegisterEmitterAndDomain(
timestamp,
chainId,
emitterChain,
emitterAddress,
domain
);
// sign the governance VAA with the testnet guardian key
const signedMessage = guardians.addSignatures(published, [0]);
// register the emitter and domain
const receipt = await circleIntegration
.registerEmitterAndDomain(signedMessage)
.then((tx: ethers.ContractTransaction) => tx.wait())
.catch((msg: string) => {
// should not happen
console.log(msg);
return null;
});
// check contract state to verify the registration
const registeredEmitter = await circleIntegration
.getRegisteredEmitter(emitterChain)
.then((bytes: ethers.BytesLike) =>
Buffer.from(ethers.utils.arrayify(bytes))
);
expect(Buffer.compare(registeredEmitter, emitterAddress)).to.equal(0);
}
async function registerAcceptedToken() {
// MockGuardians and MockCircleAttester objects
const guardians = new MockGuardians(
await wormhole.getCurrentGuardianSetIndex(),
[process.env.TESTNET_GUARDIAN_KEY!]
);
const timestamp = getTimeNow();
const chainId = Number(process.env.SOURCE_CHAIN_ID!);
// create unsigned registerAcceptedToken governance message
const published = governance.publishCircleIntegrationRegisterAcceptedToken(
timestamp,
chainId,
process.env.SOURCE_USDC_ADDRESS!
);
// sign governance message with guardian key
const signedMessage = guardians.addSignatures(published, [0]);
// register the token
const receipt = await circleIntegration
.registerAcceptedToken(signedMessage)
.then((tx: ethers.ContractTransaction) => tx.wait())
.catch((msg: string) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
// check contract state to verify the registration
const accepted = await circleIntegration.isAcceptedToken(
process.env.SOURCE_USDC_ADDRESS!
);
expect(accepted).is.true;
}
async function updateFinality() {
// MockGuardians and MockCircleAttester objects
const guardians = new MockGuardians(
await wormhole.getCurrentGuardianSetIndex(),
[process.env.TESTNET_GUARDIAN_KEY!]
);
const timestamp = getTimeNow();
const chainId = Number(process.env.SOURCE_CHAIN_ID!);
const finality = Number(process.env.SOURCE_FINALITY!);
// create unsigned registerTargetChainToken governance message
const published = governance.publishCircleIntegrationUpdateFinality(
timestamp,
chainId,
finality
);
// sign governance message with guardian key
const signedMessage = guardians.addSignatures(published, [0]);
// register the target token
const receipt = await circleIntegration
.updateWormholeFinality(signedMessage)
.then((tx: ethers.ContractTransaction) => tx.wait())
.catch((msg: string) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
}
updateFinality();

17
evm/ts/scripts/sample.env Normal file
View File

@ -0,0 +1,17 @@
SOURCE_PROVIDER=
SOURCE_CIRCLE_INTEGRATION_ADDRESS=
SOURCE_WORMHOLE=0x706abc4E45D419950511e474C7B9Ed348A4a716c
SOURCE_USDC_ADDRESS=0x07865c6E87B9F70255377e024ace6630C1Eaa37F
SOURCE_FINALITY=200
SOURCE_CHAIN_ID=2
SOURCE_DOMAIN=0
TARGET_PROVIDER=https://api.avax-test.network/ext/bc/C/rpc
TARGET_CIRCLE_INTEGRATION_ADDRESS=
TARGET_WORMHOLE=0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C
TARGET_USDC_ADDRESS=0x5425890298aed601595a70AB815c96711a31Bc65
TARGET_CHAIN_ID=6
TARGET_DOMAIN=1
WALLET_PRIVATE_KEY=
TESTNET_GUARDIAN_KEY=

17
evm/ts/src/logs.ts Normal file
View File

@ -0,0 +1,17 @@
import { ethers } from "ethers";
export function findCircleMessageInLogs(
logs: ethers.providers.Log[],
messageTransmitterAddress: string
): string | null {
for (const log of logs) {
if (log.address == messageTransmitterAddress) {
const iface = new ethers.utils.Interface([
"event MessageSent(bytes message)",
]);
return iface.parseLog(log).args.message as string;
}
}
return null;
}

View File

@ -1,5 +1,18 @@
import { ethers } from "ethers";
export interface TransferParameters {
token: string;
amount: ethers.BigNumber;
targetChain: number;
mintRecipient: Uint8Array;
}
export interface RedeemParameters {
encodedWormholeMessage: Uint8Array;
circleBridgeMessage: Uint8Array;
circleAttestation: Uint8Array;
}
export interface DepositWithPayload {
token: Buffer;
amount: ethers.BigNumber;

View File

@ -0,0 +1,528 @@
import {expect} from "chai";
import {ethers} from "ethers";
import {
CHAIN_ID_AVAX,
CHAIN_ID_ETH,
tryNativeToHexString,
} from "@certusone/wormhole-sdk";
import {
ICircleBridge__factory,
IMessageTransmitter__factory,
IUSDC__factory,
IWormhole__factory,
} from "../src/ethers-contracts";
import {
AVAX_CIRCLE_BRIDGE_ADDRESS,
AVAX_FORK_CHAIN_ID,
AVAX_LOCALHOST,
AVAX_USDC_TOKEN_ADDRESS,
AVAX_WORMHOLE_ADDRESS,
ETH_CIRCLE_BRIDGE_ADDRESS,
ETH_FORK_CHAIN_ID,
ETH_LOCALHOST,
ETH_USDC_TOKEN_ADDRESS,
ETH_WORMHOLE_ADDRESS,
GUARDIAN_PRIVATE_KEY,
WALLET_PRIVATE_KEY,
WORMHOLE_GUARDIAN_SET_INDEX,
WORMHOLE_MESSAGE_FEE,
} from "./helpers/consts";
describe("Environment Test", () => {
describe("Global", () => {
it("Environment Variables", () => {
expect(WORMHOLE_MESSAGE_FEE).is.not.undefined;
expect(WORMHOLE_GUARDIAN_SET_INDEX).is.not.undefined;
expect(GUARDIAN_PRIVATE_KEY).is.not.undefined;
expect(WALLET_PRIVATE_KEY).is.not.undefined;
});
});
describe("Ethereum Goerli Testnet Fork", () => {
describe("Environment", () => {
it("Variables", () => {
expect(ETH_LOCALHOST).is.not.undefined;
expect(ETH_FORK_CHAIN_ID).is.not.undefined;
expect(ETH_WORMHOLE_ADDRESS).is.not.undefined;
expect(ETH_USDC_TOKEN_ADDRESS).is.not.undefined;
expect(ETH_CIRCLE_BRIDGE_ADDRESS).is.not.undefined;
});
});
describe("RPC", () => {
const provider = new ethers.providers.StaticJsonRpcProvider(
ETH_LOCALHOST
);
const wormhole = IWormhole__factory.connect(
ETH_WORMHOLE_ADDRESS,
provider
);
expect(wormhole.address).to.equal(ETH_WORMHOLE_ADDRESS);
it("EVM Chain ID", async () => {
const network = await provider.getNetwork();
expect(network.chainId).to.equal(ETH_FORK_CHAIN_ID);
});
it("Wormhole", async () => {
const chainId = await wormhole.chainId();
expect(chainId).to.equal(CHAIN_ID_ETH as number);
// fetch current wormhole protocol fee
const messageFee: ethers.BigNumber = await wormhole.messageFee();
expect(messageFee.eq(WORMHOLE_MESSAGE_FEE)).to.be.true;
// Override guardian set
{
// 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);
}
});
it("Wormhole SDK", async () => {
// confirm that the Wormhole SDK is installed
const accounts = await provider.listAccounts();
expect(tryNativeToHexString(accounts[0], "ethereum")).to.equal(
"00000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1"
);
});
it("Circle", async () => {
// instantiate Circle Bridge contract
const circleBridge = ICircleBridge__factory.connect(
ETH_CIRCLE_BRIDGE_ADDRESS,
provider
);
// fetch attestation manager address
const attesterManager = await circleBridge
.localMessageTransmitter()
.then((address) =>
IMessageTransmitter__factory.connect(address, provider)
)
.then((messageTransmitter) => messageTransmitter.attesterManager());
const myAttester = new ethers.Wallet(GUARDIAN_PRIVATE_KEY, provider);
// start prank (impersonate the attesterManager)
await provider.send("anvil_impersonateAccount", [attesterManager]);
// instantiate message transmitter
const messageTransmitter = await circleBridge
.localMessageTransmitter()
.then((address) =>
IMessageTransmitter__factory.connect(
address,
provider.getSigner(attesterManager)
)
);
const existingAttester = await messageTransmitter.getEnabledAttester(0);
// enable devnet guardian as attester
{
const receipt = await messageTransmitter
.enableAttester(myAttester.address)
.then((tx) => tx.wait())
.catch((msg) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
}
// disable existing attester
{
const receipt = await messageTransmitter
.disableAttester(existingAttester)
.then((tx) => tx.wait())
.catch((msg) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
}
// stop prank
await provider.send("anvil_stopImpersonatingAccount", [
attesterManager,
]);
// confirm that the attester address swap was successful
const attester = await circleBridge
.localMessageTransmitter()
.then((address) =>
IMessageTransmitter__factory.connect(address, provider)
)
.then((messageTransmitter) =>
messageTransmitter.getEnabledAttester(0)
);
expect(myAttester.address).to.equal(attester);
});
it("USDC", async () => {
// fetch master minter address
const masterMinter = await IUSDC__factory.connect(
ETH_USDC_TOKEN_ADDRESS,
provider
).masterMinter();
const wallet = new ethers.Wallet(WALLET_PRIVATE_KEY, provider);
// start prank (impersonate the Circle masterMinter)
await provider.send("anvil_impersonateAccount", [masterMinter]);
// configure my wallet as minter
{
const usdc = IUSDC__factory.connect(
ETH_USDC_TOKEN_ADDRESS,
provider.getSigner(masterMinter)
);
const receipt = await usdc
.configureMinter(wallet.address, ethers.constants.MaxUint256)
.then((tx) => tx.wait())
.catch((msg) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
}
// stop prank
await provider.send("anvil_stopImpersonatingAccount", [masterMinter]);
// mint USDC and confirm with a balance check
{
const usdc = IUSDC__factory.connect(ETH_USDC_TOKEN_ADDRESS, wallet);
const amount = ethers.utils.parseUnits("69420", 6);
const balanceBefore = await usdc.balanceOf(wallet.address);
const receipt = await usdc
.mint(wallet.address, amount)
.then((tx) => tx.wait())
.catch((msg) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
const balanceAfter = await usdc.balanceOf(wallet.address);
expect(balanceAfter.sub(balanceBefore).eq(amount)).is.true;
}
});
});
});
describe("Avalanche Fuji Testnet Fork", () => {
describe("Environment", () => {
it("Variables", () => {
expect(AVAX_LOCALHOST).is.not.undefined;
expect(AVAX_FORK_CHAIN_ID).is.not.undefined;
expect(AVAX_WORMHOLE_ADDRESS).is.not.undefined;
expect(AVAX_USDC_TOKEN_ADDRESS).is.not.undefined;
expect(AVAX_CIRCLE_BRIDGE_ADDRESS).is.not.undefined;
});
});
describe("RPC", () => {
const provider = new ethers.providers.StaticJsonRpcProvider(
AVAX_LOCALHOST
);
const wormhole = IWormhole__factory.connect(
AVAX_WORMHOLE_ADDRESS,
provider
);
expect(wormhole.address).to.equal(AVAX_WORMHOLE_ADDRESS);
it("EVM Chain ID", async () => {
const network = await provider.getNetwork();
expect(network.chainId).to.equal(AVAX_FORK_CHAIN_ID);
});
it("Wormhole", async () => {
const chainId = await wormhole.chainId();
expect(chainId).to.equal(CHAIN_ID_AVAX as number);
// fetch current wormhole protocol fee
const messageFee = await wormhole.messageFee();
expect(messageFee.eq(WORMHOLE_MESSAGE_FEE)).to.be.true;
// override guardian set
{
// 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);
}
});
it("Wormhole SDK", async () => {
// confirm that the Wormhole SDK is installed
const accounts = await provider.listAccounts();
expect(tryNativeToHexString(accounts[0], "ethereum")).to.equal(
"00000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1"
);
});
it("Circle", async () => {
// instantiate Circle Bridge contract
const circleBridge = ICircleBridge__factory.connect(
AVAX_CIRCLE_BRIDGE_ADDRESS,
provider
);
// fetch attestation manager address
const attesterManager = await circleBridge
.localMessageTransmitter()
.then((address) =>
IMessageTransmitter__factory.connect(address, provider)
)
.then((messageTransmitter) => messageTransmitter.attesterManager());
const myAttester = new ethers.Wallet(GUARDIAN_PRIVATE_KEY, provider);
// start prank (impersonate the attesterManager)
await provider.send("anvil_impersonateAccount", [attesterManager]);
// instantiate message transmitter
const messageTransmitter = await circleBridge
.localMessageTransmitter()
.then((address) =>
IMessageTransmitter__factory.connect(
address,
provider.getSigner(attesterManager)
)
);
const existingAttester = await messageTransmitter.getEnabledAttester(0);
// enable devnet guardian as attester
{
const receipt = await messageTransmitter
.enableAttester(myAttester.address)
.then((tx) => tx.wait())
.catch((msg) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
}
// disable existing attester
{
const receipt = await messageTransmitter
.disableAttester(existingAttester)
.then((tx) => tx.wait())
.catch((msg) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
}
// stop prank
await provider.send("anvil_stopImpersonatingAccount", [
attesterManager,
]);
// confirm that the attester address swap was successful
const attester = await circleBridge
.localMessageTransmitter()
.then((address) =>
IMessageTransmitter__factory.connect(address, provider)
)
.then((messageTransmitter) =>
messageTransmitter.getEnabledAttester(0)
);
expect(myAttester.address).to.equal(attester);
});
it("USDC", async () => {
// fetch master minter address
const masterMinter = await IUSDC__factory.connect(
AVAX_USDC_TOKEN_ADDRESS,
provider
).masterMinter();
const wallet = new ethers.Wallet(WALLET_PRIVATE_KEY, provider);
// start prank (impersonate the Circle masterMinter)
await provider.send("anvil_impersonateAccount", [masterMinter]);
// configure my wallet as minter
{
const usdc = IUSDC__factory.connect(
AVAX_USDC_TOKEN_ADDRESS,
provider.getSigner(masterMinter)
);
const receipt = await usdc
.configureMinter(wallet.address, ethers.constants.MaxUint256)
.then((tx) => tx.wait())
.catch((msg) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
}
// stop prank
await provider.send("anvil_stopImpersonatingAccount", [masterMinter]);
// mint USDC and confirm with a balance check
{
const usdc = IUSDC__factory.connect(AVAX_USDC_TOKEN_ADDRESS, wallet);
const amount = ethers.utils.parseUnits("69420", 6);
const balanceBefore = await usdc.balanceOf(wallet.address);
const receipt = await usdc
.mint(wallet.address, amount)
.then((tx) => tx.wait())
.catch((msg) => {
// should not happen
console.log(msg);
return null;
});
expect(receipt).is.not.null;
const balanceAfter = await usdc.balanceOf(wallet.address);
expect(balanceAfter.sub(balanceBefore).eq(amount)).is.true;
}
});
});
});
});

View File

@ -1,112 +0,0 @@
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 { IWormhole__factory } from "../src/ethers-contracts";
describe("Fork Test", () => {
const provider = new ethers.providers.StaticJsonRpcProvider(LOCALHOST);
const wormhole = IWormhole__factory.connect(WORMHOLE_ADDRESS, provider);
describe("Verify Ethereum Goerli Testnet 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"
);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,27 @@
import { ethers } from "ethers";
import {ethers} from "ethers";
// rpc
export const LOCALHOST = "http://localhost:8545";
// ethereum goerli testnet fork
export const ETH_LOCALHOST = "http://localhost:8545";
export const ETH_FORK_CHAIN_ID = Number(process.env.ETH_FORK_CHAIN_ID!);
export const ETH_WORMHOLE_ADDRESS = process.env.ETH_WORMHOLE_ADDRESS!;
export const ETH_USDC_TOKEN_ADDRESS = process.env.ETH_USDC_TOKEN_ADDRESS!;
export const ETH_CIRCLE_BRIDGE_ADDRESS = process.env.ETH_CIRCLE_BRIDGE_ADDRESS!;
// fork
export const FORK_CHAIN_ID = Number(process.env.TESTING_FORK_CHAIN_ID!);
// avalanche fuji testnet fork
export const AVAX_LOCALHOST = "http://localhost:8546";
export const AVAX_FORK_CHAIN_ID = Number(process.env.AVAX_FORK_CHAIN_ID!);
export const AVAX_WORMHOLE_ADDRESS = process.env.AVAX_WORMHOLE_ADDRESS!;
export const AVAX_USDC_TOKEN_ADDRESS = process.env.AVAX_USDC_TOKEN_ADDRESS!;
export const AVAX_CIRCLE_BRIDGE_ADDRESS =
process.env.AVAX_CIRCLE_BRIDGE_ADDRESS!;
// wormhole
export const WORMHOLE_ADDRESS = process.env.TESTING_WORMHOLE_ADDRESS!;
export const WORMHOLE_CHAIN_ID = Number(process.env.TESTING_WORMHOLE_CHAIN_ID!);
// global
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!
);
// 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 = 0;
// Ethereum Goerli Testnet
export const ETH_USDC_TOKEN_ADDRESS = process.env.ETH_USDC_TOKEN_ADDRESS!;
// Avalanche Fuji Testnet
export const AVAX_USDC_TOKEN_ADDRESS = process.env.AVAX_USDC_TOKEN_ADDRESS!;
export const WALLET_PRIVATE_KEY_TWO = process.env.WALLET_PRIVATE_KEY_TWO!;

View File

@ -1,10 +1,10 @@
import { coalesceChainId, tryNativeToHexString } from "@certusone/wormhole-sdk";
import {coalesceChainId, tryNativeToHexString} from "@certusone/wormhole-sdk";
import {
GovernanceEmitter,
MockEmitter,
} from "@certusone/wormhole-sdk/lib/cjs/mock";
import { ethers } from "ethers";
import { DepositWithPayload, ICircleIntegration } from "../../src";
import {ethers} from "ethers";
import {DepositWithPayload, ICircleIntegration} from "../../src";
export interface Transfer {
token: string;
@ -54,6 +54,24 @@ export class CircleGovernanceEmitter extends GovernanceEmitter {
);
}
publishCircleIntegrationUpdateFinality(
timestamp: number,
chain: number,
finality: number,
uptickSequence: boolean = true
) {
const payload = Buffer.alloc(1);
payload.writeUIntBE(finality, 0, 1);
return this.publishGovernanceMessage(
timestamp,
"CircleIntegration",
payload,
1,
chain,
uptickSequence
);
}
publishCircleIntegrationRegisterEmitterAndDomain(
timestamp: number,
chain: number,

View File

@ -1,8 +1,82 @@
import { ethers } from "ethers";
import {tryNativeToHexString} from "@certusone/wormhole-sdk";
import {ethSignWithPrivate} from "@certusone/wormhole-sdk/lib/cjs/mock";
import {ethers} from "ethers";
import * as fs from "fs";
export async function getBlockTimestamp(provider: ethers.providers.Provider) {
return provider
.getBlockNumber()
.then((blockNumber) => provider.getBlock(blockNumber))
.then((block) => block.timestamp);
export function getTimeNow() {
return Math.floor(Date.now() / 1000);
}
export function readCircleIntegrationProxyAddress(chain: number): string {
return JSON.parse(
fs.readFileSync(
`${__dirname}/../../../broadcast-test/deploy_contracts.sol/${chain}/run-latest.json`,
"utf-8"
)
).transactions[2].contractAddress;
}
export function readMockIntegrationAddress(chain: number): string {
return JSON.parse(
fs.readFileSync(
`${__dirname}/../../../broadcast-test/deploy_mock_contracts.sol/${chain}/run-latest.json`,
"utf-8"
)
).transactions[0].contractAddress;
}
export function findWormholeMessageInLogs(
logs: ethers.providers.Log[],
wormholeAddress: string,
emitterChain: number
) {
for (const log of logs) {
if (log.address == wormholeAddress) {
const iface = new ethers.utils.Interface([
"event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
]);
const result = iface.parseLog(log).args;
const payload = ethers.utils.arrayify(result.payload);
const message = Buffer.alloc(51 + payload.length);
message.writeUInt32BE(getTimeNow(), 0);
message.writeUInt32BE(Number(result.nonce), 4);
message.writeUInt16BE(emitterChain, 8);
message.write(
tryNativeToHexString(result.sender.toString(), "ethereum"),
10,
"hex"
);
message.writeBigUInt64BE(BigInt(result.sequence.toString()), 42);
message.writeUInt8(Number(result.consistencyLevel), 50);
message.write(Buffer.from(payload).toString("hex"), 51, "hex");
return message;
}
}
return null;
}
export class MockCircleAttester {
privateKey: string;
constructor(privateKey: string) {
this.privateKey = privateKey;
}
attestMessage(message: Uint8Array): Uint8Array {
const signature = ethSignWithPrivate(
this.privateKey,
Buffer.from(ethers.utils.arrayify(ethers.utils.keccak256(message)))
);
const out = Buffer.alloc(65);
out.write(signature.r.toString(16).padStart(64, "0"), 0, "hex");
out.write(signature.s.toString(16).padStart(64, "0"), 32, "hex");
out.writeUInt8(signature.recoveryParam! + 27, 64);
return Uint8Array.from(out);
}
}