-
Notifications
You must be signed in to change notification settings - Fork 327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
stake root #723
base: slashing-magnitudes
Are you sure you want to change the base?
stake root #723
Conversation
33c32ac
to
835a62f
Compare
7023144
to
92252da
Compare
0e122f9
to
b871886
Compare
4187c46
to
39b9383
Compare
* | ||
* @return the list of total magnitudes for each strategy and the list of allocated magnitudes for each strategy | ||
*/ | ||
function getTotalAndAllocatedMagnitudes( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fn is a bit messy, would suggest changing to:
function getTotalAndAllocatedMagnitudes(
address operator,
OperatorSet calldata operatorSet,
IStrategy[] calldata strategies
) external view returns (uint64[] memory totalMagnitude, uint64[] memory allocatedMagnitude) {
totalMagnitude = new uint64[](strategies.length);
allocatedMagnitude = new uint64[](strategies.length);
for (uint256 i = 0; i < strategies.length; ++i) {
(totalMagnitude[i], allocatedMagnitude[i]) = _getTotalAndAllocatedMagnitude(operator, operatorSet, strategies[i]);
}
}
@@ -873,6 +873,18 @@ contract DelegationManager is | |||
return SlashingLib.descaleShares(operatorScaledShares[operator][strategy], totalMagnitude); | |||
} | |||
|
|||
/// @notice Given array of strategies, returns array of scaled shares for the operator | |||
function getOperatorScaledShares( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above:
function getOperatorScaledShares(
address operator,
IStrategy[] memory strategies
) external view returns (uint256[] memory scaledShares) {
scaledShares = new uint256[](strategies.length);
for (uint256 i = 0; i < strategies.length; ++i) {
scaledShares[i] = operatorScaledShares[operator][strategies[i]];
}
}
uint96 cumulativeChargePerStrategyLastPaid; | ||
} | ||
|
||
struct StakeRootSubmission { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Natspec
@@ -53,7 +51,7 @@ library Merkle { | |||
bytes32 leaf, | |||
uint256 index | |||
) internal pure returns (bytes32) { | |||
require(proof.length % 32 == 0, ProofLengthNotMultipleOf32()); | |||
require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, will patch
@@ -109,7 +107,10 @@ library Merkle { | |||
bytes32 leaf, | |||
uint256 index | |||
) internal view returns (bytes32) { | |||
require(proof.length != 0 && proof.length % 32 == 0, ProofLengthNotMultipleOf32()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
src/contracts/libraries/Merkle.sol
Outdated
* @return The computed Merkle root of the tree. | ||
* @dev This pads to the next power of 2. very inefficient! just for POC | ||
*/ | ||
function merkleizeKeccak256(bytes32[] memory leaves) internal pure returns (bytes32) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bitshifts are a bit cheaper than multiplying or dividing by 2 at the cost of readability.
100 >> 1
└ Decimal: 50
100 << 1
└ Decimal: 200
/// @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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reorder variables such that they pack into less slots.
Variable naming could use some work, like |
custom errors missing |
"StakeRootCompendium.getStakeRoot: operatorSetsInStakeTree vs. operatorSetRoots mismatch" | ||
); | ||
for (uint256 i = 0; i < operatorSetsInStakeTree.length; i++) { | ||
require( | ||
operatorSets[i].avs == operatorSetsInStakeTree[i].avs, | ||
"StakeRootCompendium.getStakeRoot: operatorSets vs. operatorSetsInStakeTree avs mismatch" | ||
); | ||
require( | ||
operatorSets[i].operatorSetId == operatorSetsInStakeTree[i].operatorSetId, | ||
"StakeRootCompendium.getStakeRoot: operatorSets vs. operatorSetsInStakeTree operatorSetId mismatch" | ||
); | ||
} | ||
return Merkle.merkleizeKeccak256(operatorSetRoots); | ||
} | ||
|
||
/// @inheritdoc IStakeRootCompendium | ||
function getOperatorSetLeaves( | ||
uint256 operatorSetIndex, | ||
uint256 startOperatorIndex, | ||
uint256 numOperators | ||
) external view returns (OperatorSet memory, address[] memory, OperatorLeaf[] memory) { | ||
require( | ||
operatorSetIndex < operatorSets.length, | ||
"StakeRootCompendium.getOperatorSetLeaves: operator set index out of bounds" | ||
); | ||
OperatorSet memory operatorSet = operatorSets[operatorSetIndex]; | ||
address[] memory operators = avsDirectory.getOperatorsInOperatorSet(operatorSet, startOperatorIndex, numOperators); | ||
(IStrategy[] memory strategies, uint256[] memory multipliers) = _getStrategiesAndMultipliers(operatorSet); | ||
|
||
OperatorLeaf[] memory operatorLeaves = new OperatorLeaf[](operators.length); | ||
for (uint256 i = 0; i < operatorLeaves.length; i++) { | ||
// calculate the weighted sum of the operator's shares for the strategies given the multipliers | ||
(uint256 delegatedStake, uint256 slashableStake) = _getStakes(operatorSet, strategies, multipliers, operators[i]); | ||
|
||
operatorLeaves[i] = OperatorLeaf({ | ||
delegatedStake: delegatedStake, | ||
slashableStake: slashableStake, | ||
extraData: operatorExtraDatas[operatorSet.avs][operatorSet.operatorSetId][operators[i]] | ||
}); | ||
} | ||
return (operatorSet, operators, operatorLeaves); | ||
} | ||
|
||
/// @inheritdoc IStakeRootCompendium | ||
function getOperatorSetRoot( | ||
OperatorSet calldata operatorSet, | ||
OperatorLeaf[] calldata operatorLeaves | ||
) external view returns (bytes32) { | ||
require( | ||
avsDirectory.isOperatorSet(operatorSet.avs, operatorSet.operatorSetId), | ||
"StakeRootCompendium.getOperatorSetRoot: operator set does not exist" | ||
); | ||
require( | ||
operatorLeaves.length == avsDirectory.getNumOperatorsInOperatorSet(operatorSet), | ||
"AVSSyncTree.getOperatorSetRoot: operator set size mismatch" | ||
); | ||
|
||
uint256 totalDelegatedStake; | ||
uint256 totalSlashableStake; | ||
bytes32 prevExtraData; | ||
bytes32[] memory operatorLeavesHashes = new bytes32[](operatorLeaves.length); | ||
for (uint256 i = 0; i < operatorLeaves.length; i++) { | ||
require(uint256(prevExtraData) < uint256(operatorLeaves[i].extraData), "StakeRootCompendium.getOperatorSetRoot: extraData not sorted"); | ||
prevExtraData = operatorLeaves[i].extraData; | ||
|
||
operatorLeavesHashes[i] = keccak256( | ||
abi.encodePacked( | ||
operatorLeaves[i].delegatedStake, | ||
operatorLeaves[i].slashableStake, | ||
operatorLeaves[i].extraData | ||
) | ||
); | ||
|
||
totalDelegatedStake += operatorLeaves[i].delegatedStake; | ||
totalSlashableStake += operatorLeaves[i].slashableStake; | ||
} | ||
|
||
bytes32 operatorTreeRoot = Merkle.merkleizeKeccak256(operatorLeavesHashes); | ||
return keccak256( | ||
abi.encodePacked( | ||
operatorTreeRoot, | ||
keccak256( | ||
abi.encodePacked( | ||
totalDelegatedStake, | ||
totalSlashableStake, | ||
operatorSetExtraDatas[operatorSet.avs][operatorSet.operatorSetId] | ||
) | ||
) | ||
) | ||
); | ||
} | ||
|
||
function _safeTransferETH(address to, uint256 amount) internal { | ||
(bool success,) = to.call{value: amount}(""); | ||
require(success, EthTransferFailed()); | ||
} | ||
|
||
// in case of charge problems | ||
receive() external payable {} | ||
} |
Check warning
Code scanning / Slither
Contracts that lock Ether Medium
Contract StakeRootCompendium has payable functions:
- IStakeRootCompendium.deposit(OperatorSet)
- IStakeRootCompendium.withdraw(uint32,uint256)
- StakeRootCompendium.deposit(OperatorSet)
- StakeRootCompendium.withdraw(uint32,uint256)
- StakeRootCompendium.receive()
But does not have a function to withdraw the ether
function deposit( | ||
OperatorSet calldata operatorSet | ||
) external payable { | ||
DepositInfo storage depositInfo = depositInfos[operatorSet.avs][operatorSet.operatorSetId]; | ||
if (!_isInStakeTree(operatorSet)) { | ||
(, uint256 cumulativeChargePerOperatorSet, uint256 cumulativeChargePerStrategy) = | ||
_calculateCumulativeCharges(); | ||
|
||
depositInfo.lastDemandIncreaseTimestamp = uint32(block.timestamp); | ||
depositInfo.cumulativeChargePerOperatorSetLastPaid = uint96(cumulativeChargePerOperatorSet); | ||
depositInfo.cumulativeChargePerStrategyLastPaid = uint96(cumulativeChargePerStrategy); | ||
|
||
_updateTotalStrategies( | ||
0, operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length() | ||
); | ||
|
||
operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].push( | ||
uint32(block.timestamp), uint224(operatorSets.length) | ||
); | ||
|
||
operatorSets.push(operatorSet); | ||
} | ||
|
||
depositInfo.balance += uint96(msg.value); | ||
// note that they've shown increased demand for proofs | ||
depositInfo.lastDemandIncreaseTimestamp = uint32(block.timestamp); | ||
// make sure they have enough to pay for MIN_PROOFS_PREPAID | ||
require( | ||
depositInfo.balance | ||
>= minDepositBalance( | ||
operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length() | ||
), | ||
InsufficientDepositBalance() | ||
); | ||
} |
Check warning
Code scanning / Slither
Unused return Medium
uint256 public immutable MIN_PREPAID_PROOFS; | ||
|
||
/// @notice the total number of strategies among all operator sets (with duplicates) | ||
uint256 public totalStrategies; |
Check warning
Code scanning / Slither
State variables that could be declared constant Warning
/// @dev Contains info about cumulative charges. | ||
CumulativeChargeParams public cumulativeChargeParams; | ||
/// @notice deposit balance to be deducted for operatorSets | ||
mapping(address => mapping(uint32 => DepositInfo)) public depositInfos; |
Check failure
Code scanning / Slither
Uninitialized state variables High
- StakeRootCompendium.deposit(OperatorSet)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding testnet griefing, we may also be able to relax the safety assumption over liveness and have AVSs use the operatorSetRoot
.... though the custom ERC-20 is a better idea IMO
Will comment on blacklisting in a separate review
function _isInStakeTree( | ||
OperatorSet memory operatorSet | ||
) internal view returns (bool) { | ||
(bool exists,, uint224 index) = operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].latestSnapshot(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're encoding operatorSets into bytes32 everywhere (AM, AVSD), let's do that here. We should have a library with internal encode/decode function that the above two contract, and this, should inherit from. A bit cleaner on the storage side as well :)
// 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 | ||
verifier = _verifier; | ||
imageId = _imageId; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storage gap??
operatorSetToStrategyAndMultipliers[operatorSet.avs][operatorSet.operatorSetId].length() | ||
), | ||
InsufficientDepositBalance() | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to react to demand increases, we need to run metrics on this, and thus need events that tell us:
- How much is being deposited
- Timestamp at which deposit, or demand increase is occurring
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're in charge of events brother
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @bowenli86
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's plan for an initial event audit next week cc @shotaronowhere
require(strategiesAndMultipliers.remove(address(strategies[i])), NonexistentStrategy()); | ||
} | ||
|
||
_updateTotalStrategies(numStrategiesBefore, numStrategiesBefore - strategies.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Demand has reduced, we'd probably want an event here too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not keep track of lastDemandDecreaseTimestamp
? Sounds relevant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only care about demand increases in storage since withdrawability is pushed further out in the future by increases in demand so offchain prover sotware doesn't scale to meet "demand" and then get rugged
/// CHARGE MANAGEMENT | ||
|
||
/// @inheritdoc IStakeRootCompendium | ||
function removeOperatorSetsFromStakeTree( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: it's not clear that this is a keeper method that is "penalizing" AVSs. First read sounds like it enables AVSs to remove their own operator sets from the tree (even if they're not in the red)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's permissionless!
It's intended for people to remove AVSs from the stakeTree that won't have the ability to pay soon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, but naming should be better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohhhh, i misinterpreted your statement. yes, this could be better. maybe pruneLowBalanceOperatorSets
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or even if the operatorSet
passed in to call data is in fact the last operatorSet in the opeartorSets
array (ie. equal to substituteOperatorSet
. There doesn't necessarily have to be one operator set left.
Why can't we just simplify this logic to if operatorSet == substituteOperatorSet
then you only need to push REMOVED_INDEX
and pop... else do previous in addition to handling substitute
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
didn't even think about this case. agree it should be handled gracefully
/// @dev Contains cumulative charges for operator sets, strategies, and max total charge. | ||
ChargeParams public chargeParams; | ||
/// @dev Contains info about cumulative charges. | ||
CumulativeChargeParams public cumulativeChargeParams; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proofIntervalSeconds
in CumulativeChargeParams
should be an immutable parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nvm didn't see setProofIntervalSeconds
earlier. However, I don't necessarily see why it needs to be packed into cumulativeChargeparams
if it is mutated in a separate function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this was a gas efficiency/storage packing choice by @shotaronowhere and @0xClandestine
DepositInfo storage depositInfo = depositInfos[operatorSet.avs][operatorSet.operatorSetId]; | ||
|
||
// subtract new total charge from last paid total charge | ||
uint256 pendingCharge = cumulativeChargePerOperatorSet - depositInfo.cumulativeChargePerOperatorSetLastPaid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this logic is repeated here and in getDepositBalance
) public view returns (bool) { | ||
// they must have paid for all of their prepaid proofs before withdrawing after a demand increase | ||
return block.timestamp | ||
> depositInfos[operatorSet.avs][operatorSet.operatorSetId].lastDemandIncreaseTimestamp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if demand decreases (ie. remove strategies/multipliers)? Shouldn't the AVS be on the hook for the smaller set of strategies/multipliers instead of all of them when initially set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, the withdrawability requirement is primarily to impose a positive cost for increasing your demand for compute.
otherwise you could make provers scale up a bunch offchain and then withdraw. you could also sandwitch a calculationTimestamp with and increase and decrease and withdraw right after
|
||
// debt any pending charge | ||
uint256 balance = _updateDepositInfo(operatorSet); | ||
if (balance < MIN_BALANCE_THRESHOLD + amount) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO we should have this function specifically prevent you from withdrawing amount
if you go less than MIN_BALANCE_THRESHOLD
- high potential for footguns by AVSs. Would be better to have a separate, voluntary, removeFromStakeTreeAndWithdraw
func
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great point. agree
988c440
to
6d7510e
Compare
* chore: forge fmt src/contracts * nit: organize imports * fix: bump pragma -> ^0.8.27 * refactor(optimization): significantly reduce sloads/sstores * refactor: custom errors * refactor(optimization): significantly reduce sloads/sstores missed some stuff * refactor: rename compendium -> manager * refactor: more storage optmizations * feat: safe eth transfer helper * refactor: manager -> compendium * refactor: variable renaming * refactor: rename compendium -> manager * feat: add `proofIntervalSeconds` getter * feat: fixed accounting bug and refactoring (#767) * refactor: review reconciliations * refactor: review reconciliations * fix: rename colluding param * fix: types and naming * fix: revert stakeroot calculation changes * fix: revert stakeRoot calc views --------- Co-authored-by: shotaro <[email protected]>
6d7510e
to
0be4168
Compare
The StakeRootCompendium
core
getOperatorSetLeaves
for >=150 operatorsmisc
notes for the initial review
We mainly want feedback on