Skip to content

Commit

Permalink
Contracts/communication synapse module (#2050)
Browse files Browse the repository at this point in the history
* Isolate some common logic into abstract contract

* Scaffold `ThresholdECDSAModule`

* Add more views

* Implement management tests

* Implement management actions

* Add `InterchainDB` Mock

* Reorg events following #2049

* Also emit ethSingedMsg hash

* More `InterchainModule` errors

* Implement tests for fee collector management

* Fix: use the entry hash for signing

* Implement `feeCollector` management

* Start doing source tests

* Add GasOracle interface

* Update `verifyEntry()` interface

* Add source chain tests

* Implement source chain funcs

* Add tests: `bytes` for signatures instead of `bytes[]`

* Implement library update

* Simplify math

* Implement `verifyEntry()`

* Add tests for destination chain

* Don't use infinity as default threshold

* Check source chain id in any InterchainModule

* Deprecate old SynapseModule

* Fix ClientV1 test

* Chore: cleanup

* Chore: renamoooor

* Revert "Fix ClientV1 test"

This reverts commit e724c38.

* Revert "Chore: cleanup"

This reverts commit 41949cd.

* Fix ClientV1 test

* Chore: cleanup
  • Loading branch information
ChiTimesChi authored Feb 19, 2024
1 parent 00da108 commit c9a789f
Show file tree
Hide file tree
Showing 20 changed files with 1,104 additions and 548 deletions.
12 changes: 0 additions & 12 deletions packages/contracts-communication/contracts/InterchainModule.sol

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {InterchainEntry} from "../libs/InterchainEntry.sol";

abstract contract InterchainModuleEvents {
event VerificationRequested(uint256 indexed destChainId, bytes entry, bytes32 ethSignedEntryHash);

event EntryVerified(InterchainEntry entry);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {InterchainEntry} from "../libs/InterchainEntry.sol";

abstract contract SynapseModuleEvents {
event VerificationRequested(uint256 indexed destChainId, InterchainEntry entry, bytes32 signableEntryHash);
event EntryVerified(InterchainEntry entry, bytes32 signableEntryHash);
event VerifierAdded(address verifier);
event VerifierRemoved(address verifier);
event ThresholdChanged(uint256 threshold);

event FeeCollectorChanged(address feeCollector);
event GasOracleChanged(address gasOracle);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IGasOracle {
/// @notice Convert a value from the native token of a remote chain to the local native token.
/// @dev Will revert if no price is available for the remote chain.
/// @param remoteChainId The chain id of the remote chain.
/// @param value The value to convert.
function convertRemoteValueToLocalUnits(uint256 remoteChainId, uint256 value) external view returns (uint256);

/// @notice Estimate the cost of execution a transaction on a remote chain,
/// and convert it to the local native token.
/// @dev Will revert if no price is available for the remote chain.
/// @param remoteChainId The chain id of the remote chain.
/// @param gasLimit The gas limit of the transaction.
/// @param calldataSize The size of the transaction calldata.
function estimateTxCostInLocalUnits(
uint256 remoteChainId,
uint256 gasLimit,
uint256 calldataSize
)
external
view
returns (uint256);

/// @notice Estimate the cost of execution a transaction on a remote chain,
/// and return it as is in the remote chain's native token.
/// @dev Will revert if no price is available for the remote chain.
/// @param remoteChainId The chain id of the remote chain.
/// @param gasLimit The gas limit of the transaction.
/// @param calldataSize The size of the transaction calldata.
function estimateTxCostInRemoteUnits(
uint256 remoteChainId,
uint256 gasLimit,
uint256 calldataSize
)
external
view
returns (uint256);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import {InterchainEntry} from "../libs/InterchainEntry.sol";
/// @notice Every Module may opt a different method to confirm the verified entries on destination chain,
/// therefore this is not a part of a common interface.
interface IInterchainModule {
error InterchainModule__NotInterchainDB();
error InterchainModule__IncorrectSourceChainId(uint256 chainId);
error InterchainModule__InsufficientFee(uint256 actual, uint256 required);
error InterchainModule__SameChainId();

/// @notice Request the verification of an entry in the Interchain DataBase by the module.
/// Note: a fee is paid to the module for verification, and could be retrieved by using `getModuleFee`.
/// Note: this will eventually trigger `InterchainDB.verifyEntry(entry)` function on destination chain,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {InterchainEntry} from "../libs/InterchainEntry.sol";

interface ISynapseModule {
/// @notice Sets the address of the InterchainDB contract to be used for verifying entries.
/// @dev This function can only be called by the contract owner.
/// @param _interchainDB The address of the InterchainDB contract.
function setInterchainDB(address _interchainDB) external;

/// @notice Sets the required threshold for verification.
/// @dev This function updates the threshold value that determines the minimum number of verifications required for an entry to be considered valid. Can only be called by the contract owner.
/// @param _threshold The new threshold value.
function setRequiredThreshold(uint256 _threshold) external;

/// @notice Updates the list of verifier addresses.
/// @dev This function sets the addresses that are allowed to act as verifiers for entries. Can only be called by the contract owner.
/// @param _verifiers An array of addresses to be set as verifiers.
function setVerifiers(address[] calldata _verifiers) external;

/// @notice Requests off-chain verification of an interchain entry for a specified destination chain. This function requires a fee.
/// @dev This function can only be called by the InterchainDB contract. It checks if the sent value covers the module fee for the requested destination chain, then proceeds to pay the fee for execution. Emits a VerificationRequested event upon success.
/// @param destChainId The ID of the destination chain where the entry needs to be verified.
/// @param entry The interchain entry to be verified.
function requestVerification(uint256 destChainId, InterchainEntry memory entry) external payable;

/// @notice Verifies an interchain entry using a set of verifier signatures.
/// @dev This function checks if the provided signatures meet the required threshold for verification.
/// It then calls the InterchainDB contract to verify the entry. Requires that the number of valid signatures meets or exceeds the required threshold.
/// @param entry The interchain entry to be verified.
/// @param signatures An array of signatures used to verify the entry.
function verifyEntry(InterchainEntry memory entry, bytes[] calldata signatures) external;
import {IInterchainModule} from "./IInterchainModule.sol";

interface ISynapseModule is IInterchainModule {
error SynapseModule__GasOracleNotContract(address gasOracle);

// ═══════════════════════════════════════════════ PERMISSIONED ════════════════════════════════════════════════════

/// @notice Adds a new verifier to the module.
/// @dev Could be only called by the owner. Will revert if the verifier is already added.
/// @param verifier The address of the verifier to add
function addVerifier(address verifier) external;

/// @notice Removes a verifier from the module.
/// @dev Could be only called by the owner. Will revert if the verifier is not added.
/// @param verifier The address of the verifier to remove
function removeVerifier(address verifier) external;

/// @notice Sets the threshold of the module.
/// @dev Could be only called by the owner. Will revert if the threshold is zero.
/// @param threshold The new threshold value
function setThreshold(uint256 threshold) external;

/// @notice Sets the address of the fee collector, which will have the verification fees forwarded to it.
/// @dev Could be only called by the owner.
/// @param feeCollector_ The address of the fee collector
function setFeeCollector(address feeCollector_) external;

/// @notice Sets the address of the gas oracle to be used for estimating the verification fees.
/// @dev Could be only called by the owner. Will revert if the gas oracle is not a contract.
/// @param gasOracle_ The address of the gas oracle contract
function setGasOracle(address gasOracle_) external;

// ══════════════════════════════════════════════ PERMISSIONLESS ═══════════════════════════════════════════════════

/// @notice Verifies an entry using a set of verifier signatures.
/// If the threshold is met, the entry will be marked as verified in the Interchain DataBase.
/// @dev List of recovered signers from the signatures must be sorted in the ascending order.
/// @param encodedEntry The encoded entry to verify
/// @param signatures Signatures used to verify the entry, concatenated
function verifyEntry(bytes calldata encodedEntry, bytes calldata signatures) external;

// ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════

/// @notice Returns the address of the fee collector for the module.
function feeCollector() external view returns (address);

/// @notice Returns the address of the gas oracle used for estimating the verification fees.
function gasOracle() external view returns (address);

/// @notice Returns the list of verifiers for the module.
function getVerifiers() external view returns (address[] memory);

/// @notice Gets the threshold of the module.
/// This is the minimum number of signatures required for verification.
function getThreshold() external view returns (uint256);

/// @notice Checks if the given account is a verifier for the module.
function isVerifier(address account) external view returns (bool);
}
21 changes: 16 additions & 5 deletions packages/contracts-communication/contracts/libs/ThresholdECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ library ThresholdECDSALib {
using EnumerableSet for EnumerableSet.AddressSet;

error ThresholdECDSA__AlreadySigner(address account);
error ThresholdECDSA__IncorrectSignaturesLength(uint256 length);
error ThresholdECDSA__InvalidSignature(bytes signature);
error ThresholdECDSA__NotEnoughSignatures(uint256 threshold);
error ThresholdECDSA__NotSigner(address account);
error ThresholdECDSA__RecoveredSignersNotSorted();
error ThresholdECDSA__ZeroThreshold();

uint256 private constant SIGNATURE_LENGTH = 65;

/// @notice Adds a new signer to the list of signers.
/// @dev Will revert if the account is already a signer.
function addSigner(ThresholdECDSA storage self, address account) internal {
Expand Down Expand Up @@ -69,21 +72,28 @@ library ThresholdECDSALib {
/// - Any of the payloads is not a valid signature payload.
/// - The number of signatures is less than the threshold.
/// - The recovered list of signers is not sorted in the ascending order.
function verifySignedHash(ThresholdECDSA storage self, bytes32 hash, bytes[] memory signatures) internal view {
function verifySignedHash(ThresholdECDSA storage self, bytes32 hash, bytes calldata signatures) internal view {
// Figure out the signaturesAmount of signatures provided
uint256 signaturesAmount = signatures.length / SIGNATURE_LENGTH;
if (signaturesAmount == 0 || signaturesAmount * SIGNATURE_LENGTH != signatures.length) {
revert ThresholdECDSA__IncorrectSignaturesLength(signatures.length);
}
// First, check that threshold is configured and enough signatures are provided
uint256 threshold = self._threshold;
if (threshold == 0) {
revert ThresholdECDSA__ZeroThreshold();
}
if (signatures.length < threshold) {
if (signaturesAmount < threshold) {
revert ThresholdECDSA__NotEnoughSignatures(threshold);
}
uint256 offset = 0;
uint256 validSignatures = 0;
address lastSigner = address(0);
for (uint256 i = 0; i < signatures.length; ++i) {
(address recovered, ECDSA.RecoverError error,) = ECDSA.tryRecover(hash, signatures[i]);
for (uint256 i = 0; i < signaturesAmount; ++i) {
bytes memory signature = signatures[offset:offset + SIGNATURE_LENGTH];
(address recovered, ECDSA.RecoverError error,) = ECDSA.tryRecover(hash, signature);
if (error != ECDSA.RecoverError.NoError) {
revert ThresholdECDSA__InvalidSignature(signatures[i]);
revert ThresholdECDSA__InvalidSignature(signature);
}
// Check that the recovered addresses list is strictly increasing
if (recovered <= lastSigner) {
Expand All @@ -94,6 +104,7 @@ library ThresholdECDSALib {
if (isSigner(self, recovered)) {
validSignatures += 1;
}
offset += SIGNATURE_LENGTH;
}
if (validSignatures < threshold) {
revert ThresholdECDSA__NotEnoughSignatures(threshold);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {InterchainModuleEvents} from "../events/InterchainModuleEvents.sol";
import {IInterchainDB} from "../interfaces/IInterchainDB.sol";
import {IInterchainModule} from "../interfaces/IInterchainModule.sol";

import {InterchainEntry} from "../libs/InterchainEntry.sol";

import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

/// @notice Common logic for all Interchain Modules.
abstract contract InterchainModule is InterchainModuleEvents, IInterchainModule {
address public immutable INTERCHAIN_DB;

constructor(address interchainDB) {
INTERCHAIN_DB = interchainDB;
}

/// @inheritdoc IInterchainModule
function requestVerification(uint256 destChainId, InterchainEntry memory entry) external payable {
if (msg.sender != INTERCHAIN_DB) {
revert InterchainModule__NotInterchainDB();
}
if (destChainId == block.chainid) {
revert InterchainModule__SameChainId();
}
if (entry.srcChainId != block.chainid) {
revert InterchainModule__IncorrectSourceChainId({chainId: entry.srcChainId});
}
uint256 requiredFee = _getModuleFee(destChainId);
if (msg.value < requiredFee) {
revert InterchainModule__InsufficientFee({actual: msg.value, required: requiredFee});
}
bytes memory encodedEntry = abi.encode(entry);
bytes32 ethSignedEntryHash = MessageHashUtils.toEthSignedMessageHash(keccak256(encodedEntry));
_requestVerification(destChainId, encodedEntry);
emit VerificationRequested(destChainId, encodedEntry, ethSignedEntryHash);
}

/// @inheritdoc IInterchainModule
function getModuleFee(uint256 destChainId) external view returns (uint256) {
return _getModuleFee(destChainId);
}

/// @dev Should be called once the Module has verified the entry and needs to signal this
/// to the InterchainDB.
function _verifyEntry(bytes memory encodedEntry) internal {
InterchainEntry memory entry = abi.decode(encodedEntry, (InterchainEntry));
if (entry.srcChainId == block.chainid) {
revert InterchainModule__SameChainId();
}
IInterchainDB(INTERCHAIN_DB).verifyEntry(entry);
emit EntryVerified(entry);
}

/// @dev Internal logic to request the verification of an entry on the destination chain.
function _requestVerification(uint256 destChainId, bytes memory encodedEntry) internal virtual;

/// @dev Internal logic to get the module fee for verifying an entry on the specified destination chain.
function _getModuleFee(uint256 destChainId) internal view virtual returns (uint256);
}

This file was deleted.

Loading

0 comments on commit c9a789f

Please sign in to comment.