parent
f46adedbcb
commit
fccf96433d
@ -0,0 +1,20 @@ |
||||
// 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 _outboxDomain, |
||||
address[] memory _validatorSet, |
||||
uint256 _quorumThreshold |
||||
) |
||||
MultisigValidatorManager(_outboxDomain, _validatorSet, _quorumThreshold) |
||||
{} |
||||
} |
@ -0,0 +1,265 @@ |
||||
import { ethers } from 'hardhat'; |
||||
import { expect } from 'chai'; |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||
import { types } from '@abacus-network/utils'; |
||||
|
||||
import { |
||||
TestMultisigValidatorManager, |
||||
TestMultisigValidatorManager__factory, |
||||
} from '../../types'; |
||||
import { Validator } from '../lib/core'; |
||||
|
||||
const OUTBOX_DOMAIN = 1234; |
||||
const OUTBOX_DOMAIN_HASH = ethers.utils.keccak256( |
||||
ethers.utils.solidityPack(['uint32', 'string'], [OUTBOX_DOMAIN, 'ABACUS']), |
||||
); |
||||
const QUORUM_THRESHOLD = 1; |
||||
|
||||
// const VALIDATOR = '0xdeadbeef00000000000000000000000000000000';
|
||||
|
||||
// Signs a checkpoint with the provided validators,
|
||||
// sorts
|
||||
async function getCheckpointSignatures( |
||||
root: types.HexString, |
||||
index: number, |
||||
unsortedValidators: Validator[], |
||||
): Promise<string[]> { |
||||
const validators = unsortedValidators.sort((a, b) => { |
||||
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, |
||||
); |
||||
} |
||||
|
||||
describe.only('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; |
||||
const [ |
||||
, |
||||
, |
||||
validatorSigner0, |
||||
validatorSigner1, |
||||
validatorSigner2, |
||||
validatorSigner3, |
||||
] = signers; |
||||
validator0 = await Validator.fromSigner(validatorSigner0, OUTBOX_DOMAIN); |
||||
validator1 = await Validator.fromSigner(validatorSigner1, OUTBOX_DOMAIN); |
||||
validator2 = await Validator.fromSigner(validatorSigner2, OUTBOX_DOMAIN); |
||||
validator3 = await Validator.fromSigner(validatorSigner3, 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 outboxDomain', async () => { |
||||
expect(await validatorManager.outboxDomain()).to.equal(OUTBOX_DOMAIN); |
||||
}); |
||||
|
||||
it('sets the outboxDomainHash', async () => { |
||||
expect(await validatorManager.outboxDomainHash()).to.equal( |
||||
OUTBOX_DOMAIN_HASH, |
||||
); |
||||
}); |
||||
|
||||
it('enrolls the validator set', async () => { |
||||
expect(await validatorManager.validatorSet()).to.deep.equal([ |
||||
validator0.address, |
||||
]); |
||||
}); |
||||
|
||||
it('sets the quorum threshold', async () => { |
||||
expect(await validatorManager.quorumThreshold()).to.equal([ |
||||
QUORUM_THRESHOLD, |
||||
]); |
||||
}); |
||||
}); |
||||
|
||||
describe('#enrollValidator', () => { |
||||
it('enrolls a validator into the validator set', async () => { |
||||
await validatorManager.enrollValidator(validator1.address); |
||||
|
||||
expect(await validatorManager.validatorSet()).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); |
||||
}); |
||||
|
||||
it('reverts if the validator is already enrolled', async () => { |
||||
await expect( |
||||
validatorManager.enrollValidator(validator0.address), |
||||
).to.be.revertedWith('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', () => { |
||||
it('unenrolls a validator from the validator set', async () => { |
||||
await validatorManager.unenrollValidator(validator0.address); |
||||
|
||||
expect(await validatorManager.validatorSet()).to.deep.equal([]); |
||||
}); |
||||
|
||||
it('emits the UnenrollValidator event', async () => { |
||||
expect(await validatorManager.unenrollValidator(validator0.address)) |
||||
.to.emit(validatorManager, 'UnenrollValidator') |
||||
.withArgs(validator0.address); |
||||
}); |
||||
|
||||
it('reverts if the validator is not already enrolled', async () => { |
||||
await expect( |
||||
validatorManager.unenrollValidator(validator1.address), |
||||
).to.be.revertedWith('!enrolled'); |
||||
}); |
||||
|
||||
it('reverts when called by a non-owner', async () => { |
||||
await expect( |
||||
validatorManager |
||||
.connect(nonOwner) |
||||
.unenrollValidator(validator0.address), |
||||
).to.be.revertedWith('Ownable: caller is not the owner'); |
||||
}); |
||||
}); |
||||
|
||||
describe('#setQuorumThreshold', () => { |
||||
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.setQuorumThreshold(2); |
||||
|
||||
expect(await validatorManager.quorumThreshold()).to.equal(2); |
||||
}); |
||||
|
||||
it('emits the SetQuorumThreshold event', async () => { |
||||
expect(await validatorManager.setQuorumThreshold(2)) |
||||
.to.emit(validatorManager, 'SetQuorumThreshold') |
||||
.withArgs(2); |
||||
}); |
||||
|
||||
it('reverts if the new quorum threshold is zero', async () => { |
||||
await expect(validatorManager.setQuorumThreshold(0)).to.be.revertedWith( |
||||
'!range', |
||||
); |
||||
}); |
||||
|
||||
it('reverts if the new quorum threshold is > the validator set size', async () => { |
||||
await expect(validatorManager.setQuorumThreshold(3)).to.be.revertedWith( |
||||
'!range', |
||||
); |
||||
}); |
||||
|
||||
it('reverts when called by a non-owner', async () => { |
||||
await expect( |
||||
validatorManager.connect(nonOwner).setQuorumThreshold(2), |
||||
).to.be.revertedWith('Ownable: caller is not the owner'); |
||||
}); |
||||
}); |
||||
|
||||
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.setQuorumThreshold(2); |
||||
}); |
||||
|
||||
it('returns true when there is a quorum', async () => { |
||||
const signatures = await getCheckpointSignatures(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 getCheckpointSignatures( |
||||
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 < the quorum threshold', async () => { |
||||
const signatures = await getCheckpointSignatures(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 getCheckpointSignatures( |
||||
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 getCheckpointSignatures(root, index, [validator0, validator1]) |
||||
).reverse(); |
||||
|
||||
await expect( |
||||
validatorManager.isQuorum(root, index, signatures), |
||||
).to.be.revertedWith('!sorted signers'); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue