From 70231441bbc7191348e766a5115894e1c6bff1e8 Mon Sep 17 00:00:00 2001 From: shotaro <10378902+shotaronowhere@users.noreply.github.com> Date: Sun, 15 Sep 2024 21:47:36 -0700 Subject: [PATCH] squash (#741) Co-authored-by: gpsanant --- .../devnet/operatorSets/PopulateSRC.sol | 15 +- .../devnet/operatorSets/PopulateSRC2.sol | 267 -------- src/contracts/core/StakeRootCompendium.sol | 609 ++++++++---------- .../core/StakeRootCompendiumStorage.sol | 84 +-- .../interfaces/IStakeRootCompendium.sol | 107 ++- 5 files changed, 364 insertions(+), 718 deletions(-) delete mode 100644 script/deploy/devnet/operatorSets/PopulateSRC2.sol diff --git a/script/deploy/devnet/operatorSets/PopulateSRC.sol b/script/deploy/devnet/operatorSets/PopulateSRC.sol index ede7340ec..51dda9fb1 100644 --- a/script/deploy/devnet/operatorSets/PopulateSRC.sol +++ b/script/deploy/devnet/operatorSets/PopulateSRC.sol @@ -24,8 +24,11 @@ contract PopulateSRC is Script, Test, ExistingDeploymentParser { IStakeRootCompendium stakeRootCompendiumImplementation = new StakeRootCompendium({ _delegationManager: delegationManager, _avsDirectory: avsDirectory, - _proofInterval: proofInterval, - _blacklistWindow: 12 seconds + _maxTotalCharge: 100 ether, + _minBalanceThreshold: 0 ether, + _minProofsDuration: 20, + _verifier: address(0), + _imageId: bytes32(0) }); StakeRootCompendium stakeRootCompendium = StakeRootCompendium(payable(new TransparentUpgradeableProxy( address(stakeRootCompendiumImplementation), @@ -33,7 +36,7 @@ contract PopulateSRC is Script, Test, ExistingDeploymentParser { "" // TODO: initialize ))); IStakeRootCompendium.Proof memory proof; - stakeRootCompendium.verifyStakeRoot(uint32(block.timestamp - (block.timestamp % proofInterval)), bytes32(0), address(0), proof); + stakeRootCompendium.verifyStakeRoot(uint32(block.timestamp - (block.timestamp % proofInterval)), bytes32(0), address(0), 0, proof); vm.stopBroadcast(); emit log_named_address("stakeRootCompendium", address(stakeRootCompendium)); @@ -76,7 +79,7 @@ contract PopulateSRC is Script, Test, ExistingDeploymentParser { uint64 magnitudeForOperators = 0.1 ether; vm.startBroadcast(); AVS avs = new AVS(avsDirectory, stakeRootCompendium); - payable(address(avs)).transfer(2 * stakeRootCompendium.MIN_DEPOSIT_BALANCE() * strategies.length); + payable(address(avs)).transfer(2 * stakeRootCompendium.MIN_BALANCE_THRESHOLD() * strategies.length); for (uint i = 0; i < strategies.length; i++) { avs.createOperatorSetAndRegisterOperators(uint32(i), strategies[i], operators[i]); @@ -132,7 +135,7 @@ contract AVS { if(!avsDirectory.isOperatorSet(address(this), operatorSetId)) { avsDirectory.createOperatorSets(operatorSetIdsToCreate); - stakeRootCompendium.depositForOperatorSet{value: 2 * stakeRootCompendium.MIN_DEPOSIT_BALANCE()}(IAVSDirectory.OperatorSet({ + stakeRootCompendium.deposit{value: 2 * stakeRootCompendium.MIN_BALANCE_THRESHOLD()}(IAVSDirectory.OperatorSet({ avs: address(this), operatorSetId: operatorSetId })); @@ -159,7 +162,7 @@ contract AVS { multiplier: 1 ether }); } - stakeRootCompendium.addStrategiesAndMultipliers(operatorSetId, strategiesAndMultipliers); + stakeRootCompendium.addOrModifyStrategiesAndMultipliers(operatorSetId, strategiesAndMultipliers); stakeRootCompendium.setOperatorSetExtraData(operatorSetId, keccak256(abi.encodePacked(operatorSetId))); for (uint256 i = 0; i < operators.length; ++i) { stakeRootCompendium.setOperatorExtraData(operatorSetId, operators[i], keccak256(abi.encodePacked(operators[i]))); diff --git a/script/deploy/devnet/operatorSets/PopulateSRC2.sol b/script/deploy/devnet/operatorSets/PopulateSRC2.sol deleted file mode 100644 index aa211f4f6..000000000 --- a/script/deploy/devnet/operatorSets/PopulateSRC2.sol +++ /dev/null @@ -1,267 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import "../../../../src/contracts/core/StakeRootCompendium.sol"; -import "../../../utils/ExistingDeploymentParser.sol"; -import "forge-std/Test.sol"; -import "forge-std/Script.sol"; - - -contract PopulateSRC is Script, Test, ExistingDeploymentParser { - uint32 constant NUM_OPSETS = 1; - uint32 constant NUM_BATCHES_PER_OPSET = 5; - uint32 constant NUM_OPERATORS_PER_BATCH = 50; - uint32 constant NUM_STRATS_PER_OPSET = 20; - uint256 constant TOKEN_AMOUNT_PER_OPERATOR = 1 ether; - - - function run() public { - _parseDeployedContracts("script/output/devnet/M2_from_scratch_deployment_data.json"); - // other cast default. private key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d - address proxyAdmin = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; - - vm.startBroadcast(); - uint32 proofInterval = 1 hours; - IStakeRootCompendium stakeRootCompendiumImplementation = new StakeRootCompendium({ - _delegationManager: delegationManager, - _avsDirectory: avsDirectory, - _proofInterval: proofInterval, - _blacklistWindow: 12 seconds - }); - StakeRootCompendium stakeRootCompendium = StakeRootCompendium(payable(new TransparentUpgradeableProxy( - address(stakeRootCompendiumImplementation), - proxyAdmin, - "" // TODO: initialize - ))); - IStakeRootCompendium.Proof memory proof; - stakeRootCompendium.verifyStakeRoot(uint32(block.timestamp - (block.timestamp % proofInterval)), bytes32(0), address(0), proof); - vm.stopBroadcast(); - - emit log_named_address("stakeRootCompendium", address(stakeRootCompendium)); - - address[] memory allStrategies = _parseDeployedStrategies("script/output/devnet/deployed_strategies.json"); - - // list of strategies for each operatorSet - IStrategy[][] memory strategies = new IStrategy[][](NUM_OPSETS); - for (uint256 i = 0; i < strategies.length; ++i) { - strategies[i] = new IStrategy[](NUM_STRATS_PER_OPSET); - for (uint256 j = 0; j < strategies[i].length; ++j) { - // fill in the strategies array - strategies[i][j] = IStrategy(allStrategies[i * strategies[i].length + j]); - } - } - - vm.broadcast(); - OperatorFactory operatorFactory = new OperatorFactory(delegationManager, strategyManager, avsDirectory); - address[][][] memory operators = new address[][][](strategies.length); - for (uint i = 0; i < operators.length; i++) { - // todo: send operators[i].length*1 ether of strategy token to operatorfactory - // //transfer enough tokens for every operator in the operator set for all the strategies in the operator set - - for (uint j = 0; j < strategies[i].length; j++) { - IERC20 token = strategies[i][j].underlyingToken(); - vm.startBroadcast(); - token.approve(msg.sender, type(uint256).max); - token.transfer(address(operatorFactory), NUM_BATCHES_PER_OPSET*NUM_OPERATORS_PER_BATCH*TOKEN_AMOUNT_PER_OPERATOR); - vm.stopBroadcast(); - } - - operators[i] = new address[][](NUM_BATCHES_PER_OPSET); - for (uint j = 0; j < operators[i].length; j++) { - vm.startBroadcast(); - operators[i][j] = operatorFactory.createManyOperators(strategies[i], NUM_OPERATORS_PER_BATCH); - for (uint k = 0; k < strategies[i].length; k++) { - operatorFactory.depositForOperators(strategies[i][k], operators[i][j], TOKEN_AMOUNT_PER_OPERATOR); - } - vm.stopBroadcast(); - } - } - - uint64 magnitudeForOperators = 0.1 ether; - vm.startBroadcast(); - AVS avs = new AVS(avsDirectory, stakeRootCompendium); - payable(address(avs)).transfer(2 * stakeRootCompendium.MIN_DEPOSIT_BALANCE() * strategies.length); - - for (uint i = 0; i < strategies.length; i++) { - for (uint j = 0; j < operators[i].length; j++) { - avs.createOperatorSetAndRegisterOperators(uint32(i), strategies[i], operators[i][j]); - IAVSDirectory.OperatorSet memory operatorSet = IAVSDirectory.OperatorSet({ - avs: address(avs), - operatorSetId: uint32(i) - }); - for (uint k = 0; k < strategies[i].length; k++) { - operatorFactory.allocateForOperators(strategies[i][k], operatorSet, operators[i][j], magnitudeForOperators); - } - } - } - vm.stopBroadcast(); - - /// WRITE TO JSON - - // string memory output_path = "script/output/devnet/populate_src/"; - // FsMetadata[] memory metadata = vm.fsMetadata(output_path); - // for (uint256 i = 0; i < entries.length; ++i) { - // vm.removeFile(entries[i].path); - // } - for (uint256 i = 0; i < operators.length; ++i) { - address[] memory strategyAddresses = new address[](strategies[i].length); - for (uint256 j = 0; j < strategies[i].length; ++j) { - strategyAddresses[j] = address(strategies[i][j]); - } - - string memory parent_object = "success"; - vm.serializeAddress(parent_object, "stakeRootCompendium", address(stakeRootCompendium)); - // vm.serializeAddress(parent_object, "avs", address(avs)); - // vm.serializeAddress(parent_object, "operators", operators[i]); - // vm.serializeAddress(parent_object, "strategies", strategyAddresses); - string memory finalJson = vm.serializeString("success", "success", parent_object); - vm.writeJson(finalJson, string.concat("script/output/devnet/populate_src/opset_", string.concat(vm.toString(i), ".json"))); - } - } -} - -contract AVS { - IAVSDirectory avsDirectory; - IStakeRootCompendium stakeRootCompendium; - - // creates an operator set for each list of strategies - constructor(IAVSDirectory _avsDirectory, IStakeRootCompendium _stakeRootCompendium) { - avsDirectory = _avsDirectory; - stakeRootCompendium = _stakeRootCompendium; - avsDirectory.becomeOperatorSetAVS(); - } - - function createOperatorSetAndRegisterOperators(uint32 operatorSetId, IStrategy[] memory strategies, address[] memory operators) external { - // create operator sets and become an AVS - uint32[] memory operatorSetIdsToCreate = new uint32[](1); - operatorSetIdsToCreate[0] = operatorSetId; - if(!avsDirectory.isOperatorSet(address(this), operatorSetId)) { - avsDirectory.createOperatorSets(operatorSetIdsToCreate); - - stakeRootCompendium.depositForOperatorSet{value: 2 * stakeRootCompendium.MIN_DEPOSIT_BALANCE()}(IAVSDirectory.OperatorSet({ - avs: address(this), - operatorSetId: operatorSetId - })); - } - - // register operators to operator sets - for (uint256 i = 0; i < operators.length; ++i) { - avsDirectory.registerOperatorToOperatorSets( - operators[i], - operatorSetIdsToCreate, - ISignatureUtils.SignatureWithSaltAndExpiry({ - signature: hex"", - salt: keccak256(abi.encodePacked(address(this), operatorSetIdsToCreate)), - expiry: type(uint256).max - }) - ); - } - - // loop through strategies in batches of NUM_STRATS_PER_OPERATOR for each operator set - IStakeRootCompendium.StrategyAndMultiplier[] memory strategiesAndMultipliers = new IStakeRootCompendium.StrategyAndMultiplier[](strategies.length); - for (uint256 i = 0; i < strategiesAndMultipliers.length; ++i) { - strategiesAndMultipliers[i] = IStakeRootCompendium.StrategyAndMultiplier({ - strategy: strategies[i], - multiplier: 1 ether - }); - } - stakeRootCompendium.addStrategiesAndMultipliers(operatorSetId, strategiesAndMultipliers); - stakeRootCompendium.setOperatorSetExtraData(operatorSetId, keccak256(abi.encodePacked(operatorSetId))); - for (uint256 i = 0; i < operators.length; ++i) { - stakeRootCompendium.setOperatorExtraData(operatorSetId, operators[i], keccak256(abi.encodePacked(operators[i]))); - } - } - - receive() external payable {} -} - - -contract OperatorFactory is Test { - IDelegationManager delegationManager; - IStrategyManager strategyManager; - IAVSDirectory avsDirectory; - constructor(IDelegationManager _delegationManager, IStrategyManager _strategyManager, IAVSDirectory _avsDirectory) { - delegationManager = _delegationManager; - strategyManager = _strategyManager; - avsDirectory = _avsDirectory; - } - - uint256 constant AMOUNT_TOKEN = 1000; - - function createManyOperators(IStrategy[] memory strategies, uint256 numOperatorsPerOpset) public returns(address[] memory) { - IERC20[] memory tokens = new IERC20[](strategies.length); - // approve all transfers - for (uint256 i = 0; i < strategies.length; ++i) { - tokens[i] = strategies[i].underlyingToken(); - tokens[i].approve(address(strategyManager), type(uint256).max); - } - - address[] memory operators = new address[](numOperatorsPerOpset); - for (uint256 i = 0; i < operators.length; ++i) { - operators[i] = address(new Operator(delegationManager, avsDirectory)); - } - return operators; - } - - function depositForOperators(IStrategy strategy, address[] memory operators, uint256 tokenAmountPerOperator) public { - IERC20 token = strategy.underlyingToken(); - token.approve(address(strategyManager), type(uint256).max); - - for (uint256 i = 0; i < operators.length; ++i) { - strategyManager.depositIntoStrategyWithSignature(strategy, token, tokenAmountPerOperator, operators[i], type(uint256).max, hex""); - } - } - - function allocateForOperators(IStrategy strategy, IAVSDirectory.OperatorSet calldata operatorSet, address[] memory operators, uint64 magnitudeForOperators) public { - uint64 expectedTotalMagnitude = ShareScalingLib.INITIAL_TOTAL_MAGNITUDE; - - IAVSDirectory.OperatorSet[] memory operatorSets = new IAVSDirectory.OperatorSet[](1); - operatorSets[0] = operatorSet; - - uint64[] memory magnitudes = new uint64[](1); - magnitudes[0] = magnitudeForOperators; - - IAVSDirectory.MagnitudeAllocation[] memory allocations = new IAVSDirectory.MagnitudeAllocation[](1); - allocations[0] = IAVSDirectory.MagnitudeAllocation({ - strategy: strategy, - expectedTotalMagnitude: expectedTotalMagnitude, - operatorSets: operatorSets, - magnitudes: magnitudes - }); - - ISignatureUtils.SignatureWithSaltAndExpiry memory signature = ISignatureUtils.SignatureWithSaltAndExpiry({ - signature: hex"", - salt: keccak256(abi.encode(allocations)), - expiry: type(uint256).max - }); - - for (uint256 i = 0; i < operators.length; ++i) { - avsDirectory.modifyAllocations( - operators[i], - allocations, - signature - ); - } - } -} - -contract Operator is IERC1271 { - constructor(IDelegationManager delegationManager, IAVSDirectory avsDirectory) { - // register as operator - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - __deprecated_earningsReceiver: address(this), - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - delegationManager.registerAsOperator( - operatorDetails, - 0, - "" - ); - } - - // sign everything - function isValidSignature(bytes32, bytes memory) external pure returns (bytes4 magicValue) { - return EIP1271SignatureUtils.EIP1271_MAGICVALUE; - } -} \ No newline at end of file diff --git a/src/contracts/core/StakeRootCompendium.sol b/src/contracts/core/StakeRootCompendium.sol index 03c38a023..34464b1d5 100644 --- a/src/contracts/core/StakeRootCompendium.sol +++ b/src/contracts/core/StakeRootCompendium.sol @@ -13,207 +13,272 @@ contract StakeRootCompendium is StakeRootCompendiumStorage { constructor( IDelegationManager _delegationManager, IAVSDirectory _avsDirectory, - uint32 _proofInterval, - uint32 _blacklistWindow - ) StakeRootCompendiumStorage(_delegationManager, _avsDirectory, _proofInterval, _blacklistWindow) {} - - function initialize(address initialOwner) public initializer { + uint256 _maxTotalCharge, + uint256 _minBalanceThreshold, + uint256 _minProofsDuration, + address _verifier, + bytes32 _imageId + ) StakeRootCompendiumStorage(_delegationManager, _avsDirectory, _maxTotalCharge, _minBalanceThreshold, _minProofsDuration, _verifier, _imageId) {} + + function initialize(address _owner, address _rootConfirmer, uint32 _proofIntervalSeconds, uint96 _chargePerStrategy, uint96 _chargePerOperatorSet) public initializer { __Ownable_init(); - _transferOwnership(initialOwner); + _transferOwnership(_owner); + + rootConfirmer = _rootConfirmer; + + setProofIntervalSeconds(_proofIntervalSeconds); + setChargePerProof(_chargePerOperatorSet, _chargePerStrategy); + + stakeRootSubmissions.push(StakeRootSubmission({ + calculationTimestamp: 0, + stakeRoot: bytes32(0), + confirmed: false + })); + + // note verifier and imageId are immutable and set by implementation contract + // since proof verification is in the hot path, this is a gas optimization to avoid calling the storage contract for verifier and imageId + // however the new impl does not have access to the immutable variables of the last impl so we can't reference the old verifier and imageId + // instead we emit the new verifier and imageId here + emit VerifierSet(verifier); + emit ImageIdSet(imageId); } /// OPERATORSET CONFIGURATION - /// @inheritdoc IStakeRootCompendium - function depositForOperatorSet(IAVSDirectory.OperatorSet calldata operatorSet) external payable { - require( - avsDirectory.isOperatorSet(operatorSet.avs, operatorSet.operatorSetId), - "StakeRootCompendium.depositForOperatorSet: operator set does not exist" - ); - depositBalanceInfo[operatorSet.avs][operatorSet.operatorSetId].balance += msg.value; + function deposit(IAVSDirectory.OperatorSet calldata operatorSet) external payable { + if (!_isInStakeTree(operatorSet)) { + (,uint256 totalChargePerOperatorSet, uint256 totalChargePerStrategy) = _totalCharge(); + depositInfos[operatorSet.avs][operatorSet.operatorSetId] = DepositInfo({ + balance: 0, // balance will be updated outer context + lastUpdatedTimestamp: uint32(block.timestamp), + totalChargePerOperatorSetLastPaid: uint96(totalChargePerOperatorSet), + totalChargePerStrategyLastPaid: uint96(totalChargePerStrategy) + }); + + // empty their strategies and multipliers if they were force removed before + address[] memory keys = new address[](operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length()); + for (uint256 i = 0; i < keys.length; i++) { + (address key,) = operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].at(i); + keys[i] = key; + } + for (uint256 i = 0; i < keys.length; i++) { + operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].remove(keys[i]); + } + + operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].push(uint32(block.timestamp), uint224(operatorSets.length)); + operatorSets.push(operatorSet); + } + depositInfos[operatorSet.avs][operatorSet.operatorSetId].balance += uint96(msg.value); + // make sure they have enough to pay for MIN_PROOFS_DURATION require( - depositBalanceInfo[operatorSet.avs][operatorSet.operatorSetId].balance >= 2 * MIN_DEPOSIT_BALANCE, - "StakeRootCompendium.depositForOperatorSet: depositer must have at least 2x the minimum balance on deposit" + depositInfos[operatorSet.avs][operatorSet.operatorSetId].balance >= + minDepositBalance(operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length()), + "StakeRootCompendium.addOrModifyStrategiesAndMultipliers: insufficient deposit balance" ); - - // update the deposit balance for the operator set whenever a deposit is made - _updateDepositBalanceInfo(operatorSet, true); } /// @inheritdoc IStakeRootCompendium - function addStrategiesAndMultipliers( + function addOrModifyStrategiesAndMultipliers( uint32 operatorSetId, StrategyAndMultiplier[] calldata strategiesAndMultipliers ) external { - require( - strategiesAndMultipliers.length > 0, - "StakeRootCompendium.setStrategiesAndMultipliers: no strategies and multipliers provided" - ); - require( - avsDirectory.isOperatorSet(msg.sender, operatorSetId), - "StakeRootCompendium.setStrategiesAndMultipliers: operator set does not exist" - ); - - IAVSDirectory.OperatorSet memory operatorSet = - IAVSDirectory.OperatorSet({avs: msg.sender, operatorSetId: operatorSetId}); - + IAVSDirectory.OperatorSet memory operatorSet = IAVSDirectory.OperatorSet({avs: msg.sender, operatorSetId: operatorSetId}); // update the deposit balance for the operator set whenever number of strategies is changed - _updateDepositBalanceInfo(operatorSet, true); - - uint256 numStrategiesBefore = operatorSetToStrategyAndMultipliers[msg.sender][operatorSetId].length(); - // if the operator set has been configured to have a positive number of strategies, increment the number of configured operator sets - if (numStrategiesBefore == 0) { - require( - operatorSets.length < MAX_NUM_OPERATOR_SETS, - "StakeRootCompendium.setStrategiesAndMultipliers: too many operator sets" - ); - operatorSetToIndex[msg.sender][operatorSetId].push(uint32(block.timestamp), uint208(operatorSets.length)); - operatorSets.push(operatorSet); - } + _updateDepositInfo(operatorSet); + uint256 numStrategiesBefore = operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length(); // set the strategies and multipliers for the operator set for (uint256 i = 0; i < strategiesAndMultipliers.length; i++) { - operatorSetToStrategyAndMultipliers[msg.sender][operatorSetId].set( - address(strategiesAndMultipliers[i].strategy), uint256(strategiesAndMultipliers[i].multiplier) + operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].set( + address(strategiesAndMultipliers[i].strategy), + uint256(strategiesAndMultipliers[i].multiplier) ); } + uint256 numStrategiesAfter = operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length(); + + // make sure they have enough to pay for MIN_PROOFS_DURATION require( - operatorSetToStrategyAndMultipliers[msg.sender][operatorSetId].length() <= MAX_NUM_STRATEGIES, - "StakeRootCompendium.setStrategiesAndMultipliers: too many strategies" + depositInfos[operatorSet.avs][operatorSet.operatorSetId].balance >= minDepositBalance(numStrategiesAfter), + "StakeRootCompendium.addOrModifyStrategiesAndMultipliers: insufficient deposit balance" ); - _updateTotals(numStrategiesBefore, operatorSetToStrategyAndMultipliers[msg.sender][operatorSetId].length()); + // only adding new strategies to count + _updateTotalStrategies(numStrategiesBefore, numStrategiesAfter); } /// @inheritdoc IStakeRootCompendium function removeStrategiesAndMultipliers(uint32 operatorSetId, IStrategy[] calldata strategies) external { - IAVSDirectory.OperatorSet memory operatorSet = - IAVSDirectory.OperatorSet({avs: msg.sender, operatorSetId: operatorSetId}); // update the deposit balance for the operator set whenever number of strategies is changed - _updateDepositBalanceInfo(operatorSet, true); + IAVSDirectory.OperatorSet memory operatorSet = IAVSDirectory.OperatorSet({avs: msg.sender, operatorSetId: operatorSetId}); + _updateDepositInfo(operatorSet); - // remove the strategies and multipliers for the operator set - _removeStrategiesAndMultipliers(operatorSet, strategies); + // note below either all strategies are removed or none are removed and transaction reverts + uint256 numStrategiesBefore = operatorSetToStrategyAndMultipliers[msg.sender][operatorSetId].length(); + for (uint256 i = 0; i < strategies.length; i++) { + require( + operatorSetToStrategyAndMultipliers[msg.sender][operatorSetId].remove(address(strategies[i])), + "StakeRootCompendium.removeStrategiesAndMultipliers: strategy not found" + ); + } + _updateTotalStrategies(numStrategiesBefore, numStrategiesBefore - strategies.length); } /// @inheritdoc IStakeRootCompendium function setOperatorSetExtraData(uint32 operatorSetId, bytes32 extraData) external { - (bool exists,, uint224 index) = operatorSetToIndex[msg.sender][operatorSetId].latestCheckpoint(); - require(exists && index != REMOVED_INDEX, "StakeRootCompendium.setOperatorSetExtraData: operatorSet is not in stakeTree"); + require( + _isInStakeTree(IAVSDirectory.OperatorSet({avs: msg.sender, operatorSetId: operatorSetId})), + "StakeRootCompendium.setOperatorSetExtraData: operatorSet is not in stakeTree" + ); operatorSetExtraDatas[msg.sender][operatorSetId] = extraData; } /// @inheritdoc IStakeRootCompendium function setOperatorExtraData(uint32 operatorSetId, address operator, bytes32 extraData) external { - (bool exists,, uint224 index) = operatorSetToIndex[msg.sender][operatorSetId].latestCheckpoint(); - require(exists && index != REMOVED_INDEX, "StakeRootCompendium.setOperatorExtraData: operatorSet is not in stakeTree"); + require( + _isInStakeTree(IAVSDirectory.OperatorSet({avs: msg.sender, operatorSetId: operatorSetId})), + "StakeRootCompendium.setOperatorExtraData: operatorSet is not in stakeTree" + ); operatorExtraDatas[msg.sender][operatorSetId][operator] = extraData; } + /// @notice Withdraws an amount from the operator set's deposit balance + /// @dev If the operator set's deposit balance is less than the minimum deposit balance, the operator set is removed and any excess amount is returned + /// @param operatorSetId The ID of the operator set to withdraw from + /// @param amount The amount to withdraw + /// @return The amount actually withdrawn + function withdraw(uint32 operatorSetId, uint256 amount) external payable returns (uint256) { + IAVSDirectory.OperatorSet memory operatorSet = IAVSDirectory.OperatorSet({avs: msg.sender, operatorSetId: operatorSetId}); + require( + canWithdrawDepositBalance(operatorSet), + "StakeRootCompendium.withdrawForOperatorSet: operator set is not old enough" + ); + + // debt any pending charge + uint256 balance = _updateDepositInfo(operatorSet); + if(balance < MIN_BALANCE_THRESHOLD + amount){ + // withdraw all to avoid deposit balance dropping below minimum + amount = balance; + // remove from stake tree if applicable + _removeFromStakeTree(operatorSet); + } + // debt the withdraw amount + depositInfos[msg.sender][operatorSetId].balance -= uint96(amount); + + (bool success, ) = payable(msg.sender).call{value: amount}(""); + require(success, "StakeRootCompendium.withdrawForOperatorSet: eth transfer failed"); + + return amount; + } + + /// CHARGE MANAGEMENT + + /// @inheritdoc IStakeRootCompendium + function removeOperatorSetsFromStakeTree(IAVSDirectory.OperatorSet[] calldata operatorSetsToRemove) external { + uint256 penalty = 0; + for (uint256 i = 0; i < operatorSetsToRemove.length; i++) { + if (_isInStakeTree(operatorSetsToRemove[i])) { + uint256 depositBalance = _updateDepositInfo(operatorSetsToRemove[i]); + // TODO note: this is vulnerable to frontrunning of deposits, but if we allow anyone to update deposit info's + // withdrawals of deposit balance could be prevented because lastUpdatedTimestamp would be updated + require(depositBalance < MIN_BALANCE_THRESHOLD, "StakeRootCompendium.updateBalances: deposit balance is not below minimum"); + _removeFromStakeTree(operatorSetsToRemove[i]); + penalty += depositBalance; + } + } + // todo use gas metering to make this call incentive neutral and simply refund any excess balance to AVS'? + (bool success, ) = payable(msg.sender).call{value: penalty}(""); + require(success, "StakeRootCompendium.updateBalances: eth transfer failed"); + } + /// POSTING ROOTS AND BLACKLISTING /// @inheritdoc IStakeRootCompendium function verifyStakeRoot( - uint32 calculationTimestamp, + uint256 calculationTimestamp, bytes32 stakeRoot, address chargeRecipient, - Proof calldata proof + uint256 indexChargePerProof, + Proof calldata _proof ) external { - // TODO: verify proof - - _postStakeRoot(calculationTimestamp, stakeRoot, chargeRecipient, false); - } - - /// @inheritdoc IStakeRootCompendium - function blacklistStakeRoot(uint32 submissionIndex) external onlyOwner { - // TODO: this should not be onlyOwner + require(calculationTimestamp % proofIntervalSeconds == 0, "StakeRootCompendium._postStakeRoot: timestamp must be a multiple of proofInterval"); + // no length check here is ok because the initializer adds a default submission require( - !stakeRootSubmissions[submissionIndex].blacklisted, - "StakeRootCompendium.blacklistStakeRoot: stakeRoot already blacklisted" + stakeRootSubmissions[stakeRootSubmissions.length - 1].calculationTimestamp != calculationTimestamp, + "StakeRootCompendium._postStakeRoot: timestamp already posted" ); + // credit the charge recipient + Checkpoints.Checkpoint memory _checkpoint = chargePerProofHistory._checkpoints[indexChargePerProof]; + require(_checkpoint._key <= calculationTimestamp, "StakeRootCompendium._postStakeRoot: timestamp of indexChargePerProof is greater than the calculationTimestamp"); require( - block.timestamp < stakeRootSubmissions[submissionIndex].blacklistableBefore, - "StakeRootCompendium.blacklistStakeRoot: stakeRoot cannot be blacklisted" + chargePerProofHistory.length() == indexChargePerProof + 1 + || uint256(chargePerProofHistory._checkpoints[indexChargePerProof + 1]._key) > calculationTimestamp, + "StakeRootCompendium._postStakeRoot: indexChargePerProof is not valid" ); - require( - !stakeRootSubmissions[submissionIndex].forcePosted, - "StakeRootCompendium.blacklistStakeRoot: stakeRoot was force posted" - ); - stakeRootSubmissions[submissionIndex].blacklisted = true; + stakeRootSubmissions.push(StakeRootSubmission({ + calculationTimestamp: uint32(calculationTimestamp), + stakeRoot: stakeRoot, + confirmed: false + })); + + (bool success, ) = payable(chargeRecipient).call{value: _checkpoint._value}(""); + require(success, "StakeRootCompendium.withdrawForChargeRecipient: eth transfer failed"); + + // interactions + + // note verify will be an external call, so adding to the end to apply the check, effect, interaction pattern to avoid reentrancy + // TODO: verify proof + // TODO: prevent race incentives and public mempool sniping, eg embed chargeRecipient in the proof } /// @inheritdoc IStakeRootCompendium - function forcePostStakeRoot(uint32 calculationTimestamp, bytes32 stakeRoot) external onlyOwner { - _postStakeRoot(calculationTimestamp, stakeRoot, address(0), true); + function confirmStakeRoot(uint32 index, bytes32 stakeRoot) external { + require(msg.sender == rootConfirmer, "StakeRootCompendium.confirmStakeRoot: only rootConfirmer can confirm"); + require(stakeRootSubmissions[index].stakeRoot != bytes32(0), "StakeRootCompendium.confirmStakeRoot: timestamp not posted"); + require(stakeRootSubmissions[index].stakeRoot == stakeRoot, "StakeRootCompendium.confirmStakeRoot: stake root does not match"); + require(!stakeRootSubmissions[index].confirmed, "StakeRootCompendium.confirmStakeRoot: timestamp already confirmed"); + stakeRootSubmissions[index].confirmed = true; } - /// CHARGE MANAGEMENT + /// SET FUNCTIONS - /// @inheritdoc IStakeRootCompendium - function updateDepositBalanceInfos(IAVSDirectory.OperatorSet[] calldata operatorSetsToUpdate) external { - uint256 penalty = 0; - for (uint256 i = 0; i < operatorSetsToUpdate.length; i++) { - penalty += _updateDepositBalanceInfo(operatorSetsToUpdate[i], false); - } - if (penalty > 0) { - payable(msg.sender).transfer(penalty); - } + function setChargePerProof(uint96 _chargePerStrategy, uint96 _chargePerOperatorSet) public onlyOwner { + _updateTotalCharge(); + chargePerStrategy = _chargePerStrategy; + chargePerOperatorSet = _chargePerOperatorSet; + _updateChargePerProof(); } - /// @inheritdoc IStakeRootCompendium - function processCharges(uint256 numToCharge) external { - uint256 latestChargedSubmissionIndexMemory = latestChargedSubmissionIndex; - uint256 endIndex = latestChargedSubmissionIndexMemory + numToCharge; - if (endIndex > stakeRootSubmissions.length) { - endIndex = stakeRootSubmissions.length; - } + function setProofIntervalSeconds(uint32 proofIntervalSeconds) public onlyOwner { + _updateTotalCharge(); + uint32 latestSubmittedCalculationTimestamp = stakeRootSubmissions[stakeRootSubmissions.length - 1].calculationTimestamp; + require( + latestSubmittedCalculationTimestamp == totalChargeLastUpdatedTimestamp, + "StakeRootCompendium.setProofIntervalSeconds: no proofs that have been charged but have not been submitteed" + ); + proofIntervalSeconds = proofIntervalSeconds; + } - address prevRecipient; - uint256 totalCharge; - for (uint256 i = latestChargedSubmissionIndexMemory; i < endIndex; i++) { - StakeRootSubmission memory stakeRootSubmission = stakeRootSubmissions[i]; - // if the stakeRootSubmission is blacklisted or force posted, skip it - if ( - block.timestamp < stakeRootSubmission.blacklistableBefore || stakeRootSubmission.blacklisted - || stakeRootSubmission.forcePosted - ) { - continue; - } - // if the charge recipient has changed, transfer the total charge to the previous recipient - if (stakeRootSubmission.chargeRecipient != prevRecipient) { - if (totalCharge > 0) { - payable(prevRecipient).transfer(totalCharge); - } - totalCharge = 0; - prevRecipient = stakeRootSubmission.chargeRecipient; - } - // total charge is the charge per strategy per proof times the number of strategies at the time of proof - totalCharge += totalChargeSnapshot.upperLookup(stakeRootSubmissions[i].calculationTimestamp); - } + function setRootConfirmer(address _rootConfirmer) public onlyOwner { + rootConfirmer = _rootConfirmer; } - /// PERMISSIONED SETTERS + /// VIEW FUNCTIONS /// @inheritdoc IStakeRootCompendium - function setVerifier(address _verifier) external onlyOwner { - address oldVerifier = verifier; - verifier = _verifier; - emit VerifierChanged(oldVerifier, verifier); + function minDepositBalance(uint256 numStrategies) public view returns (uint256) { + return (numStrategies * chargePerStrategy + chargePerOperatorSet) * MIN_PROOFS_DURATION; } - + /// @inheritdoc IStakeRootCompendium - function setImageId(bytes32 _imageId) external onlyOwner { - bytes32 oldImageId = imageId; - imageId = _imageId; - emit ImageIdChanged(oldImageId, imageId); + function canWithdrawDepositBalance(IAVSDirectory.OperatorSet memory operatorSet) public view returns (bool) { + return block.timestamp > depositInfos[operatorSet.avs][operatorSet.operatorSetId].lastUpdatedTimestamp + MIN_PROOFS_DURATION * proofIntervalSeconds; } - function setChargeForNumStrategies(uint64 newConstantChargePerProof, uint64 newLinearChargePerProof) external onlyOwner { - _setChargePerProof(newConstantChargePerProof, newLinearChargePerProof); + /// @inheritdoc IStakeRootCompendium + function getStakeRootSubmission(uint32 index) external view returns (StakeRootSubmission memory) { + return stakeRootSubmissions[index]; } - /// VIEW FUNCTIONS - /// @inheritdoc IStakeRootCompendium function getNumOperatorSets() external view returns (uint256) { return operatorSets.length; @@ -229,237 +294,101 @@ contract StakeRootCompendium is StakeRootCompendiumStorage { return _getStakes(operatorSet, strategies, multipliers, operator); } - /// @inheritdoc IStakeRootCompendium - function getStakeRootSubmission(uint32 index) external view returns (StakeRootSubmission memory) { - return stakeRootSubmissions[index]; - } - - /// @inheritdoc IStakeRootCompendium - function getNumStakeRootSubmissions() external view returns (uint256) { - return stakeRootSubmissions.length; - } - - /// @inheritdoc IStakeRootCompendium - function getOperatorSetIndexAtTimestamp( - IAVSDirectory.OperatorSet calldata operatorSet, - uint32 timestamp - ) external view returns (uint32) { - return uint32(operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].upperLookupRecent(timestamp)); - } - /// @inheritdoc IStakeRootCompendium function getDepositBalance(IAVSDirectory.OperatorSet memory operatorSet) - public + external view - returns (uint256 balance, uint256 penalty) + returns (uint256 balance) { - (uint224 previousConstantCumulativeCharge, uint224 previousLinearCumulativeCharge) = _getPreviousCumulativeCharges( - depositBalanceInfo[operatorSet.avs][operatorSet.operatorSetId].latestUpdateTime - ); - (uint224 currentConstantCumulativeCharge, uint224 currentLinearCumulativeCharge) = _getCurrentCumulativeCharges(_getLatestCalculationTimestamp()); - + DepositInfo memory depositInfo = depositInfos[operatorSet.avs][operatorSet.operatorSetId]; + (,uint96 totalChargePerOperatorSet, uint96 totalChargePerStrategy) = _totalCharge(); uint256 pendingCharge = - uint256(currentConstantCumulativeCharge - previousConstantCumulativeCharge) + - uint256(currentLinearCumulativeCharge - previousLinearCumulativeCharge) * + uint256(totalChargePerOperatorSet - depositInfo.totalChargePerOperatorSetLastPaid) + + uint256(totalChargePerStrategy - depositInfo.totalChargePerStrategyLastPaid) * operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length(); - uint256 storedBalance = depositBalanceInfo[operatorSet.avs][operatorSet.operatorSetId].balance; - // if the charge would take the balance below the minimum deposit balance, return 0 - balance = storedBalance < pendingCharge + MIN_DEPOSIT_BALANCE ? 0 : storedBalance - pendingCharge; - // if the balance is 0, then the charger may be able to claim the penalty for removing the operatorSet from the stakeTree - penalty = balance == 0 ? pendingCharge + MIN_DEPOSIT_BALANCE - storedBalance : 0; - return (balance, penalty); + return depositInfo.balance > pendingCharge ? depositInfo.balance - pendingCharge : 0; } - - /// Misc - - receive() external payable {} - /// INTERNAL FUNCTIONS - function _setChargePerProof(uint64 newConstantChargePerProof, uint64 newLinearChargePerProof) internal { - _updateCumulativeCharges(); - constantChargePerProof = newConstantChargePerProof; - linearChargePerProof = newLinearChargePerProof; - _updateTotals(0, 0); - } - - function _setProofInterval(uint32 proofInterval) internal { - require( - stakeRootSubmissions.length == 0 || _getLatestSubmittedCalculationTimestamp() == _getLatestCalculationTimestamp(), - "StakeRootCompendium._setProofInterval: make sure there are no pending proofs" - ); - _updateCumulativeCharges(); - proofInterval = proofInterval; - } - - function _updateCumulativeCharges() internal { - uint32 latestCalculationTimestamp = _getLatestCalculationTimestamp(); - (uint224 currentConstantCumulativeCharge, uint224 currentLinearCumulativeCharge) = _getCurrentCumulativeCharges(latestCalculationTimestamp); - - // update the cumulative charge snapshots - cumulativeContantChargeSnapshot.push( - latestCalculationTimestamp, - currentConstantCumulativeCharge - ); - cumulativeLinearChargeSnapshot.push( - latestCalculationTimestamp, - currentLinearCumulativeCharge - ); - } - - function _getCurrentCumulativeCharges(uint32 latestCalculationTimestamp) internal view returns(uint224, uint224) { - return ( - _getCurrentCumulativeCharge(cumulativeContantChargeSnapshot, constantChargePerProof, latestCalculationTimestamp), - _getCurrentCumulativeCharge(cumulativeLinearChargeSnapshot, linearChargePerProof, latestCalculationTimestamp) - ); - } - - function _getPreviousCumulativeCharges(uint32 timestamp) internal view returns(uint224, uint224) { - return ( - cumulativeContantChargeSnapshot.upperLookup(timestamp), - cumulativeLinearChargeSnapshot.upperLookup(timestamp) - ); - } - - function _getCurrentCumulativeCharge(Checkpoints.History storage cumulativeChargeSnapshot, uint64 currentCharge, uint32 latestCalculationTimestamp) internal view returns (uint224) { - (bool exists, uint32 latestSnapshotTimestamp, uint224 latestSnapshottedCumulativeCharge) = cumulativeChargeSnapshot.latestCheckpoint(); - // return the latest snapshot if it has been accounted for - if(latestSnapshotTimestamp == latestCalculationTimestamp) { - return latestSnapshottedCumulativeCharge; - } - - // use "now" if this is the first time the charge is set - if (!exists) { - latestSnapshotTimestamp = latestCalculationTimestamp; - latestSnapshottedCumulativeCharge = 0; - } - - // keep track of the cumulative charge that would have been charged since the last change - // this is: - // latestSnapshottedCumulativeCharge + proofs since latest snapshot * (charge / proof) - // latestSnapshottedCumulativeCharge + (latestCalculationTimestamp - latestSnapshotTimestamp) * (1 proof / proofInterval time) * (charge / proof) - // latestSnapshottedCumulativeCharge + (charge / proof) * (time since snapshot) * (1 proof / proofInterval time) - return latestSnapshottedCumulativeCharge + uint224(currentCharge * (latestCalculationTimestamp - latestSnapshotTimestamp) / proofInterval); - } - - function _removeStrategiesAndMultipliers(IAVSDirectory.OperatorSet memory operatorSet, IStrategy[] memory strategies) internal { - uint256 numStrategiesBefore = operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length(); - - for (uint256 i = 0; i < strategies.length; i++) { - require( - operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].remove(address(strategies[i])), - "StakeRootCompendium.removeStrategiesAndMultipliers: strategy not found" - ); - } - - _updateTotals(numStrategiesBefore, operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length()); - } - - function _removeOperatorSet(IAVSDirectory.OperatorSet memory operatorSet) internal { - if(operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length() != 0) { - bytes32[] memory strategyBytes = operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId]._inner._keys.values(); - IStrategy[] memory strategies = new IStrategy[](strategyBytes.length); - // todo: better way to do this? - for (uint256 i = 0; i < strategyBytes.length; i++) { - strategies[i] = IStrategy(address(uint160(uint256(strategyBytes[i])))); - } - - // remove the strategies and multipliers for the operator set - _removeStrategiesAndMultipliers( - operatorSet, - strategies - ); - } - + function _removeFromStakeTree(IAVSDirectory.OperatorSet memory operatorSet) internal { IAVSDirectory.OperatorSet memory substituteOperatorSet = operatorSets[operatorSets.length - 1]; uint224 operatorSetIndex = operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].latest(); operatorSets[operatorSetIndex] = substituteOperatorSet; operatorSets.pop(); // update the index of the operator sets + // note when there is only one operator set left, the index will not be updated as the operator set will be removed in the next step + operatorSetToIndex[substituteOperatorSet.avs][substituteOperatorSet.operatorSetId].push(uint32(block.timestamp), operatorSetIndex); operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].push(uint32(block.timestamp), REMOVED_INDEX); - operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].push(uint32(block.timestamp), operatorSetIndex); - } - // updates the deposit balance for the operator set and returns the penalty if the operator set has fallen below the minimum deposit balance - function _updateDepositBalanceInfo( - IAVSDirectory.OperatorSet memory operatorSet, - bool sendPenalty - ) internal returns (uint256) { - _updateCumulativeCharges(); - (uint256 depositBalance, uint256 penalty) = getDepositBalance(operatorSet); - depositBalanceInfo[operatorSet.avs][operatorSet.operatorSetId].balance = depositBalance; - depositBalanceInfo[operatorSet.avs][operatorSet.operatorSetId].latestUpdateTime = uint32(block.timestamp); - // if the operatorSet has fallen below the minimum deposit balance, remove it from the stakeTree - if (penalty > 0) { - _removeOperatorSet(operatorSet); - if (sendPenalty) { - payable(msg.sender).transfer(penalty); - } - } + depositInfos[operatorSet.avs][operatorSet.operatorSetId].balance = 0; - return penalty; + _updateTotalStrategies(operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length(), 0); } - // updates total strategies and the total charge per proof whenever an operator set's number of strategies changes - function _updateTotals(uint256 numStrategiesBefore, uint256 numStrategiesAfter) internal { - totalStrategies = totalStrategies - numStrategiesBefore + numStrategiesAfter; - totalChargeSnapshot.push( - uint32(block.timestamp), - uint224(totalStrategies * linearChargePerProof + constantChargePerProof) - ); + function _updateTotalStrategies(uint256 _countStrategiesBefore, uint256 _countStrategiesAfter) internal { + totalStrategies = totalStrategies - _countStrategiesBefore + _countStrategiesAfter; + _updateChargePerProof(); } - function _postStakeRoot( - uint32 calculationTimestamp, - bytes32 stakeRoot, - address chargeRecipient, - bool forcePosted - ) internal { - require( - calculationTimestamp % proofInterval == 0, - "StakeRootCompendium._postStakeRoot: calculationTimestamp must be a multiple of proofInterval" + function _updateChargePerProof() internal { + // note if totalStrategies is 0, the charge per proof will be 0, and provers should not post a proof + uint256 chargePerProof = operatorSets.length * chargePerOperatorSet + totalStrategies * chargePerStrategy; + require(chargePerProof <= MAX_TOTAL_CHARGE, "StakeRootCompendium._updateChargePerProof: charge per proof exceeds max total charge"); + chargePerProofHistory.push( + uint32(block.timestamp), + uint224(chargePerProof) ); + } - uint256 stakeRootSubmissionsLength = stakeRootSubmissions.length; - if (stakeRootSubmissionsLength != 0) { - require( - stakeRootSubmissions[stakeRootSubmissionsLength - 1].calculationTimestamp + proofInterval == calculationTimestamp, - "StakeRootCompendium._postStakeRoot: calculationTimestamp must be greater than the last posted calculationTimestamp" - ); + function _totalCharge() internal view returns (uint32, uint96, uint96) { + // calculate the total charge since the last update up until the latest calculation timestamp + uint32 latestCalculationTimestamp = uint32(block.timestamp) - uint32(block.timestamp % proofIntervalSeconds); + if (totalChargeLastUpdatedTimestamp == latestCalculationTimestamp) { + return (latestCalculationTimestamp, totalChargePerOperatorSetLastUpdate, totalChargePerStrategyLastUpdate); } - - stakeRootSubmissions.push( - StakeRootSubmission({ - stakeRoot: stakeRoot, - chargeRecipient: msg.sender, - calculationTimestamp: calculationTimestamp, - blacklistableBefore: uint32(block.timestamp) + blacklistWindow, - blacklisted: false, - crossPosted: false, - forcePosted: forcePosted - }) + uint256 numProofs = (latestCalculationTimestamp - totalChargeLastUpdatedTimestamp) / proofIntervalSeconds; + return ( + latestCalculationTimestamp, + uint96(totalChargePerOperatorSetLastUpdate + chargePerOperatorSet * numProofs), + uint96(totalChargePerStrategyLastUpdate + chargePerStrategy * numProofs) ); - - // todo: emit events } - /// @notice gets the latest calculation timestamp, whether a stakeRoot has been posted or not - function _getLatestCalculationTimestamp() internal view returns (uint32) { - uint256 stakeRootSubmissionsLength = stakeRootSubmissions.length; - require(stakeRootSubmissionsLength > 0, "StakeRootCompendium._getLatestCalculationTimestamp: first empty stakeRoot must be posted"); - uint32 latestCalculationTimestamp = stakeRootSubmissions[stakeRootSubmissionsLength - 1].calculationTimestamp; - if (latestCalculationTimestamp + proofInterval < block.timestamp) { - latestCalculationTimestamp += proofInterval; - } - return latestCalculationTimestamp; + function _updateTotalCharge() internal { + (uint32 latestCalculationTimestamp, uint96 totalChargePerOperatorSet, uint96 totalChargePerStrategy) = _totalCharge(); + totalChargeLastUpdatedTimestamp = latestCalculationTimestamp; + totalChargePerOperatorSetLastUpdate = totalChargePerOperatorSet; + totalChargePerStrategyLastUpdate = totalChargePerStrategy; } - function _getLatestSubmittedCalculationTimestamp() internal view returns (uint32) { - uint256 stakeRootSubmissionsLength = stakeRootSubmissions.length; - require(stakeRootSubmissionsLength > 0, "StakeRootCompendium._getLatestCalculationTimestamp: first empty stakeRoot must be posted"); - return stakeRootSubmissions[stakeRootSubmissionsLength - 1].calculationTimestamp; + // updates the deposit balance for the operator set and returns the penalty if the operator set has fallen below the minimum deposit balance + function _updateDepositInfo(IAVSDirectory.OperatorSet memory operatorSet) internal returns (uint256) { + require(_isInStakeTree(operatorSet), "StakeRootCompendium._updateDepositInfo: operatorSet is not in stakeTree"); + + (,uint256 totalChargePerOperatorSet, uint256 totalChargePerStrategy) = _totalCharge(); + DepositInfo memory depositInfo = depositInfos[operatorSet.avs][operatorSet.operatorSetId]; + + // subtract new total charge from last paid total charge + uint256 pendingCharge = + totalChargePerOperatorSet - depositInfo.totalChargePerOperatorSetLastPaid + + + ( + (totalChargePerStrategy - depositInfo.totalChargePerStrategyLastPaid) + * operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length() + ); + + uint256 balance = depositInfo.balance > pendingCharge ? depositInfo.balance - pendingCharge : 0; + + depositInfos[operatorSet.avs][operatorSet.operatorSetId] = DepositInfo({ + balance: uint96(balance), + lastUpdatedTimestamp: uint32(block.timestamp), + totalChargePerOperatorSetLastPaid: uint96(totalChargePerOperatorSet), + totalChargePerStrategyLastPaid: uint96(totalChargePerStrategy) + }); + + return balance; } function _getStrategiesAndMultipliers(IAVSDirectory.OperatorSet memory operatorSet) @@ -491,14 +420,18 @@ contract StakeRootCompendium is StakeRootCompendiumStorage { avsDirectory.getTotalAndAllocatedMagnitudes(operator, operatorSet, strategies); for (uint256 i = 0; i < strategies.length; i++) { - uint256 multipliedDelegatedShares = delegatedShares[i] * multipliers[i] / 1 ether; - delegatedStake += multipliedDelegatedShares * totalMagnitudes[i]; - slashableStake += multipliedDelegatedShares * allocatedMagnitudes[i]; + delegatedStake += delegatedShares[i] * totalMagnitudes[i] / 1 ether * multipliers[i]; + slashableStake += delegatedShares[i] * allocatedMagnitudes[i] / 1 ether * multipliers[i]; } } return (delegatedStake, slashableStake); } + function _isInStakeTree(IAVSDirectory.OperatorSet memory operatorSet) internal view returns (bool) { + (bool exists,,uint224 index) = operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].latestCheckpoint(); + return exists && index != REMOVED_INDEX; + } + // STAKE ROOT CALCULATION /// @inheritdoc IStakeRootCompendium @@ -532,7 +465,7 @@ contract StakeRootCompendium is StakeRootCompendiumStorage { uint256 operatorSetIndex, uint256 startOperatorIndex, uint256 numOperators - ) external view returns (IAVSDirectory.OperatorSet memory, address[] memory, OperatorLeaf[] memory) { + ) external view returns (OperatorLeaf[] memory) { require( operatorSetIndex < operatorSets.length, "StakeRootCompendium.getOperatorSetLeaves: operator set index out of bounds" @@ -552,7 +485,7 @@ contract StakeRootCompendium is StakeRootCompendiumStorage { extraData: operatorExtraDatas[operatorSet.avs][operatorSet.operatorSetId][operators[i]] }); } - return (operatorSet, operators, operatorLeaves); + return operatorLeaves; } /// @inheritdoc IStakeRootCompendium @@ -565,7 +498,6 @@ contract StakeRootCompendium is StakeRootCompendiumStorage { avsDirectory.isOperatorSet(operatorSet.avs, operatorSet.operatorSetId), "StakeRootCompendium.getOperatorSetRoot: operator set does not exist" ); - require(operatorLeaves.length <= MAX_OPERATOR_SET_SIZE, "AVSSyncTree._verifyOperatorStatus: operator set too large"); require( operatorLeaves.length == avsDirectory.getNumOperatorsInOperatorSet(operatorSet), "AVSSyncTree.getOperatorSetRoot: operator set size mismatch" @@ -605,4 +537,7 @@ contract StakeRootCompendium is StakeRootCompendiumStorage { ) ); } + + // in case of charge problems + receive() external payable {} } diff --git a/src/contracts/core/StakeRootCompendiumStorage.sol b/src/contracts/core/StakeRootCompendiumStorage.sol index 719930335..f031d5885 100644 --- a/src/contracts/core/StakeRootCompendiumStorage.sol +++ b/src/contracts/core/StakeRootCompendiumStorage.sol @@ -17,39 +17,47 @@ abstract contract StakeRootCompendiumStorage is IStakeRootCompendium, OwnableUpg using EnumerableMap for EnumerableMap.AddressToUintMap; using EnumerableSet for EnumerableSet.Bytes32Set; - /// @notice the maximum number of operators that can be in an operator set in the StakeTree - uint32 public constant MAX_OPERATOR_SET_SIZE = 2048; - /// @notice the maximum number of operator sets that can be in the StakeTree - uint32 public constant MAX_NUM_OPERATOR_SETS = 2048; - /// @notice the maximum number of strategies that each operator set in the StakeTree can use to weight their operator stakes - uint32 public constant MAX_NUM_STRATEGIES = 20; - /// @notice the placeholder index used for operator sets that are removed from the StakeTree - uint32 public constant REMOVED_INDEX = type(uint32).max; - - /// @notice the minimum balance that must be maintained for an operatorSet - uint256 public constant MIN_DEPOSIT_BALANCE = 0.001 ether; - /// @notice the delegation manager contract IDelegationManager public immutable delegationManager; /// @notice the AVS directory contract IAVSDirectory public immutable avsDirectory; - /// @notice the interval at which proofs can be posted, to not overcharge the operatorSets - uint32 public immutable proofInterval; - /// @notice the period of time within which a root can be marked as blacklisted - uint32 public immutable blacklistWindow; + /// @notice the maximum total charge for a proof + uint256 immutable public MAX_TOTAL_CHARGE; + + /// @notice the minimum balance that must be maintained for an operatorSet + /// @dev this balance compensates gas costs to deregister an operatorSet + uint256 immutable public MIN_BALANCE_THRESHOLD; + + /// @notice the placeholder index used for operator sets that are removed from the StakeTree + uint32 public constant REMOVED_INDEX = type(uint32).max; + /// @notice the minimum number of proofs that an operatorSet's deposit balance needs to cover and + /// the number of proofs they must pay for since their latest reconfiguration + /// @dev this prevents de-registering an operatorSet immediately after reconfiguring + uint256 immutable public MIN_PROOFS_DURATION; + + /// @notice the verifier contract that will be used to verify snark proofs + address public immutable verifier; + /// @notice the id of the program being verified when roots are posted + bytes32 public immutable imageId; + + /// @notice the interval in seconds at which proofs can be posted + uint32 public proofIntervalSeconds; + /// @notice the address allowed to confirm roots + address public rootConfirmer; /// @notice the linear charge per proof in the number of strategies - uint64 public linearChargePerProof; + uint96 public chargePerOperatorSet; /// @notice the constant charge per proof - uint64 public constantChargePerProof; - /// @notice the total amount that a operatorSet that had been in the stakeTree since genesis would have been charged in constant charges - Checkpoints.History internal cumulativeContantChargeSnapshot; - /// @notice the total amount that a operatorSet that had been in the stakeTree since genesis would have been charged in linear charges - Checkpoints.History internal cumulativeLinearChargeSnapshot; + uint96 public chargePerStrategy; + uint32 public totalChargeLastUpdatedTimestamp; + /// @notice the total constant charge per proof since deployment + uint96 public totalChargePerOperatorSetLastUpdate; + /// @notice the total linear charge per proof since deployment + uint96 public totalChargePerStrategyLastUpdate; /// @notice deposit balance to be deducted for operatorSets - mapping(address => mapping(uint32 => DepositBalanceInfo)) public depositBalanceInfo; + mapping(address => mapping(uint32 => DepositInfo)) public depositInfos; /// @notice map from operator set to a trace of their index over time mapping(address => mapping(uint32 => Checkpoints.History)) internal operatorSetToIndex; @@ -59,7 +67,7 @@ abstract contract StakeRootCompendiumStorage is IStakeRootCompendium, OwnableUpg /// @notice the total number of strategies among all operator sets (with duplicates) uint256 public totalStrategies; /// @notice the total charge for a proofs at a certain time depending on the number of strategies - Checkpoints.History internal totalChargeSnapshot; + Checkpoints.History internal chargePerProofHistory; /// @notice the strategies and multipliers for each operator set mapping(address => mapping(uint32 => EnumerableMap.AddressToUintMap)) internal operatorSetToStrategyAndMultipliers; @@ -68,26 +76,24 @@ abstract contract StakeRootCompendiumStorage is IStakeRootCompendium, OwnableUpg /// @notice the extraData for each operator in each operator set mapping(address => mapping(uint32 => mapping(address => bytes32))) internal operatorExtraDatas; - /// @notice the verifier contract that will be used to verify snark proofs - address public verifier; - /// @notice the id of the program being verified when roots are posted - bytes32 public imageId; - - /// @notice the index of the latest stake root submission that has been charged - uint256 public latestChargedSubmissionIndex; - /// @notice the stake root submissions that have been posted - StakeRootSubmission[] public stakeRootSubmissions; - + /// @notice the stake root submissions + IStakeRootCompendium.StakeRootSubmission[] public stakeRootSubmissions; + constructor( IDelegationManager _delegationManager, IAVSDirectory _avsDirectory, - uint32 _proofInterval, - uint32 _blacklistWindow + uint256 _maxTotalCharge, + uint256 _minBalanceThreshold, + uint256 _minProofsDuration, + address _verifier, + bytes32 _imageId ) { - // _disableInitializers(); delegationManager = _delegationManager; avsDirectory = _avsDirectory; - proofInterval = _proofInterval; - blacklistWindow = _blacklistWindow; + MAX_TOTAL_CHARGE = _maxTotalCharge; + MIN_BALANCE_THRESHOLD = _minBalanceThreshold; + MIN_PROOFS_DURATION = _minProofsDuration; + verifier = _verifier; + imageId = _imageId; } } diff --git a/src/contracts/interfaces/IStakeRootCompendium.sol b/src/contracts/interfaces/IStakeRootCompendium.sol index 888610dd2..643defb45 100644 --- a/src/contracts/interfaces/IStakeRootCompendium.sol +++ b/src/contracts/interfaces/IStakeRootCompendium.sol @@ -22,31 +22,24 @@ interface IStakeRootCompendium { bytes32 extraData; } - struct DepositBalanceInfo { - uint32 latestUpdateTime; - uint256 balance; + struct DepositInfo { + uint96 balance; + uint32 lastUpdatedTimestamp; + uint96 totalChargePerOperatorSetLastPaid; + uint96 totalChargePerStrategyLastPaid; } struct StakeRootSubmission { bytes32 stakeRoot; - address chargeRecipient; // the address to send the charge to - uint32 calculationTimestamp; // the timestamp the was generated against - uint32 blacklistableBefore; // the timestamp the proof submission was submitted to the contract - bool blacklisted; // whether the submission has been blacklisted by governance - bool crossPosted; - bool forcePosted; // whether the submission was posted without proof by governance + uint32 calculationTimestamp; + bool confirmed; // whether the submission was posted without proof by governance } event SnarkProofVerified(bytes journal, bytes seal); - event VerifierChanged(address oldVerifier, address newVerifier); - event ImageIdChanged(bytes32 oldImageId, bytes32 newImageId); + event VerifierSet(address newVerifier); + event ImageIdSet(bytes32 newImageId); - function MAX_OPERATOR_SET_SIZE() external view returns (uint32); - function MAX_NUM_OPERATOR_SETS() external view returns (uint32); - function MAX_NUM_STRATEGIES() external view returns (uint32); - - /// @notice the minimum balance that must be maintained for an operatorSet - function MIN_DEPOSIT_BALANCE() external view returns (uint256); + function MIN_BALANCE_THRESHOLD() external view returns (uint256); function delegationManager() external view returns (IDelegationManager); function avsDirectory() external view returns (IAVSDirectory); @@ -56,11 +49,8 @@ interface IStakeRootCompendium { /// @notice the number of operator sets in the StakeTree function getNumOperatorSets() external view returns (uint256); - /// @notice the number of stake root submissions - function getNumStakeRootSubmissions() external view returns (uint256); - /// @notice the interval at which proofs can be posted, to not overcharge the operatorSets - function proofInterval() external view returns (uint32); + function proofIntervalSeconds() external view returns (uint32); /** * @notice returns the stake root submission at the given index @@ -80,12 +70,6 @@ interface IStakeRootCompendium { view returns (uint256 delegatedStake, uint256 slashableStake); - /// @notice return the index of an operatorSet at a certain timestamp - function getOperatorSetIndexAtTimestamp( - IAVSDirectory.OperatorSet calldata operatorSet, - uint32 timestamp - ) external view returns (uint32); - /** * @notice called offchain with the operatorSet roots ordered by the operatorSet index at the timestamp to calculate the stake root * @param operatorSetsInStakeTree the operatorSets that each of the operatorSetRoots correspond to. must be the same as operatorSets storage var at the time of call @@ -115,13 +99,13 @@ interface IStakeRootCompendium { * @param operatorSetIndex the index of the operatorSet within the SRC's operatorSets list to calculate the operator set leaves for * @param startOperatorIndex the index of the first operator to get the leaves for * @param numOperators the number of operators to get the leaves for - * @return the operatorSet, the list of operators and the operatorSet leaves + * @return the operatorSet leaves */ function getOperatorSetLeaves( uint256 operatorSetIndex, uint256 startOperatorIndex, uint256 numOperators - ) external view returns (IAVSDirectory.OperatorSet memory, address[] memory, OperatorLeaf[] memory); + ) external view returns (OperatorLeaf[] memory); /** * @notice deposits funds for an operator set @@ -130,15 +114,15 @@ interface IStakeRootCompendium { * @dev the operator set must have a minimum balance of 2 * MIN_DEPOSIT_BALANCE to disallow joining with minimal cost after removal * @dev permissionless to deposit */ - function depositForOperatorSet(IAVSDirectory.OperatorSet calldata operatorSet) external payable; + function deposit(IAVSDirectory.OperatorSet calldata operatorSet) external payable; /** - * @notice called by an AVS to set their strategies and multipliers used to determine stakes for stake roots + * @notice called by an AVS to add strategies and multipliers or modify multipliers used to determine stakes for stake roots * @param operatorSetId the id of the operatorSet to set the strategies and multipliers for * @param strategiesAndMultipliers the strategies and multipliers to set for the operatorSet * @dev msg.sender is used as the AVS in determining the operatorSet */ - function addStrategiesAndMultipliers( + function addOrModifyStrategiesAndMultipliers( uint32 operatorSetId, StrategyAndMultiplier[] calldata strategiesAndMultipliers ) external; @@ -167,73 +151,58 @@ interface IStakeRootCompendium { function setOperatorExtraData(uint32 operatorSetId, address operator, bytes32 extraData) external; /** - * @notice called by watchers to update the deposit balance infos for operatorSets, usually those that have + * @notice called by watchers to update the deposit balance infos for operatorSets that have * fallen below the minimum balance, in order to remove them from the stakeTree - * @param operatorSetsToUpdate the operatorSets to update the deposit balance infos for + * @param operatorSetsToRemove the operatorSets to update the deposit balance infos for * @dev sends the caller the leftover after charging if the balance is below the minimum */ - function updateDepositBalanceInfos(IAVSDirectory.OperatorSet[] calldata operatorSetsToUpdate) external; - - /** - * @notice Process charges for the next numToCharge stakeRootSubmissions that have not been redeemed - * @param numToCharge the number of charges to process - */ - function processCharges(uint256 numToCharge) external; + function removeOperatorSetsFromStakeTree(IAVSDirectory.OperatorSet[] calldata operatorSetsToRemove) external; /** * @notice called by the claimer to claim a stake root - * @param calculationTimestamp the timestamp of the state the stakeRoot was calculated against - * @param stakeRoot the stakeRoot at calculationTimestamp - * @param chargeRecipient the address to send the charge to when processed - * @param proof todo + * @param _calculationTimestamp the timestamp of the state the stakeRoot was calculated against + * @param _stakeRoot the stakeRoot at calculationTimestamp + * @param _chargeRecipient the address to send the charge to when processed + * @param _indexChargePerProof the index of the charge per proof to use + * @param _proof todo * @dev permissionless to call */ function verifyStakeRoot( - uint32 calculationTimestamp, - bytes32 stakeRoot, - address chargeRecipient, - Proof calldata proof + uint256 _calculationTimestamp, + bytes32 _stakeRoot, + address _chargeRecipient, + uint256 _indexChargePerProof, + Proof calldata _proof ) external; - /** - * @notice called by governance to blacklist a stakeRoot in case it's incorrect - * @param submissionIndex the index of the stakeRoot submission to blacklist - * @dev called in case there's a bug in the verifier stack or program to allow an incorrect stakeRoot to be proven - * @dev only callable by governance - * @dev must be called within blacklistWindow of the stakeRoot being posted - */ - function blacklistStakeRoot(uint32 submissionIndex) external; - /** * @notice called by governance * @param calculationTimestamp the timestamp of the state the stakeRoot was calculated against * @param stakeRoot the stakeRoot at calculationTimestamp * @dev only callable by governance when a root has not been posted in forcePostWindow */ - function forcePostStakeRoot(uint32 calculationTimestamp, bytes32 stakeRoot) external; + function confirmStakeRoot(uint32 calculationTimestamp, bytes32 stakeRoot) external; /** - * @notice sets the verifier contract that will be used to verify snark proofs - * @param _verifier the address of the verifier contract - * @dev only callable by the owner + * @param numStrategies the number of strategies the operatorSet has + * @return the minimum deposit balance required for the operatorSet, which is just enough to pay for a certain number of proofs + * @dev this is enforced upon deposits and additions or modifications of strategies and multipliers */ - function setVerifier(address _verifier) external; + function minDepositBalance(uint256 numStrategies) external view returns (uint256); /** - * @notice sets/changes the id of the program being verified when roots are posted - * @param _imageId the new imageId to set - * @dev only callable by the owner + * @param operatorSet the operatorSet to check withdrawability for + * @return whether or not the operatorSet can withdraw any of their deposit balance */ - function setImageId(bytes32 _imageId) external; + function canWithdrawDepositBalance(IAVSDirectory.OperatorSet memory operatorSet) external view returns (bool); /** * @notice get the deposit balance for the operator set * @param operatorSet the operator set to get the deposit balance for * @return balance the deposit balance for the operator set - * @return penalty the penalty to be received by calling updateDepositBalanceInfos if the operator set has fallen below the minimum deposit balance */ function getDepositBalance(IAVSDirectory.OperatorSet memory operatorSet) external view - returns (uint256 balance, uint256 penalty); + returns (uint256 balance); }