Skip to content

Commit

Permalink
feat: further simplify symbiotic veto slashing logic
Browse files Browse the repository at this point in the history
  • Loading branch information
shaspitz committed Nov 7, 2024
1 parent e6155f4 commit d1f7fdd
Show file tree
Hide file tree
Showing 7 changed files with 34 additions and 150 deletions.
55 changes: 0 additions & 55 deletions contracts-abi/abi/MevCommitMiddleware.abi
Original file line number Diff line number Diff line change
Expand Up @@ -84,30 +84,6 @@
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "executeSlashes",
"inputs": [
{
"name": "blsPubkeys",
"type": "bytes[]",
"internalType": "bytes[]"
},
{
"name": "slashIndexes",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"outputs": [
{
"name": "slashedAmounts",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "getLatestSlashAmount",
Expand Down Expand Up @@ -1308,37 +1284,6 @@
],
"anonymous": false
},
{
"type": "event",
"name": "ValidatorSlashRequested",
"inputs": [
{
"name": "blsPubkey",
"type": "bytes",
"indexed": false,
"internalType": "bytes"
},
{
"name": "operator",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "vault",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "slashIndex",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "ValidatorSlashed",
Expand Down
3 changes: 0 additions & 3 deletions contracts/contracts/interfaces/IMevCommitMiddleware.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ interface IMevCommitMiddleware {
/// @notice Emmitted when a validator record is deleted by the contract owner
event ValRecordDeleted(bytes blsPubkey, address indexed msgSender);

/// @notice Emmitted when a validator slash is requested from a veto slasher
event ValidatorSlashRequested(bytes blsPubkey, address indexed operator, address indexed vault, uint256 slashIndex);

/// @notice Emmitted when a validator is slashed from an instant slasher
event ValidatorSlashed(bytes blsPubkey, address indexed operator, address indexed vault, uint256 slashedAmount);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,27 +265,6 @@ contract MevCommitMiddleware is IMevCommitMiddleware, MevCommitMiddlewareStorage
emit ValidatorPositionsSwapped(blsPubkeys, swappedVaults, swappedOperators, newPositions);
}

/// @dev Allows the slash oracle to execute slashes for validators secured by vaults with veto slashers.
/// @notice See "Slash mechanics" in README.md for more details.
/// @param blsPubkeys BLS public keys corresponding to vaults with veto slashers.
/// @param slashIndexes Slash indexes obtained from ValidatorSlashRequested event emitted in _slashValidator.
/// @return slashedAmounts The actual amount of collateral slashed for each validator.
function executeSlashes(bytes[] calldata blsPubkeys,
uint256[] calldata slashIndexes) external onlySlashOracle returns (uint256[] memory slashedAmounts) {

uint256 len = blsPubkeys.length;
require(len == slashIndexes.length, InvalidArrayLengths(len, slashIndexes.length));
slashedAmounts = new uint256[](len);
for (uint256 i = 0; i < len; ++i) {
ValidatorRecord storage valRecord = validatorRecords[blsPubkeys[i]];
address slasher = IVault(valRecord.vault).slasher();
uint64 slasherType = IEntity(slasher).TYPE();
require(slasherType == _VETO_SLASHER_TYPE, OnlyVetoSlashersRequireExecution(valRecord.vault, slasherType));
slashedAmounts[i] = IVetoSlasher(slasher).executeSlash(slashIndexes[i], new bytes(0));
}
return slashedAmounts;
}

/// @dev Pauses the contract, restricted to contract owner.
function pause() external onlyOwner { _pause(); }

Expand Down Expand Up @@ -563,15 +542,18 @@ contract MevCommitMiddleware is IMevCommitMiddleware, MevCommitMiddlewareStorage

address slasher = IVault(valRecord.vault).slasher();
uint256 slasherType = IEntity(slasher).TYPE();
uint256 slashedAmount;
if (slasherType == _VETO_SLASHER_TYPE) {
uint256 slashIndex = IVetoSlasher(slasher).requestSlash(
IVetoSlasher vetoSlasher = IVetoSlasher(slasher);
uint256 slashIndex = vetoSlasher.requestSlash(
_getSubnetwork(), valRecord.operator, amount, SafeCast.toUint48(captureTimestamp), new bytes(0));
emit ValidatorSlashRequested(blsPubkey, valRecord.operator, valRecord.vault, slashIndex);
// Since resolver = address(0), slash can be executed immediately.
slashedAmount = vetoSlasher.executeSlash(slashIndex, new bytes(0));
} else if (slasherType == _INSTANT_SLASHER_TYPE) {
uint256 slashedAmount = ISlasher(slasher).slash(
slashedAmount = ISlasher(slasher).slash(
_getSubnetwork(), valRecord.operator, amount, SafeCast.toUint48(captureTimestamp), new bytes(0));
emit ValidatorSlashed(blsPubkey, valRecord.operator, valRecord.vault, slashedAmount);
}
emit ValidatorSlashed(blsPubkey, valRecord.operator, valRecord.vault, slashedAmount);

// If validator has not already requested deregistration,
// do so to mark them as no longer opted-in.
Expand Down Expand Up @@ -634,6 +616,7 @@ contract MevCommitMiddleware is IMevCommitMiddleware, MevCommitMiddlewareStorage
} else if (delegator.TYPE() != _NETWORK_RESTAKE_DELEGATOR_TYPE) {
revert UnknownDelegatorType(vault, delegator.TYPE());
}

IVaultStorage vaultContract = IVaultStorage(vault);
uint256 vaultEpochDurationSeconds = vaultContract.epochDuration();

Expand All @@ -643,15 +626,14 @@ contract MevCommitMiddleware is IMevCommitMiddleware, MevCommitMiddlewareStorage
if (slasherType == _VETO_SLASHER_TYPE) {
IVetoSlasher vetoSlasher = IVetoSlasher(slasher);
uint256 vetoDuration = vetoSlasher.vetoDuration();
// For veto slashers, veto duration is repurposed as the phase in which the oracle can feasibly call `executeSlash`.
require(vetoDuration >= _MIN_VETO_DURATION, VetoDurationTooShort(vault, vetoDuration));
// Incorporate that veto duration will eat into portion of the epoch that oracle can feasibly request slashes.
vaultEpochDurationSeconds -= vetoDuration; /// @dev No underflow possible, vetoDuration must be less than epochDuration as enforced by VetoSlasher.sol.
require(vetoSlasher.resolver(_getSubnetwork(), new bytes(0)) == address(0),
VetoSlasherMustHaveZeroResolver(vault));
// Veto slasher must have a zero resolver s.t. slash can be executed immediately.
require(vetoSlasher.resolver(_getSubnetwork(), new bytes(0)) == address(0), VetoSlasherMustHaveZeroResolver(vault));
} else if (slasherType != _INSTANT_SLASHER_TYPE) {
revert UnknownSlasherType(vault, slasherType);
}

require(vaultEpochDurationSeconds > slashPeriodSeconds,
InvalidVaultEpochDuration(vault, vaultEpochDurationSeconds, slashPeriodSeconds));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ abstract contract MevCommitMiddlewareStorage {
/// @notice Enum TYPE for Symbiotic core VetoSlasher.
uint64 internal constant _VETO_SLASHER_TYPE = 1;

/// @notice Minimum veto duration of 60 minutes for any vault.
/// @dev This is enforced because veto duration is repurposed as the min period in which the oracle can feasibly call `executeSlash`,
/// after initially requesting a slash.
uint256 internal constant _MIN_VETO_DURATION = 1 hours;

/// @notice Symbiotic core network registry.
IRegistry public networkRegistry;

Expand Down
8 changes: 3 additions & 5 deletions contracts/contracts/validator-registry/middleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,11 @@ Vaults with veto slashers:
* must have an `epochDuration` greater than `slashPeriodSeconds` + `vetoDuration`, where `vetoDuration` is specified by the slasher.
* require the resolver to be disabled via `address(0)`, since a permissioned oracle account invokes slashing, requiring only the most basic slashing interface.

Note in the context of the mev-commit network, `vetoDuration` is repurposed as the minimum (worst case) amount of time that the oracle has to call `executeSlash` after requesting a slash. `vetoDuration` for each vault must be greater than 60 minutes as enforced by the middleware contract. This means the oracle has at most `slashPeriodSeconds` to request a slash via `slashValidators`, and at least `vetoDuration` to call `executeSlash`. Note slash execution can happen anytime after a slash is requested, so long as less than `epochDuration` has passed since `captureTimestamp`.
The oracle has at most `slashPeriodSeconds` to request a slash via `slashValidators`. Both `VetoSlasher.requestSlash` and `VetoSlasher.executeSlash` get called during `slashValidators`. Upon the oracle successfully calling `slashValidators`, the middleware contract emits a `ValidatorSlashed` event.

Upon the oracle successfully calling `slashValidators`, the middleware contract emits one of two events for each slashed validator. `ValidatorSlashed` will be emitted for slashers with `INSTANT_SLASHER_TYPE`. `ValidatorSlashRequested` will be emitted for slashers with `VETO_SLASHER_TYPE`.
Read more about Symbiotic veto slashing [here](https://docs.symbiotic.fi/core-modules/vaults#veto-slashing).

If the slasher type is `VETO_SLASHER_TYPE`, the oracle is responsible for calling `executeSlashes`, AND this call must execute on L1 prior to the oracle calling `slashValidators` again. This ensures the oracle's subsequent calls to `slashValidators` will incorporate a properly decremented slashable stake from relevant Vaults. Read more about Symbiotic veto slashing [here](https://docs.symbiotic.fi/core-modules/vaults#veto-slashing).

The described process considers Symbiotic's current design, where:
The described process considers Symbiotic's current design, where it's possible to execute a slash right after requesting one, if the resolver is disabled. See source below:

1. `requestSlash` must be called before `epochDuration` - `vetoDuration` (equal to `slashPeriodSeconds` for our network) has passed since `captureTimestamp`, [source](https://github.com/symbioticfi/core/blob/629b9faac2377a9eb9cfdc6362b30d1dc1ef48f2/src/contracts/slasher/VetoSlasher.sol#L93).
2. `executeSlash` must be called before `epochDuration` has passed since `captureTimestamp`, [source](https://github.com/symbioticfi/core/blob/629b9faac2377a9eb9cfdc6362b30d1dc1ef48f2/src/contracts/slasher/VetoSlasher.sol#L152).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ contract MevCommitMiddlewareTest is Test {
event VaultSlashAmountUpdated(address indexed vault, uint160 slashAmount);
event VaultDeregistrationRequested(address indexed vault);
event VaultDeregistered(address indexed vault);
event ValidatorSlashRequested(bytes blsPubkey, address indexed operator, address indexed vault, uint256 slashIndex);
event ValidatorSlashed(bytes blsPubkey, address indexed operator, address indexed vault, uint256 slashedAmount);
event NetworkRegistrySet(address networkRegistry);
event OperatorRegistrySet(address operatorRegistry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,11 @@ contract MevCommitMiddlewareTestCont is MevCommitMiddlewareTest {
assertTrue(mevCommitMiddleware.isValidatorOptedIn(sampleValPubkey4));
assertTrue(mevCommitMiddleware.isValidatorOptedIn(sampleValPubkey5));

// get stake amount before slashing
MockDelegator delegator = MockDelegator(vault2.delegator());
uint256 allocatedStake = delegator.stake(bytes32("subnet"), operator1);
assertEq(allocatedStake, 99);

assertTrue(mevCommitMiddleware.isValidatorSlashable(sampleValPubkey4));
assertTrue(mevCommitMiddleware.isValidatorSlashable(sampleValPubkey5));

Expand All @@ -1016,11 +1021,15 @@ contract MevCommitMiddlewareTestCont is MevCommitMiddlewareTest {
assertEq(slashRecord.numRegistered, 0);
assertEq(slashRecord.numSlashed, 0);

assertEq(mevCommitMiddleware.potentialSlashableValidators(address(vault2), operator1), 1);

uint256 slashAmount = mevCommitMiddleware.getLatestSlashAmount(address(vault2));

vm.prank(slashOracle);
vm.expectEmit(true, true, true, true);
emit ValidatorSlashRequested(sampleValPubkey4, operator1, address(vault2), 0);
emit ValidatorSlashed(sampleValPubkey4, operator1, address(vault2), slashAmount);
vm.expectEmit(true, true, true, true);
emit ValidatorSlashRequested(sampleValPubkey5, operator1, address(vault2), 1);
emit ValidatorSlashed(sampleValPubkey5, operator1, address(vault2), slashAmount);
vm.expectEmit(true, true, true, true);
address[] memory expectedVaults = new address[](2);
expectedVaults[0] = address(vault2);
Expand All @@ -1044,11 +1053,15 @@ contract MevCommitMiddlewareTestCont is MevCommitMiddlewareTest {
assertFalse(mevCommitMiddleware.isValidatorOptedIn(sampleValPubkey4));
assertFalse(mevCommitMiddleware.isValidatorOptedIn(sampleValPubkey5));

// Validators should still be slashable, since veto slasher doesn't decrement stake immediately.
assertTrue(mevCommitMiddleware.isValidatorSlashable(sampleValPubkey4));
// total stake should be 59
allocatedStake = delegator.stake(bytes32("subnet"), operator1);
assertEq(allocatedStake, 59);

// validator 4 should not be slashable, validator 5 should be since it's lower index
assertFalse(mevCommitMiddleware.isValidatorSlashable(sampleValPubkey4));
assertTrue(mevCommitMiddleware.isValidatorSlashable(sampleValPubkey5));

assertEq(mevCommitMiddleware.potentialSlashableValidators(address(vault2), operator1), 1);
assertEq(mevCommitMiddleware.potentialSlashableValidators(address(vault2), operator1), 0);

pos1 = mevCommitMiddleware.getPositionInValset(sampleValPubkey4, address(vault2), operator1);
assertEq(pos1, 3); // final position of second set
Expand All @@ -1070,52 +1083,7 @@ contract MevCommitMiddlewareTestCont is MevCommitMiddlewareTest {
assertEq(slashRecord.numRegistered, 3);
assertEq(slashRecord.numSlashed, 2);

MockDelegator delegator = MockDelegator(vault2.delegator());
uint256 allocatedStake = delegator.stake(bytes32("subnet"), operator1);
assertEq(allocatedStake, 99);

// Before slash execution, vault is colateralized in excess.
assertEq(mevCommitMiddleware.getNumSlashableVals(address(vault2), operator1), 4);
assertEq(mevCommitMiddleware.valsetLength(address(vault2), operator1), 3);

vm.roll(block.number + 20);

bytes[] memory blsPubkeys = new bytes[](2);
blsPubkeys[0] = sampleValPubkey1;
blsPubkeys[1] = sampleValPubkey2;
uint256[] memory slashIndexes = new uint256[](2);
slashIndexes[0] = 3;
slashIndexes[1] = 4;

vm.prank(slashOracle);
uint64 instantSlasherType = 0;
vm.expectRevert(
abi.encodeWithSelector(IMevCommitMiddleware.OnlyVetoSlashersRequireExecution.selector, address(vault1), instantSlasherType)
);
mevCommitMiddleware.executeSlashes(blsPubkeys, slashIndexes);

blsPubkeys[0] = sampleValPubkey4;
blsPubkeys[1] = sampleValPubkey5;
slashIndexes[0] = 0;
slashIndexes[1] = 1;

vm.expectRevert(
abi.encodeWithSelector(IMevCommitMiddleware.OnlySlashOracle.selector, slashOracle)
);
vm.prank(vm.addr(0x1119));
mevCommitMiddleware.executeSlashes(blsPubkeys, slashIndexes);

vm.expectEmit(true, true, true, true);
emit ExecuteSlash(0, 20);
vm.expectEmit(true, true, true, true);
emit ExecuteSlash(1, 20);
vm.prank(slashOracle);
mevCommitMiddleware.executeSlashes(blsPubkeys, slashIndexes);

allocatedStake = delegator.stake(bytes32("subnet"), operator1);
assertEq(allocatedStake, 59);

assertEq(mevCommitMiddleware.getNumSlashableVals(address(vault2), operator1), 2);
assertEq(mevCommitMiddleware.getNumSlashableVals(address(vault2), operator1), 2); // TODO: revisit
assertEq(mevCommitMiddleware.valsetLength(address(vault2), operator1), 3);

assertEq(mevCommitMiddleware.getPositionInValset(sampleValPubkey6, address(vault2), operator1), 1);
Expand All @@ -1134,7 +1102,7 @@ contract MevCommitMiddlewareTestCont is MevCommitMiddlewareTest {
timestamps[0] = 203;
vm.prank(slashOracle);
vm.expectEmit(true, true, true, true);
emit ValidatorSlashRequested(sampleValPubkey6, operator1, address(vault2), 2);
emit ValidatorSlashed(sampleValPubkey6, operator1, address(vault2), slashAmount);
vm.expectEmit(true, true, true, true);
expectedVaults = new address[](1);
expectedVaults[0] = address(vault2);
Expand Down

0 comments on commit d1f7fdd

Please sign in to comment.