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_CHAIN_ID=43113
|
||||||
export AVAX_FORK_BLOCK_NUMBER=15405470
|
export AVAX_FORK_BLOCK_NUMBER=15405470
|
||||||
export AVAX_USDC_TOKEN_ADDRESS=0x5425890298aed601595a70AB815c96711a31Bc65
|
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=4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
|
||||||
|
export WALLET_PRIVATE_KEY_TWO=92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
pragma solidity ^0.8.0;
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
import "forge-std/Script.sol";
|
import "forge-std/Script.sol";
|
||||||
|
import "forge-std/console.sol";
|
||||||
|
|
||||||
import {IWormhole} from "wormhole/interfaces/IWormhole.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 {WormholeSimulator} from "wormhole-forge-sdk/WormholeSimulator.sol";
|
||||||
|
|
||||||
import "forge-std/console.sol";
|
|
||||||
|
|
||||||
contract ContractScript is Script {
|
contract ContractScript is Script {
|
||||||
// Wormhole
|
// Wormhole
|
||||||
WormholeSimulator wormholeSimulator;
|
WormholeSimulator wormholeSimulator;
|
||||||
|
@ -64,7 +63,7 @@ contract ContractScript is Script {
|
||||||
// begin sending transactions
|
// begin sending transactions
|
||||||
vm.startBroadcast();
|
vm.startBroadcast();
|
||||||
|
|
||||||
// HelloWorld.sol
|
// deploy Circle Integration proxy
|
||||||
deployCircleIntegration();
|
deployCircleIntegration();
|
||||||
|
|
||||||
// finished
|
// 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 {IWormhole} from "wormhole/interfaces/IWormhole.sol";
|
||||||
import {ICircleIntegration} from "../src/interfaces/ICircleIntegration.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 {CircleIntegrationStructs} from "../src/circle_integration/CircleIntegrationStructs.sol";
|
||||||
import {CircleIntegrationSetup} from "../src/circle_integration/CircleIntegrationSetup.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";
|
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 {
|
contract CircleIntegrationTest is Test {
|
||||||
using BytesLib for bytes;
|
using BytesLib for bytes;
|
||||||
|
|
||||||
|
@ -169,7 +162,7 @@ contract CircleIntegrationTest is Test {
|
||||||
bytes32 fromAddress,
|
bytes32 fromAddress,
|
||||||
bytes32 mintRecipient,
|
bytes32 mintRecipient,
|
||||||
bytes memory payload
|
bytes memory payload
|
||||||
) public {
|
) public view {
|
||||||
vm.assume(token != bytes32(0));
|
vm.assume(token != bytes32(0));
|
||||||
vm.assume(amount > 0);
|
vm.assume(amount > 0);
|
||||||
vm.assume(targetDomain != sourceDomain);
|
vm.assume(targetDomain != sourceDomain);
|
||||||
|
@ -277,7 +270,7 @@ contract CircleIntegrationTest is Test {
|
||||||
bytes32 fromAddress,
|
bytes32 fromAddress,
|
||||||
bytes32 mintRecipient,
|
bytes32 mintRecipient,
|
||||||
bytes memory payload
|
bytes memory payload
|
||||||
) public {
|
) public view {
|
||||||
vm.assume(token != bytes32(0));
|
vm.assume(token != bytes32(0));
|
||||||
vm.assume(amount > 0);
|
vm.assume(amount > 0);
|
||||||
vm.assume(targetDomain != sourceDomain);
|
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");
|
require(guardian != 0, "devnetGuardian is zero address");
|
||||||
|
|
||||||
bytes memory body = encodeObservation(wormholeMessage);
|
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);
|
return signObservation(devnetGuardianPK, wormholeMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +182,7 @@ contract WormholeSimulator {
|
||||||
|
|
||||||
function fetchSignedMessageFromLogs(Vm.Log memory log, uint16 emitterChainId, bytes32 emitterAddress)
|
function fetchSignedMessageFromLogs(Vm.Log memory log, uint16 emitterChainId, bytes32 emitterAddress)
|
||||||
public
|
public
|
||||||
|
view
|
||||||
returns (bytes memory)
|
returns (bytes memory)
|
||||||
{
|
{
|
||||||
// Create message instance
|
// Create message instance
|
||||||
|
|
|
@ -9,6 +9,7 @@ fi
|
||||||
# ethereum goerli testnet
|
# ethereum goerli testnet
|
||||||
anvil \
|
anvil \
|
||||||
-m "myth like bonus scare over problem client lizard pioneer submit female collect" \
|
-m "myth like bonus scare over problem client lizard pioneer submit female collect" \
|
||||||
|
--port 8545 \
|
||||||
--fork-url $ETH_FORK_RPC > anvil_eth.log &
|
--fork-url $ETH_FORK_RPC > anvil_eth.log &
|
||||||
|
|
||||||
# avalanche fuji testnet
|
# avalanche fuji testnet
|
||||||
|
@ -22,18 +23,29 @@ sleep 2
|
||||||
## first key from mnemonic above
|
## first key from mnemonic above
|
||||||
export PRIVATE_KEY="0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
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
|
mkdir -p cache
|
||||||
cp -v foundry.toml cache/foundry.toml
|
cp -v foundry.toml cache/foundry.toml
|
||||||
cp -v foundry-test.toml foundry.toml
|
cp -v foundry-test.toml foundry.toml
|
||||||
|
|
||||||
echo "deploy contracts"
|
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"
|
echo "overriding foundry.toml"
|
||||||
mv -v cache/foundry.toml 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
|
// call the circle bridge to mint tokens to the recipient
|
||||||
bool success = circleTransmitter().receiveMessage(params.circleBridgeMessage, params.circleAttestation);
|
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) {
|
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";
|
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 {
|
export interface DepositWithPayload {
|
||||||
token: Buffer;
|
token: Buffer;
|
||||||
amount: ethers.BigNumber;
|
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
|
// ethereum goerli testnet fork
|
||||||
export const LOCALHOST = "http://localhost:8545";
|
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
|
// avalanche fuji testnet fork
|
||||||
export const FORK_CHAIN_ID = Number(process.env.TESTING_FORK_CHAIN_ID!);
|
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
|
// global
|
||||||
export const WORMHOLE_ADDRESS = process.env.TESTING_WORMHOLE_ADDRESS!;
|
|
||||||
export const WORMHOLE_CHAIN_ID = Number(process.env.TESTING_WORMHOLE_CHAIN_ID!);
|
|
||||||
export const WORMHOLE_MESSAGE_FEE = ethers.BigNumber.from(
|
export const WORMHOLE_MESSAGE_FEE = ethers.BigNumber.from(
|
||||||
process.env.TESTING_WORMHOLE_MESSAGE_FEE!
|
process.env.TESTING_WORMHOLE_MESSAGE_FEE!
|
||||||
);
|
);
|
||||||
export const WORMHOLE_GUARDIAN_SET_INDEX = Number(
|
export const WORMHOLE_GUARDIAN_SET_INDEX = Number(
|
||||||
process.env.TESTING_WORMHOLE_GUARDIAN_SET_INDEX!
|
process.env.TESTING_WORMHOLE_GUARDIAN_SET_INDEX!
|
||||||
);
|
);
|
||||||
|
|
||||||
// signer
|
|
||||||
export const GUARDIAN_PRIVATE_KEY = process.env.TESTING_DEVNET_GUARDIAN!;
|
export const GUARDIAN_PRIVATE_KEY = process.env.TESTING_DEVNET_GUARDIAN!;
|
||||||
export const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY!;
|
export const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY!;
|
||||||
|
export const WALLET_PRIVATE_KEY_TWO = process.env.WALLET_PRIVATE_KEY_TWO!;
|
||||||
// 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!;
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { coalesceChainId, tryNativeToHexString } from "@certusone/wormhole-sdk";
|
import {coalesceChainId, tryNativeToHexString} from "@certusone/wormhole-sdk";
|
||||||
import {
|
import {
|
||||||
GovernanceEmitter,
|
GovernanceEmitter,
|
||||||
MockEmitter,
|
MockEmitter,
|
||||||
} from "@certusone/wormhole-sdk/lib/cjs/mock";
|
} from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||||||
import { ethers } from "ethers";
|
import {ethers} from "ethers";
|
||||||
import { DepositWithPayload, ICircleIntegration } from "../../src";
|
import {DepositWithPayload, ICircleIntegration} from "../../src";
|
||||||
|
|
||||||
export interface Transfer {
|
export interface Transfer {
|
||||||
token: string;
|
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(
|
publishCircleIntegrationRegisterEmitterAndDomain(
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
chain: 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) {
|
export function getTimeNow() {
|
||||||
return provider
|
return Math.floor(Date.now() / 1000);
|
||||||
.getBlockNumber()
|
}
|
||||||
.then((blockNumber) => provider.getBlock(blockNumber))
|
|
||||||
.then((block) => block.timestamp);
|
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