Skip to content

Commit

Permalink
Merge pull request #8 from DLC-link/feat/multisig-validation-for-slash
Browse files Browse the repository at this point in the history
Multisig validation for slashing
  • Loading branch information
scolear authored Dec 16, 2024
2 parents 328512b + 982ad93 commit 0367f82
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 39 deletions.
31 changes: 25 additions & 6 deletions src/iBTC_NetworkMiddleware.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol";
import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol";
import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol";

import {MultisigValidated} from "./libraries/MultisigValidated.sol";
import {SimpleKeyRegistry32} from "./libraries/SimpleKeyRegistry32.sol";
import {MapWithTimeData} from "./libraries/MapWithTimeData.sol";

contract NetworkMiddleware is SimpleKeyRegistry32, Ownable {
contract NetworkMiddleware is SimpleKeyRegistry32, Ownable, MultisigValidated {
using EnumerableMap for EnumerableMap.AddressToUintMap;
using MapWithTimeData for EnumerableMap.AddressToUintMap;
using Subnetwork for address;
Expand Down Expand Up @@ -52,6 +53,13 @@ contract NetworkMiddleware is SimpleKeyRegistry32, Ownable {
bytes32 key;
}

struct SlashedInfo {
uint48 epoch;
address operator;
uint256 slashedAmount;
uint256 timeStamp;
}

address public immutable NETWORK;
address public immutable OPERATOR_REGISTRY;
address public immutable NETWORK_REGISTRY;
Expand All @@ -66,7 +74,9 @@ contract NetworkMiddleware is SimpleKeyRegistry32, Ownable {
uint48 private constant INSTANT_SLASHER_TYPE = 0;
uint48 private constant VETO_SLASHER_TYPE = 1;

uint256 public slashIndex;
uint256 public subnetworksCnt;
mapping(uint256 slashIndex => SlashedInfo) slashedInfos;
mapping(uint48 => uint256) public totalStakeCache;
mapping(uint48 => bool) public totalStakeCached;
mapping(uint48 epoch => mapping(address operator => uint256 amounts)) public operatorStakeCache;
Expand All @@ -90,8 +100,10 @@ contract NetworkMiddleware is SimpleKeyRegistry32, Ownable {
address _operatorNetOptin,
address _owner,
uint48 _epochDuration,
uint48 _slashingWindow
) SimpleKeyRegistry32() Ownable(_owner) {
uint48 _slashingWindow,
uint16 _threshold,
uint16 _minimumThreshold
) SimpleKeyRegistry32() MultisigValidated(_owner, _minimumThreshold, _threshold) {
if (_slashingWindow < _epochDuration) {
revert SlashingWindowTooShort();
}
Expand Down Expand Up @@ -334,7 +346,12 @@ contract NetworkMiddleware is SimpleKeyRegistry32, Ownable {
// process payload
}

function slash(uint48 epoch, address operator, uint256 amount) public onlyOwner updateStakeCache(epoch) {
function slash(
uint48 epoch,
address operator,
uint256 amount,
bytes[] calldata signatures
) public onlyMultisig(abi.encode(slashIndex, epoch, operator, amount), signatures) updateStakeCache(epoch) {
uint48 epochStartTs = getEpochStartTs(epoch);

if (epochStartTs < Time.timestamp() - SLASHING_WINDOW) {
Expand All @@ -360,12 +377,14 @@ contract NetworkMiddleware is SimpleKeyRegistry32, Ownable {
uint256 vaultStake =
IBaseDelegator(IVault(vault).delegator()).stakeAt(subnetwork, operator, epochStartTs, new bytes(0));
_slashVault(epochStartTs, vault, subnetwork, operator, (amount * vaultStake) / totalOperatorStake);
slashedInfos[slashIndex++] =
SlashedInfo({epoch: epoch, operator: operator, slashedAmount: amount, timeStamp: block.timestamp});
}
}
}

function executeSlash(
uint256 slashIndex,
uint256 slashIndex_,
address vault,
bytes calldata hints
) public onlyOwner updateStakeCache(getCurrentEpoch()) {
Expand All @@ -374,7 +393,7 @@ contract NetworkMiddleware is SimpleKeyRegistry32, Ownable {
if (slasherType != VETO_SLASHER_TYPE) {
revert NotVetoSlasher();
}
IVetoSlasher(slasher).executeSlash(slashIndex, hints);
IVetoSlasher(slasher).executeSlash(slashIndex_, hints);
}

function calcAndCacheStakes(
Expand Down
103 changes: 103 additions & 0 deletions src/libraries/MultisigValidated.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

contract MultisigValidated is Ownable, AccessControl {
bytes32 public constant APPROVED_SIGNER = keccak256("APPROVED_SIGNER");

uint16 private _minimumThreshold;
uint16 private _threshold;
uint16 private _signerCount;

// Track seen signers to prevent duplicate signatures
mapping(address => mapping(bytes32 => bool)) private _seenSigners;

error NotEnoughSignatures();
error InvalidSigner();
error DuplicateSignature();
error DuplicateSigner(address signer);
error SignerNotApproved(address signer);
error ThresholdTooLow(uint16 _minimumThreshold);
error ThresholdMinimumReached(uint16 _minimumThreshold);

event SetThreshold(uint16 newThreshold);

constructor(address initialOwner, uint16 minimumThreshold, uint16 threshold) Ownable(initialOwner) {
_grantRole(DEFAULT_ADMIN_ROLE, initialOwner);

require(minimumThreshold > 0, "Minimum threshold must be greater than 0");
require(threshold >= minimumThreshold, "Threshold must be >= minimum threshold");

_minimumThreshold = minimumThreshold;
_threshold = threshold;
_signerCount = 0;
}

modifier onlyMultisig(bytes memory message, bytes[] memory signatures) {
_validateSignatures(message, signatures);
_;
}

function _validateSignatures(bytes memory message, bytes[] memory signatures) internal {
if (signatures.length < _threshold) revert NotEnoughSignatures();

bytes32 prefixedMessageHash = MessageHashUtils.toEthSignedMessageHash(keccak256(message));

for (uint256 i = 0; i < signatures.length; i++) {
address signerPubkey = ECDSA.recover(prefixedMessageHash, signatures[i]);
if (!hasRole(APPROVED_SIGNER, signerPubkey)) {
revert InvalidSigner();
}
_checkSignerUnique(signerPubkey, prefixedMessageHash);
}
}

function _checkSignerUnique(address signerPubkey, bytes32 messageHash) internal {
if (_seenSigners[signerPubkey][messageHash]) {
revert DuplicateSigner(signerPubkey);
}
_seenSigners[signerPubkey][messageHash] = true;
}

function setThreshold(
uint16 newThreshold
) external onlyOwner {
if (newThreshold < _minimumThreshold) {
revert ThresholdTooLow(_minimumThreshold);
}
_threshold = newThreshold;
emit SetThreshold(newThreshold);
}

function getThreshold() external view returns (uint16) {
return _threshold;
}

function getMinimumThreshold() external view returns (uint16) {
return _minimumThreshold;
}

function getSignerCount() external view returns (uint16) {
return _signerCount;
}

// Role management overrides
function grantRole(bytes32 role, address account) public override {
super.grantRole(role, account);
if (role == APPROVED_SIGNER) _signerCount++;
}

function revokeRole(bytes32 role, address account) public override {
super.revokeRole(role, account);
if (role == APPROVED_SIGNER) {
if (_signerCount == _minimumThreshold) {
revert ThresholdMinimumReached(_minimumThreshold);
}
_signerCount--;
}
}
}
100 changes: 67 additions & 33 deletions test/iBTC_NetworkMiddleware.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ contract iBTC_NetworkMiddlewareTest is Test {

uint256 sepoliaFork;
string SEPOLIA_RPC_URL = vm.envString("SEPOLIA_RPC_URL");

address constant NETWORK_MIDDLEWARE_SERVICE = 0x62a1ddfD86b4c1636759d9286D3A0EC722D086e3;
address constant NETWORK_REGISTRY = 0x7d03b7343BF8d5cEC7C0C27ecE084a20113D15C9;
address constant OPERATOR_REGISTRY = 0x6F75a4ffF97326A00e52662d82EA4FdE86a2C548;
Expand All @@ -56,6 +57,7 @@ contract iBTC_NetworkMiddlewareTest is Test {
uint256 constant MAX_WITHDRAW_AMOUNT = 1e9;
uint256 constant MIN_WITHDRAW_AMOUNT = 1e4;

bytes32 public constant APPROVED_SIGNER = keccak256("APPROVED_SIGNER");
bytes32 public constant NETWORK_LIMIT_SET_ROLE = keccak256("NETWORK_LIMIT_SET_ROLE");
bytes32 public constant OPERATOR_NETWORK_SHARES_SET_ROLE = keccak256("OPERATOR_NETWORK_SHARES_SET_ROLE");

Expand Down Expand Up @@ -86,6 +88,11 @@ contract iBTC_NetworkMiddlewareTest is Test {
address bob;
uint256 bobPrivateKey;

address approvedSigner1;
uint256 approvedSigner1Key;
address approvedSigner2;
uint256 approvedSigner2Key;

OptInService network_optIn_service;
OptInService vault_optIn_service;
NetworkMiddleware public iBTC_networkMiddleware;
Expand All @@ -109,6 +116,8 @@ contract iBTC_NetworkMiddlewareTest is Test {
sepoliaFork = vm.createSelectFork(SEPOLIA_RPC_URL);
(alice, alicePrivateKey) = makeAddrAndKey("alice");
(bob, bobPrivateKey) = makeAddrAndKey("bob");
(approvedSigner1, approvedSigner1Key) = makeAddrAndKey("approvedSigner1");
(approvedSigner2, approvedSigner2Key) = makeAddrAndKey("approvedSigner2");
networkRegistry = NetworkRegistry(NETWORK_REGISTRY);
networkMiddlewareService = NetworkMiddlewareService(NETWORK_MIDDLEWARE_SERVICE);
operatorRegistry = OperatorRegistry(OPERATOR_REGISTRY);
Expand All @@ -122,6 +131,8 @@ contract iBTC_NetworkMiddlewareTest is Test {
uint64 delegatorIndex = 0;
uint64 slasherIndex = 1;
bool withSlasher = true;
uint16 threshold = 2; // for test case
uint16 minimumThreshold = 2;
vm.startPrank(OWNER);

vaultConfigurator = new VaultConfigurator(VAULT_FACTORY, DELEGATOR_FACTORY, SLASHER_FACTORY);
Expand Down Expand Up @@ -226,7 +237,9 @@ contract iBTC_NetworkMiddlewareTest is Test {
NEWTORK_OPTIN_SERVICE,
OWNER,
NETWORK_EPOCH,
SLASHING_WINDOW
SLASHING_WINDOW,
threshold,
minimumThreshold
);

vm.stopPrank();
Expand Down Expand Up @@ -319,12 +332,12 @@ contract iBTC_NetworkMiddlewareTest is Test {

function testSlashOperator() public {
bytes32 key = keccak256(abi.encodePacked("alice_key"));

uint256 depositAmount = 1e10;
uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp;
blockTimestamp = blockTimestamp + 1_720_700_948;
uint256 networkLimit = 1e10;
uint256 operatorNetworkShares1 = 1e10;

vm.prank(OWNER);
iBTC_networkMiddleware.registerVault(address(iBTC_vault));

Expand All @@ -342,15 +355,13 @@ contract iBTC_NetworkMiddlewareTest is Test {

assertEq(iBTC_delegator.stake(NETWORK.subnetwork(0), alice), 0);

uint256 slashIndex = 0;
_setResolver(0, bob);

assertEq(iBTC_slasher.resolver(NETWORK.subnetwork(0), ""), bob, "resolver should be setting correctly");

vm.prank(OWNER);
iBTC_networkMiddleware.registerOperator(alice, key);
_deposit(alice, depositAmount);
// _withdraw(alice, withdrawAmount);
assertEq(
depositAmount, iBTC_vault.activeBalanceOfAt(alice, uint48(block.timestamp), ""), "Deposit should be done"
);
Expand Down Expand Up @@ -383,8 +394,10 @@ contract iBTC_NetworkMiddlewareTest is Test {

uint256 cachedStake = iBTC_networkMiddleware.calcAndCacheStakes(epoch);
assertEq(cachedStake, operatorNetworkShares1, "cache should update");

uint256 slashAmount = 1e9;
vm.warp(Time.timestamp() + 1 days);

uint48 epochStartTs = iBTC_networkMiddleware.getEpochStartTs(epoch);
assertGe(
epochStartTs,
Expand All @@ -393,52 +406,73 @@ contract iBTC_NetworkMiddlewareTest is Test {
);
assertLt(epochStartTs, Time.timestamp(), "captureTimestamp needs less than Time.timestamp();");

// test slash using VetoSlasher
// **generate Signature**
vm.startPrank(OWNER);
iBTC_networkMiddleware.grantRole(APPROVED_SIGNER, approvedSigner1);
assertTrue(iBTC_networkMiddleware.hasRole(APPROVED_SIGNER, approvedSigner1));
iBTC_networkMiddleware.grantRole(APPROVED_SIGNER, approvedSigner2);
assertTrue(iBTC_networkMiddleware.hasRole(APPROVED_SIGNER, approvedSigner2));

vm.stopPrank();
uint256[] memory approvedSignerKeys = new uint256[](2);
approvedSignerKeys[0] = approvedSigner1Key;
approvedSignerKeys[1] = approvedSigner2Key;
uint256 slashIndex = 0;

bytes[] memory signatures = _makeSignatures(slashIndex, epoch, alice, slashAmount, approvedSignerKeys);
// **call slash*
vm.startPrank(OWNER);
iBTC_networkMiddleware.slash(epoch, alice, slashAmount);
// it's stiil veto duration
iBTC_networkMiddleware.slash(epoch, alice, slashAmount, signatures);

// **excute slash**
vm.expectRevert();
iBTC_networkMiddleware.executeSlash(0, address(iBTC_vault), "");
vm.warp(Time.timestamp() + 2 days);
iBTC_networkMiddleware.executeSlash(0, address(iBTC_vault), "");
vm.stopPrank();

uint256 amountAfterSlashed = iBTC_vault.activeBalanceOf(alice);
assertEq(amountAfterSlashed, depositAmount - slashAmount, "Cached stake should be reduced by slash amount");

// vm.expectEmit(true, true, false, false);
// emit VetoSlash(slashIndex, resolver);
// (
// bytes32 subnetwork,
// address operator,
// uint256 amount,
// uint48 captureTimestamp,
// uint48 vetoDeadline,
// bool completed
// ) = iBTC_slasher.slashRequests(0);
// console.logBytes32(subnetwork);
// vm.assertEq(subnetwork, NETWORK.subnetwork(0));
// console.log("Operator:", operator);
// console.log("Amount:", amount);
// console.log("Capture Timestamp:", captureTimestamp);
// vm.assertLe(captureTimestamp, Time.timestamp() - 2 days);
// console.log("Veto Deadline:", vetoDeadline);
// console.log("Completed:", completed);
// address captureResolver =
// iBTC_slasher.resolverAt(subnetwork, captureTimestamp,"");
// console.log("captureResolver",captureResolver );

// test veto slash
// **testing veto slash**
vm.prank(OWNER);
iBTC_networkMiddleware.slash(epoch, alice, slashAmount);
slashIndex++;
signatures = _makeSignatures(slashIndex, epoch, alice, slashAmount, approvedSignerKeys);
iBTC_networkMiddleware.slash(epoch, alice, slashAmount, signatures);
vm.prank(bob);
iBTC_slasher.vetoSlash(slashIndex + 1, "");
iBTC_slasher.vetoSlash(slashIndex, "");

uint256 amountAfterVetoSlashed = iBTC_vault.activeBalanceOf(alice);
assertEq(amountAfterVetoSlashed, amountAfterSlashed, "Cached stake should stay the same");

vm.expectRevert();
vm.prank(OWNER);
iBTC_networkMiddleware.executeSlash(0, address(iBTC_vault), "");
iBTC_networkMiddleware.executeSlash(slashIndex, address(iBTC_vault), "");
}

function _makeSignatures(
uint256 slashIndex,
uint48 epoch,
address operator,
uint256 slashAmount,
uint256[] memory approvedSignerKeys
) internal returns (bytes[] memory signatures) {
bytes memory dataToSign = abi.encode(slashIndex, epoch, operator, slashAmount);
bytes32 messageHash = keccak256(dataToSign);

signatures = new bytes[](approvedSignerKeys.length);

for (uint256 i = 0; i < approvedSignerKeys.length; i++) {
signatures[i] = _signMessage(approvedSignerKeys[i], messageHash);
}

return signatures;
}

function _signMessage(uint256 signerPrivateKey, bytes32 messageHash) internal returns (bytes memory) {
bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, ethSignedMessageHash);
return abi.encodePacked(r, s, v);
}

function _setResolver(uint96 identifier, address resolver) internal {
Expand Down

0 comments on commit 0367f82

Please sign in to comment.