Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

255 changes: 255 additions & 0 deletions ethereum/contracts/AvalancheValidatorSetRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {IAvalancheValidatorSetRegistry} from "./interfaces/IAvalancheValidatorSetRegistry.sol";
import {
Validator,
ValidatorSet,
ValidatorSetStatePayload,
ValidatorSets
} from "./utils/ValidatorSets.sol";
import {ICMMessage} from "@avalabs/avalanche-contracts/teleporter/ITeleporterMessenger.sol";
import {ICM, AddressedCall} from "./utils/ICM.sol";
import {IVerifyICMMessage} from "./interfaces/IVerifyWarpMessage.sol";

/**
* @title AvalancheValidatorSetRegistry
* @notice Registry for managing Avalanche validator sets
* @dev This contract allows registration and updates of validator sets for Avalanche blockchains.
* Updates are authenticated through signed ICM messages from the current validator set.
*/
contract AvalancheValidatorSetRegistry is
IAvalancheValidatorSetRegistry,
IVerifyICMMessage
{
uint32 public immutable avalancheNetworkID;
/**
* @notice The blockchain this registry maintains validator sets for
* @dev This should be a blockchain for which the registered validators
* represents the entire validator set. E.g., if this contract instance is
* verifying Avalanche L1 instances, this ID should be the L1 ID, not the
* P-chain ID.
*/
bytes32 public immutable avalancheBlockChainID;
uint32 public nextValidatorSetID = 0;

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.init-variables-with-default-value.init-variables-with-default-value Note

Uninitialized variables are assigned with the types default value. Explicitly initializing a variable with its default value costs unnecessary gas.

// Mapping of validator set IDs to their complete validator set data.
mapping(uint256 => ValidatorSet) private _validatorSets;

constructor(
uint32 _avalancheNetworkID,
bytes32 _avalancheBlockChainID
) {
avalancheNetworkID = _avalancheNetworkID;
avalancheBlockChainID = _avalancheBlockChainID;
}

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.non-payable-constructor.non-payable-constructor Note

Consider making costructor payable to save gas.

/**
* @notice Get the current (latest) validator set
*/
function getCurrentValidatorSet() public view returns (ValidatorSet memory) {
require(0 < nextValidatorSetID, "No validator sets exist");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.
return _validatorSets[nextValidatorSetID - 1];
}

/**
* @notice Get the ID of the current (latest) validator set
*/
function getCurrentValidatorSetID() public view returns (uint256) {
require(0 < nextValidatorSetID, "No validator sets exist");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.
return nextValidatorSetID - 1;
}

/**
* @notice Registers a new validator set
* @dev A validator set can be registered by anyone. The correctness should be verified
* with the actual validator set on the Avalanche P-Chain.
* @param message The ICM message containing the validator set to register. The message must be signed by validator set
* @param validatorBytes The serialized validator set to register.
* @return The ID of the registered validator set
*/
function registerValidatorSet(
ICMMessage calldata message,
bytes memory validatorBytes
) external override returns (uint256) {
// Parse and validate the validator set data
(
ValidatorSetStatePayload memory validatorSetStatePayload,
Validator[] memory validators,
uint64 totalWeight
) = _parseAndValidateValidatorSetData(message, validatorBytes);

// Construct the validator set and confirm the ICM was self-signed by it.
ValidatorSet memory validatorSet = ValidatorSet({
avalancheBlockchainID: validatorSetStatePayload.avalancheBlockchainID,
validators: validators,
totalWeight: totalWeight,
pChainHeight: validatorSetStatePayload.pChainHeight,
pChainTimestamp: validatorSetStatePayload.pChainTimestamp
});

// Check that blockchain ID matches the current validator set.
require(
validatorSetStatePayload.avalancheBlockchainID == avalancheBlockChainID,
"Blockchain ID mismatch"
);

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.
ICM.verifyICMMessage(message, avalancheNetworkID, avalancheBlockChainID,validatorSet);

// Store the validator set.
uint256 validatorSetID = nextValidatorSetID++;
_validatorSets[validatorSetID] = validatorSet;

emit ValidatorSetRegistered(validatorSetID, validatorSet.avalancheBlockchainID);
return validatorSetID;
}

/**
* @notice Updates a validator set
* @dev Updates are authenticated by a signed ICM message from the current validator set
* @param validatorSetID The ID of the validator set to update
* @param message The ICM message containing the update
*/
function updateValidatorSet(
uint256 validatorSetID,
ICMMessage calldata message,
bytes memory validatorBytes
) external override {
require(validatorSetID < nextValidatorSetID, "Validator set does not exist");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

ValidatorSet storage currentValidatorSet = _validatorSets[validatorSetID];

// Verify the ICM message using the current validator set
ICM.verifyICMMessage(message, avalancheNetworkID, avalancheBlockChainID,currentValidatorSet);

// Parse and validate the validator set data
(
ValidatorSetStatePayload memory validatorSetStatePayload,
Validator[] memory validators,
uint64 totalWeight
) = _parseAndValidateValidatorSetData(message, validatorBytes);

// Check that blockchain ID matches the current validator set.
require(
validatorSetStatePayload.avalancheBlockchainID
== currentValidatorSet.avalancheBlockchainID,
"Blockchain ID mismatch"
);

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

// Check that the pChain height is greater than the current validator set.
require(
validatorSetStatePayload.pChainHeight > currentValidatorSet.pChainHeight,
"P-Chain height must be greater than the current validator set"
);

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-short-revert-string.use-short-revert-string Note

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.

// Check that the pChain timestamp is greater than the current validator set.
require(
validatorSetStatePayload.pChainTimestamp > currentValidatorSet.pChainTimestamp,
"P-Chain timestamp must be greater than the current validator set"
);

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-short-revert-string.use-short-revert-string Note

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.

// Update the validator set
_validatorSets[validatorSetID] = ValidatorSet({
avalancheBlockchainID: validatorSetStatePayload.avalancheBlockchainID,
validators: validators,
totalWeight: totalWeight,
pChainHeight: validatorSetStatePayload.pChainHeight,
pChainTimestamp: validatorSetStatePayload.pChainTimestamp
});

emit ValidatorSetUpdated(validatorSetID, validatorSetStatePayload.avalancheBlockchainID);
}

/**
* @notice Gets a validator set by its ID
* @param validatorSetID The ID of the validator set to get
* @return The validator set
*/
function getValidatorSet(
uint256 validatorSetID
) external view override returns (ValidatorSet memory) {
return _getValidatorSet(validatorSetID);
}

/**
* @notice Gets the Avalanche network ID
* @return The Avalanche network ID
*/
function getAvalancheNetworkID() external view returns (uint32) {
return avalancheNetworkID;
}

/**
* @notice Verifies an ICM message against a validator set or reverts
* @dev This function validates that the message is properly signed by a sufficient quorum of validators
* from the validator set identified by validatorSetID. The verification includes checking the network ID,
* blockchain ID, and cryptographic signature verification.
* @param validatorSetID The ID of the validator set to verify the message against
* @param message The ICM message to verify
*/
function verifyICMMessageWithID(
uint256 validatorSetID,
ICMMessage calldata message
) external view {
ValidatorSet memory validatorSet = _getValidatorSet(validatorSetID);
ICM.verifyICMMessage(message, avalancheNetworkID, avalancheBlockChainID, validatorSet);
}

/**
* @notice Verify the signatures of an ICM message against the latest validator set or reverts
* @param message The ICM message to be verified
*/
function verifyICMMessage(
ICMMessage calldata message
) external view {
ValidatorSet memory validatorSet = getCurrentValidatorSet();
ICM.verifyICMMessage(message, avalancheNetworkID, avalancheBlockChainID, validatorSet);
}

function _getValidatorSet(
uint256 validatorSetID
) private view returns (ValidatorSet memory) {
require(validatorSetID < nextValidatorSetID, "Validator set does not exist");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.
return _validatorSets[validatorSetID];
}

/**
* @notice Parses and validates validator set data from an ICM message
* @dev This is a helper function that consolidates common validation logic
* @param message The ICM message containing the validator set data
* @param validatorBytes The serialized validator set
* @return validatorSetStatePayload The parsed validator set state payload
* @return validators The parsed validators array
* @return totalWeight The total weight of all validators
*/
function _parseAndValidateValidatorSetData(
ICMMessage calldata message,
bytes memory validatorBytes
)
private
pure
returns (
ValidatorSetStatePayload memory validatorSetStatePayload,
Validator[] memory validators,
uint64 totalWeight
)
{
// Parse the addressed call and validate that the source address is empty.
AddressedCall memory addressedCall = ICM.parseAddressedCall(message.unsignedMessage.payload);
require(addressedCall.sourceAddress.length == 0, "Source address must be empty");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

// Parse the validator set state payload.
validatorSetStatePayload =
ValidatorSets.parseValidatorSetStatePayload(addressedCall.payload);

// Check that the validator set hash matches the hash of the serialized validator set.
require(
validatorSetStatePayload.validatorSetHash == sha256(validatorBytes),
"Validator set hash mismatch"
);

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

// Parse the validators.
(validators, totalWeight) = ValidatorSets.parseValidators(validatorBytes);
require(validators.length > 0, "Validator set cannot be empty");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.
require(totalWeight > 0, "Total weight must be greater than 0");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-short-revert-string.use-short-revert-string Note

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.
}
}
88 changes: 88 additions & 0 deletions ethereum/contracts/EthWarp.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// (c) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// SPDX-License-Identifier: LicenseRef-Ecosystem

pragma solidity ^0.8.25;

import {
WarpMessage,
WarpBlockHash
} from "@avalabs/[email protected]/contracts/interfaces/IWarpMessenger.sol";
import {IWarpExt} from "@avalabs/avalanche-contracts/teleporter/IWarpExt.sol";
import {ICMMessage} from "@avalabs/avalanche-contracts/teleporter/ITeleporterMessenger.sol";
import {ICM} from "./utils/ICM.sol";
import {IVerifyICMMessage} from "./interfaces/IVerifyWarpMessage.sol";


contract EthWarp is IWarpExt {

/**
* @notice The chain ID of the Ethereum network the contract is deployed on.
* @dev The chain ID for Ethereum is a uint which we reinterpret as bytes32
* to remain consistent with the existing interface
*/
bytes32 public blockchainID;


/**
* @notice A mapping of avalanche chain IDs to contract addresses that know how
* to validate received Warp message.
*/
mapping(bytes32 avaBlockchainId => address verifyWarpMessage) internal _registeredChains;

constructor (uint256 blockChainId) {
blockchainID = bytes32(blockChainId);
}

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.non-payable-constructor.non-payable-constructor Note

Consider making costructor payable to save gas.

function getVerifiedICMMessage(
ICMMessage calldata icmMessage
) external view returns (WarpMessage memory warpMessage) {
require(
isChainRegistered(icmMessage.unsignedMessage.avalancheSourceBlockchainID),
"Cannot receive a Warp message from a chain whose validator set is unknown"
);

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-short-revert-string.use-short-revert-string Note

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.
IVerifyICMMessage(_registeredChains[icmMessage.unsignedMessage.avalancheSourceBlockchainID])
.verifyICMMessage(icmMessage);
warpMessage = ICM.handleMessage(icmMessage.unsignedMessage);
return warpMessage;
}

function sendWarpMessage(bytes calldata payload) external returns (bytes32) {
revert("Sending Warp messages from Ethereum is not currently supported");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-short-revert-string.use-short-revert-string Note

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.
}

function getVerifiedWarpMessage(uint32 index) external pure returns (WarpMessage calldata, bool) {
revert("This method can't be called on Ethereum, use `getVerifiedICMMessage` instead");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-short-revert-string.use-short-revert-string Note

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.
}

function getVerifiedWarpBlockHash(
uint32 index
) external pure returns (WarpBlockHash calldata, bool) {
revert("This method cannot be called on Ethereum");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-short-revert-string.use-short-revert-string Note

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.
}

function getBlockchainID() external view returns (bytes32) {
return blockchainID;
}

/**
* @notice Check if a source chain is registered with this Warp contract. If it is not,
* this contract will be unable to verify a quorum of signatures is present on the
* received message.
* @return registered A boolean indicating presence of the given key
*/
function isChainRegistered(bytes32 avaBlockchainId) public view returns (bool) {
return _registeredChains[avaBlockchainId] != address(0);
}

/**
* @notice Register a contract implementing `IVerifyWarpMessage` here to validate messages
* originating from `avaBlockchainId`.
*/
function registerChain(bytes32 avaBlockchainId, address verifyWarpMessage) external {
require(!isChainRegistered(avaBlockchainId), "This chain is already registered");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.
require(verifyWarpMessage != address(0), "Provided address does not exist");

Check notice

Code scanning / Semgrep PRO

Semgrep Finding: solidity.performance.use-custom-error-not-require.use-custom-error-not-require Note

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.
_registeredChains[avaBlockchainId] = verifyWarpMessage;
}
}
Loading
Loading