From 5990daa5e92a198be65dd514ab2b987bac32f5b3 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 27 Sep 2023 13:20:29 +0200 Subject: [PATCH 1/7] chore: add error codes for easy debugging --- src/interfaces/IMessageEscrowErrors.sol | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/interfaces/IMessageEscrowErrors.sol b/src/interfaces/IMessageEscrowErrors.sol index 5e727bb..a5992d9 100644 --- a/src/interfaces/IMessageEscrowErrors.sol +++ b/src/interfaces/IMessageEscrowErrors.sol @@ -2,18 +2,18 @@ pragma solidity ^0.8.13; interface IMessageEscrowErrors { - error NotEnoughGasProvided(uint128 expected, uint128 actual); - error InvalidTotalIncentive(uint128 expected, uint128 actual); - error MessageAlreadyBountied(); - error MessageDoesNotExist(); - error MessageAlreadyAcked(); - error NotImplementedError(); - error feeRecipitentIncorrectFormatted(uint8 expected, uint8 actual); - error MessageAlreadySpent(); - error TargetExecutionTimeInvalid(int128 difference); - error DeliveryGasPriceMustBeIncreased(); - error AckGasPriceMustBeIncreased(); - error AckHasNotBeenExecuted(); - error NoImplementationAddressSet(); - error InvalidImplementationAddress(); + error NotEnoughGasProvided(uint128 expected, uint128 actual); // 030748b5 + error InvalidTotalIncentive(uint128 expected, uint128 actual); // 79ddca92 + error MessageAlreadyBountied(); // 068a62ee + error MessageDoesNotExist(); // 970e41ec + error MessageAlreadyAcked(); // 8af35858 + error NotImplementedError(); // d41c17e7 + error feeRecipitentIncorrectFormatted(uint8 expected, uint8 actual); // e3d86532 + error MessageAlreadySpent(); // e954aba2 + error TargetExecutionTimeInvalid(int128 difference); // cf3b5fa4 + error DeliveryGasPriceMustBeIncreased(); // 39193a29 + error AckGasPriceMustBeIncreased(); // 553d8418 + error AckHasNotBeenExecuted(); // 3d1553f8 + error NoImplementationAddressSet(); // 9f994b4b + error InvalidImplementationAddress(); // c970156c } \ No newline at end of file From a4ac8c7a83917376dc2ee02854afc7e6fc5d1c45 Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:59:51 +0200 Subject: [PATCH 2/7] fix: typos (#6) * fix typos * fix typo --- README.md | 4 ++-- test/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9541949..9bb4e85 100644 --- a/README.md +++ b/README.md @@ -149,10 +149,10 @@ The destination-to-source relayer will result in the message reverting. They sho Because of the centralization associated with adding new chains / deployments, applications has to opt-in to these new chains. To understand the issue better, examine the following flow: 1. An escrow with honest logic with no flaws exist on chain Alpha. -2. An application on chain Alpha can be drained by sending the fradulent key `0xabcdef` to the source chain. Ordinarly this never happens. This application trusts Alpha. +2. An application on chain Alpha can be drained by sending the fraudulent key `0xabcdef` to the source chain. Ordinarily this never happens. This application trusts Alpha. 3. The administrator adds another deployment on chain Beta with same address as Alpha but with another bytecode deployed. Specifically, when the administrator calls this contract it sends `0xabcdef` to the application. 4. The application adds chain Beta to the allow list since the address matches the Beta address (thinking the byte code deployed must be the same). -5. The fradulent deployment on Beta sends `0xabcedf` to the application on chain Alpha +5. The fraudulent deployment on Beta sends `0xabcedf` to the application on chain Alpha 6. On Alpha the message is verified. As a result, each application needs to tell the escrow where the other escrow sits and which escrow is allowed to send it messages. These mappings are 1:1, each chain identifier is only allowed a single escrow deployment. diff --git a/test/README.md b/test/README.md index 1b55505..435bc52 100644 --- a/test/README.md +++ b/test/README.md @@ -5,7 +5,7 @@ Each folder tests a specific contract. Each subfolder tests each function within the contract. *./TestCommon.t.sol* -Contains frequently used code snippits to simplify testing. +Contains frequently used code snippets to simplify testing. *./IncentivizedMessageEscrow* Contains tests for the inheritable contract IncentivizedMessageEscrow which defines the base logic. From 379dadc284b45e22ffabd6ac87873ddda88e21c7 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 10 Oct 2023 13:02:11 +0200 Subject: [PATCH 3/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bb4e85..a8b5274 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ Contracts within this repo have not been audited and should not be used in production. -# Generalized Incentive Escrow +# Generalised Incentive Escrow This repository contains an implementation of a generalized Incentive Scheme. From 34d7bee4e541ff127d1c9caffafe25ed798f7f9f Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 13 Oct 2023 18:27:27 +0200 Subject: [PATCH 4/7] readme: Add another potential solidity issue to the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8b5274..b9f2456 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ The implementation is not perfect. Below the most notable implementation strange If a message reverts, ran out of gas, or otherwise failed to return an ack the implementation should do its best to not revert but instead send the original message back prepended with 0xff as the acknowledgment. -For EVM this is currently limited by [Solidity #13869](https://github.com/ethereum/solidity/issues/13869). Calls to contracts which doesn't implement the proper endpoint will fail. +For EVM this is currently limited by [Solidity #13869](https://github.com/ethereum/solidity/issues/13869), [Solidity #14467](https://github.com/ethereum/solidity/issues/14467). Calls to contracts which doesn't implement the proper endpoint will fail. - Relayers should emulate the call before calling the function to avoid wasting gas. - If contracts expect the call to execute (or rely on the ack), contracts need to make sure they are calling a contract that implements proper interfaces for receiving calls. From 57a0647ad632dfad073947b7c46117e8ec576e7f Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 13 Oct 2023 21:21:57 +0200 Subject: [PATCH 5/7] feat: wormhole implementation (#5) * feat: Wormhole verification implementation in calldata * feat: signatures in calldata * fix: payload incorrectly read * test: Added for 2 signatures to ensure I got the loops correct * feat: cleanup wormhole implementation slightly and make it easier for relayers to discover messages * test: round trip wormhole message * chore: remove todo * feat: add multicall for easier batch execution, setup and more * chore: increase number of optimizer runs * test: update tests with new number of optimisations * forge install: openzeppelin-contracts v4.9.3 * feat: significant cleanup --- src/apps/mock/IncentivizedMockEscrow.sol | 6 +- .../wormhole/IncentivizedWormholeEscrow.sol | 88 +++ .../external/callworm/GettersGetter.sol | 62 +++ src/apps/wormhole/external/callworm/README.md | 1 + .../external/callworm/SmallStructs.sol | 18 + .../external/callworm/WormholeVerifier.sol | 214 ++++++++ .../wormhole/external/wormhole/Getters.sol | 56 ++ .../wormhole/external/wormhole/Messages.sol | 218 ++++++++ .../wormhole/external/wormhole/Setters.sol | 57 ++ src/apps/wormhole/external/wormhole/State.sol | 52 ++ .../wormhole/external/wormhole/Structs.sol | 40 ++ .../wormhole/libraries/external/BytesLib.sol | 510 ++++++++++++++++++ src/apps/wormhole/interfaces/IWormhole.sol | 142 +++++ test/wormhole/(wh)messages.t.sol | 164 ++++++ test/wormhole/roundtrip.t.sol | 160 ++++++ test/wormhole/verifyMessage2.t.sol | 151 ++++++ test/wormhole/verifyMessages.t.sol | 127 +++++ 17 files changed, 2061 insertions(+), 5 deletions(-) create mode 100644 src/apps/wormhole/IncentivizedWormholeEscrow.sol create mode 100644 src/apps/wormhole/external/callworm/GettersGetter.sol create mode 100644 src/apps/wormhole/external/callworm/README.md create mode 100644 src/apps/wormhole/external/callworm/SmallStructs.sol create mode 100644 src/apps/wormhole/external/callworm/WormholeVerifier.sol create mode 100644 src/apps/wormhole/external/wormhole/Getters.sol create mode 100644 src/apps/wormhole/external/wormhole/Messages.sol create mode 100644 src/apps/wormhole/external/wormhole/Setters.sol create mode 100644 src/apps/wormhole/external/wormhole/State.sol create mode 100644 src/apps/wormhole/external/wormhole/Structs.sol create mode 100644 src/apps/wormhole/external/wormhole/libraries/external/BytesLib.sol create mode 100644 src/apps/wormhole/interfaces/IWormhole.sol create mode 100644 test/wormhole/(wh)messages.t.sol create mode 100644 test/wormhole/roundtrip.t.sol create mode 100644 test/wormhole/verifyMessage2.t.sol create mode 100644 test/wormhole/verifyMessages.t.sol diff --git a/src/apps/mock/IncentivizedMockEscrow.sol b/src/apps/mock/IncentivizedMockEscrow.sol index 4c414d3..e69e3c1 100644 --- a/src/apps/mock/IncentivizedMockEscrow.sol +++ b/src/apps/mock/IncentivizedMockEscrow.sol @@ -12,11 +12,7 @@ contract IncentivizedMockEscrow is IncentivizedMessageEscrow, Ownable2Step { uint256 public costOfMessages; uint256 public accumulator = 1; - event Message( - bytes32 destinationIdentifier, - bytes recipitent, - bytes message - ); + event Message(bytes32 destinationIdentifier, bytes recipitent, bytes message); constructor(bytes32 uniqueChainIndex, address signer, uint256 costOfMessages_) { UNIQUE_SOURCE_IDENTIFIER = uniqueChainIndex; diff --git a/src/apps/wormhole/IncentivizedWormholeEscrow.sol b/src/apps/wormhole/IncentivizedWormholeEscrow.sol new file mode 100644 index 0000000..1f8eae2 --- /dev/null +++ b/src/apps/wormhole/IncentivizedWormholeEscrow.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; + +import { SmallStructs } from "./external/callworm/SmallStructs.sol"; +import { WormholeVerifier } from "./external/callworm/WormholeVerifier.sol"; +import { IWormhole } from "./interfaces/IWormhole.sol"; + +// This is a mock contract which should only be used for testing. +contract IncentivizedWormholeEscrow is IncentivizedMessageEscrow, WormholeVerifier { + error BadChainIdentifier(); + + event WormholeMessage( + bytes32 destinationIdentifier, + bytes recipitent + ); + + IWormhole public immutable WORMHOLE; + + constructor(address wormhole_) WormholeVerifier(wormhole_) { + WORMHOLE = IWormhole(wormhole_); + } + + function estimateAdditionalCost() external view returns(address asset, uint256 amount) { + asset = address(0); + amount = WORMHOLE.messageFee(); + } + + function _getMessageIdentifier( + bytes32 destinationIdentifier, + bytes calldata message + ) internal override view returns(bytes32) { + return keccak256( + abi.encodePacked( + bytes32(block.number), + chainId(), + destinationIdentifier, + message + ) + ); + } + + function _verifyMessage(bytes calldata _metadata, bytes calldata _message) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + + ( + SmallStructs.SmallVM memory vm, + bytes calldata payload, + bool valid, + string memory reason + ) = parseAndVerifyVM(_message); + + require(valid, reason); + + // Load the identifier for the calling contract. + implementationIdentifier = abi.encodePacked(vm.emitterAddress); + + // Local "supposedly" this chain identifier. + bytes32 thisChainIdentifier = bytes32(payload[0:32]); + + // Check that the message is intended for this chain. + if (thisChainIdentifier != bytes32(uint256(chainId()))) revert BadChainIdentifier(); + + // Local the identifier for the source chain. + sourceIdentifier = bytes32(bytes2(vm.emitterChainId)); + + // Get the application message. + message_ = payload[32:]; + } + + function _sendMessage(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfSendMessageInNativeToken) { + // Get the cost of sending wormhole messages. + costOfSendMessageInNativeToken = uint128(WORMHOLE.messageFee()); + + // Emit context for relayers so they know where to send the message + emit WormholeMessage(destinationChainIdentifier, destinationImplementation); + + // Handoff the message to wormhole. + WORMHOLE.publishMessage{value: costOfSendMessageInNativeToken}( + 0, + abi.encodePacked( + destinationChainIdentifier, + message + ), + 0 // Finality = complete. + ); + } +} \ No newline at end of file diff --git a/src/apps/wormhole/external/callworm/GettersGetter.sol b/src/apps/wormhole/external/callworm/GettersGetter.sol new file mode 100644 index 0000000..0b5665d --- /dev/null +++ b/src/apps/wormhole/external/callworm/GettersGetter.sol @@ -0,0 +1,62 @@ +// contracts/Getters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../wormhole/Getters.sol"; + +contract GettersGetter { + Getters immutable public WORMHOLE_STATE; + + constructor(address wormholeState) { + WORMHOLE_STATE = Getters(wormholeState); + } + + function getGuardianSet(uint32 index) public view returns (Structs.GuardianSet memory) { + return WORMHOLE_STATE.getGuardianSet(index); + } + + function getCurrentGuardianSetIndex() public view returns (uint32) { + return WORMHOLE_STATE.getCurrentGuardianSetIndex(); + } + + function getGuardianSetExpiry() public view returns (uint32) { + return WORMHOLE_STATE.getGuardianSetExpiry(); + } + + function governanceActionIsConsumed(bytes32 hash) public view returns (bool) { + return WORMHOLE_STATE.governanceActionIsConsumed(hash); + } + + function isInitialized(address impl) public view returns (bool) { + return WORMHOLE_STATE.isInitialized(impl); + } + + function chainId() public view returns (uint16) { + return WORMHOLE_STATE.chainId(); + } + + function evmChainId() public view returns (uint256) { + return WORMHOLE_STATE.evmChainId(); + } + + function isFork() public view returns (bool) { + return WORMHOLE_STATE.isFork(); + } + + function governanceChainId() public view returns (uint16){ + return WORMHOLE_STATE.governanceChainId(); + } + + function governanceContract() public view returns (bytes32){ + return WORMHOLE_STATE.governanceContract(); + } + + function messageFee() public view returns (uint256) { + return WORMHOLE_STATE.messageFee(); + } + + function nextSequence(address emitter) public view returns (uint64) { + return WORMHOLE_STATE.nextSequence(emitter); + } +} \ No newline at end of file diff --git a/src/apps/wormhole/external/callworm/README.md b/src/apps/wormhole/external/callworm/README.md new file mode 100644 index 0000000..9060b8d --- /dev/null +++ b/src/apps/wormhole/external/callworm/README.md @@ -0,0 +1 @@ +This is an alternative implementation of the wormhole message verification with the purpose of significantly reducing gas cost but also simplify integration by always keeping the message in calldata. \ No newline at end of file diff --git a/src/apps/wormhole/external/callworm/SmallStructs.sol b/src/apps/wormhole/external/callworm/SmallStructs.sol new file mode 100644 index 0000000..cc5114b --- /dev/null +++ b/src/apps/wormhole/external/callworm/SmallStructs.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +interface SmallStructs { + + struct SmallVM { + // uint8 version; + // uint32 timestamp; + // uint32 nonce; + uint16 emitterChainId; + bytes32 emitterAddress; + // uint64 sequence; + // uint8 consistencyLevel; + + uint32 guardianSetIndex; + } +} \ No newline at end of file diff --git a/src/apps/wormhole/external/callworm/WormholeVerifier.sol b/src/apps/wormhole/external/callworm/WormholeVerifier.sol new file mode 100644 index 0000000..f31087e --- /dev/null +++ b/src/apps/wormhole/external/callworm/WormholeVerifier.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./GettersGetter.sol"; +import "../wormhole/Structs.sol"; +import "./SmallStructs.sol"; +import "../wormhole/libraries/external/BytesLib.sol"; + + +contract WormholeVerifier is GettersGetter { + using BytesLib for bytes; + + constructor(address wormholeState) GettersGetter(wormholeState) {} + + /// @dev parseAndVerifyVM serves to parse an encodedVM and wholy validate it for consumption + function parseAndVerifyVM(bytes calldata encodedVM) public view returns ( + SmallStructs.SmallVM memory vm, + bytes calldata payload, + bool valid, + string memory reason + ) { + bytes calldata signatures; + bytes32 bodyHash; + (vm, signatures, bodyHash, payload) = parseVM(encodedVM); + /// setting checkHash to false as we can trust the hash field in this case given that parseVM computes and then sets the hash field above + (valid, reason) = verifyVMInternal(vm, signatures, bodyHash); + } + + /** + * @dev `verifyVMInternal` serves to validate an arbitrary vm against a valid Guardian set + * if checkHash is set then the hash field of the vm is verified against the hash of its contents + * in the case that the vm is securely parsed and the hash field can be trusted, checkHash can be set to false + * as the check would be redundant + */ + function verifyVMInternal( + SmallStructs.SmallVM memory vm, + bytes calldata signatures, + bytes32 bodyHash + ) internal view returns (bool valid, string memory reason) { + /// @dev Obtain the current guardianSet for the guardianSetIndex provided + Structs.GuardianSet memory guardianSet = getGuardianSet(vm.guardianSetIndex); + + /** + * @dev Checks whether the guardianSet has zero keys + * WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure + * that guardianSet key size doesn't fall to zero and negatively impact quorum assessment. If guardianSet + * key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and + * signature verification. + */ + if(guardianSet.keys.length == 0){ + return (false, "invalid guardian set"); + } + + /// @dev Checks if VM guardian set index matches the current index (unless the current set is expired). + if(vm.guardianSetIndex != getCurrentGuardianSetIndex() && guardianSet.expirationTime < block.timestamp){ + return (false, "guardian set has expired"); + } + + /** + * @dev We're using a fixed point number transformation with 1 decimal to deal with rounding. + * WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM + * if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and + * vm.signatures length is 0, this could compromise the integrity of both vm and signature verification. + */ + if (signatures.length < quorum(guardianSet.keys.length)){ + return (false, "no quorum"); + } + + /// @dev Verify the proposed vm.signatures against the guardianSet + (bool signaturesValid, string memory invalidReason) = verifySignatures(bodyHash, signatures, guardianSet); + if(!signaturesValid){ + return (false, invalidReason); + } + + /// If we are here, we've validated the VM is a valid multi-sig that matches the guardianSet. + return (true, ""); + } + + + /** + * @dev verifySignatures serves to validate arbitrary sigatures against an arbitrary guardianSet + * - it intentionally does not solve for expectations within guardianSet (you should use verifyVM if you need these protections) + * - it intentioanlly does not solve for quorum (you should use verifyVM if you need these protections) + * - it intentionally returns true when signatures is an empty set (you should use verifyVM if you need these protections) + */ + function verifySignatures(bytes32 hash, bytes calldata signatures, Structs.GuardianSet memory guardianSet) public pure returns (bool valid, string memory reason) { + uint8 lastIndex = 0; + uint256 guardianCount = guardianSet.keys.length; + uint256 signersLen = uint8(bytes1(signatures[0])); + uint256 index = 1; + unchecked { + for (uint i = 0; i < signersLen; ++i) { + uint8 guardianIndex = uint8(bytes1(signatures[index])); + index += 1; + bytes32 r = bytes32(signatures[index: index + 32]); + index += 32; + bytes32 s = bytes32(signatures[index: index + 32]); + index += 32; + uint8 v = uint8(bytes1(signatures[index: index + 1])) + 27; + index += 1; + address signatory = ecrecover(hash, v, r, s); + // ecrecover returns 0 for invalid signatures. We explicitly require valid signatures to avoid unexpected + // behaviour due to the default storage slot value also being 0. + require(signatory != address(0), "ecrecover failed with signature"); + + + /// Ensure that provided signature indices are ascending only + require(i == 0 || guardianIndex > lastIndex, "signature indices must be ascending"); + lastIndex = guardianIndex; + + /// @dev Ensure that the provided signature index is within the + /// bounds of the guardianSet. This is implicitly checked by the array + /// index operation below, so this check is technically redundant. + /// However, reverting explicitly here ensures that a bug is not + /// introduced accidentally later due to the nontrivial storage + /// semantics of solidity. + require(guardianIndex < guardianCount, "guardian index out of bounds"); + + /// Check to see if the signer of the signature does not match a specific Guardian key at the provided index + if(signatory != guardianSet.keys[guardianIndex]){ + return (false, "VM signature invalid"); + } + } + + /// If we are here, we've validated that the provided signatures are valid for the provided guardianSet + return (true, ""); + } + } + + /** + * @dev parseVM serves to parse an encodedVM into a vm struct + * - it intentionally performs no validation functions, it simply parses raw into a struct + */ + function parseVM(bytes calldata encodedVM) public view virtual returns (SmallStructs.SmallVM memory vm, bytes calldata signatures, bytes32 bodyHash, bytes calldata payload) { + unchecked { + + + uint index = 0; + + uint8 version = uint8(bytes1(encodedVM[0:1])); + + index += 1; + + // SECURITY: Note that currently the VM.version is not part of the hash + // and for reasons described below it cannot be made part of the hash. + // This means that this field's integrity is not protected and cannot be trusted. + // This is not a problem today since there is only one accepted version, but it + // could be a problem if we wanted to allow other versions in the future. + require(version == 1, "VM version incompatible"); + + vm.guardianSetIndex = uint32(bytes4(encodedVM[1:4+1])); + index += 4; + + + // Parse Signatures + uint256 signersLen = uint8(bytes1(encodedVM[5:5+1])); + signatures = encodedVM[5:5 + 1 + signersLen*(1+32+32+1)]; + index += 1 + signersLen*(1+32+32+1); + // signatures = new Structs.Signature[](signersLen); + // for (uint i = 0; i < signersLen; ++i) { + // signatures[i].guardianIndex = uint8(bytes1(encodedVM[index:index+1])); + // index += 1; + + // signatures[i].r = bytes32(encodedVM[index:index+32]); + // index += 32; + // signatures[i].s = bytes32(encodedVM[index:index+32]); + // index += 32; + // signatures[i].v = uint8(bytes1(encodedVM[index:index+1])) + 27; + // index += 1; + // } + + /* + Hash the body + + SECURITY: Do not change the way the hash of a VM is computed! + Changing it could result into two different hashes for the same observation. + But xDapps rely on the hash of an observation for replay protection. + */ + bytes calldata body = encodedVM[index:]; + bodyHash = keccak256(abi.encodePacked(keccak256(body))); + + // Parse the body + // vm.timestamp = uint32(bytes4(encodedVM[index:index+4])); + index += 4; + + // vm.nonce = uint32(bytes4(encodedVM[index:index+4])); + index += 4; + + vm.emitterChainId = uint16(bytes2(encodedVM[index:index+2])); + index += 2; + + vm.emitterAddress = bytes32(encodedVM[index:index+32]); + index += 32; + + // vm.sequence = uint64(bytes8(encodedVM[index:index+8])); + index += 8; + + // vm.consistencyLevel = uint8(bytes1(encodedVM[index:index+1])); + index += 1; + + payload = encodedVM[index:]; + } + } + + /** + * @dev quorum serves solely to determine the number of signatures required to acheive quorum + */ + function quorum(uint numGuardians) public pure virtual returns (uint numSignaturesRequiredForQuorum) { + // The max number of guardians is 255 + require(numGuardians < 256, "too many guardians"); + return ((numGuardians * 2) / 3) + 1; + } +} \ No newline at end of file diff --git a/src/apps/wormhole/external/wormhole/Getters.sol b/src/apps/wormhole/external/wormhole/Getters.sol new file mode 100644 index 0000000..627b641 --- /dev/null +++ b/src/apps/wormhole/external/wormhole/Getters.sol @@ -0,0 +1,56 @@ +// contracts/Getters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./State.sol"; + +contract Getters is State { + function getGuardianSet(uint32 index) public view returns (Structs.GuardianSet memory) { + return _state.guardianSets[index]; + } + + function getCurrentGuardianSetIndex() public view returns (uint32) { + return _state.guardianSetIndex; + } + + function getGuardianSetExpiry() public view returns (uint32) { + return _state.guardianSetExpiry; + } + + function governanceActionIsConsumed(bytes32 hash) public view returns (bool) { + return _state.consumedGovernanceActions[hash]; + } + + function isInitialized(address impl) public view returns (bool) { + return _state.initializedImplementations[impl]; + } + + function chainId() public view returns (uint16) { + return _state.provider.chainId; + } + + function evmChainId() public view returns (uint256) { + return _state.evmChainId; + } + + function isFork() public view returns (bool) { + return evmChainId() != block.chainid; + } + + function governanceChainId() public view returns (uint16){ + return _state.provider.governanceChainId; + } + + function governanceContract() public view returns (bytes32){ + return _state.provider.governanceContract; + } + + function messageFee() public view returns (uint256) { + return _state.messageFee; + } + + function nextSequence(address emitter) public view returns (uint64) { + return _state.sequences[emitter]; + } +} \ No newline at end of file diff --git a/src/apps/wormhole/external/wormhole/Messages.sol b/src/apps/wormhole/external/wormhole/Messages.sol new file mode 100644 index 0000000..e702caf --- /dev/null +++ b/src/apps/wormhole/external/wormhole/Messages.sol @@ -0,0 +1,218 @@ +// contracts/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "./Getters.sol"; +import "./Structs.sol"; +import "./libraries/external/BytesLib.sol"; + + +contract Messages is Getters { + using BytesLib for bytes; + + /// @dev parseAndVerifyVM serves to parse an encodedVM and wholy validate it for consumption + function parseAndVerifyVM(bytes calldata encodedVM) public view returns (Structs.VM memory vm, bool valid, string memory reason) { + vm = parseVM(encodedVM); + /// setting checkHash to false as we can trust the hash field in this case given that parseVM computes and then sets the hash field above + (valid, reason) = verifyVMInternal(vm, false); + } + + /** + * @dev `verifyVM` serves to validate an arbitrary vm against a valid Guardian set + * - it aims to make sure the VM is for a known guardianSet + * - it aims to ensure the guardianSet is not expired + * - it aims to ensure the VM has reached quorum + * - it aims to verify the signatures provided against the guardianSet + * - it aims to verify the hash field provided against the contents of the vm + */ + function verifyVM(Structs.VM memory vm) public view returns (bool valid, string memory reason) { + (valid, reason) = verifyVMInternal(vm, true); + } + + /** + * @dev `verifyVMInternal` serves to validate an arbitrary vm against a valid Guardian set + * if checkHash is set then the hash field of the vm is verified against the hash of its contents + * in the case that the vm is securely parsed and the hash field can be trusted, checkHash can be set to false + * as the check would be redundant + */ + function verifyVMInternal(Structs.VM memory vm, bool checkHash) internal view returns (bool valid, string memory reason) { + /// @dev Obtain the current guardianSet for the guardianSetIndex provided + Structs.GuardianSet memory guardianSet = getGuardianSet(vm.guardianSetIndex); + + /** + * Verify that the hash field in the vm matches with the hash of the contents of the vm if checkHash is set + * WARNING: This hash check is critical to ensure that the vm.hash provided matches with the hash of the body. + * Without this check, it would not be safe to call verifyVM on it's own as vm.hash can be a valid signed hash + * but the body of the vm could be completely different from what was actually signed by the guardians + */ + if(checkHash){ + bytes memory body = abi.encodePacked( + vm.timestamp, + vm.nonce, + vm.emitterChainId, + vm.emitterAddress, + vm.sequence, + vm.consistencyLevel, + vm.payload + ); + + bytes32 vmHash = keccak256(abi.encodePacked(keccak256(body))); + + if(vmHash != vm.hash){ + return (false, "vm.hash doesn't match body"); + } + } + + /** + * @dev Checks whether the guardianSet has zero keys + * WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure + * that guardianSet key size doesn't fall to zero and negatively impact quorum assessment. If guardianSet + * key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and + * signature verification. + */ + if(guardianSet.keys.length == 0){ + return (false, "invalid guardian set"); + } + + /// @dev Checks if VM guardian set index matches the current index (unless the current set is expired). + if(vm.guardianSetIndex != getCurrentGuardianSetIndex() && guardianSet.expirationTime < block.timestamp){ + return (false, "guardian set has expired"); + } + + /** + * @dev We're using a fixed point number transformation with 1 decimal to deal with rounding. + * WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM + * if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and + * vm.signatures length is 0, this could compromise the integrity of both vm and signature verification. + */ + if (vm.signatures.length < quorum(guardianSet.keys.length)){ + return (false, "no quorum"); + } + + /// @dev Verify the proposed vm.signatures against the guardianSet + (bool signaturesValid, string memory invalidReason) = verifySignatures(vm.hash, vm.signatures, guardianSet); + if(!signaturesValid){ + return (false, invalidReason); + } + + /// If we are here, we've validated the VM is a valid multi-sig that matches the guardianSet. + return (true, ""); + } + + + /** + * @dev verifySignatures serves to validate arbitrary sigatures against an arbitrary guardianSet + * - it intentionally does not solve for expectations within guardianSet (you should use verifyVM if you need these protections) + * - it intentioanlly does not solve for quorum (you should use verifyVM if you need these protections) + * - it intentionally returns true when signatures is an empty set (you should use verifyVM if you need these protections) + */ + function verifySignatures(bytes32 hash, Structs.Signature[] memory signatures, Structs.GuardianSet memory guardianSet) public pure returns (bool valid, string memory reason) { + uint8 lastIndex = 0; + uint256 guardianCount = guardianSet.keys.length; + for (uint i = 0; i < signatures.length; i++) { + Structs.Signature memory sig = signatures[i]; + address signatory = ecrecover(hash, sig.v, sig.r, sig.s); + // ecrecover returns 0 for invalid signatures. We explicitly require valid signatures to avoid unexpected + // behaviour due to the default storage slot value also being 0. + require(signatory != address(0), "ecrecover failed with signature"); + + /// Ensure that provided signature indices are ascending only + require(i == 0 || sig.guardianIndex > lastIndex, "signature indices must be ascending"); + lastIndex = sig.guardianIndex; + + /// @dev Ensure that the provided signature index is within the + /// bounds of the guardianSet. This is implicitly checked by the array + /// index operation below, so this check is technically redundant. + /// However, reverting explicitly here ensures that a bug is not + /// introduced accidentally later due to the nontrivial storage + /// semantics of solidity. + require(sig.guardianIndex < guardianCount, "guardian index out of bounds"); + + /// Check to see if the signer of the signature does not match a specific Guardian key at the provided index + if(signatory != guardianSet.keys[sig.guardianIndex]){ + return (false, "VM signature invalid"); + } + } + + /// If we are here, we've validated that the provided signatures are valid for the provided guardianSet + return (true, ""); + } + + /** + * @dev parseVM serves to parse an encodedVM into a vm struct + * - it intentionally performs no validation functions, it simply parses raw into a struct + */ + function parseVM(bytes memory encodedVM) public pure virtual returns (Structs.VM memory vm) { + uint index = 0; + + vm.version = encodedVM.toUint8(index); + index += 1; + // SECURITY: Note that currently the VM.version is not part of the hash + // and for reasons described below it cannot be made part of the hash. + // This means that this field's integrity is not protected and cannot be trusted. + // This is not a problem today since there is only one accepted version, but it + // could be a problem if we wanted to allow other versions in the future. + require(vm.version == 1, "VM version incompatible"); + + vm.guardianSetIndex = encodedVM.toUint32(index); + index += 4; + + // Parse Signatures + uint256 signersLen = encodedVM.toUint8(index); + index += 1; + vm.signatures = new Structs.Signature[](signersLen); + for (uint i = 0; i < signersLen; i++) { + vm.signatures[i].guardianIndex = encodedVM.toUint8(index); + index += 1; + + vm.signatures[i].r = encodedVM.toBytes32(index); + index += 32; + vm.signatures[i].s = encodedVM.toBytes32(index); + index += 32; + vm.signatures[i].v = encodedVM.toUint8(index) + 27; + index += 1; + } + + /* + Hash the body + + SECURITY: Do not change the way the hash of a VM is computed! + Changing it could result into two different hashes for the same observation. + But xDapps rely on the hash of an observation for replay protection. + */ + bytes memory body = encodedVM.slice(index, encodedVM.length - index); + vm.hash = keccak256(abi.encodePacked(keccak256(body))); + + // Parse the body + vm.timestamp = encodedVM.toUint32(index); + index += 4; + + vm.nonce = encodedVM.toUint32(index); + index += 4; + + vm.emitterChainId = encodedVM.toUint16(index); + index += 2; + + vm.emitterAddress = encodedVM.toBytes32(index); + index += 32; + + vm.sequence = encodedVM.toUint64(index); + index += 8; + + vm.consistencyLevel = encodedVM.toUint8(index); + index += 1; + + vm.payload = encodedVM.slice(index, encodedVM.length - index); + } + + /** + * @dev quorum serves solely to determine the number of signatures required to acheive quorum + */ + function quorum(uint numGuardians) public pure virtual returns (uint numSignaturesRequiredForQuorum) { + // The max number of guardians is 255 + require(numGuardians < 256, "too many guardians"); + return ((numGuardians * 2) / 3) + 1; + } +} \ No newline at end of file diff --git a/src/apps/wormhole/external/wormhole/Setters.sol b/src/apps/wormhole/external/wormhole/Setters.sol new file mode 100644 index 0000000..17f8828 --- /dev/null +++ b/src/apps/wormhole/external/wormhole/Setters.sol @@ -0,0 +1,57 @@ +// contracts/Setters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./State.sol"; + +contract Setters is State { + function updateGuardianSetIndex(uint32 newIndex) internal { + _state.guardianSetIndex = newIndex; + } + + function expireGuardianSet(uint32 index) internal { + _state.guardianSets[index].expirationTime = uint32(block.timestamp) + 86400; + } + + function storeGuardianSet(Structs.GuardianSet memory set, uint32 index) internal { + uint setLength = set.keys.length; + for (uint i = 0; i < setLength; i++) { + require(set.keys[i] != address(0), "Invalid key"); + } + _state.guardianSets[index] = set; + } + + function setInitialized(address implementatiom) internal { + _state.initializedImplementations[implementatiom] = true; + } + + function setGovernanceActionConsumed(bytes32 hash) internal { + _state.consumedGovernanceActions[hash] = true; + } + + function setChainId(uint16 chainId) internal { + _state.provider.chainId = chainId; + } + + function setGovernanceChainId(uint16 chainId) internal { + _state.provider.governanceChainId = chainId; + } + + function setGovernanceContract(bytes32 governanceContract) internal { + _state.provider.governanceContract = governanceContract; + } + + function setMessageFee(uint256 newFee) internal { + _state.messageFee = newFee; + } + + function setNextSequence(address emitter, uint64 sequence) internal { + _state.sequences[emitter] = sequence; + } + + function setEvmChainId(uint256 evmChainId) internal { + require(evmChainId == block.chainid, "invalid evmChainId"); + _state.evmChainId = evmChainId; + } +} \ No newline at end of file diff --git a/src/apps/wormhole/external/wormhole/State.sol b/src/apps/wormhole/external/wormhole/State.sol new file mode 100644 index 0000000..327e773 --- /dev/null +++ b/src/apps/wormhole/external/wormhole/State.sol @@ -0,0 +1,52 @@ +// contracts/State.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./Structs.sol"; + +contract Events { + event LogGuardianSetChanged( + uint32 oldGuardianIndex, + uint32 newGuardianIndex + ); + + event LogMessagePublished( + address emitter_address, + uint32 nonce, + bytes payload + ); +} + +contract Storage { + struct WormholeState { + Structs.Provider provider; + + // Mapping of guardian_set_index => guardian set + mapping(uint32 => Structs.GuardianSet) guardianSets; + + // Current active guardian set index + uint32 guardianSetIndex; + + // Period for which a guardian set stays active after it has been replaced + uint32 guardianSetExpiry; + + // Sequence numbers per emitter + mapping(address => uint64) sequences; + + // Mapping of consumed governance actions + mapping(bytes32 => bool) consumedGovernanceActions; + + // Mapping of initialized implementations + mapping(address => bool) initializedImplementations; + + uint256 messageFee; + + // EIP-155 Chain ID + uint256 evmChainId; + } +} + +contract State { + Storage.WormholeState _state; +} \ No newline at end of file diff --git a/src/apps/wormhole/external/wormhole/Structs.sol b/src/apps/wormhole/external/wormhole/Structs.sol new file mode 100644 index 0000000..b4d3329 --- /dev/null +++ b/src/apps/wormhole/external/wormhole/Structs.sol @@ -0,0 +1,40 @@ +// contracts/Structs.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +interface Structs { + struct Provider { + uint16 chainId; + uint16 governanceChainId; + bytes32 governanceContract; + } + + struct GuardianSet { + address[] keys; + uint32 expirationTime; + } + + struct Signature { + bytes32 r; + bytes32 s; + uint8 v; + uint8 guardianIndex; + } + + struct VM { + uint8 version; + uint32 timestamp; + uint32 nonce; + uint16 emitterChainId; + bytes32 emitterAddress; + uint64 sequence; + uint8 consistencyLevel; + bytes payload; + + uint32 guardianSetIndex; + Signature[] signatures; + + bytes32 hash; + } +} \ No newline at end of file diff --git a/src/apps/wormhole/external/wormhole/libraries/external/BytesLib.sol b/src/apps/wormhole/external/wormhole/libraries/external/BytesLib.sol new file mode 100644 index 0000000..58b8f51 --- /dev/null +++ b/src/apps/wormhole/external/wormhole/libraries/external/BytesLib.sol @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: Unlicense +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +pragma solidity >=0.8.0 <0.9.0; + + +library BytesLib { + function concat( + bytes memory _preBytes, + bytes memory _postBytes + ) + internal + pure + returns (bytes memory) + { + bytes memory tempBytes; + + assembly { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // Store the length of the first bytes array at the beginning of + // the memory for tempBytes. + let length := mload(_preBytes) + mstore(tempBytes, length) + + // Maintain a memory counter for the current write location in the + // temp bytes array by adding the 32 bytes for the array length to + // the starting location. + let mc := add(tempBytes, 0x20) + // Stop copying when the memory counter reaches the length of the + // first bytes array. + let end := add(mc, length) + + for { + // Initialize a copy counter to the start of the _preBytes data, + // 32 bytes into its memory. + let cc := add(_preBytes, 0x20) + } lt(mc, end) { + // Increase both counters by 32 bytes each iteration. + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // Write the _preBytes data into the tempBytes memory 32 bytes + // at a time. + mstore(mc, mload(cc)) + } + + // Add the length of _postBytes to the current length of tempBytes + // and store it as the new length in the first 32 bytes of the + // tempBytes memory. + length := mload(_postBytes) + mstore(tempBytes, add(length, mload(tempBytes))) + + // Move the memory counter back from a multiple of 0x20 to the + // actual end of the _preBytes data. + mc := end + // Stop copying when the memory counter reaches the new combined + // length of the arrays. + end := add(mc, length) + + for { + let cc := add(_postBytes, 0x20) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + // Update the free-memory pointer by padding our last write location + // to 32 bytes: add 31 bytes to the end of tempBytes to move to the + // next 32 byte block, then round down to the nearest multiple of + // 32. If the sum of the length of the two arrays is zero then add + // one before rounding down to leave a blank 32 bytes (the length block with 0). + mstore(0x40, and( + add(add(end, iszero(add(length, mload(_preBytes)))), 31), + not(31) // Round down to the nearest 32 bytes. + )) + } + + return tempBytes; + } + + function concatStorage(bytes storage _preBytes, bytes memory _postBytes) internal { + assembly { + // Read the first 32 bytes of _preBytes storage, which is the length + // of the array. (We don't need to use the offset into the slot + // because arrays use the entire slot.) + let fslot := sload(_preBytes.slot) + // Arrays of 31 bytes or less have an even value in their slot, + // while longer arrays have an odd value. The actual length is + // the slot divided by two for odd values, and the lowest order + // byte divided by two for even values. + // If the slot is even, bitwise and the slot with 255 and divide by + // two to get the length. If the slot is odd, bitwise and the slot + // with -1 and divide by two. + let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) + let mlength := mload(_postBytes) + let newlength := add(slength, mlength) + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + switch add(lt(slength, 32), lt(newlength, 32)) + case 2 { + // Since the new array still fits in the slot, we just need to + // update the contents of the slot. + // uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length + sstore( + _preBytes.slot, + // all the modifications to the slot are inside this + // next block + add( + // we can just add to the slot contents because the + // bytes we want to change are the LSBs + fslot, + add( + mul( + div( + // load the bytes from memory + mload(add(_postBytes, 0x20)), + // zero all bytes to the right + exp(0x100, sub(32, mlength)) + ), + // and now shift left the number of bytes to + // leave space for the length in the slot + exp(0x100, sub(32, newlength)) + ), + // increase length by the double of the memory + // bytes length + mul(mlength, 2) + ) + ) + ) + } + case 1 { + // The stored value fits in the slot, but the combined value + // will exceed it. + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // The contents of the _postBytes array start 32 bytes into + // the structure. Our first read should obtain the `submod` + // bytes that can fit into the unused space in the last word + // of the stored array. To get this, we read 32 bytes starting + // from `submod`, so the data we read overlaps with the array + // contents by `submod` bytes. Masking the lowest-order + // `submod` bytes allows us to add that value directly to the + // stored value. + + let submod := sub(32, slength) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore( + sc, + add( + and( + fslot, + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00 + ), + and(mload(mc), mask) + ) + ) + + for { + mc := add(mc, 0x20) + sc := add(sc, 1) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + sstore(sc, mload(mc)) + } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + default { + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + // Start copying to the last used word of the stored array. + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // Copy over the first `submod` bytes of the new data as in + // case 1 above. + let slengthmod := mod(slength, 32) + let mlengthmod := mod(mlength, 32) + let submod := sub(32, slengthmod) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore(sc, add(sload(sc), and(mload(mc), mask))) + + for { + sc := add(sc, 1) + mc := add(mc, 0x20) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + sstore(sc, mload(mc)) + } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + } + } + + function slice( + bytes memory _bytes, + uint256 _start, + uint256 _length + ) + internal + pure + returns (bytes memory) + { + require(_length + 31 >= _length, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) { + require(_bytes.length >= _start + 1 , "toUint8_outOfBounds"); + uint8 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x1), _start)) + } + + return tempUint; + } + + function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16) { + require(_bytes.length >= _start + 2, "toUint16_outOfBounds"); + uint16 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x2), _start)) + } + + return tempUint; + } + + function toUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32) { + require(_bytes.length >= _start + 4, "toUint32_outOfBounds"); + uint32 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x4), _start)) + } + + return tempUint; + } + + function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) { + require(_bytes.length >= _start + 8, "toUint64_outOfBounds"); + uint64 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x8), _start)) + } + + return tempUint; + } + + function toUint96(bytes memory _bytes, uint256 _start) internal pure returns (uint96) { + require(_bytes.length >= _start + 12, "toUint96_outOfBounds"); + uint96 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0xc), _start)) + } + + return tempUint; + } + + function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) { + require(_bytes.length >= _start + 16, "toUint128_outOfBounds"); + uint128 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x10), _start)) + } + + return tempUint; + } + + function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256) { + require(_bytes.length >= _start + 32, "toUint256_outOfBounds"); + uint256 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x20), _start)) + } + + return tempUint; + } + + function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) { + require(_bytes.length >= _start + 32, "toBytes32_outOfBounds"); + bytes32 tempBytes32; + + assembly { + tempBytes32 := mload(add(add(_bytes, 0x20), _start)) + } + + return tempBytes32; + } + + function equal(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) { + bool success = true; + + assembly { + let length := mload(_preBytes) + + // if lengths don't match the arrays are not equal + switch eq(length, mload(_postBytes)) + case 1 { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + let mc := add(_preBytes, 0x20) + let end := add(mc, length) + + for { + let cc := add(_postBytes, 0x20) + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + } eq(add(lt(mc, end), cb), 2) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // if any of these checks fails then arrays are not equal + if iszero(eq(mload(mc), mload(cc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } + + function equalStorage( + bytes storage _preBytes, + bytes memory _postBytes + ) + internal + view + returns (bool) + { + bool success = true; + + assembly { + // we know _preBytes_offset is 0 + let fslot := sload(_preBytes.slot) + // Decode the length of the stored array like in concatStorage(). + let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) + let mlength := mload(_postBytes) + + // if lengths don't match the arrays are not equal + switch eq(slength, mlength) + case 1 { + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + if iszero(iszero(slength)) { + switch lt(slength, 32) + case 1 { + // blank the last byte which is the length + fslot := mul(div(fslot, 0x100), 0x100) + + if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) { + // unsuccess: + success := 0 + } + } + default { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := keccak256(0x0, 0x20) + + let mc := add(_postBytes, 0x20) + let end := add(mc, mlength) + + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + for {} eq(add(lt(mc, end), cb), 2) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + if iszero(eq(sload(sc), mload(mc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } +} \ No newline at end of file diff --git a/src/apps/wormhole/interfaces/IWormhole.sol b/src/apps/wormhole/interfaces/IWormhole.sol new file mode 100644 index 0000000..29a0968 --- /dev/null +++ b/src/apps/wormhole/interfaces/IWormhole.sol @@ -0,0 +1,142 @@ +// contracts/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +interface IWormhole { + struct GuardianSet { + address[] keys; + uint32 expirationTime; + } + + struct Signature { + bytes32 r; + bytes32 s; + uint8 v; + uint8 guardianIndex; + } + + struct VM { + uint8 version; + uint32 timestamp; + uint32 nonce; + uint16 emitterChainId; + bytes32 emitterAddress; + uint64 sequence; + uint8 consistencyLevel; + bytes payload; + + uint32 guardianSetIndex; + Signature[] signatures; + + bytes32 hash; + } + + struct ContractUpgrade { + bytes32 module; + uint8 action; + uint16 chain; + + address newContract; + } + + struct GuardianSetUpgrade { + bytes32 module; + uint8 action; + uint16 chain; + + GuardianSet newGuardianSet; + uint32 newGuardianSetIndex; + } + + struct SetMessageFee { + bytes32 module; + uint8 action; + uint16 chain; + + uint256 messageFee; + } + + struct TransferFees { + bytes32 module; + uint8 action; + uint16 chain; + + uint256 amount; + bytes32 recipient; + } + + struct RecoverChainId { + bytes32 module; + uint8 action; + + uint256 evmChainId; + uint16 newChainId; + } + + event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel); + event ContractUpgraded(address indexed oldContract, address indexed newContract); + event GuardianSetAdded(uint32 indexed index); + + function publishMessage( + uint32 nonce, + bytes memory payload, + uint8 consistencyLevel + ) external payable returns (uint64 sequence); + + function initialize() external; + + function parseAndVerifyVM(bytes calldata encodedVM) external view returns (VM memory vm, bool valid, string memory reason); + + function verifyVM(VM memory vm) external view returns (bool valid, string memory reason); + + function verifySignatures(bytes32 hash, Signature[] memory signatures, GuardianSet memory guardianSet) external pure returns (bool valid, string memory reason); + + function parseVM(bytes memory encodedVM) external pure returns (VM memory vm); + + function quorum(uint numGuardians) external pure returns (uint numSignaturesRequiredForQuorum); + + function getGuardianSet(uint32 index) external view returns (GuardianSet memory); + + function getCurrentGuardianSetIndex() external view returns (uint32); + + function getGuardianSetExpiry() external view returns (uint32); + + function governanceActionIsConsumed(bytes32 hash) external view returns (bool); + + function isInitialized(address impl) external view returns (bool); + + function chainId() external view returns (uint16); + + function isFork() external view returns (bool); + + function governanceChainId() external view returns (uint16); + + function governanceContract() external view returns (bytes32); + + function messageFee() external view returns (uint256); + + function evmChainId() external view returns (uint256); + + function nextSequence(address emitter) external view returns (uint64); + + function parseContractUpgrade(bytes memory encodedUpgrade) external pure returns (ContractUpgrade memory cu); + + function parseGuardianSetUpgrade(bytes memory encodedUpgrade) external pure returns (GuardianSetUpgrade memory gsu); + + function parseSetMessageFee(bytes memory encodedSetMessageFee) external pure returns (SetMessageFee memory smf); + + function parseTransferFees(bytes memory encodedTransferFees) external pure returns (TransferFees memory tf); + + function parseRecoverChainId(bytes memory encodedRecoverChainId) external pure returns (RecoverChainId memory rci); + + function submitContractUpgrade(bytes memory _vm) external; + + function submitSetMessageFee(bytes memory _vm) external; + + function submitNewGuardianSet(bytes memory _vm) external; + + function submitTransferFees(bytes memory _vm) external; + + function submitRecoverChainId(bytes memory _vm) external; +} \ No newline at end of file diff --git a/test/wormhole/(wh)messages.t.sol b/test/wormhole/(wh)messages.t.sol new file mode 100644 index 0000000..a30a84d --- /dev/null +++ b/test/wormhole/(wh)messages.t.sol @@ -0,0 +1,164 @@ +// test/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../../src/apps/wormhole/external/wormhole/Messages.sol"; +import "../../src/apps/wormhole/external/wormhole/Setters.sol"; +import "../../src/apps/wormhole/external/wormhole/Structs.sol"; +import "forge-std/Test.sol"; + +contract ExportedMessages is Messages, Setters { + function storeGuardianSetPub(Structs.GuardianSet memory set, uint32 index) public { + return super.storeGuardianSet(set, index); + } +} + +contract TestMessages is Test { + address constant testGuardianPub = 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe; + + // A valid VM with one signature from the testGuardianPublic key + bytes validVM = hex"01000000000100867b55fec41778414f0683e80a430b766b78801b7070f9198ded5e62f48ac7a44b379a6cf9920e42dbd06c5ebf5ec07a934a00a572aefc201e9f91c33ba766d900000003e800000001000b0000000000000000000000000000000000000000000000000000000000000eee00000000000005390faaaa"; + + uint256 constant testGuardian = 93941733246223705020089879371323733820373732307041878556247502674739205313440; + + ExportedMessages messages; + + Structs.GuardianSet guardianSet; + + function setUp() public { + messages = new ExportedMessages(); + + // initialize guardian set with one guardian + address[] memory keys = new address[](1); + keys[0] = vm.addr(testGuardian); + guardianSet = Structs.GuardianSet(keys, 0); + require(messages.quorum(guardianSet.keys.length) == 1, "Quorum should be 1"); + } + + function testQuorum() public { + assertEq(messages.quorum(0), 1); + assertEq(messages.quorum(1), 1); + assertEq(messages.quorum(2), 2); + assertEq(messages.quorum(3), 3); + assertEq(messages.quorum(4), 3); + assertEq(messages.quorum(5), 4); + assertEq(messages.quorum(6), 5); + assertEq(messages.quorum(7), 5); + assertEq(messages.quorum(8), 6); + assertEq(messages.quorum(9), 7); + assertEq(messages.quorum(10), 7); + assertEq(messages.quorum(11), 8); + assertEq(messages.quorum(12), 9); + assertEq(messages.quorum(19), 13); + assertEq(messages.quorum(20), 14); + } + + function testQuorumCanAlwaysBeReached(uint256 numGuardians) public { + vm.assume(numGuardians > 0); + + if (numGuardians >= 256) { + vm.expectRevert("too many guardians"); + } + // test that quorums is never greater than the number of guardians + assert(messages.quorum(numGuardians) <= numGuardians); + } + + // This test ensures that submitting more signatures than expected will + // trigger a "guardian index out of bounds" error. + function testCannotVerifySignaturesWithOutOfBoundsSignature(bytes memory encoded) public { + vm.assume(encoded.length > 0); + + bytes32 message = keccak256(encoded); + + // First generate a legitimate signature. + Structs.Signature memory goodSignature = Structs.Signature(message, 0, 0, 0); + (goodSignature.v, goodSignature.r, goodSignature.s) = vm.sign(testGuardian, message); + assertEq(ecrecover(message, goodSignature.v, goodSignature.r, goodSignature.s), vm.addr(testGuardian)); + + // Reuse legitimate signature above for the next signature. This will + // bypass the "invalid signature" revert. + Structs.Signature memory outOfBoundsSignature = goodSignature; + outOfBoundsSignature.guardianIndex = 1; + + // Attempt to verify signatures. + Structs.Signature[] memory sigs = new Structs.Signature[](2); + sigs[0] = goodSignature; + sigs[1] = outOfBoundsSignature; + + vm.expectRevert("guardian index out of bounds"); + messages.verifySignatures(message, sigs, guardianSet); + } + + // This test ensures that submitting an invalid signature fails when + // verifySignatures is called. Calling ecrecover should fail. + function testCannotVerifySignaturesWithInvalidSignature(bytes memory encoded) public { + vm.assume(encoded.length > 0); + + bytes32 message = keccak256(encoded); + + // Generate an invalid signature. + Structs.Signature memory badSignature = Structs.Signature(message, 0, 0, 0); + assertEq(ecrecover(message, badSignature.v, badSignature.r, badSignature.s), address(0)); + + // Attempt to verify signatures. + Structs.Signature[] memory sigs = new Structs.Signature[](2); + sigs[0] = badSignature; + + vm.expectRevert("ecrecover failed with signature"); + messages.verifySignatures(message, sigs, guardianSet); + } + + function testVerifySignatures(bytes memory encoded) public { + vm.assume(encoded.length > 0); + + bytes32 message = keccak256(encoded); + + // Generate legitimate signature. + Structs.Signature memory goodSignature; + (goodSignature.v, goodSignature.r, goodSignature.s) = vm.sign(testGuardian, message); + assertEq(ecrecover(message, goodSignature.v, goodSignature.r, goodSignature.s), vm.addr(testGuardian)); + goodSignature.guardianIndex = 0; + + // Attempt to verify signatures. + Structs.Signature[] memory sigs = new Structs.Signature[](1); + sigs[0] = goodSignature; + + (bool valid, string memory reason) = messages.verifySignatures(message, sigs, guardianSet); + assertEq(valid, true); + assertEq(bytes(reason).length, 0); + } + + // This test checks the possibility of getting a unsigned message verified through verifyVM + function testHashMismatchedVMIsNotVerified() public { + // Set the initial guardian set + address[] memory initialGuardians = new address[](1); + initialGuardians[0] = testGuardianPub; + + // Create a guardian set + Structs.GuardianSet memory initialGuardianSet = Structs.GuardianSet({ + keys: initialGuardians, + expirationTime: 0 + }); + + messages.storeGuardianSetPub(initialGuardianSet, uint32(0)); + + // Confirm that the test VM is valid + (Structs.VM memory parsedValidVm, bool valid, string memory reason) = messages.parseAndVerifyVM(validVM); + require(valid, reason); + assertEq(valid, true); + assertEq(reason, ""); + + // Manipulate the payload of the vm + Structs.VM memory invalidVm = parsedValidVm; + invalidVm.payload = abi.encodePacked( + parsedValidVm.payload, + "malicious bytes in payload" + ); + + // Confirm that the verifyVM fails on invalid VM + (valid, reason) = messages.verifyVM(invalidVm); + assertEq(valid, false); + assertEq(reason, "vm.hash doesn't match body"); + } +} diff --git a/test/wormhole/roundtrip.t.sol b/test/wormhole/roundtrip.t.sol new file mode 100644 index 0000000..c8200ea --- /dev/null +++ b/test/wormhole/roundtrip.t.sol @@ -0,0 +1,160 @@ +// test/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../../src/apps/wormhole/external/wormhole/Messages.sol"; +import "../../src/apps/wormhole/external/wormhole/Setters.sol"; +import "../../src/apps/wormhole/external/wormhole/Structs.sol"; +import { IMessageEscrowStructs } from "../../src/interfaces/IMessageEscrowStructs.sol"; +import { WormholeVerifier } from "../../src/apps/wormhole/external/callworm/WormholeVerifier.sol"; +import { SmallStructs } from "../../src/apps/wormhole/external/callworm/SmallStructs.sol"; +import { IncentivizedWormholeEscrow } from "../../src/apps/wormhole/IncentivizedWormholeEscrow.sol"; +import { Bytes65 } from "../../src/utils/Bytes65.sol"; +import "../../src/interfaces/IIncentivizedMessageEscrow.sol"; +import "forge-std/Test.sol"; + +contract ExportedMessages is Messages, Setters { + + function storeGuardianSetPub(Structs.GuardianSet memory set, uint32 index) public { + return super.storeGuardianSet(set, index); + } + + event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel); + + function publishMessage( + uint32 nonce, + bytes memory payload, + uint8 consistencyLevel + ) external payable returns (uint64 sequence) { + sequence = nextSequence(msg.sender); + emit LogMessagePublished( + msg.sender, + sequence, + nonce, + payload, + consistencyLevel + ); + } +} + +contract TestRoundtrip is Test, IMessageEscrowStructs, Bytes65 { + event Debug(bytes); + event Debug(bytes32); + IncentiveDescription _INCENTIVE = IncentiveDescription({ + maxGasDelivery: 1199199, + maxGasAck: 1188188, + refundGasTo: address(uint160(1)), + priceOfDeliveryGas: 123321, + priceOfAckGas: 321123, + targetDelta: 30 minutes + }); + + function _getTotalIncentive(IncentiveDescription memory incentive) internal pure returns(uint256) { + return incentive.maxGasDelivery * incentive.priceOfDeliveryGas + incentive.maxGasAck * incentive.priceOfAckGas; + } + + event WormholeMessage(bytes32 destinationIdentifier, bytes32 recipitent); + + bytes32 _DESTINATION_IDENTIFIER; + + address testGuardianPub; + uint256 testGuardian; + + ExportedMessages messages; + + IIncentivizedMessageEscrow public escrow; + + Structs.GuardianSet guardianSet; + + function setUp() public { + (testGuardianPub, testGuardian) = makeAddrAndKey("signer"); + + messages = new ExportedMessages(); + + escrow = new IncentivizedWormholeEscrow(address(messages)); + + _DESTINATION_IDENTIFIER = bytes32(uint256(messages.chainId())); + + escrow.setRemoteEscrowImplementation(_DESTINATION_IDENTIFIER, abi.encode(address(escrow))); + + // initialize guardian set with one guardian + address[] memory keys = new address[](1); + keys[0] = vm.addr(testGuardian); + guardianSet = Structs.GuardianSet(keys, 0); + require(messages.quorum(guardianSet.keys.length) == 1, "Quorum should be 1"); + + // Set the initial guardian set + address[] memory initialGuardians = new address[](1); + initialGuardians[0] = testGuardianPub; + + // Create a guardian set + Structs.GuardianSet memory initialGuardianSet = Structs.GuardianSet({ + keys: initialGuardians, + expirationTime: 0 + }); + + messages.storeGuardianSetPub(initialGuardianSet, uint32(0)); + } + + function makeValidVM(bytes memory payload, uint16 emitterChainid, bytes32 emitterAddress) internal returns(bytes memory validVM) { + bytes memory presigsVM = abi.encodePacked( + uint8(1), // version + uint32(0), // guardianSetIndex + uint8(1) // signersLen + ); + bytes memory postsigsVM = abi.encodePacked( + uint32(block.timestamp), + uint32(0), // nonce + emitterChainid, + emitterAddress, + uint64(0), // sequence + uint8(0), // consistencyLevel + payload + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(testGuardian, keccak256(abi.encodePacked(keccak256(postsigsVM)))); + + validVM = abi.encodePacked( + presigsVM, + uint8(0), + r, s, v - 27, + postsigsVM + ); + } + + function test_escrow_wormhole_message(bytes calldata message) public { + vm.assume(message.length != 0); + + IncentiveDescription storage incentive = _INCENTIVE; + + vm.recordLogs(); + (uint256 gasRefund, bytes32 messageIdentifier) = escrow.escrowMessage{value: _getTotalIncentive(_INCENTIVE)}( + _DESTINATION_IDENTIFIER, + convertEVMTo65(address(this)), + message, + incentive + ); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + (uint64 sequence, uint32 nonce, bytes memory payload, uint8 consistencyLevel) = abi.decode( + entries[2].data, + (uint64, uint32, bytes, uint8) + ); + + bytes memory validVM = makeValidVM(payload, uint16(uint256(_DESTINATION_IDENTIFIER)), bytes32(uint256(uint160(address(escrow))))); + + vm.recordLogs(); + escrow.processMessage(hex"", validVM, bytes32(uint256(0xdead))); + entries = vm.getRecordedLogs(); + + (sequence, nonce, payload, consistencyLevel) = abi.decode( + entries[2].data, + (uint64, uint32, bytes, uint8) + ); + + validVM = makeValidVM(payload, uint16(uint256(_DESTINATION_IDENTIFIER)), bytes32(uint256(uint160(address(escrow))))); + + escrow.processMessage(hex"", validVM, bytes32(uint256(0xdead))); + } +} diff --git a/test/wormhole/verifyMessage2.t.sol b/test/wormhole/verifyMessage2.t.sol new file mode 100644 index 0000000..b9b315b --- /dev/null +++ b/test/wormhole/verifyMessage2.t.sol @@ -0,0 +1,151 @@ +// test/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../../src/apps/wormhole/external/wormhole/Messages.sol"; +import "../../src/apps/wormhole/external/wormhole/Setters.sol"; +import "../../src/apps/wormhole/external/wormhole/Structs.sol"; +import { WormholeVerifier } from "../../src/apps/wormhole/external/callworm/WormholeVerifier.sol"; +import { SmallStructs } from "../../src/apps/wormhole/external/callworm/SmallStructs.sol"; +import "forge-std/Test.sol"; + +contract ExportedMessages is Messages, Setters { + function storeGuardianSetPub(Structs.GuardianSet memory set, uint32 index) public { + return super.storeGuardianSet(set, index); + } +} + +contract TestMessagesC2Sigs is Test { + address constant testGuardianPub1 = 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe; + address testGuardianPub2; + + // A valid VM with one signature from the testGuardianPublic key + bytes prevalidVM = hex"01000000000200867b55fec41778414f0683e80a430b766b78801b7070f9198ded5e62f48ac7a44b379a6cf9920e42dbd06c5ebf5ec07a934a00a572aefc201e9f91c33ba766d900"; + bytes postvalidVM = hex"000003e800000001000b0000000000000000000000000000000000000000000000000000000000000eee00000000000005390faaaa"; + bytes32 vmHash; + bytes validVM; + + uint256 constant testGuardian1 = 93941733246223705020089879371323733820373732307041878556247502674739205313440; + uint256 testGuardian2; + + ExportedMessages messages; + + WormholeVerifier messages2; + + Structs.GuardianSet guardianSet; + + function setUp() public { + + + (testGuardianPub2, testGuardian2) = makeAddrAndKey("signer2"); + vmHash = keccak256(abi.encodePacked(keccak256(postvalidVM))); + + // Make validVM + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(testGuardian2, vmHash); + + validVM = abi.encodePacked( + prevalidVM, + uint8(1), + r, s, v - 27, + postvalidVM + ); + + messages = new ExportedMessages(); + + messages2 = new WormholeVerifier(address(messages)); + + // initialize guardian set with one guardian + address[] memory keys = new address[](2); + keys[0] = vm.addr(testGuardian1); + keys[1] = vm.addr(testGuardian2); + guardianSet = Structs.GuardianSet(keys, 0); + require(messages.quorum(guardianSet.keys.length) == 2, "Quorum should be 2"); + } + + // This test checks the possibility of getting a unsigned message verified through verifyVM + function test_compare_wormhole_implementation_and_calldata_version() public { + // Set the initial guardian set + address[] memory initialGuardians = new address[](2); + initialGuardians[0] = testGuardianPub1; + initialGuardians[1] = testGuardianPub2; + + // Create a guardian set + Structs.GuardianSet memory initialGuardianSet = Structs.GuardianSet({ + keys: initialGuardians, + expirationTime: 0 + }); + + messages.storeGuardianSetPub(initialGuardianSet, uint32(0)); + + // Confirm that the test VM is valid + (Structs.VM memory parsedValidVm, bool valid, string memory reason) = messages.parseAndVerifyVM(validVM); + ( + SmallStructs.SmallVM memory smallVM, + bytes memory payload, + bool valid2, + string memory reason2 + ) = messages2.parseAndVerifyVM(validVM); + + require(valid, reason); + assertEq(valid, true); + assertEq(reason, ""); + + assertEq( + valid, valid2 + ); + assertEq( + reason, reason2 + ); + + assertEq( + parsedValidVm.payload, payload, "payload" + ); + assertEq( + parsedValidVm.emitterChainId, smallVM.emitterChainId, "emitterChainId" + ); + assertEq( + parsedValidVm.emitterAddress, smallVM.emitterAddress, "emitterAddress" + ); + assertEq( + parsedValidVm.guardianSetIndex, smallVM.guardianSetIndex, "guardianSetIndex" + ); + } + + function test_error_invalid_vm() public { + // Set the initial guardian set + address[] memory initialGuardians = new address[](2); + initialGuardians[0] = testGuardianPub1; + initialGuardians[1] = testGuardianPub2; + + // Create a guardian set + Structs.GuardianSet memory initialGuardianSet = Structs.GuardianSet({ + keys: initialGuardians, + expirationTime: 0 + }); + + messages.storeGuardianSetPub(initialGuardianSet, uint32(0)); + bytes memory invalidVM = abi.encodePacked(validVM, uint8(1)); + + // Confirm that the test VM is valid + (Structs.VM memory parsedInValidVm, bool valid, string memory reason) = messages.parseAndVerifyVM(invalidVM); + ( + SmallStructs.SmallVM memory smallVM, + bytes memory payload, + bool valid2, + string memory reason2 + ) = messages2.parseAndVerifyVM(invalidVM); + + + assertEq( + valid, valid2 + ); + assertEq( + reason, reason2 + ); + + assertEq(valid2, false); + assertEq(reason2, "VM signature invalid"); + } +} diff --git a/test/wormhole/verifyMessages.t.sol b/test/wormhole/verifyMessages.t.sol new file mode 100644 index 0000000..2dcc5f2 --- /dev/null +++ b/test/wormhole/verifyMessages.t.sol @@ -0,0 +1,127 @@ +// test/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../../src/apps/wormhole/external/wormhole/Messages.sol"; +import "../../src/apps/wormhole/external/wormhole/Setters.sol"; +import "../../src/apps/wormhole/external/wormhole/Structs.sol"; +import { WormholeVerifier } from "../../src/apps/wormhole/external/callworm/WormholeVerifier.sol"; +import { SmallStructs } from "../../src/apps/wormhole/external/callworm/SmallStructs.sol"; +import "forge-std/Test.sol"; + +contract ExportedMessages is Messages, Setters { + function storeGuardianSetPub(Structs.GuardianSet memory set, uint32 index) public { + return super.storeGuardianSet(set, index); + } +} + +contract TestMessagesC is Test { + address constant testGuardianPub = 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe; + + // A valid VM with one signature from the testGuardianPublic key + bytes validVM = hex"01000000000100867b55fec41778414f0683e80a430b766b78801b7070f9198ded5e62f48ac7a44b379a6cf9920e42dbd06c5ebf5ec07a934a00a572aefc201e9f91c33ba766d900000003e800000001000b0000000000000000000000000000000000000000000000000000000000000eee00000000000005390faaaa"; + + uint256 constant testGuardian = 93941733246223705020089879371323733820373732307041878556247502674739205313440; + + ExportedMessages messages; + + WormholeVerifier messages2; + + Structs.GuardianSet guardianSet; + + function setUp() public { + messages = new ExportedMessages(); + + messages2 = new WormholeVerifier(address(messages)); + + // initialize guardian set with one guardian + address[] memory keys = new address[](1); + keys[0] = vm.addr(testGuardian); + guardianSet = Structs.GuardianSet(keys, 0); + require(messages.quorum(guardianSet.keys.length) == 1, "Quorum should be 1"); + } + + // This test checks the possibility of getting a unsigned message verified through verifyVM + function test_compare_wormhole_implementation_and_calldata_version() public { + // Set the initial guardian set + address[] memory initialGuardians = new address[](1); + initialGuardians[0] = testGuardianPub; + + // Create a guardian set + Structs.GuardianSet memory initialGuardianSet = Structs.GuardianSet({ + keys: initialGuardians, + expirationTime: 0 + }); + + messages.storeGuardianSetPub(initialGuardianSet, uint32(0)); + + // Confirm that the test VM is valid + (Structs.VM memory parsedValidVm, bool valid, string memory reason) = messages.parseAndVerifyVM(validVM); + ( + SmallStructs.SmallVM memory smallVM, + bytes memory payload, + bool valid2, + string memory reason2 + ) = messages2.parseAndVerifyVM(validVM); + + require(valid, reason); + assertEq(valid, true); + assertEq(reason, ""); + + assertEq( + valid, valid2 + ); + assertEq( + reason, reason2 + ); + + assertEq( + parsedValidVm.payload, payload, "payload" + ); + assertEq( + parsedValidVm.emitterChainId, smallVM.emitterChainId, "emitterChainId" + ); + assertEq( + parsedValidVm.emitterAddress, smallVM.emitterAddress, "emitterAddress" + ); + assertEq( + parsedValidVm.guardianSetIndex, smallVM.guardianSetIndex, "guardianSetIndex" + ); + } + + function test_error_invalid_vm() public { + // Set the initial guardian set + address[] memory initialGuardians = new address[](1); + initialGuardians[0] = testGuardianPub; + + // Create a guardian set + Structs.GuardianSet memory initialGuardianSet = Structs.GuardianSet({ + keys: initialGuardians, + expirationTime: 0 + }); + + messages.storeGuardianSetPub(initialGuardianSet, uint32(0)); + bytes memory invalidVM = abi.encodePacked(validVM, uint8(1)); + + // Confirm that the test VM is valid + (Structs.VM memory parsedInValidVm, bool valid, string memory reason) = messages.parseAndVerifyVM(invalidVM); + ( + SmallStructs.SmallVM memory smallVM, + bytes memory payload, + bool valid2, + string memory reason2 + ) = messages2.parseAndVerifyVM(invalidVM); + + + assertEq( + valid, valid2 + ); + assertEq( + reason, reason2 + ); + + assertEq(valid2, false); + assertEq(reason2, "VM signature invalid"); + } +} From 7ef72c951abcc6ac80ccccc96c1ed0f0e5b6e366 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 13 Oct 2023 21:24:01 +0200 Subject: [PATCH 6/7] Update test.yml Set workflow to always run --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09880b1..fa50715 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: test -on: workflow_dispatch +on: [push, fork] env: FOUNDRY_PROFILE: ci From 162433f70b3181b65d533ddcf6830fc96dfe2b01 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 17 Oct 2023 13:09:17 +0200 Subject: [PATCH 7/7] feat: return bomb protection by inline assembly call. (#7) * feat: return bomb protection by inline assembly call. * fix: correctly name the returnbomb test * chore: more comments --- .gas-snapshot | 27 ++++---- src/IncentivizedMessageEscrow.sol | 14 +++- .../processMessage/ReturnBomb.t.sol | 65 +++++++++++++++++++ test/TestCommon.t.sol | 4 +- test/mocks/BadContract.sol | 5 +- test/mocks/ReturnBomber.sol | 56 ++++++++++++++++ 6 files changed, 151 insertions(+), 20 deletions(-) create mode 100644 test/IncentivizedMessageEscrow/processMessage/ReturnBomb.t.sol create mode 100644 test/mocks/ReturnBomber.sol diff --git a/.gas-snapshot b/.gas-snapshot index d72a37e..f5af47a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,12 +1,12 @@ -AckReentryTest:test_reentry_on_ack_message() (gas: 531516) +AckReentryTest:test_reentry_on_ack_message() (gas: 531465) CallReentryTest:test_reentry_on_call_message() (gas: 560516) EscrowInformationTest:test_check_escrow_events() (gas: 100189) EscrowInformationTest:test_check_escrow_state() (gas: 97583) -EscrowInformationTest:test_gas_refund(uint256) (runs: 256, μ: 148866, ~: 153744) +EscrowInformationTest:test_gas_refund(uint256) (runs: 256, μ: 149291, ~: 153744) EscrowWrongGasPaymentTest:test_fail_not_enough_gas_sent() (gas: 94126) EscrowWrongGasPaymentTest:test_place_incentive() (gas: 90028) -GasSpendControlTest:test_fail_relayer_has_to_provide_enough_gas() (gas: 610477) -GasSpendControlTest:test_process_ack_gas() (gas: 626132) +GasSpendControlTest:test_fail_relayer_has_to_provide_enough_gas() (gas: 605769) +GasSpendControlTest:test_process_ack_gas() (gas: 621372) GasSpendControlTest:test_process_delivery_gas() (gas: 361159) IncreaseBountyTest:test_fail_bounty_does_not_exist() (gas: 18092) IncreaseBountyTest:test_fail_overpay() (gas: 128551) @@ -18,20 +18,21 @@ MessageIdentifierTest:test_non_unique_bounty(bytes) (runs: 256, μ: 105179, ~: 1 MessageIdentifierTest:test_unique_identifier_block_10() (gas: 97356) MessageIdentifierTest:test_unique_identifier_block_11() (gas: 97312) NoImplementationAddressSetTest:test_error_no_implementation_address_set() (gas: 342409) -ProcessMessageAckTest:test_ack_called_event() (gas: 219107) -ProcessMessageAckTest:test_ack_different_recipitents() (gas: 253864) -ProcessMessageAckTest:test_ack_less_time_than_expected(uint64,uint64) (runs: 256, μ: 256527, ~: 258708) -ProcessMessageAckTest:test_ack_more_time_than_expected(uint64,uint64) (runs: 256, μ: 259487, ~: 259498) -ProcessMessageAckTest:test_ack_process_message() (gas: 214314) +ProcessMessageAckTest:test_ack_called_event() (gas: 219056) +ProcessMessageAckTest:test_ack_different_recipitents() (gas: 253702) +ProcessMessageAckTest:test_ack_less_time_than_expected(uint64,uint64) (runs: 256, μ: 256715, ~: 258585) +ProcessMessageAckTest:test_ack_more_time_than_expected(uint64,uint64) (runs: 256, μ: 259370, ~: 259381) +ProcessMessageAckTest:test_ack_process_message() (gas: 214263) ProcessMessageCallTest:test_call_process_message() (gas: 177646) ProcessMessageCallTest:test_call_process_message_twice() (gas: 173180) ProcessMessageCallTest:test_expect_caller(address) (runs: 256, μ: 228411, ~: 228411) -ProcessMessageNoReceiveTest:test_application_does_not_implement_interface() (gas: 176127) +ProcessMessageNoReceiveTest:test_application_does_not_implement_interface() (gas: 174683) +ReturnBombTest:test_process_ack_gas() (gas: 5338932) SendMessagePaymentTest:test_error_send_message_without_additional_cost() (gas: 110120) SendMessagePaymentTest:test_estimate_cost() (gas: 8086) SendMessagePaymentTest:test_process_message_with_additional_payment(bytes) (runs: 256, μ: 175500, ~: 175286) SendMessagePaymentTest:test_process_message_without_additional_payment(bytes) (runs: 256, μ: 176060, ~: 175846) SendMessagePaymentTest:test_send_message_with_additional_cost() (gas: 102845) -TargetDeltaZeroTest:test_target_delta_zero(uint16) (runs: 256, μ: 257346, ~: 257346) -TimeOverflowTest:test_larger_than_uint_time_is_fine() (gas: 253582) -TimeOverflowTest:test_overflow_in_unchecked_is_fine() (gas: 255794) \ No newline at end of file +TargetDeltaZeroTest:test_target_delta_zero(uint16) (runs: 256, μ: 257184, ~: 257184) +TimeOverflowTest:test_larger_than_uint_time_is_fine() (gas: 253420) +TimeOverflowTest:test_overflow_in_unchecked_is_fine() (gas: 255632) \ No newline at end of file diff --git a/src/IncentivizedMessageEscrow.sol b/src/IncentivizedMessageEscrow.sol index 554051f..929352a 100644 --- a/src/IncentivizedMessageEscrow.sol +++ b/src/IncentivizedMessageEscrow.sol @@ -349,9 +349,17 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes // Ensure that if the call reverts it doesn't boil up. // We don't need any return values and don't care if the call reverts. // This call implies we need reentry protection, since we need to call it before we delete the incentive map. - fromApplication.call{gas: maxGasAck}( - abi.encodeWithSignature("ackMessage(bytes32,bytes32,bytes)", destinationIdentifier, messageIdentifier, message[CTX1_MESSAGE_START: ]) - ); + bytes memory payload = abi.encodeWithSignature("ackMessage(bytes32,bytes32,bytes)", destinationIdentifier, messageIdentifier, message[CTX1_MESSAGE_START: ]); + assembly ("memory-safe") { + // Because Solidity always create RETURNDATACOPY for external calls, even low-level calls where no variables are assigned, + // the contract can be attacked by a so called return bomb. This incur additional cost to the relayer they aren't paid for. + // To protect the relayer, the call is made in inline assembly. + let success := call(maxGasAck, fromApplication, 0, add(payload, 0x20), mload(payload), 0, 0) + // This is what the call would look like non-assembly. + // fromApplication.call{gas: maxGasAck}( + // abi.encodeWithSignature("ackMessage(bytes32,bytes32,bytes)", destinationIdentifier, messageIdentifier, message[CTX1_MESSAGE_START: ]) + // ); + } // Get the gas used by the destination call. uint256 gasSpentOnDestination = uint48(bytes6(message[CTX1_GAS_SPENT_START:CTX1_GAS_SPENT_END])); diff --git a/test/IncentivizedMessageEscrow/processMessage/ReturnBomb.t.sol b/test/IncentivizedMessageEscrow/processMessage/ReturnBomb.t.sol new file mode 100644 index 0000000..db65899 --- /dev/null +++ b/test/IncentivizedMessageEscrow/processMessage/ReturnBomb.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import { TestCommon } from "../../TestCommon.t.sol"; +import { ReturnBomber } from "../../mocks/ReturnBomber.sol"; +import { ICrossChainReceiver } from "../../../src/interfaces/ICrossChainReceiver.sol"; + + +contract ReturnBombTest is TestCommon { + event Message( + bytes32 destinationIdentifier, + bytes recipitent, + bytes message + ); + + bytes _DESTINATION_ADDRESS_SPENDGAS; + + function setUp() override public { + super.setUp(); + // Set a new application + application = ICrossChainReceiver(address(new ReturnBomber(address(escrow)))); + + // Set implementations to the escrow address. + vm.prank(address(application)); + escrow.setRemoteEscrowImplementation(_DESTINATION_IDENTIFIER, abi.encode(address(escrow))); + + _DESTINATION_ADDRESS_APPLICATION = abi.encodePacked( + uint8(20), + bytes32(0), + bytes32(uint256(uint160(address(application)))) + ); + } + + + function test_process_ack_gas() public { + bytes32 destinationFeeRecipitent = bytes32(uint256(uint160(address(this)))); + + _INCENTIVE.maxGasAck = 10000000; // This is not enough gas to execute the Ack. We should expect the sub-call to revert but the main call shouldn't. + + (, bytes memory messageWithContext) = setupForAck(address(application), abi.encodePacked(bytes2(uint16(1))), destinationFeeRecipitent); + + + (uint8 v, bytes32 r, bytes32 s) = signMessageForMock(messageWithContext); + bytes memory mockContext = abi.encode(v, r, s); + + uint256 beforeReturnBomb = gasleft(); + escrow.processMessage( + mockContext, + messageWithContext, + destinationFeeRecipitent + ); + uint256 afterReturnBomb = gasleft(); + + assertGt( + _INCENTIVE.maxGasAck, + beforeReturnBomb - afterReturnBomb, + "Return bomb used more gas than expected" + ); + } + + // relayer incentives will be sent here + receive() payable external { + } +} \ No newline at end of file diff --git a/test/TestCommon.t.sol b/test/TestCommon.t.sol index fd6acc7..e9009f9 100644 --- a/test/TestCommon.t.sol +++ b/test/TestCommon.t.sol @@ -20,9 +20,9 @@ interface ICanEscrowMessage is IMessageEscrowStructs{ contract TestCommon is Test, IMessageEscrowEvents, IMessageEscrowStructs { - uint256 constant GAS_SPENT_ON_SOURCE = 6397; + uint256 constant GAS_SPENT_ON_SOURCE = 6346; uint256 constant GAS_SPENT_ON_DESTINATION = 33443; - uint256 constant GAS_RECEIVE_CONSTANT = 6178448034; + uint256 constant GAS_RECEIVE_CONSTANT = 6162070761; bytes32 constant _DESTINATION_IDENTIFIER = bytes32(uint256(0x123123) + uint256(2**255)); diff --git a/test/mocks/BadContract.sol b/test/mocks/BadContract.sol index 91eb78e..32cd677 100644 --- a/test/mocks/BadContract.sol +++ b/test/mocks/BadContract.sol @@ -8,11 +8,12 @@ import { ICrossChainReceiver } from "../../src/interfaces/ICrossChainReceiver.so * @title BadContract */ contract BadContract is ICrossChainReceiver { - function ackMessage(bytes32 /* destinationIdentifier */, bytes32 /* messageIdentifier */, bytes calldata acknowledgement) pure external { + function ackMessage(bytes32 /* destinationIdentifier */, bytes32 /* messageIdentifier */, bytes calldata /* acknowledgement */) pure external { require(false); } - function receiveMessage(bytes32 /* sourceIdentifierbytes */, bytes32 /* messageIdentifier */, bytes calldata /* fromApplication */, bytes calldata message) pure external returns(bytes memory acknowledgement) { + function receiveMessage(bytes32 /* sourceIdentifierbytes */, bytes32 /* messageIdentifier */, bytes calldata /* fromApplication */, bytes calldata /* message */) pure external returns(bytes memory acknowledgement) { require(false); + return acknowledgement = abi.encode(); } } diff --git a/test/mocks/ReturnBomber.sol b/test/mocks/ReturnBomber.sol new file mode 100644 index 0000000..c5d10b4 --- /dev/null +++ b/test/mocks/ReturnBomber.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { IIncentivizedMessageEscrow } from "../../src/interfaces/IIncentivizedMessageEscrow.sol"; +import { ICrossChainReceiver } from "../../src/interfaces/ICrossChainReceiver.sol"; + +/** + * @title ReturnBomber + * This contract tries to return bomb (https://github.com/ethereum/solidity/issues/12306) + * the incentive contract when ackMessage is called. + */ +contract ReturnBomber is ICrossChainReceiver { + IIncentivizedMessageEscrow immutable MESSAGE_ESCROW; + + constructor(address messageEscrow_) { + MESSAGE_ESCROW = IIncentivizedMessageEscrow(messageEscrow_); + } + + function escrowMessage( + bytes32 destinationIdentifier, + bytes calldata destinationAddress, + bytes calldata message, + IIncentivizedMessageEscrow.IncentiveDescription calldata incentive + ) external payable returns(uint256 gasRefund, bytes32 messageIdentifier) { + (gasRefund, messageIdentifier) = MESSAGE_ESCROW.escrowMessage{value: msg.value}( + destinationIdentifier, + destinationAddress, + message, + incentive + ); + + // emit EscrowMessage(gasRefund, messageIdentifier); + } + + function ackMessage(bytes32 /* destinationIdentifier */, bytes32 /* messageIdentifier */, bytes calldata /* acknowledgement */) view external { + // approximate solution to Cmem for new_mem_size_words + uint256 rsize = sqrt(gasleft() / 2 * 512); + assembly { + return(0x0, mul(rsize, 0x20)) + } + } + + function sqrt(uint x) private pure returns (uint y) { + uint z = (x + 1) / 2; + y = x; + while (z < y) { + y = z; + z = (x / z + z) / 2; + } + } + + function receiveMessage(bytes32 /* sourceIdentifierbytes */, bytes32 /* messageIdentifier */, bytes calldata /* fromApplication */, bytes calldata /* message */) pure external returns(bytes memory acknowledgement) { + require(false); + return acknowledgement = abi.encode(); + } +}