Multisig validator manager contracts & tooling (#334)

* All contracts working together, need to separate local vs remote, clean up natspec, and add tests

* Create Inbox/Outbox specific MultisigValidatorManagers

* New Inbox.sol tests

* MultisigValidatorManager tests

* Comment for getCheckpointSignatures

* Fix build

* InboxMultisigValidatorManager tests

* OutboxMultisigValidatorManager tests

* Prettier

* Test rust-produced domain hashes

* Delete old ValidatorManager, rm IValidatorManager reference in Common.sol

* rm validatorManager test, rm unused AbacusDeployment

* Rm test/index.ts

* Modify TestAbacusDeploy to work with new validator managers

* Add inbox / outbox multisig validator managers to core contracts

* Update core test addresses

* Ensure validator set unenrolling does not violate a quorum threshold

* Prettier

* self nits

* All builds passing, fixed invariant checker

* nits

* Minor fixes

* rm IValidatorManager

* Nit

* ValidatorManager -> validator manager in comments

* more moving away from ValidatorManager

* some more...

* Make domainHash internal as _domainHash, publicly expose in TestMultisigValidatorManager

* Update solidity/core/contracts/validator-manager/InboxMultisigValidatorManager.sol

Co-authored-by: Yorke Rhodes <yorke@useabacus.network>

* Revert "Update solidity/core/contracts/validator-manager/InboxMultisigValidatorManager.sol"

This reverts commit cf54d4b765.

* some natspec fixes

* PR comments, mostly small renames

* Add backticks to natspec

* Apply suggestions from code review

Co-authored-by: Yorke Rhodes <yorke@useabacus.network>

* PR comments

* Rename quorumThreshold -> threshold

* Add isValidator and validatorCount

* Add validatorCount to enroll/unenroll events

* Remove multisig from sdk / deploy names

* Rename InboxMultisig... and OutboxMultisig... to Inbox... and Outbox...

* Better checking, check threshold

* rm validator managers from TestAbacusDeploy

* clean up comments

* PR comments

Co-authored-by: Yorke Rhodes <yorke@useabacus.network>
pull/340/head
Trevor Porter 3 years ago committed by GitHub
parent 9cc2a4388b
commit 39047bbf9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 37
      solidity/core/contracts/Common.sol
  2. 31
      solidity/core/contracts/Inbox.sol
  3. 29
      solidity/core/contracts/Outbox.sol
  4. 131
      solidity/core/contracts/ValidatorManager.sol
  5. 27
      solidity/core/contracts/test/TestMultisigValidatorManager.sol
  6. 7
      solidity/core/contracts/test/TestOutbox.sol
  7. 18
      solidity/core/contracts/test/TestValidatorManager.sol
  8. 53
      solidity/core/contracts/validator-manager/InboxValidatorManager.sol
  9. 253
      solidity/core/contracts/validator-manager/MultisigValidatorManager.sol
  10. 73
      solidity/core/contracts/validator-manager/OutboxValidatorManager.sol
  11. 3
      solidity/core/interfaces/IInbox.sol
  12. 7
      solidity/core/interfaces/IOutbox.sol
  13. 11
      solidity/core/interfaces/IValidatorManager.sol
  14. 49
      solidity/core/test/inbox.test.ts
  15. 1
      solidity/core/test/index.ts
  16. 197
      solidity/core/test/lib/AbacusDeployment.ts
  17. 78
      solidity/core/test/validator-manager/inboxValidatorManager.test.ts
  18. 269
      solidity/core/test/validator-manager/multisigValidatorManager.test.ts
  19. 112
      solidity/core/test/validator-manager/outboxValidatorManager.test.ts
  20. 30
      solidity/core/test/validator-manager/utils.ts
  21. 150
      solidity/core/test/validatorManager.test.ts
  22. 57
      typescript/hardhat/src/TestAbacusDeploy.ts
  23. 22
      typescript/infra/config/environments/test/core.ts
  24. 140
      typescript/infra/src/core/check.ts
  25. 79
      typescript/infra/src/core/deploy.ts
  26. 40
      typescript/infra/src/core/govern.ts
  27. 7
      typescript/infra/src/core/types.ts
  28. 10
      typescript/infra/src/utils/utils.ts
  29. 11
      typescript/sdk/src/core/app.ts
  30. 26
      typescript/sdk/src/core/contracts.ts
  31. 146
      typescript/sdk/src/core/environments/test.ts
  32. 10
      typescript/sdk/test/utils.ts

@ -2,7 +2,6 @@
pragma solidity >=0.6.11;
// ============ Internal Imports ============
import {IValidatorManager} from "../interfaces/IValidatorManager.sol";
import {ICommon} from "../interfaces/ICommon.sol";
// ============ External Imports ============
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
@ -25,8 +24,8 @@ abstract contract Common is ICommon, OwnableUpgradeable {
mapping(bytes32 => uint256) public checkpoints;
// The latest checkpointed root
bytes32 public checkpointedRoot;
// Address of ValidatorManager contract.
IValidatorManager public validatorManager;
// Address of the validator manager contract.
address public validatorManager;
// ============ Upgrade Gap ============
@ -44,13 +43,21 @@ abstract contract Common is ICommon, OwnableUpgradeable {
event Checkpoint(bytes32 indexed root, uint256 indexed index);
/**
* @notice Emitted when the ValidatorManager contract is changed
* @notice Emitted when the validator manager contract is changed
* @param validatorManager The address of the new validatorManager
*/
event NewValidatorManager(address validatorManager);
// ============ Modifiers ============
/**
* @notice Ensures that a function is called by the validator manager contract.
*/
modifier onlyValidatorManager() {
require(msg.sender == validatorManager, "!validatorManager");
_;
}
// ============ Constructor ============
constructor(uint32 _localDomain) {
@ -65,20 +72,20 @@ abstract contract Common is ICommon, OwnableUpgradeable {
{
// initialize owner
__Ownable_init();
_setValidatorManager(IValidatorManager(_validatorManager));
_setValidatorManager(_validatorManager);
}
// ============ External Functions ============
/**
* @notice Set a new ValidatorManager contract
* @dev Outbox(es) will initially be initialized using a trusted ValidatorManager contract;
* @notice Set a new validator manager contract
* @dev Mailbox(es) will initially be initialized using a trusted validator manager contract;
* we will progressively decentralize by swapping the trusted contract with a new implementation
* that implements Validator bonding & slashing, and rules for Validator selection & rotation
* @param _validatorManager the new ValidatorManager contract
* @param _validatorManager the new validator manager contract
*/
function setValidatorManager(address _validatorManager) external onlyOwner {
_setValidatorManager(IValidatorManager(_validatorManager));
_setValidatorManager(_validatorManager);
}
/**
@ -99,18 +106,16 @@ abstract contract Common is ICommon, OwnableUpgradeable {
// ============ Internal Functions ============
/**
* @notice Set the ValidatorManager
* @param _validatorManager Address of the ValidatorManager
* @notice Set the validator manager
* @param _validatorManager Address of the validator manager
*/
function _setValidatorManager(IValidatorManager _validatorManager)
internal
{
function _setValidatorManager(address _validatorManager) internal {
require(
Address.isContract(address(_validatorManager)),
Address.isContract(_validatorManager),
"!contract validatorManager"
);
validatorManager = _validatorManager;
emit NewValidatorManager(address(_validatorManager));
emit NewValidatorManager(_validatorManager);
}
/**

@ -81,29 +81,20 @@ contract Inbox is IInbox, Version0, Common {
// ============ External Functions ============
/**
* @notice Checkpoints the provided root and index given a signature.
* @notice Checkpoints the provided root and index.
* @dev Called by the validator manager, which is responsible for verifying a
* quorum of validator signatures on the checkpoint.
* @dev Reverts if checkpoints's index is not greater than our latest index.
* @param _root Checkpoint's merkle root
* @param _index Checkpoint's index
* @param _signature Validator's signature on `_root` and `_index`
* @param _root Checkpoint's merkle root.
* @param _index Checkpoint's index.
*/
function checkpoint(
bytes32 _root,
uint256 _index,
bytes calldata _signature
) external override {
// ensure that update is more recent than the latest we've seen
function checkpoint(bytes32 _root, uint256 _index)
external
override
onlyValidatorManager
{
// Ensure that the checkpoint is more recent than the latest we've seen.
require(_index > checkpoints[checkpointedRoot], "old checkpoint");
// validate validator signature
require(
validatorManager.isValidatorSignature(
remoteDomain,
_root,
_index,
_signature
),
"!validator sig"
);
_checkpoint(_root, _index);
}

@ -99,14 +99,6 @@ contract Outbox is IOutbox, Version0, MerkleTreeManager, Common {
_;
}
/**
* @notice Ensures that function is called by the ValidatorManager contract
*/
modifier onlyValidatorManager() {
require(msg.sender == address(validatorManager), "!validatorManager");
_;
}
// ============ External Functions ============
/**
@ -167,14 +159,31 @@ contract Outbox is IOutbox, Version0, MerkleTreeManager, Common {
/**
* @notice Set contract state to FAILED.
* @dev Called by the ValidatorManager when fraud is proven.
* @dev Called by the validator manager when fraud is proven.
*/
function fail() external onlyValidatorManager {
function fail() external override onlyValidatorManager {
// set contract to FAILED
state = States.Failed;
emit Fail();
}
/**
* @notice Returns whether the provided root and index are a known
* checkpoint.
* @param _root The merkle root.
* @param _index The index.
* @return TRUE iff `_root` and `_index` are a known checkpoint.
*/
function isCheckpoint(bytes32 _root, uint256 _index)
external
view
override
returns (bool)
{
// Checkpoint indices are one-indexed.
return _index > 0 && checkpoints[_root] == _index;
}
// ============ Internal Functions ============
/**

@ -1,131 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;
// ============ Internal Imports ============
import {IValidatorManager} from "../interfaces/IValidatorManager.sol";
import {Outbox} from "./Outbox.sol";
// ============ External Imports ============
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ECDSA} from "@openzeppelin/contracts/cryptography/ECDSA.sol";
/**
* @title ValidatorManager
* @author Celo Labs Inc.
* @notice MVP version of contract that will manage Validator selection and
* rotataion.
*/
contract ValidatorManager is IValidatorManager, Ownable {
// Mapping of domain -> validator address.
mapping(uint32 => address) public validators;
// ============ Events ============
/**
* @notice Emitted when a validator is enrolled
* @param domain The domain for which the validator is being enrolled
* @param validator The address of the validator
*/
event ValidatorEnrolled(uint32 indexed domain, address indexed validator);
/**
* @notice Emitted when proof of an improper checkpoint is submitted,
* which sets the contract to FAILED state
* @param root Root of the improper checkpoint
* @param index Index of the improper checkpoint
* @param signature Signature on `root` and `index`
*/
event ImproperCheckpoint(
address indexed outbox,
uint32 indexed domain,
address indexed validator,
bytes32 root,
uint256 index,
bytes signature
);
// ============ Constructor ============
constructor() Ownable() {}
// ============ External Functions ============
/**
* @notice Enroll a validator for the given domain
* @dev only callable by trusted owner
* @param _domain The domain for which the validator is being set
* @param _validator The address of the validator
*/
function enrollValidator(uint32 _domain, address _validator)
external
onlyOwner
{
validators[_domain] = _validator;
emit ValidatorEnrolled(_domain, _validator);
}
/**
* @notice Check if an Checkpoint is an Improper Checkpoint;
* if so, set the provided Outbox contract to FAILED state.
*
* An Improper Checkpoint is an checkpoint that was not previously checkpointed.
* @param _outbox Address of the Outbox contract to set to FAILED.
* @param _root Merkle root of the improper checkpoint
* @param _index Index root of the improper checkpoint
* @param _signature Validator signature on `_root` and `_index`
* @return TRUE if checkpoint was an Improper Checkpoint (implying Validator was slashed)
*/
function improperCheckpoint(
address _outbox,
bytes32 _root,
uint256 _index,
bytes calldata _signature
) external returns (bool) {
uint32 _domain = Outbox(_outbox).localDomain();
require(
isValidatorSignature(_domain, _root, _index, _signature),
"!validator sig"
);
require(Outbox(_outbox).checkpoints(_root) != _index, "!improper");
Outbox(_outbox).fail();
emit ImproperCheckpoint(
_outbox,
_domain,
validators[_domain],
_root,
_index,
_signature
);
return true;
}
// ============ Public Functions ============
/**
* @notice Checks that signature was signed by Validator
* @param _domain Domain of Outbox contract
* @param _root Merkle root
* @param _index Corresponding leaf index
* @param _signature Signature on `_root` and `_index`
* @return TRUE iff signature is valid signed by validator
**/
function isValidatorSignature(
uint32 _domain,
bytes32 _root,
uint256 _index,
bytes calldata _signature
) public view override returns (bool) {
bytes32 _digest = keccak256(
abi.encodePacked(domainHash(_domain), _root, _index)
);
_digest = ECDSA.toEthSignedMessageHash(_digest);
return (ECDSA.recover(_digest, _signature) == validators[_domain]);
}
/**
* @notice Hash of domain concatenated with "ABACUS"
* @param _domain the domain to hash
*/
function domainHash(uint32 _domain) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_domain, "ABACUS"));
}
}

@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;
pragma abicoder v2;
import {MultisigValidatorManager} from "../validator-manager/MultisigValidatorManager.sol";
/**
* This contract exists to test MultisigValidatorManager.sol, which is abstract
* and cannot be deployed directly.
*/
contract TestMultisigValidatorManager is MultisigValidatorManager {
// solhint-disable-next-line no-empty-blocks
constructor(
uint32 _domain,
address[] memory _validators,
uint256 _threshold
) MultisigValidatorManager(_domain, _validators, _threshold) {}
/**
* @notice Hash of domain concatenated with "ABACUS".
* @dev This is a public getter of _domainHash to test with.
* @param _domain The domain to hash.
*/
function getDomainHash(uint32 _domain) external pure returns (bytes32) {
return _domainHash(_domain);
}
}

@ -3,7 +3,6 @@ pragma solidity >=0.6.11;
// ============ Internal Imports ============
import "../Outbox.sol";
import {IValidatorManager} from "../../interfaces/IValidatorManager.sol";
contract TestOutbox is Outbox {
constructor(uint32 _localDomain) Outbox(_localDomain) {} // solhint-disable-line no-empty-blocks
@ -17,10 +16,10 @@ contract TestOutbox is Outbox {
}
/**
* @notice Set the ValidatorManager
* @param _validatorManager Address of the ValidatorManager
* @notice Set the validator manager
* @param _validatorManager Address of the validator manager
*/
function testSetValidatorManager(address _validatorManager) external {
validatorManager = IValidatorManager(_validatorManager);
validatorManager = _validatorManager;
}
}

@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;
import {IInbox} from "../../interfaces/IInbox.sol";
/**
* Intended for testing Inbox.sol, which requires its validator manager
* to be a contract.
*/
contract TestValidatorManager {
function checkpoint(
IInbox _inbox,
bytes32 _root,
uint256 _index
) external {
_inbox.checkpoint(_root, _index);
}
}

@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;
pragma abicoder v2;
// ============ Internal Imports ============
import {IInbox} from "../../interfaces/IInbox.sol";
import {MultisigValidatorManager} from "./MultisigValidatorManager.sol";
/**
* @title InboxValidatorManager
* @notice Verifies checkpoints are signed by a quorum of validators and submits
* them to an Inbox.
*/
contract InboxValidatorManager is MultisigValidatorManager {
// ============ Constructor ============
/**
* @dev Reverts if `_validators` has any duplicates.
* @param _remoteDomain The remote domain of the outbox chain.
* @param _validators The set of validator addresses.
* @param _threshold The quorum threshold. Must be greater than or equal
* to the length of `_validators`.
*/
// solhint-disable-next-line no-empty-blocks
constructor(
uint32 _remoteDomain,
address[] memory _validators,
uint256 _threshold
) MultisigValidatorManager(_remoteDomain, _validators, _threshold) {}
// ============ External Functions ============
/**
* @notice Submits a checkpoint signed by a quorum of validators to an Inbox.
* @dev Reverts if `_signatures` is not a quorum of validator signatures.
* @dev Reverts if `_signatures` is not sorted in ascending order by the signer
* address, which is required for duplicate detection.
* @param _inbox The inbox to submit the checkpoint to.
* @param _root The merkle root of the checkpoint.
* @param _index The index of the checkpoint.
* @param _signatures Signatures over the checkpoint to be checked for a validator
* quorum. Must be sorted in ascending order by signer address.
*/
function checkpoint(
IInbox _inbox,
bytes32 _root,
uint256 _index,
bytes[] calldata _signatures
) external {
require(isQuorum(_root, _index, _signatures), "!quorum");
_inbox.checkpoint(_root, _index);
}
}

@ -0,0 +1,253 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;
pragma abicoder v2;
// ============ External Imports ============
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ECDSA} from "@openzeppelin/contracts/cryptography/ECDSA.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/EnumerableSet.sol";
/**
* @title MultisigValidatorManager
* @notice Manages an ownable set of validators that ECDSA sign checkpoints to
* reach a quorum.
*/
abstract contract MultisigValidatorManager is Ownable {
// ============ Libraries ============
using EnumerableSet for EnumerableSet.AddressSet;
// ============ Immutables ============
// The domain of the validator set's outbox chain.
uint32 public immutable domain;
// The domain hash of the validator set's outbox chain.
bytes32 public immutable domainHash;
// ============ Mutable Storage ============
// The minimum threshold of validator signatures to constitute a quorum.
uint256 public threshold;
// The set of validators.
EnumerableSet.AddressSet private validatorSet;
// ============ Events ============
/**
* @notice Emitted when a validator is enrolled in the validator set.
* @param validator The address of the validator.
* @param validatorCount The new number of enrolled validators in the validator set.
*/
event EnrollValidator(address indexed validator, uint256 validatorCount);
/**
* @notice Emitted when a validator is unenrolled from the validator set.
* @param validator The address of the validator.
* @param validatorCount The new number of enrolled validators in the validator set.
*/
event UnenrollValidator(address indexed validator, uint256 validatorCount);
/**
* @notice Emitted when the quorum threshold is set.
* @param threshold The new quorum threshold.
*/
event SetThreshold(uint256 threshold);
// ============ Constructor ============
/**
* @dev Reverts if `_validators` has any duplicates.
* @param _domain The domain of the outbox the validator set is for.
* @param _validators The set of validator addresses.
* @param _threshold The quorum threshold. Must be greater than or equal
* to the length of `_validators`.
*/
constructor(
uint32 _domain,
address[] memory _validators,
uint256 _threshold
) Ownable() {
// Set immutables.
domain = _domain;
domainHash = _domainHash(_domain);
// Enroll validators. Reverts if there are any duplicates.
uint256 _numValidators = _validators.length;
for (uint256 i = 0; i < _numValidators; i++) {
_enrollValidator(_validators[i]);
}
_setThreshold(_threshold);
}
// ============ External Functions ============
/**
* @notice Enrolls a validator into the validator set.
* @dev Reverts if `_validator` is already in the validator set.
* @param _validator The validator to add to the validator set.
*/
function enrollValidator(address _validator) external onlyOwner {
_enrollValidator(_validator);
}
/**
* @notice Unenrolls a validator from the validator set.
* @dev Reverts if `_validator` is not in the validator set.
* @param _validator The validator to remove from the validator set.
*/
function unenrollValidator(address _validator) external onlyOwner {
_unenrollValidator(_validator);
}
/**
* @notice Sets the quorum threshold.
* @param _threshold The new quorum threshold.
*/
function setThreshold(uint256 _threshold) external onlyOwner {
_setThreshold(_threshold);
}
/**
* @notice Gets the addresses of the current validator set.
* @dev There are no ordering guarantees due to the semantics of EnumerableSet.AddressSet.
* @return The addresses of the validator set.
*/
function validators() external view returns (address[] memory) {
uint256 _numValidators = validatorSet.length();
address[] memory _validators = new address[](_numValidators);
for (uint256 i = 0; i < _numValidators; i++) {
_validators[i] = validatorSet.at(i);
}
return _validators;
}
// ============ Public Functions ============
/**
* @notice Returns whether provided signatures over a checkpoint constitute
* a quorum of validator signatures.
* @dev Reverts if `_signatures` is not sorted in ascending order by the signer
* address, which is required for duplicate detection.
* @dev Does not revert if a signature's signer is not in the validator set.
* @param _root The merkle root of the checkpoint.
* @param _index The index of the checkpoint.
* @param _signatures Signatures over the checkpoint to be checked for a validator
* quorum. Must be sorted in ascending order by signer address.
* @return TRUE iff `_signatures` constitute a quorum of validator signatures over
* the checkpoint.
*/
function isQuorum(
bytes32 _root,
uint256 _index,
bytes[] calldata _signatures
) public view returns (bool) {
uint256 _numSignatures = _signatures.length;
// If there are fewer signatures provided than the required quorum threshold,
// this is not a quorum.
if (_numSignatures < threshold) {
return false;
}
// To identify duplicates, the signers recovered from _signatures
// must be sorted in ascending order. previousSigner is used to
// enforce ordering.
address _previousSigner = address(0);
uint256 _validatorSignatureCount = 0;
for (uint256 i = 0; i < _numSignatures; i++) {
address _signer = _recoverCheckpointSigner(
_root,
_index,
_signatures[i]
);
// Revert if the signer violates the required sort order.
require(_previousSigner < _signer, "!sorted signers");
// If the signer is a validator, increment _validatorSignatureCount.
if (isValidator(_signer)) {
_validatorSignatureCount++;
}
_previousSigner = _signer;
}
return _validatorSignatureCount >= threshold;
}
/**
* @notice Returns if `_validator` is enrolled in the validator set.
* @param _validator The address of the validator.
* @return TRUE iff `_validator` is enrolled in the validator set.
*/
function isValidator(address _validator) public view returns (bool) {
return validatorSet.contains(_validator);
}
/**
* @notice Returns the number of validators enrolled in the validator set.
* @return The number of validators enrolled in the validator set.
*/
function validatorCount() public view returns (uint256) {
return validatorSet.length();
}
// ============ Internal Functions ============
/**
* @notice Recovers the signer from a signature of a checkpoint.
* @param _root The checkpoint's merkle root.
* @param _index The checkpoint's index.
* @param _signature Signature on the the checkpoint.
* @return The signer of the checkpoint signature.
**/
function _recoverCheckpointSigner(
bytes32 _root,
uint256 _index,
bytes calldata _signature
) internal view returns (address) {
bytes32 _digest = keccak256(
abi.encodePacked(domainHash, _root, _index)
);
return ECDSA.recover(ECDSA.toEthSignedMessageHash(_digest), _signature);
}
/**
* @notice Enrolls a validator into the validator set.
* @dev Reverts if `_validator` is already in the validator set.
* @param _validator The validator to add to the validator set.
*/
function _enrollValidator(address _validator) internal {
require(validatorSet.add(_validator), "already enrolled");
emit EnrollValidator(_validator, validatorCount());
}
/**
* @notice Unenrolls a validator from the validator set.
* @dev Reverts if the resulting validator set length is less than
* the quorum threshold.
* @dev Reverts if `_validator` is not in the validator set.
* @param _validator The validator to remove from the validator set.
*/
function _unenrollValidator(address _validator) internal {
require(validatorSet.remove(_validator), "!enrolled");
uint256 _numValidators = validatorCount();
require(_numValidators >= threshold, "violates quorum threshold");
emit UnenrollValidator(_validator, _numValidators);
}
/**
* @notice Sets the quorum threshold.
* @param _threshold The new quorum threshold.
*/
function _setThreshold(uint256 _threshold) internal {
require(_threshold > 0 && _threshold <= validatorCount(), "!range");
threshold = _threshold;
emit SetThreshold(_threshold);
}
/**
* @notice Hash of `_domain` concatenated with "ABACUS".
* @param _domain The domain to hash.
*/
function _domainHash(uint32 _domain) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(_domain, "ABACUS"));
}
}

@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;
pragma abicoder v2;
// ============ Internal Imports ============
import {IOutbox} from "../../interfaces/IOutbox.sol";
import {MultisigValidatorManager} from "./MultisigValidatorManager.sol";
/**
* @title OutboxValidatorManager
* @notice Verifies if an improper checkpoint has been signed by a quorum of
* validators and reports it to an Outbox.
*/
contract OutboxValidatorManager is MultisigValidatorManager {
// ============ Events ============
/**
* @notice Emitted when proof of an improper checkpoint is submitted.
* @dev Observers of this event should filter by the outbox address.
* @param outbox The outbox.
* @param root Root of the improper checkpoint.
* @param index Index of the improper checkpoint.
* @param signatures A quorum of signatures on the improper checkpoint.
* May include non-validator signatures.
*/
event ImproperCheckpoint(
address indexed outbox,
bytes32 root,
uint256 index,
bytes[] signatures
);
// ============ Constructor ============
/**
* @dev Reverts if `_validators` has any duplicates.
* @param _localDomain The local domain.
* @param _validators The set of validator addresses.
* @param _threshold The quorum threshold. Must be greater than or equal
* to the length of `_validators`.
*/
// solhint-disable-next-line no-empty-blocks
constructor(
uint32 _localDomain,
address[] memory _validators,
uint256 _threshold
) MultisigValidatorManager(_localDomain, _validators, _threshold) {}
// ============ External Functions ============
/**
* @notice Determines if a quorum of validators have signed an improper checkpoint,
* failing the Outbox if so.
* @dev Improper checkpoints signed by individual validators are not handled to prevent
* a single byzantine validator from failing the Outbox.
* @param _outbox The outbox.
* @param _root The merkle root of the checkpoint.
* @param _index The index of the checkpoint.
* @param _signatures Signatures over the checkpoint to be checked for a validator
* quorum. Must be sorted in ascending order by signer address.
*/
function improperCheckpoint(
IOutbox _outbox,
bytes32 _root,
uint256 _index,
bytes[] calldata _signatures
) external {
require(isQuorum(_root, _index, _signatures), "!quorum");
require(!_outbox.isCheckpoint(_root, _index), "!improper checkpoint");
_outbox.fail();
emit ImproperCheckpoint(address(_outbox), _root, _index, _signatures);
}
}

@ -6,8 +6,7 @@ import {ICommon} from "./ICommon.sol";
interface IInbox is ICommon {
function checkpoint(
bytes32 _root,
uint256 _index,
bytes calldata _signature
uint256 _index
) external;
function remoteDomain() external returns (uint32);

@ -11,4 +11,11 @@ interface IOutbox is ICommon {
) external returns (uint256);
function checkpoint() external;
function isCheckpoint(
bytes32 _root,
uint256 _index
) external returns (bool);
function fail() external;
}

@ -1,11 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;
interface IValidatorManager {
function isValidatorSignature(
uint32 _domain,
bytes32 _root,
uint256 _index,
bytes calldata _signature
) external view returns (bool);
}

@ -1,7 +1,7 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { ethers } from 'hardhat';
import { expect } from 'chai';
import { types, utils, Validator } from '@abacus-network/utils';
import { types, utils } from '@abacus-network/utils';
import {
BadRecipient1__factory,
BadRecipient3__factory,
@ -10,9 +10,9 @@ import {
BadRecipientHandle__factory,
TestInbox,
TestInbox__factory,
ValidatorManager,
ValidatorManager__factory,
TestRecipient__factory,
TestValidatorManager,
TestValidatorManager__factory,
} from '../types';
const merkleTestCases = require('../../../vectors/merkle.json');
@ -30,20 +30,19 @@ describe('Inbox', async () => {
];
let inbox: TestInbox,
validatorManager: ValidatorManager,
signer: SignerWithAddress,
fakeSigner: SignerWithAddress,
abacusMessageSender: SignerWithAddress,
validator: Validator,
fakeValidator: Validator;
validatorManager: TestValidatorManager;
before(async () => {
[signer, fakeSigner, abacusMessageSender] = await ethers.getSigners();
validator = await Validator.fromSigner(signer, remoteDomain);
fakeValidator = await Validator.fromSigner(fakeSigner, remoteDomain);
const validatorManagerFactory = new ValidatorManager__factory(signer);
validatorManager = await validatorManagerFactory.deploy();
await validatorManager.enrollValidator(remoteDomain, validator.address);
[signer, abacusMessageSender] = await ethers.getSigners();
// Inbox.initialize will ensure the validator manager is a contract.
// TestValidatorManager doesn't have any special logic, it just submits
// checkpoints without any signature verification.
const testValidatorManagerFactory = new TestValidatorManager__factory(
signer,
);
validatorManager = await testValidatorManagerFactory.deploy();
});
beforeEach(async () => {
@ -68,40 +67,36 @@ describe('Inbox', async () => {
).to.be.revertedWith('Initializable: contract is already initialized');
});
it('Accepts signed checkpoint from validator', async () => {
it('Accepts checkpoint from validator manager', async () => {
const root = ethers.utils.formatBytes32String('first new root');
const index = 1;
const { signature } = await validator.signCheckpoint(root, index);
await inbox.checkpoint(root, index, signature);
await validatorManager.checkpoint(inbox.address, root, index);
const [croot, cindex] = await inbox.latestCheckpoint();
expect(croot).to.equal(root);
expect(cindex).to.equal(index);
});
it('Rejects signed checkpoint from non-validator', async () => {
it('Rejects checkpoint from non-validator manager', async () => {
const root = ethers.utils.formatBytes32String('first new root');
const index = 1;
const { signature } = await fakeValidator.signCheckpoint(root, index);
await expect(inbox.checkpoint(root, index, signature)).to.be.revertedWith(
'!validator sig',
await expect(inbox.checkpoint(root, index)).to.be.revertedWith(
'!validatorManager',
);
});
it('Rejects old signed checkpoint from validator', async () => {
it('Rejects old checkpoint from validator manager', async () => {
let root = ethers.utils.formatBytes32String('first new root');
let index = 10;
let { signature } = await validator.signCheckpoint(root, index);
await inbox.checkpoint(root, index, signature);
await validatorManager.checkpoint(inbox.address, root, index);
const [croot, cindex] = await inbox.latestCheckpoint();
expect(croot).to.equal(root);
expect(cindex).to.equal(index);
root = ethers.utils.formatBytes32String('second new root');
index = 9;
({ signature } = await validator.signCheckpoint(root, index));
await expect(inbox.checkpoint(root, index, signature)).to.be.revertedWith(
'old checkpoint',
);
await expect(
validatorManager.checkpoint(inbox.address, root, index),
).to.be.revertedWith('old checkpoint');
});
it('Proves a valid message', async () => {

@ -1 +0,0 @@
export { AbacusDeployment } from './lib/AbacusDeployment';

@ -1,197 +0,0 @@
import { assert } from 'chai';
import * as ethers from 'ethers';
import { types, Validator } from '@abacus-network/utils';
import {
TestOutbox,
TestOutbox__factory,
ValidatorManager,
ValidatorManager__factory,
UpgradeBeaconController,
UpgradeBeaconController__factory,
XAppConnectionManager,
XAppConnectionManager__factory,
TestInbox,
TestInbox__factory,
} from '../../types';
export interface AbacusInstance {
domain: types.Domain;
validator: Validator;
validatorManager: ValidatorManager;
outbox: TestOutbox;
connectionManager: XAppConnectionManager;
ubc: UpgradeBeaconController;
inboxs: Record<number, TestInbox>;
}
export class AbacusDeployment {
constructor(
public readonly domains: types.Domain[],
public readonly instances: Record<number, AbacusInstance>,
public readonly signer: ethers.Signer,
) {}
static async fromDomains(domains: types.Domain[], signer: ethers.Signer) {
const instances: Record<number, AbacusInstance> = {};
for (const local of domains) {
const instance = await AbacusDeployment.deployInstance(
local,
domains.filter((d) => d !== local),
signer,
);
instances[local] = instance;
}
return new AbacusDeployment(domains, instances, signer);
}
static async deployInstance(
local: types.Domain,
remotes: types.Domain[],
signer: ethers.Signer,
): Promise<AbacusInstance> {
const validatorManagerFactory = new ValidatorManager__factory(signer);
const validatorManager = await validatorManagerFactory.deploy();
await validatorManager.enrollValidator(local, await signer.getAddress());
await Promise.all(
remotes.map(async (remoteDomain) =>
validatorManager.enrollValidator(
remoteDomain,
await signer.getAddress(),
),
),
);
const ubcFactory = new UpgradeBeaconController__factory(signer);
const ubc = await ubcFactory.deploy();
const outboxFactory = new TestOutbox__factory(signer);
const outbox = await outboxFactory.deploy(local);
await outbox.initialize(validatorManager.address);
const connectionManagerFactory = new XAppConnectionManager__factory(signer);
const connectionManager = await connectionManagerFactory.deploy();
await connectionManager.setOutbox(outbox.address);
const inboxFactory = new TestInbox__factory(signer);
const inboxs: Record<number, TestInbox> = {};
const deploys = remotes.map(async (remoteDomain) => {
const inbox = await inboxFactory.deploy(local);
await inbox.initialize(
remoteDomain,
validatorManager.address,
ethers.constants.HashZero,
0,
);
await connectionManager.enrollInbox(remoteDomain, inbox.address);
inboxs[remoteDomain] = inbox;
});
await Promise.all(deploys);
return {
domain: local,
validator: await Validator.fromSigner(signer, local),
outbox,
connectionManager,
validatorManager,
inboxs,
ubc,
};
}
async transferOwnership(domain: types.Domain, address: types.Address) {
await this.outbox(domain).transferOwnership(address);
await this.ubc(domain).transferOwnership(address);
await this.connectionManager(domain).transferOwnership(address);
await this.validatorManager(domain).transferOwnership(address);
for (const remote of this.domains) {
if (remote !== domain) {
await this.inbox(domain, remote).transferOwnership(address);
}
}
}
outbox(domain: types.Domain): TestOutbox {
return this.instances[domain].outbox;
}
ubc(domain: types.Domain): UpgradeBeaconController {
return this.instances[domain].ubc;
}
validator(domain: types.Domain): Validator {
return this.instances[domain].validator;
}
inbox(local: types.Domain, remote: types.Domain): TestInbox {
return this.instances[local].inboxs[remote];
}
connectionManager(domain: types.Domain): XAppConnectionManager {
return this.instances[domain].connectionManager;
}
validatorManager(domain: types.Domain): ValidatorManager {
return this.instances[domain].validatorManager;
}
async processMessages() {
await Promise.all(
this.domains.map((d) => this.processMessagesFromDomain(d)),
);
}
async processMessagesFromDomain(local: types.Domain) {
const outbox = this.outbox(local);
const [checkpointedRoot, checkpointedIndex] =
await outbox.latestCheckpoint();
const latestIndex = await outbox.tree();
if (latestIndex.eq(checkpointedIndex)) return;
// Find the block number of the last checkpoint submitted on Outbox.
const checkpointFilter = outbox.filters.Checkpoint(checkpointedRoot);
const checkpoints = await outbox.queryFilter(checkpointFilter);
assert(checkpoints.length === 0 || checkpoints.length === 1);
const fromBlock = checkpoints.length === 0 ? 0 : checkpoints[0].blockNumber;
await outbox.checkpoint();
const [root, index] = await outbox.latestCheckpoint();
// If there have been no checkpoints since the last checkpoint, return.
if (
index.eq(0) ||
(checkpoints.length == 1 && index.eq(checkpoints[0].args.index))
) {
return;
}
// Update the Outbox and Inboxs to the latest roots.
// This is technically not necessary given that we are not proving against
// a root in the TestInbox.
const validator = this.validator(local);
const { signature } = await validator.signCheckpoint(
root,
index.toNumber(),
);
for (const remote of this.domains) {
if (remote !== local) {
const inbox = this.inbox(remote, local);
await inbox.checkpoint(root, index, signature);
}
}
// Find all messages dispatched on the outbox since the previous checkpoint.
const dispatchFilter = outbox.filters.Dispatch();
const dispatches = await outbox.queryFilter(dispatchFilter, fromBlock);
for (const dispatch of dispatches) {
const destination = dispatch.args.destinationAndNonce.shr(32).toNumber();
if (destination !== local) {
const inbox = this.inbox(destination, local);
await inbox.setMessageProven(dispatch.args.message);
await inbox.testProcess(dispatch.args.message);
}
}
}
}
export const abacus: any = {
AbacusDeployment,
};

@ -0,0 +1,78 @@
import { ethers } from 'hardhat';
import { Validator } from '@abacus-network/utils';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import {
Inbox,
Inbox__factory,
InboxValidatorManager,
InboxValidatorManager__factory,
} from '../../types';
import { signCheckpoint } from './utils';
import { expect } from 'chai';
const OUTBOX_DOMAIN = 1234;
const INBOX_DOMAIN = 4321;
const QUORUM_THRESHOLD = 2;
describe('InboxValidatorManager', () => {
let validatorManager: InboxValidatorManager,
inbox: Inbox,
signer: SignerWithAddress,
validator0: Validator,
validator1: Validator;
before(async () => {
const signers = await ethers.getSigners();
signer = signers[0];
validator0 = await Validator.fromSigner(signers[1], OUTBOX_DOMAIN);
validator1 = await Validator.fromSigner(signers[2], OUTBOX_DOMAIN);
});
beforeEach(async () => {
const validatorManagerFactory = new InboxValidatorManager__factory(signer);
validatorManager = await validatorManagerFactory.deploy(
OUTBOX_DOMAIN,
[validator0.address, validator1.address],
QUORUM_THRESHOLD,
);
const inboxFactory = new Inbox__factory(signer);
inbox = await inboxFactory.deploy(INBOX_DOMAIN);
await inbox.initialize(
OUTBOX_DOMAIN,
validatorManager.address,
ethers.constants.HashZero,
0,
);
});
describe('#checkpoint', () => {
const root = ethers.utils.formatBytes32String('test root');
const index = 1;
it('submits a checkpoint to the Inbox if there is a quorum', async () => {
const signatures = await signCheckpoint(
root,
index,
[validator0, validator1], // 2/2 signers, making a quorum
);
await validatorManager.checkpoint(inbox.address, root, index, signatures);
expect(await inbox.checkpoints(root)).to.equal(index);
});
it('reverts if there is not a quorum', async () => {
const signatures = await signCheckpoint(
root,
index,
[validator0], // 1/2 signers is not a quorum
);
await expect(
validatorManager.checkpoint(inbox.address, root, index, signatures),
).to.be.revertedWith('!quorum');
});
});
});

@ -0,0 +1,269 @@
import { expect } from 'chai';
import { ethers } from 'hardhat';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { Validator } from '@abacus-network/utils';
import {
TestMultisigValidatorManager,
TestMultisigValidatorManager__factory,
} from '../../types';
import { signCheckpoint } from './utils';
const OUTBOX_DOMAIN = 1234;
const QUORUM_THRESHOLD = 1;
const domainHashTestCases = require('../../../../vectors/domainHash.json');
describe('MultisigValidatorManager', async () => {
let validatorManager: TestMultisigValidatorManager,
signer: SignerWithAddress,
nonOwner: SignerWithAddress,
validator0: Validator,
validator1: Validator,
validator2: Validator,
validator3: Validator;
before(async () => {
const signers = await ethers.getSigners();
[signer, nonOwner] = signers;
validator0 = await Validator.fromSigner(signers[2], OUTBOX_DOMAIN);
validator1 = await Validator.fromSigner(signers[3], OUTBOX_DOMAIN);
validator2 = await Validator.fromSigner(signers[4], OUTBOX_DOMAIN);
validator3 = await Validator.fromSigner(signers[5], OUTBOX_DOMAIN);
});
beforeEach(async () => {
const validatorManagerFactory = new TestMultisigValidatorManager__factory(
signer,
);
validatorManager = await validatorManagerFactory.deploy(
OUTBOX_DOMAIN,
[validator0.address],
QUORUM_THRESHOLD,
);
});
describe('#constructor', () => {
it('sets the domain', async () => {
expect(await validatorManager.domain()).to.equal(OUTBOX_DOMAIN);
});
it('sets the domainHash', async () => {
const domainHash = await validatorManager.getDomainHash(OUTBOX_DOMAIN);
expect(await validatorManager.domainHash()).to.equal(domainHash);
});
it('enrolls the validator set', async () => {
expect(await validatorManager.validators()).to.deep.equal([
validator0.address,
]);
});
it('sets the quorum threshold', async () => {
expect(await validatorManager.threshold()).to.equal([QUORUM_THRESHOLD]);
});
});
describe('#enrollValidator', () => {
it('enrolls a validator into the validator set', async () => {
await validatorManager.enrollValidator(validator1.address);
expect(await validatorManager.validators()).to.deep.equal([
validator0.address,
validator1.address,
]);
});
it('emits the EnrollValidator event', async () => {
expect(await validatorManager.enrollValidator(validator1.address))
.to.emit(validatorManager, 'EnrollValidator')
.withArgs(validator1.address, 2);
});
it('reverts if the validator is already enrolled', async () => {
await expect(
validatorManager.enrollValidator(validator0.address),
).to.be.revertedWith('already enrolled');
});
it('reverts when called by a non-owner', async () => {
await expect(
validatorManager.connect(nonOwner).enrollValidator(validator1.address),
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
describe('#unenrollValidator', () => {
beforeEach(async () => {
// Enroll a second validator
await validatorManager.enrollValidator(validator1.address);
});
it('unenrolls a validator from the validator set', async () => {
await validatorManager.unenrollValidator(validator1.address);
expect(await validatorManager.validators()).to.deep.equal([
validator0.address,
]);
});
it('emits the UnenrollValidator event', async () => {
expect(await validatorManager.unenrollValidator(validator1.address))
.to.emit(validatorManager, 'UnenrollValidator')
.withArgs(validator1.address, 1);
});
it('reverts if the resulting validator set size will be less than the quorum threshold', async () => {
await validatorManager.setThreshold(2);
await expect(
validatorManager.unenrollValidator(validator1.address),
).to.be.revertedWith('violates quorum threshold');
});
it('reverts if the validator is not already enrolled', async () => {
await expect(
validatorManager.unenrollValidator(validator2.address),
).to.be.revertedWith('!enrolled');
});
it('reverts when called by a non-owner', async () => {
await expect(
validatorManager
.connect(nonOwner)
.unenrollValidator(validator1.address),
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
describe('#setThreshold', () => {
beforeEach(async () => {
// Have 2 validators to allow us to have more than 1 valid
// quorum threshold
await validatorManager.enrollValidator(validator1.address);
});
it('sets the quorum threshold', async () => {
await validatorManager.setThreshold(2);
expect(await validatorManager.threshold()).to.equal(2);
});
it('emits the SetThreshold event', async () => {
expect(await validatorManager.setThreshold(2))
.to.emit(validatorManager, 'SetThreshold')
.withArgs(2);
});
it('reverts if the new quorum threshold is zero', async () => {
await expect(validatorManager.setThreshold(0)).to.be.revertedWith(
'!range',
);
});
it('reverts if the new quorum threshold is greater than the validator set size', async () => {
await expect(validatorManager.setThreshold(3)).to.be.revertedWith(
'!range',
);
});
it('reverts when called by a non-owner', async () => {
await expect(
validatorManager.connect(nonOwner).setThreshold(2),
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
describe('#validatorCount', () => {
it('returns the number of validators enrolled in the validator set', async () => {
expect(await validatorManager.validatorCount()).to.equal(1);
});
});
describe('#isQuorum', () => {
const root = ethers.utils.formatBytes32String('test root');
const index = 1;
beforeEach(async () => {
// Have 3 validators and a quorum of 2
await validatorManager.enrollValidator(validator1.address);
await validatorManager.enrollValidator(validator2.address);
await validatorManager.setThreshold(2);
});
it('returns true when there is a quorum', async () => {
const signatures = await signCheckpoint(root, index, [
validator0,
validator1,
]);
expect(await validatorManager.isQuorum(root, index, signatures)).to.be
.true;
});
it('returns true when a quorum exists even if provided with non-validator signatures', async () => {
const signatures = await signCheckpoint(
root,
index,
[validator0, validator1, validator3], // validator 3 is not enrolled
);
expect(await validatorManager.isQuorum(root, index, signatures)).to.be
.true;
});
it('returns false when the signature count is less than the quorum threshold', async () => {
const signatures = await signCheckpoint(root, index, [validator0]);
expect(await validatorManager.isQuorum(root, index, signatures)).to.be
.false;
});
it('returns false when some signatures are not from enrolled validators', async () => {
const signatures = await signCheckpoint(
root,
index,
[validator0, validator3], // validator 3 is not enrolled
);
expect(await validatorManager.isQuorum(root, index, signatures)).to.be
.false;
});
it('reverts when signatures are not ordered by their signer', async () => {
// Reverse the signature order, purposely messing up the
// ascending sort
const signatures = (
await signCheckpoint(root, index, [validator0, validator1])
).reverse();
await expect(
validatorManager.isQuorum(root, index, signatures),
).to.be.revertedWith('!sorted signers');
});
});
describe('#isValidator', () => {
it('returns true if an address is enrolled in the validator set', async () => {
expect(await validatorManager.isValidator(validator0.address)).to.be.true;
});
it('returns false if an address is not enrolled in the validator set', async () => {
expect(await validatorManager.isValidator(validator1.address)).to.be
.false;
});
});
describe('#_domainHash', () => {
it('matches Rust-produced domain hashes', async () => {
// Compare Rust output in json file to solidity output (json file matches
// hash for local domain of 1000)
for (let testCase of domainHashTestCases) {
const { expectedDomainHash } = testCase;
// This public function on TestMultisigValidatorManager exposes
// the internal _domainHash on MultisigValidatorManager.
const domainHash = await validatorManager.getDomainHash(
testCase.outboxDomain,
);
expect(domainHash).to.equal(expectedDomainHash);
}
});
});
});

@ -0,0 +1,112 @@
import { expect } from 'chai';
import { ethers } from 'hardhat';
import { types, utils, Validator } from '@abacus-network/utils';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import {
Outbox,
Outbox__factory,
OutboxValidatorManager,
OutboxValidatorManager__factory,
} from '../../types';
import { signCheckpoint } from './utils';
const OUTBOX_DOMAIN = 1234;
const INBOX_DOMAIN = 4321;
const QUORUM_THRESHOLD = 2;
describe('OutboxValidatorManager', () => {
let validatorManager: OutboxValidatorManager,
outbox: Outbox,
signer: SignerWithAddress,
validator0: Validator,
validator1: Validator;
before(async () => {
const signers = await ethers.getSigners();
signer = signers[0];
validator0 = await Validator.fromSigner(signers[1], OUTBOX_DOMAIN);
validator1 = await Validator.fromSigner(signers[2], OUTBOX_DOMAIN);
});
beforeEach(async () => {
const validatorManagerFactory = new OutboxValidatorManager__factory(signer);
validatorManager = await validatorManagerFactory.deploy(
OUTBOX_DOMAIN,
[validator0.address, validator1.address],
QUORUM_THRESHOLD,
);
const outboxFactory = new Outbox__factory(signer);
outbox = await outboxFactory.deploy(OUTBOX_DOMAIN);
await outbox.initialize(validatorManager.address);
});
describe('#improperCheckpoint', () => {
const root = ethers.utils.formatBytes32String('test root');
const index = 1;
it('accepts an improper checkpoint if there is a quorum', async () => {
const signatures = await signCheckpoint(
root,
index,
[validator0, validator1], // 2/2 signers, making a quorum
);
await expect(
validatorManager.improperCheckpoint(
outbox.address,
root,
index,
signatures,
),
)
.to.emit(validatorManager, 'ImproperCheckpoint')
.withArgs(outbox.address, root, index, signatures);
expect(await outbox.state()).to.equal(types.AbacusState.FAILED);
});
it('reverts if there is not a quorum', async () => {
const signatures = await signCheckpoint(
root,
index,
[validator0], // 1/2 signers is not a quorum
);
await expect(
validatorManager.improperCheckpoint(
outbox.address,
root,
index,
signatures,
),
).to.be.revertedWith('!quorum');
});
it('reverts if the checkpoint is not improper', async () => {
const message = `0x${Buffer.alloc(10).toString('hex')}`;
await outbox.dispatch(
INBOX_DOMAIN,
utils.addressToBytes32(signer.address),
message,
);
await outbox.checkpoint();
const [root, index] = await outbox.latestCheckpoint();
const signatures = await signCheckpoint(
root,
index.toNumber(),
[validator0, validator1], // 2/2 signers, making a quorum
);
await expect(
validatorManager.improperCheckpoint(
outbox.address,
root,
index,
signatures,
),
).to.be.revertedWith('!improper');
});
});
});

@ -0,0 +1,30 @@
import { types, Validator } from '@abacus-network/utils';
// Signs a checkpoint with the provided validators and returns
// the signatures sorted by validator addresses in ascending order
export async function signCheckpoint(
root: types.HexString,
index: number,
unsortedValidators: Validator[],
): Promise<string[]> {
const validators = unsortedValidators.sort((a, b) => {
// Remove the checksums for accurate comparison
const aAddress = a.address.toLowerCase();
const bAddress = b.address.toLowerCase();
if (aAddress < bAddress) {
return -1;
} else if (aAddress > bAddress) {
return 1;
} else {
return 0;
}
});
const signedCheckpoints = await Promise.all(
validators.map((validator) => validator.signCheckpoint(root, index)),
);
return signedCheckpoints.map(
(signedCheckpoint) => signedCheckpoint.signature,
);
}

@ -1,150 +0,0 @@
import { ethers } from 'hardhat';
import { expect } from 'chai';
import { types, utils, Validator } from '@abacus-network/utils';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import {
Outbox__factory,
Outbox,
ValidatorManager__factory,
ValidatorManager,
} from '../types';
const domainHashCases = require('../../../vectors/domainHash.json');
const localDomain = 1000;
describe('ValidatorManager', async () => {
let signer: SignerWithAddress,
fakeSigner: SignerWithAddress,
validatorManager: ValidatorManager,
validator: Validator,
fakeValidator: Validator;
before(async () => {
[signer, fakeSigner] = await ethers.getSigners();
validator = await Validator.fromSigner(signer, localDomain);
fakeValidator = await Validator.fromSigner(fakeSigner, localDomain);
});
beforeEach(async () => {
const validatorManagerFactory = new ValidatorManager__factory(signer);
validatorManager = await validatorManagerFactory.deploy();
await validatorManager.enrollValidator(localDomain, validator.address);
});
it('Accepts validator signature', async () => {
const root = ethers.utils.formatBytes32String('root');
const index = 1;
const { signature } = await validator.signCheckpoint(root, index);
const isValid = await validatorManager.isValidatorSignature(
localDomain,
root,
index,
signature,
);
expect(isValid).to.be.true;
});
it('Rejects non-validator signature', async () => {
const root = ethers.utils.formatBytes32String('root');
const index = 1;
const { signature } = await fakeValidator.signCheckpoint(root, index);
const isValid = await validatorManager.isValidatorSignature(
localDomain,
root,
index,
signature,
);
expect(isValid).to.be.false;
});
it('Calculated domain hash matches Rust-produced domain hash', async () => {
// Compare Rust output in json file to solidity output (json file matches
// hash for local domain of 1000)
for (let testCase of domainHashCases) {
const { expectedDomainHash } = testCase;
const domainHash = await validatorManager.domainHash(
testCase.outboxDomain,
);
expect(domainHash).to.equal(expectedDomainHash);
}
});
describe('improper checkpoints', async () => {
let outbox: Outbox;
beforeEach(async () => {
const outboxFactory = new Outbox__factory(signer);
outbox = await outboxFactory.deploy(localDomain);
await outbox.initialize(validatorManager.address);
});
it('Accepts improper checkpoint from validator', async () => {
const root = ethers.utils.formatBytes32String('root');
const index = 1;
const { signature } = await validator.signCheckpoint(root, index);
// Send message with signer address as msg.sender
await expect(
validatorManager.improperCheckpoint(
outbox.address,
root,
index,
signature,
),
)
.to.emit(validatorManager, 'ImproperCheckpoint')
.withArgs(
outbox.address,
localDomain,
validator.address,
root,
index,
signature,
);
expect(await outbox.state()).to.equal(types.AbacusState.FAILED);
});
it('Rejects improper checkpoint from non-validator', async () => {
const root = ethers.utils.formatBytes32String('root');
const index = 1;
const { signature } = await fakeValidator.signCheckpoint(root, index);
// Send message with signer address as msg.sender
await expect(
validatorManager.improperCheckpoint(
outbox.address,
root,
index,
signature,
),
).to.be.revertedWith('!validator sig');
});
it('Rejects proper checkpoint from validator', async () => {
const message = `0x${Buffer.alloc(10).toString('hex')}`;
await outbox.dispatch(
localDomain,
utils.addressToBytes32(signer.address),
message,
);
await outbox.checkpoint();
const [root, index] = await outbox.latestCheckpoint();
const { signature } = await validator.signCheckpoint(
root,
index.toNumber(),
);
// Send message with signer address as msg.sender
await expect(
validatorManager.improperCheckpoint(
outbox.address,
root,
index,
signature,
),
).to.be.revertedWith('!improper');
});
});
});

@ -1,3 +1,5 @@
import { ethers } from "ethers";
import { types } from "@abacus-network/utils";
import {
InterchainGasPaymaster,
InterchainGasPaymaster__factory,
@ -7,21 +9,17 @@ import {
TestInbox__factory,
UpgradeBeaconController,
UpgradeBeaconController__factory,
ValidatorManager,
ValidatorManager__factory,
XAppConnectionManager,
XAppConnectionManager__factory,
} from "@abacus-network/core";
import { types, Validator } from "@abacus-network/utils";
import { ethers } from "ethers";
import { TestDeploy } from "./TestDeploy";
export type TestAbacusConfig = {
signer: Record<types.Domain, ethers.Signer>;
};
// Outbox & inbox validator managers are not required for testing and are therefore omitted.
export type TestAbacusInstance = {
validatorManager: ValidatorManager;
outbox: Outbox;
xAppConnectionManager: XAppConnectionManager;
upgradeBeaconController: UpgradeBeaconController;
@ -49,17 +47,6 @@ export class TestAbacusDeploy extends TestDeploy<
async deployInstance(domain: types.Domain): Promise<TestAbacusInstance> {
const signer = this.config.signer[domain];
const signerAddress = await signer.getAddress();
const validatorManagerFactory = new ValidatorManager__factory(signer);
const validatorManager = await validatorManagerFactory.deploy();
await validatorManager.enrollValidator(domain, signerAddress);
// this.remotes reads this.instances which has not yet been set.
const remotes = Object.keys(this.config.signer).map((d) => parseInt(d));
await Promise.all(
remotes.map(async (remote) =>
validatorManager.enrollValidator(remote, signerAddress)
)
);
const upgradeBeaconControllerFactory = new UpgradeBeaconController__factory(
signer
@ -69,7 +56,11 @@ export class TestAbacusDeploy extends TestDeploy<
const outboxFactory = new Outbox__factory(signer);
const outbox = await outboxFactory.deploy(domain);
await outbox.initialize(validatorManager.address);
// Outbox will require the validator manager to be a contract. We don't
// actually make use of the validator manager, so just we pass in the
// upgradeBeaconController as the validator manager to satisfy the contract
// requirement and avoid deploying a new validator manager.
await outbox.initialize(upgradeBeaconController.address);
const xAppConnectionManagerFactory = new XAppConnectionManager__factory(
signer
@ -87,11 +78,18 @@ export class TestAbacusDeploy extends TestDeploy<
const inboxFactory = new TestInbox__factory(signer);
const inboxes: Record<types.Domain, TestInbox> = {};
// this.remotes reads this.instances which has not yet been set.
const remotes = Object.keys(this.config.signer).map((d) => parseInt(d));
const deploys = remotes.map(async (remote) => {
const inbox = await inboxFactory.deploy(domain);
// Inbox will require the validator manager to be a contract. We don't
// actually make use of the validator manager, so we just pass in the
// upgradeBeaconController as the validator manager to satisfy the contract
// requirement and avoid deploying a new validator manager.
await inbox.initialize(
remote,
validatorManager.address,
upgradeBeaconController.address,
ethers.constants.HashZero,
0
);
@ -103,7 +101,6 @@ export class TestAbacusDeploy extends TestDeploy<
outbox,
xAppConnectionManager,
interchainGasPaymaster,
validatorManager,
inboxes,
upgradeBeaconController,
};
@ -113,9 +110,8 @@ export class TestAbacusDeploy extends TestDeploy<
await this.outbox(domain).transferOwnership(address);
await this.upgradeBeaconController(domain).transferOwnership(address);
await this.xAppConnectionManager(domain).transferOwnership(address);
await this.validatorManager(domain).transferOwnership(address);
for (const origin of this.remotes(domain)) {
await this.inbox(origin, domain).transferOwnership(address);
for (const remote of this.remotes(domain)) {
await this.inbox(domain, remote).transferOwnership(address);
}
}
@ -139,10 +135,6 @@ export class TestAbacusDeploy extends TestDeploy<
return this.instances[domain].xAppConnectionManager;
}
validatorManager(domain: types.Domain): ValidatorManager {
return this.instances[domain].validatorManager;
}
async processMessages(): Promise<
Map<types.Domain, Map<types.Domain, ethers.providers.TransactionResponse[]>>
> {
@ -175,20 +167,9 @@ export class TestAbacusDeploy extends TestDeploy<
await outbox.checkpoint();
const [root, index] = await outbox.latestCheckpoint();
// Sign the checkpoint and update the Inboxes to the latest root.
// This is technically not necessary given that we are not proving against
// a root in the TestInbox.
const validator = await Validator.fromSigner(
this.config.signer[origin],
origin
);
const { signature } = await validator.signCheckpoint(
root,
index.toNumber()
);
for (const destination of this.remotes(origin)) {
const inbox = this.inbox(origin, destination);
await inbox.checkpoint(root, index, signature);
await inbox.setCheckpoint(root, index);
}
// Find all unprocessed messages dispatched on the outbox since the previous checkpoint.

@ -1,11 +1,23 @@
import { CoreConfig } from '../../../src/core';
export const core: CoreConfig = {
validators: {
validatorManagers: {
// Hardhat accounts 1-4
alfajores: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
fuji: '0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc',
kovan: '0x90f79bf6eb2c4f870365e785982e1f101e93b906',
mumbai: '0x15d34aaf54267db7d7c367839aaf71a00a2c6a65',
alfajores: {
validators: ['0x70997970c51812dc3a010c7d01b50e0d17dc79c8'],
threshold: 1,
},
fuji: {
validators: ['0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc'],
threshold: 1,
},
kovan: {
validators: ['0x90f79bf6eb2c4f870365e785982e1f101e93b906'],
threshold: 1,
},
mumbai: {
validators: ['0x15d34aaf54267db7d7c367839aaf71a00a2c6a65'],
threshold: 1,
},
},
};

@ -1,14 +1,22 @@
import { expect } from 'chai';
import { MultisigValidatorManager } from '@abacus-network/core';
import { types } from '@abacus-network/utils';
import { AbacusCore } from '@abacus-network/sdk';
import { AbacusAppChecker, CheckerViolation } from '@abacus-network/deploy';
import { CoreConfig } from './types';
import { setDifference } from '../utils/utils';
export enum CoreViolationType {
ValidatorManager = 'ValidatorManager',
Validator = 'Validator',
}
export enum ValidatorViolationType {
EnrollValidator = 'EnrollValidator',
UnenrollValidator = 'UnenrollValidator',
Threshold = 'Threshold',
}
export interface ValidatorManagerViolation extends CheckerViolation {
type: CoreViolationType.ValidatorManager;
}
@ -16,7 +24,8 @@ export interface ValidatorManagerViolation extends CheckerViolation {
export interface ValidatorViolation extends CheckerViolation {
type: CoreViolationType.Validator;
data: {
remote: number;
type: ValidatorViolationType;
validatorManagerAddress: string;
};
}
@ -42,7 +51,7 @@ export class AbacusCoreChecker extends AbacusAppChecker<
await this.checkOutbox(domain);
await this.checkInboxes(domain);
await this.checkXAppConnectionManager(domain);
await this.checkValidatorManager(domain);
await this.checkValidatorManagers(domain);
}
async checkOwnership(
@ -51,13 +60,16 @@ export class AbacusCoreChecker extends AbacusAppChecker<
): Promise<void> {
const contracts = this.app.mustGetContracts(domain);
const owners = [
contracts.validatorManager.owner(),
contracts.xAppConnectionManager.owner(),
contracts.upgradeBeaconController.owner(),
contracts.outbox.owner(),
contracts.outboxValidatorManager.owner(),
];
this.app.remoteDomainNumbers(domain).map((remote) => {
owners.push(this.app.mustGetInbox(remote, domain).owner());
owners.push(
this.app.mustGetInboxValidatorManager(remote, domain).owner(),
);
});
const actual = await Promise.all(owners);
actual.map((_) => expect(_).to.equal(owner));
@ -68,7 +80,7 @@ export class AbacusCoreChecker extends AbacusAppChecker<
const outbox = contracts.outbox;
// validatorManager is set on Outbox
const actualManager = await outbox.validatorManager();
const expectedManager = contracts.validatorManager.address;
const expectedManager = contracts.outboxValidatorManager.address;
if (actualManager !== expectedManager) {
const violation: ValidatorManagerViolation = {
domain,
@ -80,27 +92,101 @@ export class AbacusCoreChecker extends AbacusAppChecker<
}
}
async checkValidatorManager(domain: types.Domain): Promise<void> {
const manager = this.app.mustGetContracts(domain).validatorManager;
for (const d of this.app.domainNumbers) {
const name = this.app.mustResolveDomainName(d);
const expected = this.config.validators[name];
expect(expected).to.not.be.undefined;
const actual = await manager.validators(d);
expect(actual).to.not.be.undefined;
if (actual !== expected && expected !== undefined) {
const violation: ValidatorViolation = {
// Checks validator sets of the OutboxValidatorManager and all
// InboxValidatorManagers on the domain.
async checkValidatorManagers(domain: types.Domain): Promise<void> {
const promises = this.app.domainNumbers.map((outboxDomain: number) => {
let validatorManager: MultisigValidatorManager;
// Check the OutboxValidatorManager
if (domain === outboxDomain) {
validatorManager =
this.app.mustGetContracts(domain).outboxValidatorManager;
} else {
// Check an InboxValidatorManager
validatorManager = this.app.mustGetInboxValidatorManager(
outboxDomain,
domain,
type: CoreViolationType.Validator,
actual,
expected,
data: {
remote: d,
},
};
this.addViolation(violation);
);
}
return this.checkValidatorManager(domain, outboxDomain, validatorManager);
});
await Promise.all(promises);
}
// Checks the validator set for a MultisigValidatorManager on the localDomain that tracks
// the validator set for the outboxDomain.
// If localDomain == outboxDomain, this checks the OutboxValidatorManager, otherwise
// it checks an InboxValidatorManager.
async checkValidatorManager(
localDomain: types.Domain,
outboxDomain: types.Domain,
validatorManager: MultisigValidatorManager,
): Promise<void> {
const outboxDomainName = this.app.mustResolveDomainName(outboxDomain);
const validatorManagerConfig =
this.config.validatorManagers[outboxDomainName];
expect(validatorManagerConfig).to.not.be.undefined;
const expectedValidators = validatorManagerConfig?.validators;
expect(expectedValidators).to.not.be.undefined;
const actualValidators = await validatorManager.validators();
expect(actualValidators).to.not.be.undefined;
const expectedSet = new Set<string>(expectedValidators);
const actualSet = new Set<string>(actualValidators);
const toEnroll = setDifference(expectedSet, actualSet);
const toUnenroll = setDifference(actualSet, expectedSet);
// Validators that should be enrolled
for (const validatorToEnroll of toEnroll) {
const violation: ValidatorViolation = {
domain: localDomain,
type: CoreViolationType.Validator,
actual: undefined,
expected: validatorToEnroll,
data: {
type: ValidatorViolationType.EnrollValidator,
validatorManagerAddress: validatorManager.address,
},
};
this.addViolation(violation);
}
// Validators that should be unenrolled
for (const validatorToUnenroll of toUnenroll) {
const violation: ValidatorViolation = {
domain: localDomain,
type: CoreViolationType.Validator,
actual: validatorToUnenroll,
expected: undefined,
data: {
type: ValidatorViolationType.UnenrollValidator,
validatorManagerAddress: validatorManager.address,
},
};
this.addViolation(violation);
}
const expectedThreshold = validatorManagerConfig?.threshold;
expect(expectedThreshold).to.not.be.undefined;
const actualThreshold = await validatorManager.threshold();
if (expectedThreshold !== actualThreshold.toNumber()) {
const violation: ValidatorViolation = {
domain: localDomain,
type: CoreViolationType.Validator,
actual: actualThreshold,
expected: expectedThreshold,
data: {
type: ValidatorViolationType.Threshold,
validatorManagerAddress: validatorManager.address,
},
};
this.addViolation(violation);
}
}
@ -108,12 +194,16 @@ export class AbacusCoreChecker extends AbacusAppChecker<
// Check that all inboxes on this domain are pointed to the right validator
// manager.
const contracts = this.app.mustGetContracts(domain);
const validatorManager = contracts.validatorManager;
await Promise.all(
this.app.remoteDomainNumbers(domain).map(async (remote) => {
const expectedValidatorManager = this.app.mustGetInboxValidatorManager(
remote,
domain,
);
expect(
await this.app.mustGetInbox(remote, domain).validatorManager(),
).to.equal(validatorManager.address);
).to.equal(expectedValidatorManager.address);
}),
);

@ -11,17 +11,19 @@ import { AbacusAppDeployer, ProxiedContract } from '@abacus-network/deploy';
import {
UpgradeBeaconController,
XAppConnectionManager,
ValidatorManager,
InboxValidatorManager,
InboxValidatorManager__factory,
OutboxValidatorManager,
OutboxValidatorManager__factory,
Inbox,
UpgradeBeaconController__factory,
XAppConnectionManager__factory,
ValidatorManager__factory,
Outbox__factory,
Inbox__factory,
InterchainGasPaymaster__factory,
} from '@abacus-network/core';
import { DeployEnvironment, RustConfig } from '../config';
import { CoreConfig } from './types';
import { CoreConfig, ValidatorManagerConfig } from './types';
export class AbacusCoreDeployer extends AbacusAppDeployer<
CoreContractAddresses,
@ -40,21 +42,19 @@ export class AbacusCoreDeployer extends AbacusAppDeployer<
new UpgradeBeaconController__factory(signer),
);
const validatorManager: ValidatorManager = await this.deployContract(
const outboxValidatorManagerConfig = this.validatorManagerConfig(
config,
domain,
'ValidatorManager',
new ValidatorManager__factory(signer),
);
for (const name of this.domainNames) {
const validator = config.validators[name];
if (!validator) throw new Error(`No validator for ${name}`);
await validatorManager.enrollValidator(
this.resolveDomain(name),
validator,
overrides,
const outboxValidatorManager: OutboxValidatorManager =
await this.deployContract(
domain,
'OutboxValidatorManager',
new OutboxValidatorManager__factory(signer),
domain,
outboxValidatorManagerConfig.validators,
outboxValidatorManagerConfig.threshold,
);
}
const outbox = await this.deployProxiedContract(
domain,
@ -62,7 +62,7 @@ export class AbacusCoreDeployer extends AbacusAppDeployer<
new Outbox__factory(signer),
upgradeBeaconController.address,
[domain],
[validatorManager.address],
[outboxValidatorManager.address],
);
const interchainGasPaymaster = await this.deployContract(
@ -83,14 +83,39 @@ export class AbacusCoreDeployer extends AbacusAppDeployer<
overrides,
);
const inboxValidatorManagers: Record<types.Domain, InboxValidatorManager> =
{};
const inboxValidatorManagerAddresses: Partial<
Record<ChainName, types.Address>
> = {};
const inboxes: Record<types.Domain, ProxiedContract<Inbox>> = {};
const inboxAddresses: Partial<Record<ChainName, ProxiedAddress>> = {};
const remotes = this.remoteDomainNumbers(domain);
for (let i = 0; i < remotes.length; i++) {
const remote = remotes[i];
const remoteName = this.mustResolveDomainName(remote);
const validatorManagerConfig = this.validatorManagerConfig(
config,
remote,
);
const inboxValidatorManager: InboxValidatorManager =
await this.deployContract(
domain,
'InboxValidatorManager',
new InboxValidatorManager__factory(signer),
remote,
validatorManagerConfig.validators,
validatorManagerConfig.threshold,
);
inboxValidatorManagers[remote] = inboxValidatorManager;
inboxValidatorManagerAddresses[remoteName] =
inboxValidatorManager.address;
const initArgs = [
remote,
validatorManager.address,
inboxValidatorManager.address,
ethers.constants.HashZero,
0,
];
@ -124,8 +149,9 @@ export class AbacusCoreDeployer extends AbacusAppDeployer<
const addresses = {
upgradeBeaconController: upgradeBeaconController.address,
xAppConnectionManager: xAppConnectionManager.address,
validatorManager: validatorManager.address,
interchainGasPaymaster: interchainGasPaymaster.address,
outboxValidatorManager: outboxValidatorManager.address,
inboxValidatorManagers: inboxValidatorManagerAddresses,
outbox: outbox.addresses,
inboxes: inboxAddresses,
};
@ -202,15 +228,30 @@ export class AbacusCoreDeployer extends AbacusAppDeployer<
): Promise<ethers.ContractReceipt> {
const contracts = core.mustGetContracts(domain);
const overrides = core.getOverrides(domain);
await contracts.validatorManager.transferOwnership(owner, overrides);
await contracts.outboxValidatorManager.transferOwnership(owner, overrides);
await contracts.xAppConnectionManager.transferOwnership(owner, overrides);
await contracts.upgradeBeaconController.transferOwnership(owner, overrides);
for (const chain of Object.keys(
contracts.addresses.inboxes,
) as ChainName[]) {
await contracts
.inboxValidatorManager(chain)
.transferOwnership(owner, overrides);
await contracts.inbox(chain).transferOwnership(owner, overrides);
}
const tx = await contracts.outbox.transferOwnership(owner, overrides);
return tx.wait(core.getConfirmations(domain));
}
validatorManagerConfig(
config: CoreConfig,
domain: types.Domain,
): ValidatorManagerConfig {
const domainName = this.mustResolveDomainName(domain);
const validatorManagerConfig = config.validatorManagers[domainName];
if (!validatorManagerConfig) {
throw new Error(`No validator manager config for ${domainName}`);
}
return validatorManagerConfig;
}
}

@ -10,8 +10,11 @@ import {
AbacusCoreChecker,
CoreViolationType,
ValidatorViolation,
ValidatorViolationType,
} from './check';
import { CoreConfig } from './types';
import { MultisigValidatorManager__factory } from '@abacus-network/core';
import { PopulatedTransaction } from 'ethers';
interface DomainedCall {
domain: number;
@ -68,12 +71,39 @@ export class AbacusCoreGovernor extends AbacusCoreChecker {
violation: ValidatorViolation,
): Promise<DomainedCall> {
const domain = violation.domain;
const manager = this.app.mustGetContracts(domain).validatorManager;
expect(manager).to.not.be.undefined;
const tx = await manager.populateTransaction.enrollValidator(
violation.data.remote,
violation.expected,
const provider = this.app.mustGetProvider(domain);
const validatorManager = MultisigValidatorManager__factory.connect(
violation.data.validatorManagerAddress,
provider,
);
let tx: PopulatedTransaction;
switch (violation.data.type) {
case ValidatorViolationType.EnrollValidator:
// Enrolling a new validator
tx = await validatorManager.populateTransaction.enrollValidator(
violation.expected,
);
break;
case ValidatorViolationType.UnenrollValidator:
// Unenrolling an existing validator
tx = await validatorManager.populateTransaction.unenrollValidator(
violation.actual,
);
break;
case ValidatorViolationType.Threshold:
tx = await validatorManager.populateTransaction.setThreshold(
violation.expected,
);
break;
default:
throw new Error(
`Invalid validator violation type: ${violation.data.type}`,
);
}
if (tx.to === undefined) throw new Error('undefined tx.to');
return { domain, call: tx as Call };
}

@ -1,6 +1,11 @@
import { types } from '@abacus-network/utils';
import { ChainName } from '@abacus-network/sdk';
export type ValidatorManagerConfig = {
validators: Array<types.Address>;
threshold: number;
};
export type CoreConfig = {
validators: Partial<Record<ChainName, types.Address>>;
validatorManagers: Partial<Record<ChainName, ValidatorManagerConfig>>;
};

@ -157,3 +157,13 @@ export function writeJSON(directory: string, filename: string, obj: any) {
JSON.stringify(obj, null, 2),
);
}
// Returns a \ b
// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#implementing_basic_set_operations
export function setDifference<T>(a: Set<T>, b: Set<T>) {
let diff = new Set(a);
for (const element of b) {
diff.delete(element);
}
return diff;
}

@ -1,4 +1,4 @@
import { Inbox } from '@abacus-network/core';
import { Inbox, InboxValidatorManager } from '@abacus-network/core';
import { AbacusApp } from '../app';
import { domains } from '../domains';
@ -25,4 +25,13 @@ export class AbacusCore extends AbacusApp<
const srcName = this.mustGetDomain(src).name;
return contracts.inbox(srcName);
}
mustGetInboxValidatorManager(
src: NameOrDomain,
dest: NameOrDomain,
): InboxValidatorManager {
const contracts = this.mustGetContracts(dest);
const srcName = this.mustGetDomain(src).name;
return contracts.inboxValidatorManager(srcName);
}
}

@ -3,8 +3,10 @@ import {
XAppConnectionManager__factory,
UpgradeBeaconController,
UpgradeBeaconController__factory,
ValidatorManager,
ValidatorManager__factory,
OutboxValidatorManager,
OutboxValidatorManager__factory,
InboxValidatorManager,
InboxValidatorManager__factory,
Outbox,
Outbox__factory,
Inbox,
@ -20,10 +22,11 @@ import { ChainName, ProxiedAddress } from '../types';
export type CoreContractAddresses = {
upgradeBeaconController: types.Address;
xAppConnectionManager: types.Address;
validatorManager: types.Address;
interchainGasPaymaster: types.Address;
outbox: ProxiedAddress;
inboxes: Partial<Record<ChainName, ProxiedAddress>>;
outboxValidatorManager: types.Address;
inboxValidatorManagers: Partial<Record<ChainName, types.Address>>;
};
export class CoreContracts extends AbacusAppContracts<CoreContractAddresses> {
@ -35,6 +38,17 @@ export class CoreContracts extends AbacusAppContracts<CoreContractAddresses> {
return Inbox__factory.connect(inbox.proxy, this.connection);
}
inboxValidatorManager(chain: ChainName): InboxValidatorManager {
const inboxValidatorManager = this.addresses.inboxValidatorManagers[chain];
if (!inboxValidatorManager) {
throw new Error(`No inboxValidatorManager for ${chain}`);
}
return InboxValidatorManager__factory.connect(
inboxValidatorManager,
this.connection,
);
}
get outbox(): Outbox {
return Outbox__factory.connect(
this.addresses.outbox.proxy,
@ -42,9 +56,9 @@ export class CoreContracts extends AbacusAppContracts<CoreContractAddresses> {
);
}
get validatorManager(): ValidatorManager {
return ValidatorManager__factory.connect(
this.addresses.validatorManager,
get outboxValidatorManager(): OutboxValidatorManager {
return OutboxValidatorManager__factory.connect(
this.addresses.outboxValidatorManager,
this.connection,
);
}

@ -1,113 +1,133 @@
export const addresses = {
alfajores: {
upgradeBeaconController: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
xAppConnectionManager: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788',
validatorManager: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
interchainGasPaymaster: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318',
xAppConnectionManager: '0x0165878A594ca255338adfa4d48449f69242Eb8F',
interchainGasPaymaster: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707',
outboxValidatorManager: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
inboxValidatorManagers: {
kovan: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318',
mumbai: '0x9A676e781A523b5d0C0e43731313A708CB607508',
fuji: '0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE',
},
outbox: {
proxy: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6',
implementation: '0x0165878A594ca255338adfa4d48449f69242Eb8F',
beacon: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853',
proxy: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9',
implementation: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0',
beacon: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
},
inboxes: {
kovan: {
proxy: '0x0B306BF915C4d645ff596e518fAf3F9669b97016',
implementation: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82',
beacon: '0x9A676e781A523b5d0C0e43731313A708CB607508',
proxy: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0',
implementation: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788',
beacon: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e',
},
mumbai: {
proxy: '0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE',
implementation: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82',
beacon: '0x9A676e781A523b5d0C0e43731313A708CB607508',
proxy: '0x0B306BF915C4d645ff596e518fAf3F9669b97016',
implementation: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788',
beacon: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e',
},
fuji: {
proxy: '0x3Aa5ebB10DC797CAC828524e59A333d0A371443c',
implementation: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82',
beacon: '0x9A676e781A523b5d0C0e43731313A708CB607508',
proxy: '0x68B1D87F95878fE05B998F19b66F4baba5De1aed',
implementation: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788',
beacon: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e',
},
},
},
kovan: {
upgradeBeaconController: '0x59b670e9fA9D0A427751Af201D676719a970857b',
xAppConnectionManager: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690',
validatorManager: '0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1',
interchainGasPaymaster: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E',
upgradeBeaconController: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d',
xAppConnectionManager: '0x7a2088a1bFc9d81c55368AE168C2C02570cB814F',
interchainGasPaymaster: '0x4A679253410272dd5232B3Ff7cF5dbB88f295319',
outboxValidatorManager: '0x59b670e9fA9D0A427751Af201D676719a970857b',
inboxValidatorManagers: {
alfajores: '0x67d269191c92Caf3cD7723F116c85e6E9bf55933',
mumbai: '0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9',
fuji: '0xf5059a5D33d5853360D16C683c16e67980206f36',
},
outbox: {
proxy: '0x67d269191c92Caf3cD7723F116c85e6E9bf55933',
implementation: '0x09635F643e140090A9A8Dcd712eD6285858ceBef',
beacon: '0xc5a5C42992dECbae36851359345FE25997F5C42d',
proxy: '0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f',
implementation: '0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1',
beacon: '0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44',
},
inboxes: {
alfajores: {
proxy: '0x851356ae760d987E095750cCeb3bC6014560891C',
implementation: '0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9',
beacon: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8',
proxy: '0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB',
implementation: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E',
beacon: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690',
},
mumbai: {
proxy: '0x95401dc811bb5740090279Ba06cfA8fcF6113778',
implementation: '0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9',
beacon: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8',
proxy: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8',
implementation: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E',
beacon: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690',
},
fuji: {
proxy: '0x70e0bA845a1A0F2DA3359C97E0285013525FFC49',
implementation: '0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9',
beacon: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8',
proxy: '0x95401dc811bb5740090279Ba06cfA8fcF6113778',
implementation: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E',
beacon: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690',
},
},
},
mumbai: {
upgradeBeaconController: '0x99bbA657f2BbC93c02D617f8bA121cB8Fc104Acf',
xAppConnectionManager: '0xb7278A61aa25c888815aFC32Ad3cC52fF24fE575',
validatorManager: '0x0E801D84Fa97b50751Dbf25036d067dCf18858bF',
interchainGasPaymaster: '0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154',
upgradeBeaconController: '0x70e0bA845a1A0F2DA3359C97E0285013525FFC49',
xAppConnectionManager: '0x5eb3Bc0a489C5A8288765d2336659EbCA68FCd00',
interchainGasPaymaster: '0x9d4454B023096f34B160D6B654540c56A1F81688',
outboxValidatorManager: '0x4826533B4897376654Bb4d4AD88B7faFD0C98528',
inboxValidatorManagers: {
alfajores: '0x4c5859f0F772848b2D91F1D83E2Fe57935348029',
kovan: '0x82e01223d51Eb87e16A03E24687EDF0F294da6f1',
fuji: '0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650',
},
outbox: {
proxy: '0x1291Be112d480055DaFd8a610b7d1e203891C274',
implementation: '0x809d550fca64d94Bd9F66E60752A544199cfAC3D',
beacon: '0x4c5859f0F772848b2D91F1D83E2Fe57935348029',
proxy: '0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf',
implementation: '0x99bbA657f2BbC93c02D617f8bA121cB8Fc104Acf',
beacon: '0x0E801D84Fa97b50751Dbf25036d067dCf18858bF',
},
inboxes: {
alfajores: {
proxy: '0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650',
implementation: '0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3',
beacon: '0x7969c5eD335650692Bc04293B07F5BF2e7A673C0',
proxy: '0xb7278A61aa25c888815aFC32Ad3cC52fF24fE575',
implementation: '0x1291Be112d480055DaFd8a610b7d1e203891C274',
beacon: '0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154',
},
kovan: {
proxy: '0xFD471836031dc5108809D173A067e8486B9047A3',
implementation: '0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3',
beacon: '0x7969c5eD335650692Bc04293B07F5BF2e7A673C0',
proxy: '0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3',
implementation: '0x1291Be112d480055DaFd8a610b7d1e203891C274',
beacon: '0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154',
},
fuji: {
proxy: '0x1429859428C0aBc9C2C47C8Ee9FBaf82cFA0F20f',
implementation: '0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3',
beacon: '0x7969c5eD335650692Bc04293B07F5BF2e7A673C0',
proxy: '0xc351628EB244ec633d5f21fBD6621e1a683B1181',
implementation: '0x1291Be112d480055DaFd8a610b7d1e203891C274',
beacon: '0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154',
},
},
},
fuji: {
upgradeBeaconController: '0x162A433068F51e18b7d13932F27e66a3f99E6890',
xAppConnectionManager: '0xDC11f7E700A4c898AE5CAddB1082cFfa76512aDD',
validatorManager: '0x922D6956C99E12DFeB3224DEA977D0939758A1Fe',
interchainGasPaymaster: '0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43',
upgradeBeaconController: '0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc',
xAppConnectionManager: '0x1fA02b2d6A771842690194Cf62D91bdd92BfE28d',
interchainGasPaymaster: '0x5081a39b8A5f0E35a8D959395a630b68B74Dd30f',
outboxValidatorManager: '0x1429859428C0aBc9C2C47C8Ee9FBaf82cFA0F20f',
inboxValidatorManagers: {
alfajores: '0x4C4a2f8c81640e47606d3fd77B353E87Ba015584',
kovan: '0x51A1ceB83B83F1985a81C295d1fF28Afef186E02',
mumbai: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726',
},
outbox: {
proxy: '0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2',
implementation: '0x4C4a2f8c81640e47606d3fd77B353E87Ba015584',
beacon: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823',
proxy: '0x922D6956C99E12DFeB3224DEA977D0939758A1Fe',
implementation: '0xB0D4afd8879eD9F52b28595d31B441D079B2Ca07',
beacon: '0x162A433068F51e18b7d13932F27e66a3f99E6890',
},
inboxes: {
alfajores: {
proxy: '0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB',
implementation: '0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7',
beacon: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726',
proxy: '0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43',
implementation: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823',
beacon: '0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2',
},
kovan: {
proxy: '0x172076E0166D1F9Cc711C77Adf8488051744980C',
implementation: '0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7',
beacon: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726',
proxy: '0x36b58F5C1969B7b6591D752ea6F5486D069010AB',
implementation: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823',
beacon: '0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2',
},
mumbai: {
proxy: '0xBEc49fA140aCaA83533fB00A2BB19bDdd0290f25',
implementation: '0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7',
beacon: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726',
proxy: '0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB',
implementation: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823',
beacon: '0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2',
},
},
},

@ -7,8 +7,11 @@ export const testAddresses = {
test1: {
upgradeBeaconController: ZERO_ADDRESS,
xAppConnectionManager: ZERO_ADDRESS,
validatorManager: ZERO_ADDRESS,
interchainGasPaymaster: ZERO_ADDRESS,
outboxValidatorManager: ZERO_ADDRESS,
inboxValidatorManagers: {
test2: ZERO_ADDRESS,
},
outbox: {
proxy: ZERO_ADDRESS,
implementation: ZERO_ADDRESS,
@ -25,8 +28,11 @@ export const testAddresses = {
test2: {
upgradeBeaconController: ZERO_ADDRESS,
xAppConnectionManager: ZERO_ADDRESS,
validatorManager: ZERO_ADDRESS,
interchainGasPaymaster: ZERO_ADDRESS,
outboxValidatorManager: ZERO_ADDRESS,
inboxValidatorManagers: {
test1: ZERO_ADDRESS,
},
outbox: {
proxy: ZERO_ADDRESS,
implementation: ZERO_ADDRESS,

Loading…
Cancel
Save