diff --git a/evm/env/testing.env b/evm/env/testing.env index e240769..ed7f47e 100644 --- a/evm/env/testing.env +++ b/evm/env/testing.env @@ -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 diff --git a/evm/forge-scripts/deploy_contracts.sol b/evm/forge-scripts/deploy_contracts.sol index 8c5c7a1..6b6e503 100644 --- a/evm/forge-scripts/deploy_contracts.sol +++ b/evm/forge-scripts/deploy_contracts.sol @@ -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 diff --git a/evm/forge-scripts/deploy_mock_contracts.sol b/evm/forge-scripts/deploy_mock_contracts.sol new file mode 100644 index 0000000..6b684f5 --- /dev/null +++ b/evm/forge-scripts/deploy_mock_contracts.sol @@ -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(); + } +} diff --git a/evm/forge-test/CircleIntegration.t.sol b/evm/forge-test/CircleIntegration.t.sol index 67f6d57..e4974f3 100644 --- a/evm/forge-test/CircleIntegration.t.sol +++ b/evm/forge-test/CircleIntegration.t.sol @@ -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); diff --git a/evm/modules/src/WormholeSimulator.sol b/evm/modules/src/WormholeSimulator.sol index fa6369e..43acfee 100644 --- a/evm/modules/src/WormholeSimulator.sol +++ b/evm/modules/src/WormholeSimulator.sol @@ -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 diff --git a/evm/shell-scripts/run_integration_tests.sh b/evm/shell-scripts/run_integration_tests.sh index e088089..006d28c 100644 --- a/evm/shell-scripts/run_integration_tests.sh +++ b/evm/shell-scripts/run_integration_tests.sh @@ -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 diff --git a/evm/src/circle_integration/CircleIntegration.sol b/evm/src/circle_integration/CircleIntegration.sol index 4fc8def..17a90c3 100644 --- a/evm/src/circle_integration/CircleIntegration.sol +++ b/evm/src/circle_integration/CircleIntegration.sol @@ -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) { diff --git a/evm/src/interfaces/circle/IUSDC.sol b/evm/src/interfaces/circle/IUSDC.sol new file mode 100644 index 0000000..ed80e32 --- /dev/null +++ b/evm/src/interfaces/circle/IUSDC.sol @@ -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); +} diff --git a/evm/src/interfaces/mock/IMockIntegration.sol b/evm/src/interfaces/mock/IMockIntegration.sol new file mode 100644 index 0000000..e3b397a --- /dev/null +++ b/evm/src/interfaces/mock/IMockIntegration.sol @@ -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; +} diff --git a/evm/src/mock/MockIntegration.sol b/evm/src/mock/MockIntegration.sol new file mode 100644 index 0000000..2435023 --- /dev/null +++ b/evm/src/mock/MockIntegration.sol @@ -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"); + _; + } +} diff --git a/evm/ts-test/00_wormhole.ts b/evm/ts-test/00_wormhole.ts deleted file mode 100644 index 2dc2d7e..0000000 --- a/evm/ts-test/00_wormhole.ts +++ /dev/null @@ -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" - ); - }); - }); -}); diff --git a/evm/ts-test/helpers/consts.ts b/evm/ts-test/helpers/consts.ts deleted file mode 100644 index 3c456f8..0000000 --- a/evm/ts-test/helpers/consts.ts +++ /dev/null @@ -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)", -]; diff --git a/evm/ts-test/helpers/io.ts b/evm/ts-test/helpers/io.ts deleted file mode 100644 index d70848a..0000000 --- a/evm/ts-test/helpers/io.ts +++ /dev/null @@ -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; -} diff --git a/evm/ts-test/testnet/test-circle-integration.ts b/evm/ts-test/testnet/test-circle-integration.ts deleted file mode 100644 index 2221de6..0000000 --- a/evm/ts-test/testnet/test-circle-integration.ts +++ /dev/null @@ -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 { - // 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 { - 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 { - 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 { - 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 { - 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(); diff --git a/evm/ts/scripts/contract_governance.ts b/evm/ts/scripts/contract_governance.ts new file mode 100644 index 0000000..501e12d --- /dev/null +++ b/evm/ts/scripts/contract_governance.ts @@ -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(); diff --git a/evm/ts/scripts/sample.env b/evm/ts/scripts/sample.env new file mode 100644 index 0000000..4d2ef08 --- /dev/null +++ b/evm/ts/scripts/sample.env @@ -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= diff --git a/evm/ts/src/logs.ts b/evm/ts/src/logs.ts new file mode 100644 index 0000000..0f4d741 --- /dev/null +++ b/evm/ts/src/logs.ts @@ -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; +} diff --git a/evm/ts/src/types.ts b/evm/ts/src/types.ts index eaf117b..6006f61 100644 --- a/evm/ts/src/types.ts +++ b/evm/ts/src/types.ts @@ -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; diff --git a/evm/ts/test/00_environment.ts b/evm/ts/test/00_environment.ts new file mode 100644 index 0000000..e73a72b --- /dev/null +++ b/evm/ts/test/00_environment.ts @@ -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; + } + }); + }); + }); +}); diff --git a/evm/ts/test/00_wormhole.ts b/evm/ts/test/00_wormhole.ts deleted file mode 100644 index 3c4d95a..0000000 --- a/evm/ts/test/00_wormhole.ts +++ /dev/null @@ -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" - ); - }); - }); -}); diff --git a/evm/ts/test/01_circle_integration.ts b/evm/ts/test/01_circle_integration.ts index 6408bfe..1300107 100644 --- a/evm/ts/test/01_circle_integration.ts +++ b/evm/ts/test/01_circle_integration.ts @@ -1,166 +1,1227 @@ -import { expect } from "chai"; -import { ethers } from "ethers"; -import { tryNativeToUint8Array } from "@certusone/wormhole-sdk"; +import {expect} from "chai"; +import {ethers} from "ethers"; +import { + CHAIN_ID_ALGORAND, + CHAIN_ID_AVAX, + CHAIN_ID_ETH, + tryNativeToUint8Array, +} from "@certusone/wormhole-sdk"; import { AVAX_USDC_TOKEN_ADDRESS, ETH_USDC_TOKEN_ADDRESS, GUARDIAN_PRIVATE_KEY, - GUARDIAN_SET_INDEX, - LOCALHOST, + WORMHOLE_GUARDIAN_SET_INDEX, + ETH_LOCALHOST, WALLET_PRIVATE_KEY, - WORMHOLE_ADDRESS, + WALLET_PRIVATE_KEY_TWO, + AVAX_LOCALHOST, + ETH_FORK_CHAIN_ID, + AVAX_FORK_CHAIN_ID, } from "./helpers/consts"; import { - IWormhole__factory, ICircleIntegration__factory, + IUSDC__factory, + IMockIntegration__factory, } from "../src/ethers-contracts"; -import { MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { MockCircleIntegration, CircleGovernanceEmitter } from "./helpers/mock"; -import { getBlockTimestamp } from "./helpers/utils"; -import * as fs from "fs"; +import {MockGuardians} from "@certusone/wormhole-sdk/lib/cjs/mock"; +import {RedeemParameters, TransferParameters} from "../src"; +import {findCircleMessageInLogs} from "../src/logs"; + +import {CircleGovernanceEmitter} from "./helpers/mock"; +import { + getTimeNow, + MockCircleAttester, + readCircleIntegrationProxyAddress, + readMockIntegrationAddress, + findWormholeMessageInLogs, +} from "./helpers/utils"; describe("Circle Integration Test", () => { - const provider = new ethers.providers.StaticJsonRpcProvider(LOCALHOST); - const wallet = new ethers.Wallet(WALLET_PRIVATE_KEY, provider); + // ethereum wallet, CircleIntegration contract and USDC contract + const ethProvider = new ethers.providers.StaticJsonRpcProvider(ETH_LOCALHOST); + const ethWallet = new ethers.Wallet(WALLET_PRIVATE_KEY, ethProvider); + const ethCircleIntegration = ICircleIntegration__factory.connect( + readCircleIntegrationProxyAddress(ETH_FORK_CHAIN_ID), + ethWallet + ); + const ethUsdc = IUSDC__factory.connect(ETH_USDC_TOKEN_ADDRESS, ethWallet); - const wormhole = IWormhole__factory.connect(WORMHOLE_ADDRESS, provider); - const circleIntegration = ICircleIntegration__factory.connect( - readCircleIntegrationProxyAddress(5), - wallet + // avalanche wallet, CircleIntegration contract and USDC contract + const avaxProvider = new ethers.providers.StaticJsonRpcProvider( + AVAX_LOCALHOST + ); + const avaxWallet = new ethers.Wallet(WALLET_PRIVATE_KEY, avaxProvider); + const avaxCircleIntegration = ICircleIntegration__factory.connect( + readCircleIntegrationProxyAddress(AVAX_FORK_CHAIN_ID), + avaxWallet + ); + const avaxUsdc = IUSDC__factory.connect(AVAX_USDC_TOKEN_ADDRESS, avaxWallet); + + // mock integration contract on avax + const avaxMockIntegration = IMockIntegration__factory.connect( + readMockIntegrationAddress(AVAX_FORK_CHAIN_ID), + avaxWallet ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, [ + // MockGuardians and MockCircleAttester objects + const guardians = new MockGuardians(WORMHOLE_GUARDIAN_SET_INDEX, [ GUARDIAN_PRIVATE_KEY, ]); + const circleAttester = new MockCircleAttester(GUARDIAN_PRIVATE_KEY); - const foreignCircleIntegration = new MockCircleIntegration( - circleIntegration.address, - 6, // chainId - 1, // domain - circleIntegration - ); - - describe("Circle Integration Registrations", () => { + describe("Registrations", () => { + // produces governance VAAs for CircleAttestation contract const governance = new CircleGovernanceEmitter(); - it("Register Foreign Circle Integration", async () => { - const timestamp = await getBlockTimestamp(provider); - const chainId = await circleIntegration.chainId(); - - const published = - governance.publishCircleIntegrationRegisterEmitterAndDomain( - timestamp, - chainId, - foreignCircleIntegration.chain, - foreignCircleIntegration.address, - foreignCircleIntegration.domain + describe("Ethereum Goerli Testnet", () => { + it("Should Register Foreign Circle Integration", async () => { + const timestamp = getTimeNow(); + const chainId = await ethCircleIntegration.chainId(); + const emitterChain = await avaxCircleIntegration.chainId(); + const emitterAddress = Buffer.from( + tryNativeToUint8Array(avaxCircleIntegration.address, "avalanche") ); - const signedMessage = guardians.addSignatures(published, [0]); + const domain = await avaxCircleIntegration.localDomain(); - const receipt = await circleIntegration - .registerEmitterAndDomain(signedMessage) - .then((tx) => tx.wait()) + // create unsigned registerEmitterAndDomain governance message + const published = + governance.publishCircleIntegrationRegisterEmitterAndDomain( + timestamp, + chainId, + emitterChain, + emitterAddress, + domain + ); + + // sign governance message with guardian key + const signedMessage = guardians.addSignatures(published, [0]); + + // register the emitter and domain + const receipt = await ethCircleIntegration + .registerEmitterAndDomain(signedMessage) + .then((tx) => tx.wait()) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check contract state to verify the registration + const registeredEmitter = await ethCircleIntegration + .getRegisteredEmitter(emitterChain) + .then((bytes) => Buffer.from(ethers.utils.arrayify(bytes))); + expect(Buffer.compare(registeredEmitter, emitterAddress)).to.equal(0); + }); + + it("Should Register Accepted Token", async () => { + const timestamp = getTimeNow(); + const chainId = await ethCircleIntegration.chainId(); + + // create unsigned registerAcceptedToken governance message + const published = + governance.publishCircleIntegrationRegisterAcceptedToken( + timestamp, + chainId, + ETH_USDC_TOKEN_ADDRESS + ); + + // sign governance message with guardian key + const signedMessage = guardians.addSignatures(published, [0]); + + // register the token + const receipt = await ethCircleIntegration + .registerAcceptedToken(signedMessage) + .then((tx) => tx.wait()) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check contract state to verify the registration + const accepted = await ethCircleIntegration.isAcceptedToken( + ETH_USDC_TOKEN_ADDRESS + ); + expect(accepted).is.true; + }); + + it("Should Register Target Chain Token", async () => { + const timestamp = getTimeNow(); + const chainId = await ethCircleIntegration.chainId(); + const targetChain = await avaxCircleIntegration.chainId(); + const targetToken = Buffer.from( + tryNativeToUint8Array(AVAX_USDC_TOKEN_ADDRESS, "avalanche") + ); + + // create unsigned registerTargetChainToken governance message + const published = + governance.publishCircleIntegrationRegisterTargetChainToken( + timestamp, + chainId, + ETH_USDC_TOKEN_ADDRESS, + targetChain, + targetToken + ); + + // sign governance message with guardian key + const signedMessage = guardians.addSignatures(published, [0]); + + // register the target token + const receipt = await ethCircleIntegration + .registerTargetChainToken(signedMessage) + .then((tx) => tx.wait()) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check contract state to verify the registration + const registeredTargetToken = await ethCircleIntegration + .targetAcceptedToken(ETH_USDC_TOKEN_ADDRESS, targetChain) + .then((bytes) => Buffer.from(ethers.utils.arrayify(bytes))); + expect(Buffer.compare(registeredTargetToken, targetToken)).to.equal(0); + }); + }); + + describe("Avalanche Fuji Testnet", () => { + it("Should Register Foreign Circle Integration", async () => { + const timestamp = getTimeNow(); + const chainId = await avaxCircleIntegration.chainId(); + const emitterChain = await ethCircleIntegration.chainId(); + const emitterAddress = Buffer.from( + tryNativeToUint8Array(ethCircleIntegration.address, "avalanche") + ); + const domain = await ethCircleIntegration.localDomain(); + + // create unsigned registerEmitterAndDomain governance message + const published = + governance.publishCircleIntegrationRegisterEmitterAndDomain( + timestamp, + chainId, + emitterChain, + emitterAddress, + domain + ); + const signedMessage = guardians.addSignatures(published, [0]); + + // sign governance message with guardian key + const receipt = await avaxCircleIntegration + .registerEmitterAndDomain(signedMessage) + .then((tx) => tx.wait()) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check contract state to verify the registration + const registeredEmitter = await avaxCircleIntegration + .getRegisteredEmitter(emitterChain) + .then((bytes) => Buffer.from(ethers.utils.arrayify(bytes))); + expect(Buffer.compare(registeredEmitter, emitterAddress)).to.equal(0); + }); + + it("Should Register Accepted Token", async () => { + const timestamp = getTimeNow(); + const chainId = await avaxCircleIntegration.chainId(); + + // create unsigned registerAcceptedToken governance message + const published = + governance.publishCircleIntegrationRegisterAcceptedToken( + timestamp, + chainId, + AVAX_USDC_TOKEN_ADDRESS + ); + + // sign governance message with guardian key + const signedMessage = guardians.addSignatures(published, [0]); + + // register the token + const receipt = await avaxCircleIntegration + .registerAcceptedToken(signedMessage) + .then((tx) => tx.wait()) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check contract state to verify the registration + const accepted = await avaxCircleIntegration.isAcceptedToken( + AVAX_USDC_TOKEN_ADDRESS + ); + expect(accepted).is.true; + }); + + it("Should Register Target Chain Token", async () => { + const timestamp = getTimeNow(); + const chainId = await avaxCircleIntegration.chainId(); + const targetChain = await ethCircleIntegration.chainId(); + const targetToken = Buffer.from( + tryNativeToUint8Array(ETH_USDC_TOKEN_ADDRESS, "avalanche") + ); + + // create unsigned registerTargetChainToken governance message + const published = + governance.publishCircleIntegrationRegisterTargetChainToken( + timestamp, + chainId, + AVAX_USDC_TOKEN_ADDRESS, + targetChain, + targetToken + ); + + // sign governance message with guardian key + const signedMessage = guardians.addSignatures(published, [0]); + + // register the target token + const receipt = await avaxCircleIntegration + .registerTargetChainToken(signedMessage) + .then((tx) => tx.wait()) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + /// check contract state to verify the registration + const registeredTargetToken = await avaxCircleIntegration + .targetAcceptedToken(AVAX_USDC_TOKEN_ADDRESS, targetChain) + .then((bytes) => Buffer.from(ethers.utils.arrayify(bytes))); + expect(Buffer.compare(registeredTargetToken, targetToken)).to.equal(0); + }); + }); + }); + + describe("Transfer With Payload Logic", () => { + const amountFromEth = ethers.BigNumber.from("69"); + const amountFromAvax = ethers.BigNumber.from("420"); + + let localVariables: any = {}; + + it("Should Transfer Tokens With Payload On Ethereum", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: ETH_USDC_TOKEN_ADDRESS, + amount: amountFromEth, + targetChain: CHAIN_ID_AVAX as number, + mintRecipient: tryNativeToUint8Array(avaxWallet.address, "avalanche"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("All your base are belong to us."); + + // increase allowance + { + const receipt = await ethUsdc + .approve(ethCircleIntegration.address, amountFromEth) + .then((tx) => tx.wait()); + } + + // grab USDC balance before performing the transfer + const balanceBefore = await ethUsdc.balanceOf(ethWallet.address); + + // call transferTokensWithPayload + const receipt = await ethCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) .catch((msg) => { // should not happen - console.log(msg.error.reason); + console.log(msg); return null; }); expect(receipt).is.not.null; - // check state - const registeredEmitter = await circleIntegration - .getRegisteredEmitter(foreignCircleIntegration.chain) - .then((bytes) => Buffer.from(ethers.utils.arrayify(bytes))); - expect( - Buffer.compare(registeredEmitter, foreignCircleIntegration.address) - ).to.equal(0); + // check USDC balance after to confirm the transfer worked + const balanceAfter = await ethUsdc.balanceOf(ethWallet.address); + expect(balanceBefore.sub(balanceAfter).eq(amountFromEth)).is.true; + + // grab Circle message from logs + const circleMessage = await ethCircleIntegration + .circleTransmitter() + .then((address) => findCircleMessageInLogs(receipt!.logs, address)); + expect(circleMessage).is.not.null; + + // grab attestation + const circleAttestation = circleAttester.attestMessage( + ethers.utils.arrayify(circleMessage!) + ); + + // now grab the Wormhole message + const wormholeMessage = await ethCircleIntegration + .wormhole() + .then((address) => + findWormholeMessageInLogs( + receipt!.logs, + address, + CHAIN_ID_ETH as number + ) + ); + expect(wormholeMessage).is.not.null; + + // sign the DepositWithPayload message + const encodedWormholeMessage = Uint8Array.from( + guardians.addSignatures(wormholeMessage!, [0]) + ); + + // save all of the redeem parameters + localVariables.circleBridgeMessage = circleMessage!; + localVariables.circleAttestation = circleAttestation!; + localVariables.encodedWormholeMessage = encodedWormholeMessage; }); - it("Register Accepted Token", async () => { - const timestamp = await getBlockTimestamp(provider); - const chainId = await circleIntegration.chainId(); + it("Should Redeem Tokens With Payload On Avax", async () => { + // create RedeemParameters struct to invoke the target contract with + const redeemParameters: RedeemParameters = { + circleBridgeMessage: localVariables.circleBridgeMessage!, + circleAttestation: localVariables.circleAttestation!, + encodedWormholeMessage: localVariables.encodedWormholeMessage!, + }; + // clear the localVariables object + localVariables = {}; + + // grab the balance before redeeming the transfer + const balanceBefore = await avaxUsdc.balanceOf(avaxWallet.address); + + // redeem the transfer + const receipt = await avaxCircleIntegration + .redeemTokensWithPayload(redeemParameters) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // confirm expected balance change + const balanceAfter = await avaxUsdc.balanceOf(avaxWallet.address); + expect(balanceAfter.sub(balanceBefore).eq(amountFromEth)).is.true; + }); + + it("Should Transfer Tokens With Payload On Avax", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: AVAX_USDC_TOKEN_ADDRESS, + amount: amountFromAvax, + targetChain: CHAIN_ID_ETH as number, + mintRecipient: tryNativeToUint8Array(avaxWallet.address, "ethereum"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Send me back to Ethereum!"); + + // increase allowance + { + const receipt = await avaxUsdc + .approve(avaxCircleIntegration.address, amountFromAvax) + .then((tx) => tx.wait()); + } + + // grab USDC balance before performing the transfer + const balanceBefore = await avaxUsdc.balanceOf(avaxWallet.address); + + // call transferTokensWithPayload + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check USDC balance after to confirm the transfer worked + const balanceAfter = await avaxUsdc.balanceOf(avaxWallet.address); + expect(balanceBefore.sub(balanceAfter).eq(amountFromAvax)).is.true; + + // grab Circle message from logs + const circleMessage = await avaxCircleIntegration + .circleTransmitter() + .then((address) => findCircleMessageInLogs(receipt!.logs, address)); + expect(circleMessage).is.not.null; + + // grab attestation + const circleAttestation = circleAttester.attestMessage( + ethers.utils.arrayify(circleMessage!) + ); + + // now grab the Wormhole message + const wormholeMessage = await avaxCircleIntegration + .wormhole() + .then((address) => + findWormholeMessageInLogs( + receipt!.logs, + address, + CHAIN_ID_AVAX as number + ) + ); + expect(wormholeMessage).is.not.null; + + // sign the Wormhole message + const encodedWormholeMessage = Uint8Array.from( + guardians.addSignatures(wormholeMessage!, [0]) + ); + + // save all of the redeem parameters + localVariables.circleBridgeMessage = circleMessage!; + localVariables.circleAttestation = circleAttestation!; + localVariables.encodedWormholeMessage = encodedWormholeMessage; + }); + + it("Should Redeem Tokens With Payload On Ethereum", async () => { + // create RedeemParameters struct to invoke the target contract with + const redeemParameters: RedeemParameters = { + circleBridgeMessage: localVariables.circleBridgeMessage!, + circleAttestation: localVariables.circleAttestation!, + encodedWormholeMessage: localVariables.encodedWormholeMessage!, + }; + + // NOTICE: don't clear the localVariables object, the values are used in the next test + + // grab the balance before redeeming the transfer + const balanceBefore = await ethUsdc.balanceOf(ethWallet.address); + + // redeem the transfer + const receipt = await ethCircleIntegration + .redeemTokensWithPayload(redeemParameters) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // confirm expected balance change + const balanceAfter = await ethUsdc.balanceOf(ethWallet.address); + expect(balanceAfter.sub(balanceBefore).eq(amountFromAvax)).is.true; + }); + + it("Should Not Redeem a Transfer More Than Once", async () => { + // Reuse the RedeemParameters from the previous test to try to redeem again + const redeemParameters: RedeemParameters = { + circleBridgeMessage: localVariables.circleBridgeMessage!, + circleAttestation: localVariables.circleAttestation!, + encodedWormholeMessage: localVariables.encodedWormholeMessage!, + }; + + // clear the localVariables object + localVariables = {}; + + // grab the balance before redeeming the transfer + const balanceBefore = await ethUsdc.balanceOf(ethWallet.address); + + // try to redeem the transfer again + let failed: boolean = false; + try { + const receipt = await ethCircleIntegration + .redeemTokensWithPayload(redeemParameters) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect(e.error.reason, "execution reverted: message already consumed") + .to.be.equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; + + // confirm expected balance change + const balanceAfter = await ethUsdc.balanceOf(ethWallet.address); + expect(balanceAfter.eq(balanceBefore)).is.true; + }); + + it("Should Not Allow Transfers for Zero Amount", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: avaxWallet.address, + amount: ethers.BigNumber.from("0"), // zero amount + targetChain: CHAIN_ID_ETH as number, + mintRecipient: tryNativeToUint8Array(ethWallet.address, "ethereum"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Send zero tokens :)"); + + // try to initiate a transfer with an amount of zero + let failed: boolean = false; + try { + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect(e.error.reason, "execution reverted: amount must be > 0").to.be + .equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; + }); + + it("Should Not Allow Transfers to the Zero Address", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: avaxWallet.address, + amount: amountFromAvax, + targetChain: CHAIN_ID_ETH as number, + mintRecipient: tryNativeToUint8Array("0x", "ethereum"), // zero address + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Sending to bytes32(0) mintRecipient :)"); + + // try to initiate a transfer to the zero address + let failed: boolean = false; + try { + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect(e.error.reason, "execution reverted: invalid mint recipient").to + .be.equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; + }); + + it("Should Not Allow Transfers for Unregistered Tokens", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: avaxWallet.address, // unregistered "token" + amount: amountFromAvax, + targetChain: CHAIN_ID_ETH as number, + mintRecipient: tryNativeToUint8Array(ethWallet.address, "ethereum"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Sending an unregistered token :)"); + + // try to initiate a transfer for an unregistered token + let failed: boolean = false; + try { + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect(e.error.reason, "execution reverted: token not accepted").to.be + .equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; + }); + + it("Should Not Allow Transfers to Unregistered Contracts", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: avaxWallet.address, + amount: amountFromAvax, + targetChain: CHAIN_ID_ALGORAND as number, // unregistered chain + mintRecipient: tryNativeToUint8Array(ethWallet.address, "ethereum"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Sending to an unregistered chain :)"); + + // try to initiate a transfer to an unregistered CircleIntegration contract + let failed: boolean = false; + try { + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect( + e.error.reason, + "execution reverted: target contract not registered" + ).to.be.equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; + }); + + it("Should Not Allow Transfers for Unregistered Target Tokens", async () => { + // initialize governance module + const governance = new CircleGovernanceEmitter(); + + // store EUROC address + const eurocAddress = "0x53d80871b92dadeD34A4BdFA6838DdFC7f214240"; + const timestamp = getTimeNow(); + const chainId = await avaxCircleIntegration.chainId(); + + // publish an unsigned registerAcceptedToken governance message const published = governance.publishCircleIntegrationRegisterAcceptedToken( timestamp, chainId, - ETH_USDC_TOKEN_ADDRESS + eurocAddress ); + + // sign the governance message with the guardian key const signedMessage = guardians.addSignatures(published, [0]); - const receipt = await circleIntegration + // register the token + const receipt = await avaxCircleIntegration .registerAcceptedToken(signedMessage) .then((tx) => tx.wait()) .catch((msg) => { // should not happen - console.log(msg.error.reason); + console.log(msg); return null; }); expect(receipt).is.not.null; - // check state - const accepted = await circleIntegration.isAcceptedToken( - ETH_USDC_TOKEN_ADDRESS + // check state to confirm the token was registered + const accepted = await avaxCircleIntegration.isAcceptedToken( + eurocAddress ); expect(accepted).is.true; + + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: eurocAddress, + amount: amountFromAvax, + targetChain: CHAIN_ID_ALGORAND as number, // unregistered chain + mintRecipient: tryNativeToUint8Array(ethWallet.address, "ethereum"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Sending an unregistered target token :)"); + + // try to transfer with an unregistered target token + let failed: boolean = false; + try { + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect( + e.error.reason, + "execution reverted: target token not registered" + ).to.be.equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; }); - it("Register Target Chain Token", async () => { - const timestamp = await getBlockTimestamp(provider); - const chainId = await circleIntegration.chainId(); - const targetToken = Buffer.from( - tryNativeToUint8Array(AVAX_USDC_TOKEN_ADDRESS, "avalanche") - ); + it("Should Only Mint Tokens to the Mint Recipient", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: AVAX_USDC_TOKEN_ADDRESS, + amount: amountFromAvax, + targetChain: CHAIN_ID_ETH as number, + mintRecipient: tryNativeToUint8Array(ethWallet.address, "ethereum"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Send me back to Ethereum!"); - const published = - governance.publishCircleIntegrationRegisterTargetChainToken( - timestamp, - chainId, - ETH_USDC_TOKEN_ADDRESS, - foreignCircleIntegration.chain, - targetToken + // increase allowance + const receipt = await avaxUsdc + .approve(avaxCircleIntegration.address, amountFromAvax) + .then((tx) => tx.wait()); + + // call transfer with payload and save redeemParameters struct + let redeemParameters = {} as RedeemParameters; + { + // grab USDC balance before performing the transfer + const balanceBefore = await avaxUsdc.balanceOf(avaxWallet.address); + + // call transferTokensWithPayload + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check USDC balance after to confirm the transfer worked + const balanceAfter = await avaxUsdc.balanceOf(avaxWallet.address); + expect(balanceBefore.sub(balanceAfter).eq(amountFromAvax)).is.true; + + // grab Circle message from logs + const circleMessage = await avaxCircleIntegration + .circleTransmitter() + .then((address) => findCircleMessageInLogs(receipt!.logs, address)); + expect(circleMessage).is.not.null; + + // grab attestation + const circleAttestation = circleAttester.attestMessage( + ethers.utils.arrayify(circleMessage!) ); - const signedMessage = guardians.addSignatures(published, [0]); - const receipt = await circleIntegration - .registerTargetChainToken(signedMessage) + // now grab the Wormhole Message + const wormholeMessage = await avaxCircleIntegration + .wormhole() + .then((address) => + findWormholeMessageInLogs( + receipt!.logs, + address, + CHAIN_ID_AVAX as number + ) + ); + expect(wormholeMessage).is.not.null; + + // sign the wormhole message with the guardian key + const encodedWormholeMessage = Uint8Array.from( + guardians.addSignatures(wormholeMessage!, [0]) + ); + + // save redeemParameters struct + redeemParameters = { + circleBridgeMessage: ethers.utils.arrayify(circleMessage!), + circleAttestation: circleAttestation!, + encodedWormholeMessage: encodedWormholeMessage!, + }; + } + + // try to redeem the transfer from a different wallet + { + // create wallet with different private key + const invalidEthWallet = new ethers.Wallet( + WALLET_PRIVATE_KEY_TWO, + ethProvider + ); + + // connect to contract with new wallet for redemption + const ethCircleIntegration = ICircleIntegration__factory.connect( + readCircleIntegrationProxyAddress(ETH_FORK_CHAIN_ID), + invalidEthWallet + ); + + let failed: boolean = false; + try { + // call redeemTokensWithPayload + const receipt = await ethCircleIntegration + .redeemTokensWithPayload(redeemParameters) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect( + e.error.reason, + "execution reverted: caller must be mintRecipient" + ).to.be.equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; + } + + // clear the localVariables object + localVariables = {}; + }); + + it("Should Not Redeem Tokens With a Bad Message Pair", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: AVAX_USDC_TOKEN_ADDRESS, + amount: amountFromAvax, + targetChain: CHAIN_ID_ETH as number, + mintRecipient: tryNativeToUint8Array(ethWallet.address, "ethereum"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Scrambled Messageggs!"); + + // increase the token allowance by 2x, since we will do two transfers + const receipt = await avaxUsdc + .approve(avaxCircleIntegration.address, amountFromAvax.mul(2)) + .then((tx) => tx.wait()); + + // send the same transfer twice and save the redeemParameters + let redeemParameters = {} as RedeemParameters[]; + { + for (let i = 0; i < 2; i++) { + // grab USDC balance before performing the transfer + const balanceBefore = await avaxUsdc.balanceOf(avaxWallet.address); + + // call transferTokensWithPayload + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check USDC balance after to confirm the transfer worked + const balanceAfter = await avaxUsdc.balanceOf(avaxWallet.address); + expect(balanceBefore.sub(balanceAfter).eq(amountFromAvax)).is.true; + + // grab Circle message from logs + const circleMessage = await avaxCircleIntegration + .circleTransmitter() + .then((address) => findCircleMessageInLogs(receipt!.logs, address)); + expect(circleMessage).is.not.null; + + // grab attestation + const circleAttestation = circleAttester.attestMessage( + ethers.utils.arrayify(circleMessage!) + ); + + // now grab the Wormhole Message + const wormholeMessage = await avaxCircleIntegration + .wormhole() + .then((address) => + findWormholeMessageInLogs( + receipt!.logs, + address, + CHAIN_ID_AVAX as number + ) + ); + expect(wormholeMessage).is.not.null; + + // sign the wormhole message with the guardian key + const encodedWormholeMessage = Uint8Array.from( + guardians.addSignatures(wormholeMessage!, [0]) + ); + + // save redeemParameters struct + redeemParameters[i] = { + circleBridgeMessage: ethers.utils.arrayify(circleMessage!), + circleAttestation: circleAttestation!, + encodedWormholeMessage: encodedWormholeMessage!, + }; + } + } + + // Create new redeemParameters with an invalid message pair, by + // pairing the Wormhole message from the second transfer with + // the Circle message and attestation from the first transfer. + const invalidRedeemParameters: RedeemParameters = { + circleBridgeMessage: redeemParameters[0].circleBridgeMessage, + circleAttestation: redeemParameters[0].circleAttestation, + encodedWormholeMessage: redeemParameters[1].encodedWormholeMessage, + }; + + { + let failed: boolean = false; + try { + // call redeemTokensWithPayload + const receipt = await ethCircleIntegration + .redeemTokensWithPayload(invalidRedeemParameters) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect(e.error.reason, "execution reverted: invalid message pair").to + .be.equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; + } + + // clear the localVariables object + localVariables = {}; + }); + + it("Should Revert if Circle Receiver Call Fails", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: AVAX_USDC_TOKEN_ADDRESS, + amount: amountFromAvax, + targetChain: CHAIN_ID_ETH as number, + mintRecipient: tryNativeToUint8Array(ethWallet.address, "ethereum"), + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("To the moon!"); + + // increase allowance + const receipt = await avaxUsdc + .approve(avaxCircleIntegration.address, amountFromAvax) + .then((tx) => tx.wait()); + + // call transfer with payload and save redeemParameters struct + let redeemParameters = {} as RedeemParameters; + { + // grab USDC balance before performing the transfer + const balanceBefore = await avaxUsdc.balanceOf(avaxWallet.address); + + // call transferTokensWithPayload + const receipt = await avaxCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check USDC balance after to confirm the transfer worked + const balanceAfter = await avaxUsdc.balanceOf(avaxWallet.address); + expect(balanceBefore.sub(balanceAfter).eq(amountFromAvax)).is.true; + + // grab Circle message from logs + const circleMessage = await avaxCircleIntegration + .circleTransmitter() + .then((address) => findCircleMessageInLogs(receipt!.logs, address)); + expect(circleMessage).is.not.null; + + // now grab the Wormhole Message + const wormholeMessage = await avaxCircleIntegration + .wormhole() + .then((address) => + findWormholeMessageInLogs( + receipt!.logs, + address, + CHAIN_ID_AVAX as number + ) + ); + expect(wormholeMessage).is.not.null; + + // sign the wormhole message with the guardian key + const encodedWormholeMessage = Uint8Array.from( + guardians.addSignatures(wormholeMessage!, [0]) + ); + + // save redeemParameters struct + redeemParameters = { + circleBridgeMessage: ethers.utils.arrayify(circleMessage!), + circleAttestation: ethers.utils.arrayify("0x"), + encodedWormholeMessage: encodedWormholeMessage!, + }; + } + + // try to redeem the transfer from a different wallet + { + let failed: boolean = false; + try { + // call redeemTokensWithPayload + const receipt = await ethCircleIntegration + .redeemTokensWithPayload(redeemParameters) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }); + } catch (e: any) { + expect( + e.error.reason, + "execution reverted: CIRCLE_INTEGRATION: failed to mint tokens" + ).to.be.equal; + failed = true; + } + + // confirm that the call failed + expect(failed).is.true; + } + }); + }); + + describe("Mock Integration Contract", () => { + const amountFromEth = ethers.BigNumber.from("42069"); + + // create new avax wallet for mock integration contract interaction + const avaxMockWallet = new ethers.Wallet( + WALLET_PRIVATE_KEY_TWO, + avaxProvider + ); + + let localVariables: any = {}; + + it("Should Set Up Mock Integration Contract on Avax", async () => { + // call the `setup` method on the MockIntegration contract + const receipt = await avaxMockIntegration + .setup(avaxCircleIntegration.address, ethWallet.address, CHAIN_ID_ETH) .then((tx) => tx.wait()) .catch((msg) => { // should not happen - console.log(msg.error.reason); + console.log(msg); return null; }); expect(receipt).is.not.null; - // check state - const registeredTargetToken = await circleIntegration - .targetAcceptedToken( - ETH_USDC_TOKEN_ADDRESS, - foreignCircleIntegration.chain - ) - .then((bytes) => Buffer.from(ethers.utils.arrayify(bytes))); - expect(Buffer.compare(registeredTargetToken, targetToken)).to.equal(0); - }); - }); + // confirm that the contract is set up correctly by querying the getters + const trustedChainId = await avaxMockIntegration.trustedChainId(); + expect(trustedChainId).to.equal(CHAIN_ID_ETH); - describe("ETH -> AVAX", () => { - it("transferWithPayload", async () => { - // TODO - }); - }); + const trustedSender = await avaxMockIntegration.trustedSender(); + expect(trustedSender).to.equal(ethWallet.address); - describe("AVAX -> ETH", () => { - it("redeemWithPayload", async () => { - // TODO + const circleIntegration = await avaxMockIntegration.circleIntegration(); + expect(circleIntegration).to.equal(avaxCircleIntegration.address); + }); + + it("Should Transfer Tokens With Payload On Ethereum", async () => { + // define transferTokensWithPayload function arguments + const params: TransferParameters = { + token: ETH_USDC_TOKEN_ADDRESS, + amount: amountFromEth, + targetChain: CHAIN_ID_AVAX as number, + mintRecipient: tryNativeToUint8Array( + avaxMockIntegration.address, + "avalanche" + ), // set mint recipient as the avax mock integration contract + }; + const batchId = 0; // opt out of batching + const payload = Buffer.from("Coming to a mock contract near you."); + + // increase allowance + { + const receipt = await ethUsdc + .approve(ethCircleIntegration.address, amountFromEth) + .then((tx) => tx.wait()); + } + + // grab USDC balance before performing the transfer + const balanceBefore = await ethUsdc.balanceOf(ethWallet.address); + + // call transferTokensWithPayload + const receipt = await ethCircleIntegration + .transferTokensWithPayload(params, batchId, payload) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // check USDC balance after to confirm the transfer worked + const balanceAfter = await ethUsdc.balanceOf(ethWallet.address); + expect(balanceBefore.sub(balanceAfter).eq(amountFromEth)).is.true; + + // grab Circle message from logs + const circleMessage = await ethCircleIntegration + .circleTransmitter() + .then((address) => findCircleMessageInLogs(receipt!.logs, address)); + expect(circleMessage).is.not.null; + + // grab attestation + const circleAttestation = circleAttester.attestMessage( + ethers.utils.arrayify(circleMessage!) + ); + + // now grab the Wormhole Message + const wormholeMessage = await ethCircleIntegration + .wormhole() + .then((address) => + findWormholeMessageInLogs( + receipt!.logs, + address, + CHAIN_ID_ETH as number + ) + ); + expect(wormholeMessage).is.not.null; + + // sign the wormhole message with the guardian key + const encodedWormholeMessage = Uint8Array.from( + guardians.addSignatures(wormholeMessage!, [0]) + ); + + // save redeem parameters and custom payload + localVariables.circleBridgeMessage = circleMessage!; + localVariables.circleAttestation = circleAttestation!; + localVariables.encodedWormholeMessage = encodedWormholeMessage; + localVariables.payload = ethers.utils.hexlify(payload); + }); + + it("Should Redeem Tokens Via Mock Integration Contract on Avax and Verify the Saved Payload", async () => { + // create RedeemParameters struct to invoke the target contract with + const redeemParameters: RedeemParameters = { + circleBridgeMessage: localVariables.circleBridgeMessage!, + circleAttestation: localVariables.circleAttestation!, + encodedWormholeMessage: localVariables.encodedWormholeMessage!, + }; + + // grab USDC balance before redeeming the token transfer + const balanceBefore = await avaxUsdc.balanceOf(avaxMockWallet.address); + + // Invoke the mock contract with the trusted sender wallet, + // which shares address with eth wallet. + const receipt = await avaxMockIntegration + .redeemTokensWithPayload(redeemParameters, avaxMockWallet.address) + .then(async (tx) => { + const receipt = await tx.wait(); + return receipt; + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(receipt).is.not.null; + + // confirm the expected balance change for the mock avax wallet + const balanceAfter = await avaxUsdc.balanceOf(avaxMockWallet.address); + expect(balanceAfter.sub(balanceBefore).eq(amountFromEth)).is.true; + + // query the mock contract and confirm that the payload was saved correctly + const savedPayload = await await avaxMockIntegration + .redemptionSequence() + .then(async (sequence) => { + return await avaxMockIntegration.getPayload(sequence); + }) + .catch((msg) => { + // should not happen + console.log(msg); + return null; + }); + expect(savedPayload).is.equal(localVariables.payload); + + // clear the localVariables object + localVariables = {}; }); }); }); - -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; -} diff --git a/evm/ts/test/helpers/consts.ts b/evm/ts/test/helpers/consts.ts index 29b89e3..d03575b 100644 --- a/evm/ts/test/helpers/consts.ts +++ b/evm/ts/test/helpers/consts.ts @@ -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!; diff --git a/evm/ts/test/helpers/mock.ts b/evm/ts/test/helpers/mock.ts index 7e317ee..e1c2476 100644 --- a/evm/ts/test/helpers/mock.ts +++ b/evm/ts/test/helpers/mock.ts @@ -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, diff --git a/evm/ts/test/helpers/utils.ts b/evm/ts/test/helpers/utils.ts index 2b2075e..dd538ac 100644 --- a/evm/ts/test/helpers/utils.ts +++ b/evm/ts/test/helpers/utils.ts @@ -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); + } }