@@ -20,6 +20,7 @@ import {
2020 ETH_DEDUCTED_DIGITS ,
2121} from '../../common/constants.ts' ;
2222import { Events } from "../../common/events.ts" ;
23+ import { Errors } from "../../common/errors.ts" ;
2324import {
2425 mineBlocks ,
2526 getBlockNumber ,
@@ -466,4 +467,137 @@ describe("Cross-Cutting: Multi-Step Flows", () => {
466467 } ) ;
467468 } ) ;
468469 } ) ;
470+
471+ describe ( "Fee declaration interleavings with EB updates" , ( ) => {
472+ it ( "declaring fee, then updating EB, then executing settles pre-exec blocks at old fee" , async function ( ) {
473+ const { network, views, ssvToken } =
474+ await networkHelpers . loadFixture ( deployFixture ) ;
475+ const provider = connection . ethers . provider ;
476+
477+ const operatorIds = await registerOperators ( network , operatorOwner , 4 ) ;
478+ await whitelistAddresses ( network , operatorOwner , operatorIds , [ clusterOwner . address ] ) ;
479+
480+ const networkAddress = await network . getAddress ( ) ;
481+ const stakeAmount = ethers . parseEther ( "100" ) ;
482+ await ssvToken . transfer ( staker . address , stakeAmount ) ;
483+ await ssvToken . connect ( staker ) . approve ( networkAddress , stakeAmount ) ;
484+ await network . connect ( staker ) . stake ( stakeAmount ) ;
485+ await network . replaceOracle ( 1 , oracle1 . address ) ;
486+ await network . replaceOracle ( 2 , oracle2 . address ) ;
487+ await network . replaceOracle ( 3 , oracle3 . address ) ;
488+
489+ const registerTx = await network . connect ( clusterOwner ) . registerValidator (
490+ makePublicKey ( 9101 ) , operatorIds , DEFAULT_SHARES , EMPTY_CLUSTER ,
491+ { value : DEFAULT_ETH_REGISTER_VALUE } ,
492+ ) ;
493+ const registerReceipt = await registerTx . wait ( ) ;
494+ const clusterAfterRegister = parseClusterFromEvent ( network , registerReceipt , Events . VALIDATOR_ADDED ) ;
495+
496+ const clusterId = ethers . keccak256 (
497+ ethers . solidityPacked ( [ "address" , "uint64[]" ] , [ clusterOwner . address , operatorIds ] ) ,
498+ ) ;
499+ const oldFeeWei = BigInt ( ( await views . getOperatorById ( BigInt ( operatorIds [ 0 ] ) ) ) . fee ) ;
500+ const oldFeePacked = oldFeeWei / ETH_DEDUCTED_DIGITS ;
501+
502+ const { root : root64 , proofs : proofs64 } = generateMerkleForClusterEB ( connection , [
503+ { clusterId, effectiveBalance : 64 } ,
504+ ] ) ;
505+ const rootBlock64 = await getBlockNumber ( provider ) ;
506+ await network . connect ( oracle1 ) . commitRoot ( root64 , rootBlock64 ) ;
507+ await network . connect ( oracle2 ) . commitRoot ( root64 , rootBlock64 ) ;
508+ await network . connect ( oracle3 ) . commitRoot ( root64 , rootBlock64 ) ;
509+ const txEb64 = await network . updateClusterBalance (
510+ rootBlock64 , clusterOwner . address , operatorIds , clusterAfterRegister , 64 , proofs64 [ clusterId ] ,
511+ ) ;
512+ const clusterAfterEb64 = parseClusterFromEvent ( network , await txEb64 . wait ( ) , Events . CLUSTER_BALANCE_UPDATED ) ;
513+
514+ const newFee = await getValidOperatorFeeIncrease ( views , BigInt ( operatorIds [ 0 ] ) ) ;
515+ await network . connect ( operatorOwner ) . declareOperatorFee ( operatorIds [ 0 ] , newFee ) ;
516+
517+ const { root : root128 , proofs : proofs128 } = generateMerkleForClusterEB ( connection , [
518+ { clusterId, effectiveBalance : 128 } ,
519+ ] ) ;
520+ const rootBlock128 = await getBlockNumber ( provider ) ;
521+ await network . connect ( oracle1 ) . commitRoot ( root128 , rootBlock128 ) ;
522+ await network . connect ( oracle2 ) . commitRoot ( root128 , rootBlock128 ) ;
523+ await network . connect ( oracle3 ) . commitRoot ( root128 , rootBlock128 ) ;
524+ const txEb128 = await network . updateClusterBalance (
525+ rootBlock128 , clusterOwner . address , operatorIds , clusterAfterEb64 , 128 , proofs128 [ clusterId ] ,
526+ ) ;
527+ const receiptEb128 = await txEb128 . wait ( ) ;
528+ const earningsBeforeExecute = BigInt ( await views . getOperatorEarnings ( BigInt ( operatorIds [ 0 ] ) ) ) ;
529+
530+ const feePeriods = await views . getOperatorFeePeriods ( ) ;
531+ const declareDelay = BigInt ( feePeriods [ 0 ] ) ;
532+ await provider . send ( "evm_increaseTime" , [ Number ( declareDelay ) + 1 ] ) ;
533+ await mineBlocks ( provider , 1 ) ;
534+
535+ const execTx = await network . connect ( operatorOwner ) . executeOperatorFee ( operatorIds [ 0 ] ) ;
536+ const execReceipt = await execTx . wait ( ) ;
537+ const execBlock = BigInt ( execReceipt ! . blockNumber ) ;
538+ const eb128Block = BigInt ( receiptEb128 ! . blockNumber ) ;
539+
540+ const earningsAfterExecute = BigInt ( await views . getOperatorEarnings ( BigInt ( operatorIds [ 0 ] ) ) ) ;
541+ const expectedDelta = calcOperatorFeeAccrual (
542+ execBlock - eb128Block ,
543+ oldFeePacked ,
544+ calcVUnits ( 128n ) ,
545+ ) * ETH_DEDUCTED_DIGITS ;
546+ expect ( earningsAfterExecute - earningsBeforeExecute ) . to . equal ( expectedDelta ) ;
547+
548+ const updatedOperator = await views . getOperatorById ( BigInt ( operatorIds [ 0 ] ) ) ;
549+ expect ( BigInt ( updatedOperator . fee ) ) . to . equal ( BigInt ( newFee ) ) ;
550+ } ) ;
551+
552+ it ( "executeOperatorFee reverts after operator removal on explicit-EB cluster" , async function ( ) {
553+ const { network, views, ssvToken } =
554+ await networkHelpers . loadFixture ( deployFixture ) ;
555+ const provider = connection . ethers . provider ;
556+
557+ const operatorIds = await registerOperators ( network , operatorOwner , 4 ) ;
558+ await whitelistAddresses ( network , operatorOwner , operatorIds , [ clusterOwner . address ] ) ;
559+
560+ const networkAddress = await network . getAddress ( ) ;
561+ const stakeAmount = ethers . parseEther ( "100" ) ;
562+ await ssvToken . transfer ( staker . address , stakeAmount ) ;
563+ await ssvToken . connect ( staker ) . approve ( networkAddress , stakeAmount ) ;
564+ await network . connect ( staker ) . stake ( stakeAmount ) ;
565+ await network . replaceOracle ( 1 , oracle1 . address ) ;
566+ await network . replaceOracle ( 2 , oracle2 . address ) ;
567+ await network . replaceOracle ( 3 , oracle3 . address ) ;
568+
569+ const registerTx = await network . connect ( clusterOwner ) . registerValidator (
570+ makePublicKey ( 9201 ) , operatorIds , DEFAULT_SHARES , EMPTY_CLUSTER ,
571+ { value : DEFAULT_ETH_REGISTER_VALUE } ,
572+ ) ;
573+ const clusterAfterRegister = parseClusterFromEvent ( network , await registerTx . wait ( ) , Events . VALIDATOR_ADDED ) ;
574+
575+ const clusterId = ethers . keccak256 (
576+ ethers . solidityPacked ( [ "address" , "uint64[]" ] , [ clusterOwner . address , operatorIds ] ) ,
577+ ) ;
578+ const { root, proofs } = generateMerkleForClusterEB ( connection , [
579+ { clusterId, effectiveBalance : 64 } ,
580+ ] ) ;
581+ const rootBlock = await getBlockNumber ( provider ) ;
582+ await network . connect ( oracle1 ) . commitRoot ( root , rootBlock ) ;
583+ await network . connect ( oracle2 ) . commitRoot ( root , rootBlock ) ;
584+ await network . connect ( oracle3 ) . commitRoot ( root , rootBlock ) ;
585+ await network . updateClusterBalance (
586+ rootBlock , clusterOwner . address , operatorIds , clusterAfterRegister , 64 , proofs [ clusterId ] ,
587+ ) ;
588+
589+ const declaredFee = await getValidOperatorFeeIncrease ( views , BigInt ( operatorIds [ 0 ] ) ) ;
590+ await network . connect ( operatorOwner ) . declareOperatorFee ( operatorIds [ 0 ] , declaredFee ) ;
591+ await network . connect ( operatorOwner ) . removeOperator ( operatorIds [ 0 ] ) ;
592+
593+ const feePeriods = await views . getOperatorFeePeriods ( ) ;
594+ const declareDelay = BigInt ( feePeriods [ 0 ] ) ;
595+ await provider . send ( "evm_increaseTime" , [ Number ( declareDelay ) + 1 ] ) ;
596+ await mineBlocks ( provider , 1 ) ;
597+
598+ await expect (
599+ network . connect ( operatorOwner ) . executeOperatorFee ( operatorIds [ 0 ] ) ,
600+ ) . to . be . revertedWithCustomError ( network , Errors . OPERATOR_DOES_NOT_EXIST ) ;
601+ } ) ;
602+ } ) ;
469603} ) ;
0 commit comments