diff --git a/audits/internal9/README.md b/audits/internal9/README.md index 6c65e94..ad8e4e0 100644 --- a/audits/internal9/README.md +++ b/audits/internal9/README.md @@ -20,6 +20,7 @@ File | % Stmts | % Branch | % Funcs | % Lines |Uncovere WormholeMessenger.sol | 0 | 0 | 0 | 0 |... 201,202,207 | ``` Pay attention, please! No tests for contract on scope. +[x] Fixed #### Wormhole security list https://docs.wormhole.com/wormhole/quick-start/cross-chain-dev/standard-relayer#other-considerations @@ -35,6 +36,7 @@ Refunding overpayment of gasLimit - [?] - requires discussion Refunding overpayment of value sent - [?] - requires discussion ref: https://github.com/wormhole-foundation/hello-wormhole/blob/main/src/extensions/HelloWormholeRefunds.sol ``` +[x] Good point, need to set up these parameters and call the corresponding function on the L1 Relayer (added to test scripts) ### Security issues Details in [slither_full](https://github.com/valory-xyz/autonolas-governance/blob/main/audits/internal9/analysis/slither_full.txt)
@@ -56,6 +58,8 @@ so avoid casting bytes32 to address EVM. } ref: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/Base.sol#L30C52-L30C74 SDK of Wormhole ``` +[x] Fixed + ##### Re-design contracts Part of the code is common and has the nature of a repeatable pattern for all contracts.
It makes sense to separate it into a separate contract and replace magic numbers (20,12,4) with constants. @@ -109,6 +113,7 @@ It makes sense to separate it into a separate contract and replace magic numbers } } ``` +[x] Fixed diff --git a/contracts/bridges/BridgeMessenger.sol b/contracts/bridges/BridgeMessenger.sol index 55d8f0d..469a6b8 100644 --- a/contracts/bridges/BridgeMessenger.sol +++ b/contracts/bridges/BridgeMessenger.sol @@ -9,39 +9,16 @@ import {IBridgeErrors} from "../interfaces/IBridgeErrors.sol"; /// @author Mariapia Moscatiello - abstract contract BridgeMessenger is IBridgeErrors { event FundsReceived(address indexed sender, uint256 value); - event SourceGovernorUpdated(address indexed sourceGovernor); // Default payload data length includes the number of bytes of at least one address (20 bytes or 160 bits), // value (12 bytes or 96 bits) and the payload size (4 bytes or 32 bits) uint256 public constant DEFAULT_DATA_LENGTH = 36; - // Source governor address on L1 that is authorized to propagate the transaction execution across the bridge - address public sourceGovernor; /// @dev Receives native network token. receive() external payable { emit FundsReceived(msg.sender, msg.value); } - /// @dev Changes the source governor address (original Timelock). - /// @notice The only way to change the source governor address is by the Timelock on L1 to request that change. - /// This triggers a self-contract transaction of BridgeMessenger that changes the source governor address. - /// @param newSourceGovernor New source governor address. - function changeSourceGovernor(address newSourceGovernor) external virtual { - // Check if the change is authorized by the previous governor itself - // This is possible only if all the checks in the message process function pass and the contract calls itself - if (msg.sender != address(this)) { - revert SelfCallOnly(msg.sender, address(this)); - } - - // Check for the zero address - if (newSourceGovernor == address(0)) { - revert ZeroAddress(); - } - - sourceGovernor = newSourceGovernor; - emit SourceGovernorUpdated(newSourceGovernor); - } - /// @dev Processes received data. /// @param data Bytes message sent from L2 Wormhole Relayer contract. The data must be encoded as a set of /// continuous transactions packed into a single buffer, where each transaction is composed as follows: diff --git a/contracts/bridges/OptimismMessenger.sol b/contracts/bridges/OptimismMessenger.sol index 9fd2ec3..3bc1292 100644 --- a/contracts/bridges/OptimismMessenger.sol +++ b/contracts/bridges/OptimismMessenger.sol @@ -13,10 +13,13 @@ interface ICrossDomainMessenger { /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - contract OptimismMessenger is BridgeMessenger { + event SourceGovernorUpdated(address indexed sourceGovernor); event MessageReceived(address indexed sourceMessageSender, bytes data); // CDM Contract Proxy (Home) address on L2 that receives the message across the bridge from the source L1 network address public immutable CDMContractProxyHome; + // Source governor address on L1 that is authorized to propagate the transaction execution across the bridge + address public sourceGovernor; /// @dev OptimismMessenger constructor. /// @param _CDMContractProxyHome CDM Contract Proxy (Home) address (Optimism). @@ -31,6 +34,26 @@ contract OptimismMessenger is BridgeMessenger { sourceGovernor = _sourceGovernor; } + /// @dev Changes the source governor address (original Timelock). + /// @notice The only way to change the source governor address is by the Timelock on L1 to request that change. + /// This triggers a self-contract transaction of BridgeMessenger that changes the source governor address. + /// @param newSourceGovernor New source governor address. + function changeSourceGovernor(address newSourceGovernor) external virtual { + // Check if the change is authorized by the previous governor itself + // This is possible only if all the checks in the message process function pass and the contract calls itself + if (msg.sender != address(this)) { + revert SelfCallOnly(msg.sender, address(this)); + } + + // Check for the zero address + if (newSourceGovernor == address(0)) { + revert ZeroAddress(); + } + + sourceGovernor = newSourceGovernor; + emit SourceGovernorUpdated(newSourceGovernor); + } + /// @dev Processes a message received from the CDM Contract Proxy (Home) contract. /// @notice The sender must be the Source Governor address (Timelock). /// @param data Bytes message sent from the CDM Contract Proxy (Home) contract. The data must be encoded as a set of diff --git a/contracts/bridges/WormholeMessenger.sol b/contracts/bridges/WormholeMessenger.sol index 1828882..015939f 100644 --- a/contracts/bridges/WormholeMessenger.sol +++ b/contracts/bridges/WormholeMessenger.sol @@ -8,27 +8,30 @@ import {BridgeMessenger} from "./BridgeMessenger.sol"; /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - contract WormholeMessenger is BridgeMessenger { - event MessageReceived(address indexed sourceMessageSender, bytes data, bytes32 deliveryHash, uint256 sourceChain); + event SourceGovernorUpdated(bytes32 indexed sourceGovernor); + event MessageReceived(bytes32 indexed sourceMessageSender, bytes data, bytes32 deliveryHash, uint256 sourceChain); // L2 Wormhole Relayer address that receives the message across the bridge from the source L1 network address public immutable wormholeRelayer; // Source governor chain Id uint16 public immutable sourceGovernorChainId; // Source governor address on L1 that is authorized to propagate the transaction execution across the bridge + bytes32 public sourceGovernor; + // Source governor address on L1 that is authorized to propagate the transaction execution across the bridge mapping(bytes32 => bool) public mapDeliveryHashes; /// @dev WormholeMessenger constructor. /// @param _wormholeRelayer L2 Wormhole Relayer address. /// @param _sourceGovernor Source governor address (ETH). /// @param _sourceGovernorChainId Source governor wormhole format chain Id. - constructor(address _wormholeRelayer, address _sourceGovernor, uint16 _sourceGovernorChainId) { + constructor(address _wormholeRelayer, bytes32 _sourceGovernor, uint16 _sourceGovernorChainId) { // Check for zero addresses - if (_wormholeRelayer == address(0) || _sourceGovernor == address(0)) { + if (_wormholeRelayer == address(0)) { revert ZeroAddress(); } // Check source governor chain Id - if (_sourceGovernorChainId == 0) { + if (_sourceGovernor == 0 || _sourceGovernorChainId == 0) { revert ZeroValue(); } @@ -37,6 +40,26 @@ contract WormholeMessenger is BridgeMessenger { sourceGovernorChainId = _sourceGovernorChainId; } + /// @dev Changes the source governor address (original Timelock). + /// @notice The only way to change the source governor address is by the Timelock on L1 to request that change. + /// This triggers a self-contract transaction of BridgeMessenger that changes the source governor address. + /// @param newSourceGovernor New source governor address. + function changeSourceGovernor(bytes32 newSourceGovernor) external virtual { + // Check if the change is authorized by the previous governor itself + // This is possible only if all the checks in the message process function pass and the contract calls itself + if (msg.sender != address(this)) { + revert SelfCallOnly(msg.sender, address(this)); + } + + // Check for the zero address + if (newSourceGovernor == 0) { + revert ZeroValue(); + } + + sourceGovernor = newSourceGovernor; + emit SourceGovernorUpdated(newSourceGovernor); + } + /// @dev Processes a message received from L2 Wormhole Relayer contract. /// @notice The sender must be the source governor address (Timelock). /// @param data Bytes message sent from L2 Wormhole Relayer contract. The data must be encoded as a set of @@ -66,10 +89,9 @@ contract WormholeMessenger is BridgeMessenger { } // Check for the source governor address - address governor = sourceGovernor; - address bridgeGovernor = address(uint160(uint256(sourceAddress))); - if (bridgeGovernor != governor) { - revert SourceGovernorOnly(bridgeGovernor, governor); + bytes32 governor = sourceGovernor; + if (governor != sourceAddress) { + revert SourceGovernorOnly32(sourceAddress, governor); } // Check the delivery hash uniqueness diff --git a/contracts/interfaces/IBridgeErrors.sol b/contracts/interfaces/IBridgeErrors.sol index 8a3d0e9..113430a 100644 --- a/contracts/interfaces/IBridgeErrors.sol +++ b/contracts/interfaces/IBridgeErrors.sol @@ -29,6 +29,11 @@ interface IBridgeErrors { /// @param sourceGovernor Required source governor address. error SourceGovernorOnly(address sender, address sourceGovernor); + /// @dev Only on behalf of `sourceGovernor` the function is allowed to process the data. + /// @param sender Sender address. + /// @param sourceGovernor Required source governor address. + error SourceGovernorOnly32(bytes32 sender, bytes32 sourceGovernor); + /// @dev The message with a specified hash has already been delivered. /// @param deliveryHash Delivery hash. error AlreadyDelivered(bytes32 deliveryHash); diff --git a/test/OptimismMessenger.js b/test/OptimismMessenger.js new file mode 100644 index 0000000..8147cb7 --- /dev/null +++ b/test/OptimismMessenger.js @@ -0,0 +1,271 @@ +/*global describe, context, beforeEach, it*/ + +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +describe("OptimismMessenger", function () { + let cdmContractProxy; + let optimismMessenger; + let olas; + let signers; + let deployer; + const AddressZero = ethers.constants.AddressZero; + + beforeEach(async function () { + signers = await ethers.getSigners(); + deployer = signers[0]; + + // Deploy the mock of CDMContractProxy contract + const CDMContractProxy = await ethers.getContractFactory("MockL2Relayer"); + cdmContractProxy = await CDMContractProxy.deploy(deployer.address, deployer.address); + await cdmContractProxy.deployed(); + + // The deployer is the analogue of the Timelock on L1 and the FxRoot mock on L1 as well + const OptimismMessenger = await ethers.getContractFactory("OptimismMessenger"); + optimismMessenger = await OptimismMessenger.deploy(cdmContractProxy.address, deployer.address); + await optimismMessenger.deployed(); + + // Change the OptimismMessenger contract address in the CDMContractProxy + await cdmContractProxy.changeBridgeMessenger(optimismMessenger.address); + + // OLAS represents a contract deployed on L2 + const OLAS = await ethers.getContractFactory("OLAS"); + olas = await OLAS.deploy(); + await olas.deployed(); + }); + + context("Initialization", async function () { + it("Deploying with zero addresses", async function () { + const OptimismMessenger = await ethers.getContractFactory("OptimismMessenger"); + await expect( + OptimismMessenger.deploy(AddressZero, AddressZero) + ).to.be.revertedWithCustomError(optimismMessenger, "ZeroAddress"); + + await expect( + OptimismMessenger.deploy(signers[1].address, AddressZero) + ).to.be.revertedWithCustomError(optimismMessenger, "ZeroAddress"); + }); + }); + + context("Process message from source", async function () { + it("Should fail when trying to call from incorrect contract addresses", async function () { + await expect( + optimismMessenger.connect(deployer).processMessageFromSource("0x") + ).to.be.revertedWithCustomError(optimismMessenger, "TargetRelayerOnly"); + + // Simulate incorrect sourceGovernor address + await cdmContractProxy.changeSourceGovernor(AddressZero); + await expect( + cdmContractProxy.processMessageFromSource("0x") + ).to.be.revertedWithCustomError(optimismMessenger, "SourceGovernorOnly"); + }); + + it("Should fail when trying to process message with the incorrect minimal payload length", async function () { + await expect( + cdmContractProxy.processMessageFromSource("0x") + ).to.be.revertedWithCustomError(optimismMessenger, "IncorrectDataLength"); + }); + + it("Should fail when trying to change the source governor from any other contract / EOA", async function () { + await expect( + optimismMessenger.connect(deployer).changeSourceGovernor(signers[1].address) + ).to.be.revertedWithCustomError(optimismMessenger, "SelfCallOnly"); + }); + + it("Should fail when trying to call with the zero address", async function () { + const target = AddressZero; + const value = 0; + const payload = ""; + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32"], + [target, value, payload.length] + ); + + await expect( + cdmContractProxy.processMessageFromSource(data) + ).to.be.revertedWithCustomError(optimismMessenger, "ZeroAddress"); + }); + + it("Should fail when trying to call with the incorrectly provided payload", async function () { + const target = olas.address; + const value = 0; + const rawPayload = "0x" + "abcd".repeat(10); + const payload = ethers.utils.arrayify(rawPayload); + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ); + + await expect( + cdmContractProxy.processMessageFromSource(data) + ).to.be.revertedWithCustomError(optimismMessenger, "TargetExecFailed"); + }); + + it("Unpack the data and call one specified target on the OLAS contract", async function () { + // Minter of OLAS must be the optimismMessenger contract + await olas.connect(deployer).changeMinter(optimismMessenger.address); + + // OLAS contract across the bridge must mint 100 OLAS for the deployer + const amountToMint = 100; + const rawPayload = olas.interface.encodeFunctionData("mint", [deployer.address, amountToMint]); + + // Pack the data into one contiguous buffer + const target = olas.address; + const value = 0; + const payload = ethers.utils.arrayify(rawPayload); + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ); + + // Execute the unpacked transaction + await cdmContractProxy.processMessageFromSource(data); + + // Check that OLAS tokens were minted to the deployer + const balance = Number(await olas.balanceOf(deployer.address)); + expect(balance).to.equal(amountToMint); + }); + + it("Unpack the data and call two specified targets on the OLAS contract", async function () { + // Minter of OLAS must be the optimismMessenger contract + await olas.connect(deployer).changeMinter(optimismMessenger.address); + // Change OLAS owner to the optimismMessenger contract + await olas.connect(deployer).changeOwner(optimismMessenger.address); + + // FxGivernorTunnel changes the minter to self as being the owner, then mint 100 OLAS for the deployer + const amountToMint = 100; + const payloads = [olas.interface.encodeFunctionData("changeMinter", [optimismMessenger.address]), + olas.interface.encodeFunctionData("mint", [deployer.address, amountToMint])]; + + // Pack the data into one contiguous buffer + const targets = [olas.address, olas.address]; + const values = [0, 0]; + let data = "0x"; + for (let i = 0; i < targets.length; i++) { + const payload = ethers.utils.arrayify(payloads[i]); + const encoded = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [targets[i], values[i], payload.length, payload] + ); + data += encoded.slice(2); + } + + // Execute the unpacked transaction + await cdmContractProxy.processMessageFromSource(data); + + // Check that OLAS tokens were minted to the deployer + const balance = Number(await olas.balanceOf(deployer.address)); + expect(balance).to.equal(amountToMint); + }); + + it("Change the source governor", async function () { + const rawPayload = optimismMessenger.interface.encodeFunctionData("changeSourceGovernor", [signers[1].address]); + + // Pack the data into one contiguous buffer + const target = optimismMessenger.address; + const value = 0; + const payload = ethers.utils.arrayify(rawPayload); + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ); + + // Execute the unpacked transaction + await cdmContractProxy.processMessageFromSource(data); + + // Check that the new source governor is signers[1].address + const sourceGovernor = await optimismMessenger.sourceGovernor(); + expect(sourceGovernor).to.equal(signers[1].address); + }); + + it("Should fail when trying to change the source governor to the zero address", async function () { + const rawPayload = optimismMessenger.interface.encodeFunctionData("changeSourceGovernor", [AddressZero]); + + // Pack the data into one contiguous buffer + const target = optimismMessenger.address; + const value = 0; + const payload = ethers.utils.arrayify(rawPayload); + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ); + + // Execute the unpacked transaction + await expect( + cdmContractProxy.processMessageFromSource(data) + ).to.be.revertedWithCustomError(optimismMessenger, "TargetExecFailed"); + }); + + it("Unpack the data and call one specified target to send funds", async function () { + const amount = ethers.utils.parseEther("1"); + // Pack the data into one contiguous buffer + const target = deployer.address; + const value = amount; + const payloadLength = 0; + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32"], + [target, value, payloadLength] + ); + + // Try to execute the unpacked transaction without the contract having enough balance + await expect( + cdmContractProxy.processMessageFromSource(data) + ).to.be.revertedWithCustomError(optimismMessenger, "InsufficientBalance"); + + // Send funds to the contract + await deployer.sendTransaction({to: optimismMessenger.address, value: amount}); + + // Now the funds can be transferred + const balanceBefore = await ethers.provider.getBalance(deployer.address); + const tx = await cdmContractProxy.processMessageFromSource(data); + const receipt = await tx.wait(); + const gasCost = ethers.BigNumber.from(receipt.gasUsed).mul(ethers.BigNumber.from(tx.gasPrice)); + const balanceAfter = await ethers.provider.getBalance(deployer.address); + const balanceDiff = balanceAfter.sub(balanceBefore).add(gasCost); + expect(balanceDiff).to.equal(amount); + }); + + it("Unpack the data and call one specified target to send funds and to mint OLAS", async function () { + const amount = ethers.utils.parseEther("1"); + const amountToMint = 100; + // Minter of OLAS must be the optimismMessenger contract + await olas.connect(deployer).changeMinter(optimismMessenger.address); + + // Pack the first part of data with the zero payload + let target = deployer.address; + let value = amount; + const payloadLength = 0; + let data = ethers.utils.solidityPack( + ["address", "uint96", "uint32"], + [target, value, payloadLength] + ); + + // OLAS contract across the bridge must mint 100 OLAS for the deployer + const rawPayload = olas.interface.encodeFunctionData("mint", [deployer.address, amountToMint]); + // Pack the second part of data + target = olas.address; + value = 0; + const payload = ethers.utils.arrayify(rawPayload); + data += ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ).slice(2); + + // Send funds to the contract + await deployer.sendTransaction({to: optimismMessenger.address, value: amount}); + + // Execute the function and check for the deployer balance + const balanceBefore = await ethers.provider.getBalance(deployer.address); + const tx = await cdmContractProxy.processMessageFromSource(data); + const receipt = await tx.wait(); + const gasCost = ethers.BigNumber.from(receipt.gasUsed).mul(ethers.BigNumber.from(tx.gasPrice)); + const balanceAfter = await ethers.provider.getBalance(deployer.address); + const balanceDiff = balanceAfter.sub(balanceBefore).add(gasCost); + expect(balanceDiff).to.equal(amount); + + // Check that OLAS tokens were minted to the deployer + const olasBalance = Number(await olas.balanceOf(deployer.address)); + expect(olasBalance).to.equal(amountToMint); + }); + }); +}); diff --git a/test/WormholeMessenger.js b/test/WormholeMessenger.js new file mode 100644 index 0000000..b488960 --- /dev/null +++ b/test/WormholeMessenger.js @@ -0,0 +1,291 @@ +/*global describe, context, beforeEach, it*/ + +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +describe("WormholeMessenger", function () { + let targetRelayer; + let wormholeMessenger; + let olas; + let signers; + let deployer; + const AddressZero = ethers.constants.AddressZero; + const Bytes32Zero = "0x" + "0".repeat(64); + const sourceChain = 2; + const addressPrefix = "0x" + "0".repeat(24); + + beforeEach(async function () { + signers = await ethers.getSigners(); + deployer = signers[0]; + + // Deploy the mock of TargetRelayer contract + const TargetRelayer = await ethers.getContractFactory("MockL2Relayer"); + targetRelayer = await TargetRelayer.deploy(deployer.address, deployer.address); + await targetRelayer.deployed(); + + // The deployer is the analogue of the Timelock on L1 and the FxRoot mock on L1 as well + const WormholeMessenger = await ethers.getContractFactory("WormholeMessenger"); + wormholeMessenger = await WormholeMessenger.deploy(targetRelayer.address, + addressPrefix + deployer.address.slice(2), sourceChain); + await wormholeMessenger.deployed(); + + // Change the WormholeMessenger contract address in the TargetRelayer + await targetRelayer.changeBridgeMessenger(wormholeMessenger.address); + + // OLAS represents a contract deployed on L2 + const OLAS = await ethers.getContractFactory("OLAS"); + olas = await OLAS.deploy(); + await olas.deployed(); + }); + + context("Initialization", async function () { + it("Deploying with zero addresses", async function () { + const WormholeMessenger = await ethers.getContractFactory("WormholeMessenger"); + await expect( + WormholeMessenger.deploy(AddressZero, Bytes32Zero, 0) + ).to.be.revertedWithCustomError(wormholeMessenger, "ZeroAddress"); + + await expect( + WormholeMessenger.deploy(signers[1].address, Bytes32Zero, 0) + ).to.be.revertedWithCustomError(wormholeMessenger, "ZeroValue"); + + await expect( + WormholeMessenger.deploy(signers[1].address, addressPrefix + signers[2].address.slice(2), 0) + ).to.be.revertedWithCustomError(wormholeMessenger, "ZeroValue"); + }); + }); + + context("Process message from source", async function () { + it("Should fail when trying to call from incorrect contract addresses and chain Id", async function () { + await expect( + wormholeMessenger.connect(deployer).receiveWormholeMessages("0x", [], Bytes32Zero, 0, Bytes32Zero) + ).to.be.revertedWithCustomError(wormholeMessenger, "TargetRelayerOnly"); + + // Simulate incorrect sourceGovernor address + await targetRelayer.changeSourceGovernor(AddressZero); + await expect( + targetRelayer.receiveWormholeMessages("0x") + ).to.be.revertedWithCustomError(wormholeMessenger, "SourceGovernorOnly32"); + + // Simulate incorrect source chain Id + await targetRelayer.changeSourceChain(0); + await expect( + targetRelayer.receiveWormholeMessages("0x") + ).to.be.revertedWithCustomError(wormholeMessenger, "WrongSourceChainId"); + }); + + it("Should fail when trying to process message with the incorrect minimal payload length", async function () { + await expect( + targetRelayer.receiveWormholeMessages("0x") + ).to.be.revertedWithCustomError(wormholeMessenger, "IncorrectDataLength"); + }); + + it("Should fail when trying to change the source governor from any other contract / EOA", async function () { + await expect( + wormholeMessenger.connect(deployer).changeSourceGovernor(addressPrefix + signers[1].address.slice(2)) + ).to.be.revertedWithCustomError(wormholeMessenger, "SelfCallOnly"); + }); + + it("Should fail when trying to call with the zero address", async function () { + const target = AddressZero; + const value = 0; + const payload = ""; + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32"], + [target, value, payload.length] + ); + + await expect( + targetRelayer.receiveWormholeMessages(data) + ).to.be.revertedWithCustomError(wormholeMessenger, "ZeroAddress"); + }); + + it("Should fail when trying to call with the incorrectly provided payload", async function () { + const target = olas.address; + const value = 0; + const rawPayload = "0x" + "abcd".repeat(10); + const payload = ethers.utils.arrayify(rawPayload); + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ); + + await expect( + targetRelayer.receiveWormholeMessages(data) + ).to.be.revertedWithCustomError(wormholeMessenger, "TargetExecFailed"); + }); + + it("Unpack the data and call one specified target on the OLAS contract", async function () { + // Minter of OLAS must be the wormholeMessenger contract + await olas.connect(deployer).changeMinter(wormholeMessenger.address); + + // OLAS contract across the bridge must mint 100 OLAS for the deployer + const amountToMint = 100; + const rawPayload = olas.interface.encodeFunctionData("mint", [deployer.address, amountToMint]); + + // Pack the data into one contiguous buffer + const target = olas.address; + const value = 0; + const payload = ethers.utils.arrayify(rawPayload); + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ); + + // Execute the unpacked transaction + await targetRelayer.receiveWormholeMessages(data); + + // Check that OLAS tokens were minted to the deployer + const balance = Number(await olas.balanceOf(deployer.address)); + expect(balance).to.equal(amountToMint); + + // Try to unpack with the same delivery hash + await expect( + targetRelayer.receiveWormholeMessages(data) + ).to.be.revertedWithCustomError(wormholeMessenger, "AlreadyDelivered"); + }); + + it("Unpack the data and call two specified targets on the OLAS contract", async function () { + // Minter of OLAS must be the wormholeMessenger contract + await olas.connect(deployer).changeMinter(wormholeMessenger.address); + // Change OLAS owner to the wormholeMessenger contract + await olas.connect(deployer).changeOwner(wormholeMessenger.address); + + // FxGivernorTunnel changes the minter to self as being the owner, then mint 100 OLAS for the deployer + const amountToMint = 100; + const payloads = [olas.interface.encodeFunctionData("changeMinter", [wormholeMessenger.address]), + olas.interface.encodeFunctionData("mint", [deployer.address, amountToMint])]; + + // Pack the data into one contiguous buffer + const targets = [olas.address, olas.address]; + const values = [0, 0]; + let data = "0x"; + for (let i = 0; i < targets.length; i++) { + const payload = ethers.utils.arrayify(payloads[i]); + const encoded = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [targets[i], values[i], payload.length, payload] + ); + data += encoded.slice(2); + } + + // Execute the unpacked transaction + await targetRelayer.receiveWormholeMessages(data); + + // Check that OLAS tokens were minted to the deployer + const balance = Number(await olas.balanceOf(deployer.address)); + expect(balance).to.equal(amountToMint); + }); + + it("Change the source governor", async function () { + const rawPayload = wormholeMessenger.interface.encodeFunctionData("changeSourceGovernor", + [addressPrefix + signers[1].address.slice(2)]); + + // Pack the data into one contiguous buffer + const target = wormholeMessenger.address; + const value = 0; + const payload = ethers.utils.arrayify(rawPayload); + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ); + + // Execute the unpacked transaction + await targetRelayer.receiveWormholeMessages(data); + + // Check that the new source governor is signers[1].address + const sourceGovernor = await wormholeMessenger.sourceGovernor(); + expect(sourceGovernor).to.equal((addressPrefix + signers[1].address.slice(2)).toLowerCase()); + }); + + it("Should fail when trying to change the source governor to the zero address", async function () { + const rawPayload = wormholeMessenger.interface.encodeFunctionData("changeSourceGovernor", [Bytes32Zero]); + + // Pack the data into one contiguous buffer + const target = wormholeMessenger.address; + const value = 0; + const payload = ethers.utils.arrayify(rawPayload); + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ); + + // Execute the unpacked transaction + await expect( + targetRelayer.receiveWormholeMessages(data) + ).to.be.revertedWithCustomError(wormholeMessenger, "TargetExecFailed"); + }); + + it("Unpack the data and call one specified target to send funds", async function () { + const amount = ethers.utils.parseEther("1"); + // Pack the data into one contiguous buffer + const target = deployer.address; + const value = amount; + const payloadLength = 0; + const data = ethers.utils.solidityPack( + ["address", "uint96", "uint32"], + [target, value, payloadLength] + ); + + // Try to execute the unpacked transaction without the contract having enough balance + await expect( + targetRelayer.receiveWormholeMessages(data) + ).to.be.revertedWithCustomError(wormholeMessenger, "InsufficientBalance"); + + // Send funds to the contract + await deployer.sendTransaction({to: wormholeMessenger.address, value: amount}); + + // Now the funds can be transferred + const balanceBefore = await ethers.provider.getBalance(deployer.address); + const tx = await targetRelayer.receiveWormholeMessages(data); + const receipt = await tx.wait(); + const gasCost = ethers.BigNumber.from(receipt.gasUsed).mul(ethers.BigNumber.from(tx.gasPrice)); + const balanceAfter = await ethers.provider.getBalance(deployer.address); + const balanceDiff = balanceAfter.sub(balanceBefore).add(gasCost); + expect(balanceDiff).to.equal(amount); + }); + + it("Unpack the data and call one specified target to send funds and to mint OLAS", async function () { + const amount = ethers.utils.parseEther("1"); + const amountToMint = 100; + // Minter of OLAS must be the wormholeMessenger contract + await olas.connect(deployer).changeMinter(wormholeMessenger.address); + + // Pack the first part of data with the zero payload + let target = deployer.address; + let value = amount; + const payloadLength = 0; + let data = ethers.utils.solidityPack( + ["address", "uint96", "uint32"], + [target, value, payloadLength] + ); + + // OLAS contract across the bridge must mint 100 OLAS for the deployer + const rawPayload = olas.interface.encodeFunctionData("mint", [deployer.address, amountToMint]); + // Pack the second part of data + target = olas.address; + value = 0; + const payload = ethers.utils.arrayify(rawPayload); + data += ethers.utils.solidityPack( + ["address", "uint96", "uint32", "bytes"], + [target, value, payload.length, payload] + ).slice(2); + + // Send funds to the contract + await deployer.sendTransaction({to: wormholeMessenger.address, value: amount}); + + // Execute the function and check for the deployer balance + const balanceBefore = await ethers.provider.getBalance(deployer.address); + const tx = await targetRelayer.receiveWormholeMessages(data); + const receipt = await tx.wait(); + const gasCost = ethers.BigNumber.from(receipt.gasUsed).mul(ethers.BigNumber.from(tx.gasPrice)); + const balanceAfter = await ethers.provider.getBalance(deployer.address); + const balanceDiff = balanceAfter.sub(balanceBefore).add(gasCost); + expect(balanceDiff).to.equal(amount); + + // Check that OLAS tokens were minted to the deployer + const olasBalance = Number(await olas.balanceOf(deployer.address)); + expect(olasBalance).to.equal(amountToMint); + }); + }); +});