From bc5398000f25b642da8229c6c5b09c3d61caf857 Mon Sep 17 00:00:00 2001 From: Dmitry Savonin Date: Mon, 7 Mar 2022 15:12:42 +0300 Subject: [PATCH] min staking amount is managed by governance for validator and delegataor, increased precision for storing compat balance --- contracts/ChainConfig.sol | 32 +++++++++- contracts/Staking.sol | 85 +++++++++++++++++---------- contracts/interfaces/IChainConfig.sol | 8 +++ create-genesis.go | 10 +++- test/helper.js | 6 +- test/injector.js | 2 +- test/staking.js | 12 ++-- 7 files changed, 114 insertions(+), 41 deletions(-) diff --git a/contracts/ChainConfig.sol b/contracts/ChainConfig.sol index c536493..37c076d 100644 --- a/contracts/ChainConfig.sol +++ b/contracts/ChainConfig.sol @@ -11,6 +11,8 @@ contract ChainConfig is InjectorContextHolder, IChainConfig { event FelonyThresholdChanged(uint32 prevValue, uint32 newValue); event ValidatorJailEpochLengthChanged(uint32 prevValue, uint32 newValue); event UndelegatePeriodChanged(uint32 prevValue, uint32 newValue); + event MinValidatorStakeAmountChanged(uint64 prevValue, uint64 newValue); + event MinStakingAmountChanged(uint64 prevValue, uint64 newValue); struct ConsensusParams { uint32 activeValidatorsLength; @@ -19,6 +21,8 @@ contract ChainConfig is InjectorContextHolder, IChainConfig { uint32 felonyThreshold; uint32 validatorJailEpochLength; uint32 undelegatePeriod; + uint64 minValidatorStakeAmount; + uint64 minStakingAmount; } ConsensusParams private _consensusParams; @@ -29,7 +33,9 @@ contract ChainConfig is InjectorContextHolder, IChainConfig { uint32 misdemeanorThreshold, uint32 felonyThreshold, uint32 validatorJailEpochLength, - uint32 undelegatePeriod + uint32 undelegatePeriod, + uint64 minValidatorStakeAmount, + uint64 minStakingAmount ) { _consensusParams.activeValidatorsLength = activeValidatorsLength; emit ActiveValidatorsLengthChanged(0, activeValidatorsLength); @@ -43,6 +49,10 @@ contract ChainConfig is InjectorContextHolder, IChainConfig { emit ValidatorJailEpochLengthChanged(0, validatorJailEpochLength); _consensusParams.undelegatePeriod = undelegatePeriod; emit UndelegatePeriodChanged(0, undelegatePeriod); + _consensusParams.minValidatorStakeAmount = minValidatorStakeAmount; + emit MinValidatorStakeAmountChanged(0, minValidatorStakeAmount); + _consensusParams.minStakingAmount = minStakingAmount; + emit MinStakingAmountChanged(0, minStakingAmount); } function getActiveValidatorsLength() external view override returns (uint32) { @@ -104,4 +114,24 @@ contract ChainConfig is InjectorContextHolder, IChainConfig { _consensusParams.undelegatePeriod = newValue; emit UndelegatePeriodChanged(prevValue, newValue); } + + function getMinValidatorStakeAmount() external view returns (uint64) { + return _consensusParams.minValidatorStakeAmount; + } + + function setMinValidatorStakeAmount(uint64 newValue) external { + uint64 prevValue = _consensusParams.minValidatorStakeAmount; + _consensusParams.minValidatorStakeAmount = newValue; + emit MinValidatorStakeAmountChanged(prevValue, newValue); + } + + function getMinStakingAmount() external view returns (uint64) { + return _consensusParams.minStakingAmount; + } + + function setMinStakingAmount(uint64 newValue) external { + uint64 prevValue = _consensusParams.minStakingAmount; + _consensusParams.minStakingAmount = newValue; + emit MinStakingAmountChanged(prevValue, newValue); + } } \ No newline at end of file diff --git a/contracts/Staking.sol b/contracts/Staking.sol index 013285f..e9532a1 100644 --- a/contracts/Staking.sol +++ b/contracts/Staking.sol @@ -6,14 +6,28 @@ import "./Injector.sol"; contract Staking is IStaking, InjectorContextHolder { /** - * This gas limit is used for internal transfers, BSC doesn't support berlin and it - * might cause problems with smart contracts who used to stake transparent proxies or - * beacon proxies that have a lot of expensive SLOAD instructions. + * This constant indicates precision of storing compact balances in the storage or floating point. Since default + * balance precision is 256 bits it might gain some overhead on the storage because we don't need to store such huge + * amount range. That is why we compact balances in uint64 values instead of uint256. By managing this value + * you can set the precision of your balances, aka min and max possible staking amount. This value depends + * mostly on your asset price in USD, for example ETH costs 4000$ then if we use 1 ether precision it takes 4000$ + * as min amount that might be problematic for users to do the stake. We can set 1 gwei precision and in this case + * we increase min staking amount in 1e9 times, but also decreases max staking amount or total amount of staked assets. + * + * Here is an universal formula, if your asset is cheap in USD equivalent, like ~1$, then use 1 ether precision, + * otherwise it might be better to use 1 gwei precision or any other amount that your want. + * + * Also be careful with setting `minValidatorStakeAmount` and `minStakingAmount`, because these values has + * the same precision as specified here. It means that if you set precision 1 ether, then min staking amount of 10 + * tokens should have 10 raw value. For 1 gwei precision 10 tokens min amount should be stored as 10000000000. + * + * WARNING: precision must be a 1eN format (A=1, N>0) */ - uint64 internal constant TRANSFER_GAS_LIMIT = 30000; + uint256 internal constant BALANCE_COMPACT_PRECISION = 1 ether; /** - * Here is min/max commission rates, lets don't allow to set more than 30% of validator commission - * Commission rate is a percents divided by 100 stored with 0 decimals as percents*100 (=pc/1e2*1e4) + * Here is min/max commission rates. Lets don't allow to set more than 30% of validator commission, because it's + * too big commission for validator. Commission rate is a percents divided by 100 stored with 0 decimals as percents*100 (=pc/1e2*1e4) + * * Here is some examples: * + 0.3% => 0.3*100=30 * + 3% => 3*100=300 @@ -21,6 +35,12 @@ contract Staking is IStaking, InjectorContextHolder { */ uint16 internal constant COMMISSION_RATE_MIN_VALUE = 0; // 0% uint16 internal constant COMMISSION_RATE_MAX_VALUE = 3000; // 30% + /** + * This gas limit is used for internal transfers, BSC doesn't support berlin and it + * might cause problems with smart contracts who used to stake transparent proxies or + * beacon proxies that have a lot of expensive SLOAD instructions. + */ + uint64 internal constant TRANSFER_GAS_LIMIT = 30000; // validator events event ValidatorAdded(address indexed validator, address owner, uint8 status, uint16 commissionRate); @@ -29,6 +49,7 @@ contract Staking is IStaking, InjectorContextHolder { event ValidatorOwnerClaimed(address indexed validator, uint256 amount, uint64 epoch); event ValidatorSlashed(address indexed validator, uint32 slashes, uint64 epoch); event ValidatorJailed(address indexed validator, uint64 epoch); + // staker events event Delegated(address indexed validator, address indexed staker, uint256 amount, uint64 epoch); event Undelegated(address indexed validator, address indexed staker, uint256 amount, uint64 epoch); @@ -101,7 +122,7 @@ contract Staking is IStaking, InjectorContextHolder { return (delegatedAmount = 0, atEpoch = 0); } DelegationOpDelegate memory snapshot = delegation.delegateQueue[delegation.delegateQueue.length - 1]; - return (delegatedAmount = uint256(snapshot.amount) * 1 gwei, atEpoch = snapshot.epoch); + return (delegatedAmount = uint256(snapshot.amount) * BALANCE_COMPACT_PRECISION, atEpoch = snapshot.epoch); } function getValidatorStatus(address validatorAddress) external view override returns ( @@ -118,7 +139,7 @@ contract Staking is IStaking, InjectorContextHolder { return ( ownerAddress = validator.ownerAddress, status = uint8(validator.status), - totalDelegated = uint256(snapshot.totalDelegated) * 1 gwei, + totalDelegated = uint256(snapshot.totalDelegated) * BALANCE_COMPACT_PRECISION, slashesCount = snapshot.slashesCount, changedAt = validator.changedAt, jailedBefore = validator.jailedBefore, @@ -140,7 +161,7 @@ contract Staking is IStaking, InjectorContextHolder { return ( ownerAddress = validator.ownerAddress, status = uint8(validator.status), - totalDelegated = uint256(snapshot.totalDelegated) * 1 gwei, + totalDelegated = uint256(snapshot.totalDelegated) * BALANCE_COMPACT_PRECISION, slashesCount = snapshot.slashesCount, changedAt = validator.changedAt, jailedBefore = validator.jailedBefore, @@ -166,7 +187,7 @@ contract Staking is IStaking, InjectorContextHolder { function _totalDelegatedToValidator(Validator memory validator) internal view returns (uint256) { ValidatorSnapshot memory snapshot = _validatorSnapshots[validator.validatorAddress][validator.changedAt]; - return uint256(snapshot.totalDelegated) * 1 gwei; + return uint256(snapshot.totalDelegated) * BALANCE_COMPACT_PRECISION; } function delegate(address validatorAddress) payable external override { @@ -224,9 +245,10 @@ contract Staking is IStaking, InjectorContextHolder { } function _delegateTo(address fromDelegator, address toValidator, uint256 amount) internal { - // 1 ether is minimum delegate amount - require(amount >= 1 ether, "Staking: amount too low"); - require(amount % 1 ether == 0, "Staking: amount shouldn't have a remainder"); + // check is minimum delegate amount + require(amount / BALANCE_COMPACT_PRECISION >= _chainConfigContract.getMinStakingAmount(), "Staking: amount is too low"); + require(amount % BALANCE_COMPACT_PRECISION == 0, "Staking: amount have a remainder"); + // make sure amount is greater than min staking amount // make sure validator exists at least Validator memory validator = _validatorsMap[toValidator]; require(validator.status != ValidatorStatus.NotFound, "Staking: validator not found"); @@ -236,7 +258,7 @@ contract Staking is IStaking, InjectorContextHolder { // + increase total delegated amount in the next epoch for this validator // + re-save validator because last affected epoch might change ValidatorSnapshot storage validatorSnapshot = _touchValidatorSnapshot(validator, nextEpoch); - validatorSnapshot.totalDelegated += uint64(amount / 1 gwei); + validatorSnapshot.totalDelegated += uint64(amount / BALANCE_COMPACT_PRECISION); _validatorsMap[toValidator] = validator; // if last pending delegate has the same next epoch then its safe to just increase total // staked amount because it can't affect current validator set, but otherwise we must create @@ -247,22 +269,22 @@ contract Staking is IStaking, InjectorContextHolder { // if we already have pending snapshot for the next epoch then just increase new amount, // otherwise create next pending snapshot. (tbh it can't be greater, but what we can do here instead?) if (recentDelegateOp.epoch >= nextEpoch) { - recentDelegateOp.amount += uint64(amount / 1 gwei); + recentDelegateOp.amount += uint64(amount / BALANCE_COMPACT_PRECISION); } else { - delegation.delegateQueue.push(DelegationOpDelegate({epoch : nextEpoch, amount : recentDelegateOp.amount + uint64(amount / 1 gwei)})); + delegation.delegateQueue.push(DelegationOpDelegate({epoch : nextEpoch, amount : recentDelegateOp.amount + uint64(amount / BALANCE_COMPACT_PRECISION)})); } } else { // there is no any delegations at al, lets create the first one - delegation.delegateQueue.push(DelegationOpDelegate({epoch : nextEpoch, amount : uint64(amount / 1 gwei)})); + delegation.delegateQueue.push(DelegationOpDelegate({epoch : nextEpoch, amount : uint64(amount / BALANCE_COMPACT_PRECISION)})); } // emit event with the next epoch emit Delegated(toValidator, fromDelegator, amount, nextEpoch); } function _undelegateFrom(address toDelegator, address fromValidator, uint256 amount) internal { - // 1 ether is minimum delegate amount - require(amount >= 1 ether, "Staking: amount too low"); - require(amount % 1 ether == 0, "Staking: amount shouldn't have a remainder"); + // check minimum delegate amount + require(amount / BALANCE_COMPACT_PRECISION >= _chainConfigContract.getMinStakingAmount(), "Staking: amount is too low"); + require(amount % BALANCE_COMPACT_PRECISION == 0, "Staking: amount have a remainder"); // make sure validator exists at least Validator memory validator = _validatorsMap[fromValidator]; require(validator.status != ValidatorStatus.NotFound, "Staking: validator not found"); @@ -272,8 +294,8 @@ contract Staking is IStaking, InjectorContextHolder { // + increase total delegated amount in the next epoch for this validator // + re-save validator because last affected epoch might change ValidatorSnapshot storage validatorSnapshot = _touchValidatorSnapshot(validator, nextEpoch); - require(validatorSnapshot.totalDelegated >= uint64(amount / 1 gwei), "Staking: insufficient balance"); - validatorSnapshot.totalDelegated -= uint64(amount / 1 gwei); + require(validatorSnapshot.totalDelegated >= uint64(amount / BALANCE_COMPACT_PRECISION), "Staking: insufficient balance"); + validatorSnapshot.totalDelegated -= uint64(amount / BALANCE_COMPACT_PRECISION); _validatorsMap[fromValidator] = validator; // if last pending delegate has the same next epoch then its safe to just increase total // staked amount because it can't affect current validator set, but otherwise we must create @@ -281,8 +303,8 @@ contract Staking is IStaking, InjectorContextHolder { ValidatorDelegation storage delegation = _validatorDelegations[fromValidator][toDelegator]; require(delegation.delegateQueue.length > 0, "Staking: delegation queue is empty"); DelegationOpDelegate storage recentDelegateOp = delegation.delegateQueue[delegation.delegateQueue.length - 1]; - require(recentDelegateOp.amount >= uint64(amount / 1 gwei), "Staking: insufficient balance"); - uint64 nextDelegatedAmount = recentDelegateOp.amount - uint64(amount / 1 gwei); + require(recentDelegateOp.amount >= uint64(amount / BALANCE_COMPACT_PRECISION), "Staking: insufficient balance"); + uint64 nextDelegatedAmount = recentDelegateOp.amount - uint64(amount / BALANCE_COMPACT_PRECISION); if (recentDelegateOp.epoch >= nextEpoch) { // decrease total delegated amount for the next epoch recentDelegateOp.amount = nextDelegatedAmount; @@ -291,7 +313,7 @@ contract Staking is IStaking, InjectorContextHolder { delegation.delegateQueue.push(DelegationOpDelegate({epoch : nextEpoch, amount : nextDelegatedAmount})); } // create new undelegate queue operation with soft lock - delegation.undelegateQueue.push(DelegationOpUndelegate({amount : uint64(amount / 1 gwei), epoch : nextEpoch + _chainConfigContract.getUndelegatePeriod()})); + delegation.undelegateQueue.push(DelegationOpUndelegate({amount : uint64(amount / BALANCE_COMPACT_PRECISION), epoch : nextEpoch + _chainConfigContract.getUndelegatePeriod()})); // emit event with the next epoch number emit Undelegated(fromValidator, toDelegator, amount, nextEpoch); } @@ -333,7 +355,7 @@ contract Staking is IStaking, InjectorContextHolder { if (undelegateOp.epoch > beforeEpoch) { break; } - availableFunds += uint256(undelegateOp.amount) * 1 gwei; + availableFunds += uint256(undelegateOp.amount) * BALANCE_COMPACT_PRECISION; delete delegation.undelegateQueue[delegation.undelegateGap]; ++delegation.undelegateGap; } @@ -372,7 +394,7 @@ contract Staking is IStaking, InjectorContextHolder { if (undelegateOp.epoch > beforeEpoch) { break; } - availableFunds += uint256(undelegateOp.amount) * 1 gwei; + availableFunds += uint256(undelegateOp.amount) * BALANCE_COMPACT_PRECISION; ++delegation.undelegateGap; } // return available for claim funds @@ -425,10 +447,11 @@ contract Staking is IStaking, InjectorContextHolder { address validatorOwner = msg.sender; uint256 initialStake = msg.value; // initial stake requirements - require(initialStake >= 1 ether, "Staking: amount too low"); - require(initialStake % 1 ether == 0, "Staking: amount shouldn't have a remainder"); + require(initialStake / BALANCE_COMPACT_PRECISION >= _chainConfigContract.getMinValidatorStakeAmount(), "Staking: initial stake is too low"); + require(initialStake % BALANCE_COMPACT_PRECISION == 0, "Staking: amount have a remainder"); + // initial stake amount should be greater than minimum validator staking amount // add new pending validator - _addValidator(validatorAddress, validatorOwner, ValidatorStatus.Pending, commissionRate, uint64(initialStake / 1 gwei), _nextEpoch()); + _addValidator(validatorAddress, validatorOwner, ValidatorStatus.Pending, commissionRate, uint64(initialStake / BALANCE_COMPACT_PRECISION), _nextEpoch()); } function addValidator(address account) external onlyFromGovernance virtual override { @@ -641,7 +664,7 @@ contract Staking is IStaking, InjectorContextHolder { } function _safeTransferWithGasLimit(address payable recipient, uint256 amount) internal { - (bool success,) = recipient.call{value: amount, gas: TRANSFER_GAS_LIMIT}(""); + (bool success,) = recipient.call{value : amount, gas : TRANSFER_GAS_LIMIT}(""); require(success, "Staking: failed to safe transfer"); } diff --git a/contracts/interfaces/IChainConfig.sol b/contracts/interfaces/IChainConfig.sol index eaf66cc..5623e4b 100644 --- a/contracts/interfaces/IChainConfig.sol +++ b/contracts/interfaces/IChainConfig.sol @@ -26,4 +26,12 @@ interface IChainConfig { function getUndelegatePeriod() external view returns (uint32); function setUndelegatePeriod(uint32 newValue) external; + + function getMinValidatorStakeAmount() external view returns (uint64); + + function setMinValidatorStakeAmount(uint64 newValue) external; + + function getMinStakingAmount() external view returns (uint64); + + function setMinStakingAmount(uint64 newValue) external; } \ No newline at end of file diff --git a/create-genesis.go b/create-genesis.go index ec86cbc..b67d1cd 100644 --- a/create-genesis.go +++ b/create-genesis.go @@ -149,6 +149,8 @@ type consensusParams struct { FelonyThreshold uint32 ValidatorJailEpochLength uint32 UndelegatePeriod uint32 + MinValidatorStakeAmount uint64 + MinStakingAmount uint64 } type genesisConfig struct { @@ -180,13 +182,15 @@ func createGenesisConfig(config genesisConfig, targetFile string) error { invokeConstructorOrPanic(genesis, stakingAddress, stakingRawArtifact, []string{"address[]"}, []interface{}{ config.Validators, }) - invokeConstructorOrPanic(genesis, chainConfigAddress, chainConfigRawArtifact, []string{"uint32", "uint32", "uint32", "uint32", "uint32", "uint32"}, []interface{}{ + invokeConstructorOrPanic(genesis, chainConfigAddress, chainConfigRawArtifact, []string{"uint32", "uint32", "uint32", "uint32", "uint32", "uint32", "uint64", "uint64"}, []interface{}{ config.ConsensusParams.ActiveValidatorsLength, config.ConsensusParams.EpochBlockInterval, config.ConsensusParams.MisdemeanorThreshold, config.ConsensusParams.FelonyThreshold, config.ConsensusParams.ValidatorJailEpochLength, config.ConsensusParams.UndelegatePeriod, + config.ConsensusParams.MinValidatorStakeAmount * 1e18, + config.ConsensusParams.MinStakingAmount * 1e18, }) invokeConstructorOrPanic(genesis, slashingIndicatorAddress, slashingIndicatorRawArtifact, []string{}, []interface{}{}) invokeConstructorOrPanic(genesis, systemRewardAddress, systemRewardRawArtifact, []string{"address"}, []interface{}{ @@ -272,6 +276,8 @@ var devnetConfig = genesisConfig{ FelonyThreshold: 100, ValidatorJailEpochLength: 1, UndelegatePeriod: 0, + MinValidatorStakeAmount: 1, + MinStakingAmount: 1, }, // owner of the governance VotingPeriod: 20, // 1 minute @@ -303,6 +309,8 @@ var testnetConfig = genesisConfig{ FelonyThreshold: 150, // after missing this amount of blocks per day validator goes in jail for N epochs ValidatorJailEpochLength: 7, // how many epochs validator should stay in jail (7 epochs = ~7 days) UndelegatePeriod: 6, // allow claiming funds only after 6 epochs (~7 days) + MinValidatorStakeAmount: 1, // how much CHZ validator must stake to create a validator (in ether) + MinStakingAmount: 1, // minimum staking amount for delegators (in ether) }, // owner of the governance VotingPeriod: 60, // 3 minutes diff --git a/test/helper.js b/test/helper.js index a6931a2..02640bc 100644 --- a/test/helper.js +++ b/test/helper.js @@ -20,6 +20,8 @@ const DEFAULT_MOCK_PARAMS = { felonyThreshold: '150', validatorJailEpochLength: '7', undelegatePeriod: '0', + minValidatorStakeAmount: '1', + minStakingAmount: '1', genesisDeployers: [], genesisValidators: [], }; @@ -52,6 +54,8 @@ const newContractUsingTypes = async (owner, params, types = {}) => { genesisDeployers, genesisValidators, undelegatePeriod, + minValidatorStakeAmount, + minStakingAmount, } = Object.assign({}, DEFAULT_MOCK_PARAMS, params) // factory contracts const staking = await Staking.new(genesisValidators); @@ -59,7 +63,7 @@ const newContractUsingTypes = async (owner, params, types = {}) => { const systemReward = await SystemReward.new(systemTreasury); const contractDeployer = await ContractDeployer.new(genesisDeployers); const governance = await Governance.new(1); - const chainConfig = await ChainConfig.new(activeValidatorsLength, epochBlockInterval, misdemeanorThreshold, felonyThreshold, validatorJailEpochLength, undelegatePeriod); + const chainConfig = await ChainConfig.new(activeValidatorsLength, epochBlockInterval, misdemeanorThreshold, felonyThreshold, validatorJailEpochLength, undelegatePeriod, minValidatorStakeAmount, minStakingAmount); // init them all for (const contract of [chainConfig, staking, slashingIndicator, systemReward, contractDeployer, governance]) { await contract.initManually( diff --git a/test/injector.js b/test/injector.js index 8b6693b..0cdb4e7 100644 --- a/test/injector.js +++ b/test/injector.js @@ -38,6 +38,6 @@ contract("Injector", async (accounts) => { await testInjector(Deployer, []) await testInjector(Governance, '1') await testInjector(Staking, []) - await testInjector(ChainConfig, '0', '0', '0', '0', '0', '0') + await testInjector(ChainConfig, '0', '0', '0', '0', '0', '0', '0', '0') }) }); diff --git a/test/staking.js b/test/staking.js index 7915296..209ff37 100644 --- a/test/staking.js +++ b/test/staking.js @@ -95,8 +95,8 @@ contract("Staking", async (accounts) => { assert.deepEqual(Array.from(await parlia.getValidators()), [validator2, validator1]) assert.equal((await parlia.getValidatorStatus(validator1)).totalDelegated.toString(), '1000000000000000000') assert.equal((await parlia.getValidatorStatus(validator2)).totalDelegated.toString(), '2000000000000000000') - await expectError(parlia.undelegate(validator2, '1', {from: staker2}), 'Staking: amount too low'); - await expectError(parlia.undelegate(validator2, '1000000000000000001', {from: staker2}), 'Staking: amount shouldn\'t have a remainder'); + await expectError(parlia.undelegate(validator2, '1', {from: staker2}), 'Staking: amount is too low'); + await expectError(parlia.undelegate(validator2, '1000000000000000001', {from: staker2}), 'Staking: amount have a remainder'); await expectError(parlia.undelegate(validator3, '1000000000000000000', {from: staker2}), 'Staking: validator not found'); let res = await parlia.undelegate(validator2, '1000000000000000000', {from: staker2}); assert.equal(res.logs[0].args.validator, validator2); @@ -324,15 +324,15 @@ contract("Staking", async (accounts) => { await expectError(parlia.delegate(validator1, { from: staker1, value: '100000000000000000' - }), 'Staking: amount too low') // 0.1 + }), 'Staking: amount is too low') // 0.1 await expectError(parlia.delegate(validator1, { from: staker1, value: '00000000000000000' - }), 'Staking: amount too low') // 0 + }), 'Staking: amount is too low') // 0 await expectError(parlia.delegate(validator1, { from: staker1, - value: '1100000000000000000' - }), 'Staking: amount shouldn\'t have a remainder') // 1.1 + value: '1010000000000000000' + }), 'Staking: amount have a remainder') // 1.01 }); it("put validator in jail after N misses", async () => { const {parlia} = await newMockContract(owner, {