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:
parent
5c6479d370
commit
dbc7d7d10f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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");
|
||||
_;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)",
|
||||
];
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
|
@ -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();
|
|
@ -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=
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
@ -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!;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue