From 23636076a7170f5e341a2cf8cdf4e4ad40dce776 Mon Sep 17 00:00:00 2001 From: Kresh Date: Mon, 7 Oct 2024 19:05:08 +0400 Subject: [PATCH 1/2] feat: add signatures to opt-in service --- script/deploy/Core.s.sol | 6 +- script/deploy/OptInService.s.sol | 4 +- src/contracts/hints/OptInServiceHints.sol | 2 +- src/contracts/service/OptInService.sol | 121 +++++++- src/interfaces/service/IOptInService.sol | 44 ++- test/DelegatorFactory.t.sol | 6 +- test/POCBase.t.sol | 6 +- test/SlasherFactory.t.sol | 6 +- test/VaultConfigurator.t.sol | 6 +- test/VaultFactory.t.sol | 6 +- test/delegator/FullRestakeDelegator.t.sol | 6 +- test/delegator/NetworkRestakeDelegator.t.sol | 6 +- test/service/OptInService.t.sol | 285 ++++++++++++++++++- test/slasher/Slasher.t.sol | 6 +- test/slasher/VetoSlasher.t.sol | 6 +- test/vault/Vault.t.sol | 6 +- test/vault/VaultTokenized.t.sol | 6 +- 17 files changed, 476 insertions(+), 52 deletions(-) diff --git a/script/deploy/Core.s.sol b/script/deploy/Core.s.sol index 3e7f7bc1..0888c031 100644 --- a/script/deploy/Core.s.sol +++ b/script/deploy/Core.s.sol @@ -35,8 +35,10 @@ contract CoreScript is Script { MetadataService operatorMetadataService = new MetadataService(address(operatorRegistry)); MetadataService networkMetadataService = new MetadataService(address(networkRegistry)); NetworkMiddlewareService networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - OptInService operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - OptInService operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + OptInService operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + OptInService operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/script/deploy/OptInService.s.sol b/script/deploy/OptInService.s.sol index 336046a2..6b308699 100644 --- a/script/deploy/OptInService.s.sol +++ b/script/deploy/OptInService.s.sol @@ -6,10 +6,10 @@ import "forge-std/Script.sol"; import {OptInService} from "../../src/contracts/service/OptInService.sol"; contract OptInServiceScript is Script { - function run(address whoRegistry, address whereRegistry) public { + function run(address whoRegistry, address whereRegistry, string calldata name) public { vm.startBroadcast(); - new OptInService(whoRegistry, whereRegistry); + new OptInService(whoRegistry, whereRegistry, name); vm.stopBroadcast(); } diff --git a/src/contracts/hints/OptInServiceHints.sol b/src/contracts/hints/OptInServiceHints.sol index 2ecde58f..22d88e93 100644 --- a/src/contracts/hints/OptInServiceHints.sol +++ b/src/contracts/hints/OptInServiceHints.sol @@ -9,7 +9,7 @@ import {Checkpoints} from "../libraries/Checkpoints.sol"; contract OptInServiceHints is Hints, OptInService { using Checkpoints for Checkpoints.Trace208; - constructor() OptInService(address(0), address(0)) {} + constructor() OptInService(address(0), address(0), "") {} function optInHintInternal( address who, diff --git a/src/contracts/service/OptInService.sol b/src/contracts/service/OptInService.sol index 4608b6d1..72e47694 100644 --- a/src/contracts/service/OptInService.sol +++ b/src/contracts/service/OptInService.sol @@ -8,9 +8,11 @@ import {IRegistry} from "../../interfaces/common/IRegistry.sol"; import {Checkpoints} from "../libraries/Checkpoints.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; -contract OptInService is StaticDelegateCallable, IOptInService { +contract OptInService is StaticDelegateCallable, EIP712, IOptInService { using Checkpoints for Checkpoints.Trace208; /** @@ -23,9 +25,34 @@ contract OptInService is StaticDelegateCallable, IOptInService { */ address public immutable WHERE_REGISTRY; + bytes32 private constant OPT_IN_TYPEHASH = + keccak256("OptIn(address who,address where,uint256 nonce,uint48 deadline)"); + + bytes32 private constant OPT_OUT_TYPEHASH = + keccak256("OptOut(address who,address where,uint256 nonce,uint48 deadline)"); + + /** + * @inheritdoc IOptInService + */ + mapping(address who => mapping(address where => uint256 nonce)) public optInNonces; + + /** + * @inheritdoc IOptInService + */ + mapping(address who => mapping(address where => uint256 nonce)) public optOutNonces; + mapping(address who => mapping(address where => Checkpoints.Trace208 value)) internal _isOptedIn; - constructor(address whoRegistry, address whereRegistry) { + modifier checkDeadline( + uint48 deadline + ) { + if (deadline < Time.timestamp()) { + revert ExpiredSignature(); + } + _; + } + + constructor(address whoRegistry, address whereRegistry, string memory name) EIP712(name, "1") { WHO_REGISTRY = whoRegistry; WHERE_REGISTRY = whereRegistry; } @@ -55,7 +82,60 @@ contract OptInService is StaticDelegateCallable, IOptInService { function optIn( address where ) external { - if (!IRegistry(WHO_REGISTRY).isEntity(msg.sender)) { + _optIn(msg.sender, where); + } + + /** + * @inheritdoc IOptInService + */ + function optIn( + address who, + address where, + uint48 deadline, + bytes calldata signature + ) external checkDeadline(deadline) { + if ( + !SignatureChecker.isValidSignatureNow( + who, _hash(true, who, where, optInNonces[who][where], deadline), signature + ) + ) { + revert InvalidSignature(); + } + + _optIn(who, where); + } + + /** + * @inheritdoc IOptInService + */ + function optOut( + address where + ) external { + _optOut(msg.sender, where); + } + + /** + * @inheritdoc IOptInService + */ + function optOut( + address who, + address where, + uint48 deadline, + bytes calldata signature + ) external checkDeadline(deadline) { + if ( + !SignatureChecker.isValidSignatureNow( + who, _hash(false, who, where, optOutNonces[who][where], deadline), signature + ) + ) { + revert InvalidSignature(); + } + + _optOut(who, where); + } + + function _optIn(address who, address where) internal { + if (!IRegistry(WHO_REGISTRY).isEntity(who)) { revert NotWho(); } @@ -63,22 +143,19 @@ contract OptInService is StaticDelegateCallable, IOptInService { revert NotWhereEntity(); } - if (isOptedIn(msg.sender, where)) { + if (isOptedIn(who, where)) { revert AlreadyOptedIn(); } - _isOptedIn[msg.sender][where].push(Time.timestamp(), 1); + _isOptedIn[who][where].push(Time.timestamp(), 1); + + ++optInNonces[who][where]; - emit OptIn(msg.sender, where); + emit OptIn(who, where); } - /** - * @inheritdoc IOptInService - */ - function optOut( - address where - ) external { - (, uint48 latestTimestamp, uint208 latestValue) = _isOptedIn[msg.sender][where].latestCheckpoint(); + function _optOut(address who, address where) internal { + (, uint48 latestTimestamp, uint208 latestValue) = _isOptedIn[who][where].latestCheckpoint(); if (latestValue == 0) { revert NotOptedIn(); @@ -88,8 +165,22 @@ contract OptInService is StaticDelegateCallable, IOptInService { revert OptOutCooldown(); } - _isOptedIn[msg.sender][where].push(Time.timestamp(), 0); + _isOptedIn[who][where].push(Time.timestamp(), 0); + + ++optOutNonces[who][where]; + + emit OptOut(who, where); + } - emit OptOut(msg.sender, where); + function _hash( + bool ifOptIn, + address who, + address where, + uint256 nonce, + uint48 deadline + ) internal view returns (bytes32) { + return _hashTypedDataV4( + keccak256(abi.encode(ifOptIn ? OPT_IN_TYPEHASH : OPT_OUT_TYPEHASH, who, where, nonce, deadline)) + ); } } diff --git a/src/interfaces/service/IOptInService.sol b/src/interfaces/service/IOptInService.sol index 6e6b6aa3..9ff258e9 100644 --- a/src/interfaces/service/IOptInService.sol +++ b/src/interfaces/service/IOptInService.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.0; interface IOptInService { error AlreadyOptedIn(); + error ExpiredSignature(); + error InvalidSignature(); error NotOptedIn(); error NotWhereEntity(); error NotWho(); @@ -37,7 +39,7 @@ interface IOptInService { /** * @notice Get if a given "who" is opted-in to a particular "where" entity at a given timestamp using a hint. * @param who address of the "who" - * @param where address of the "where" registry + * @param where address of the "where" entity * @param timestamp time point to get if the "who" is opted-in at * @param hint hint for the checkpoint index * @return if the "who" is opted-in at the given timestamp @@ -52,24 +54,58 @@ interface IOptInService { /** * @notice Check if a given "who" is opted-in to a particular "where" entity. * @param who address of the "who" - * @param where address of the "where" registry + * @param where address of the "where" entity * @return if the "who" is opted-in */ function isOptedIn(address who, address where) external view returns (bool); + /** + * @notice Get the opt-in nonce of a given "who" to a particular "where" entity. + * @param who address of the "who" + * @param where address of the "where" entity + * @return opt-in nonce + */ + function optInNonces(address who, address where) external view returns (uint256); + + /** + * @notice Get the opt-out nonce of a given "who" to a particular "where" entity. + * @param who address of the "who" + * @param where address of the "where" entity + * @return opt-out nonce + */ + function optOutNonces(address who, address where) external view returns (uint256); + /** * @notice Opt-in a calling "who" to a particular "where" entity. - * @param where address of the "where" registry + * @param where address of the "where" entity */ function optIn( address where ) external; + /** + * @notice Opt-in a "who" to a particular "where" entity with a signature. + * @param who address of the "who" + * @param where address of the "where" entity + * @param deadline time point until the signature is valid (inclusively) + * @param signature signature of the "who" + */ + function optIn(address who, address where, uint48 deadline, bytes calldata signature) external; + /** * @notice Opt-out a calling "who" from a particular "where" entity. - * @param where address of the "where" registry + * @param where address of the "where" entity */ function optOut( address where ) external; + + /** + * @notice Opt-out a "who" from a particular "where" entity with a signature. + * @param who address of the "who" + * @param where address of the "where" entity + * @param deadline time point until the signature is valid (inclusively) + * @param signature signature of the "who" + */ + function optOut(address who, address where, uint48 deadline, bytes calldata signature) external; } diff --git a/test/DelegatorFactory.t.sol b/test/DelegatorFactory.t.sol index 7aa538e8..0cdeffd2 100644 --- a/test/DelegatorFactory.t.sol +++ b/test/DelegatorFactory.t.sol @@ -61,8 +61,10 @@ contract DelegatorFactoryTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/POCBase.t.sol b/test/POCBase.t.sol index 05917a13..191f27f3 100644 --- a/test/POCBase.t.sol +++ b/test/POCBase.t.sol @@ -90,8 +90,10 @@ contract POCBaseTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/SlasherFactory.t.sol b/test/SlasherFactory.t.sol index 43a6b507..7ecbb2a0 100644 --- a/test/SlasherFactory.t.sol +++ b/test/SlasherFactory.t.sol @@ -62,8 +62,10 @@ contract SlasherFactoryTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/VaultConfigurator.t.sol b/test/VaultConfigurator.t.sol index 265776b4..e6b0dbb5 100644 --- a/test/VaultConfigurator.t.sol +++ b/test/VaultConfigurator.t.sol @@ -65,8 +65,10 @@ contract VaultConfiguratorTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/VaultFactory.t.sol b/test/VaultFactory.t.sol index c1624120..bb982715 100644 --- a/test/VaultFactory.t.sol +++ b/test/VaultFactory.t.sol @@ -61,8 +61,10 @@ contract VaultFactoryTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/delegator/FullRestakeDelegator.t.sol b/test/delegator/FullRestakeDelegator.t.sol index 02371266..b0e9412f 100644 --- a/test/delegator/FullRestakeDelegator.t.sol +++ b/test/delegator/FullRestakeDelegator.t.sol @@ -80,8 +80,10 @@ contract FullRestakeDelegatorTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/delegator/NetworkRestakeDelegator.t.sol b/test/delegator/NetworkRestakeDelegator.t.sol index 60622d94..b5a1d76f 100644 --- a/test/delegator/NetworkRestakeDelegator.t.sol +++ b/test/delegator/NetworkRestakeDelegator.t.sol @@ -81,8 +81,10 @@ contract NetworkRestakeDelegatorTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/service/OptInService.t.sol b/test/service/OptInService.t.sol index e7b3afdc..f96d0116 100644 --- a/test/service/OptInService.t.sol +++ b/test/service/OptInService.t.sol @@ -11,6 +11,8 @@ import {IOptInService} from "../../src/interfaces/service/IOptInService.sol"; import {OptInServiceHints} from "../../src/contracts/hints/OptInServiceHints.sol"; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; + contract OperatorOptInServiceTest is Test { address owner; address alice; @@ -37,7 +39,11 @@ contract OperatorOptInServiceTest is Test { blockTimestamp = blockTimestamp + 1_720_700_948; vm.warp(blockTimestamp); - service = IOptInService(address(new OptInService(address(operatorRegistry), address(networkRegistry)))); + service = IOptInService( + address( + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService") + ) + ); assertEq(service.WHERE_REGISTRY(), address(networkRegistry)); assertEq(service.isOptedInAt(alice, alice, 0, ""), false); @@ -103,7 +109,11 @@ contract OperatorOptInServiceTest is Test { } function test_OptInRevertNotEntity() public { - service = IOptInService(address(new OptInService(address(operatorRegistry), address(networkRegistry)))); + service = IOptInService( + address( + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService") + ) + ); address operator = alice; address where = bob; @@ -119,7 +129,11 @@ contract OperatorOptInServiceTest is Test { } function test_OptInRevertNotWhereEntity() public { - service = IOptInService(address(new OptInService(address(operatorRegistry), address(networkRegistry)))); + service = IOptInService( + address( + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService") + ) + ); address operator = alice; address where = bob; @@ -135,7 +149,11 @@ contract OperatorOptInServiceTest is Test { } function test_OptInRevertAlreadyOptedIn() public { - service = IOptInService(address(new OptInService(address(operatorRegistry), address(networkRegistry)))); + service = IOptInService( + address( + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService") + ) + ); address operator = alice; address where = bob; @@ -159,7 +177,11 @@ contract OperatorOptInServiceTest is Test { } function test_OptOutRevertNotOptedIn() public { - service = IOptInService(address(new OptInService(address(operatorRegistry), address(networkRegistry)))); + service = IOptInService( + address( + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService") + ) + ); address operator = alice; address where = bob; @@ -198,7 +220,7 @@ contract OperatorOptInServiceTest is Test { // blockTimestamp = blockTimestamp + 1_720_700_948; // vm.warp(blockTimestamp); - // service = IOptInService(address(new OptInService(address(operatorRegistry), address(networkRegistry)))); + // service = IOptInService(address(new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"))); // address operator = alice; // address where = bob; @@ -253,4 +275,255 @@ contract OperatorOptInServiceTest is Test { // gasStruct.gasSpent2 = vm.lastCallGas().gasTotalUsed; // assertGe(gasStruct.gasSpent1, gasStruct.gasSpent2); // } + + function test_OptInWithSignature() public { + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp; + blockTimestamp = blockTimestamp + 1_720_700_948; + vm.warp(blockTimestamp); + + service = new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); + + address operator = alice; + address where = bob; + + vm.startPrank(operator); + operatorRegistry.registerOperator(); + vm.stopPrank(); + + vm.startPrank(where); + networkRegistry.registerNetwork(); + vm.stopPrank(); + + uint256 nonce = service.optInNonces(operator, where); + uint48 deadline = uint48(blockTimestamp); + + bytes32 digest = computeOptInDigest(service, operator, where, nonce, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + service.optIn(operator, where, deadline, signature); + + assertEq(service.isOptedIn(operator, where), true); + + assertEq(service.optInNonces(operator, where), nonce + 1); + } + + function test_OptInWithInvalidSignature() public { + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp; + blockTimestamp = blockTimestamp + 1_720_700_948; + vm.warp(blockTimestamp); + + service = new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); + + address operator = alice; + address where = bob; + + vm.startPrank(operator); + operatorRegistry.registerOperator(); + vm.stopPrank(); + + vm.startPrank(where); + networkRegistry.registerNetwork(); + vm.stopPrank(); + + uint256 nonce = service.optInNonces(operator, where); + uint48 deadline = uint48(blockTimestamp); + + bytes32 digest = computeOptInDigest(service, operator, where, nonce, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(IOptInService.InvalidSignature.selector); + service.optIn(operator, where, deadline, signature); + } + + function test_OptInWithExpiredDeadline() public { + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp; + blockTimestamp = blockTimestamp + 1_720_700_948; + vm.warp(blockTimestamp); + + service = new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); + + address operator = alice; + address where = bob; + + vm.startPrank(operator); + operatorRegistry.registerOperator(); + vm.stopPrank(); + + vm.startPrank(where); + networkRegistry.registerNetwork(); + vm.stopPrank(); + + uint256 nonce = service.optInNonces(operator, where); + uint48 deadline = uint48(blockTimestamp - 1); + + bytes32 digest = computeOptInDigest(service, operator, where, nonce, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(IOptInService.ExpiredSignature.selector); + service.optIn(operator, where, deadline, signature); + } + + function test_OptOutWithSignature() public { + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp; + blockTimestamp = blockTimestamp + 1_720_700_948; + vm.warp(blockTimestamp); + + service = new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); + + address operator = alice; + address where = bob; + + vm.startPrank(operator); + operatorRegistry.registerOperator(); + vm.stopPrank(); + + vm.startPrank(where); + networkRegistry.registerNetwork(); + vm.stopPrank(); + + vm.startPrank(operator); + service.optIn(where); + vm.stopPrank(); + + blockTimestamp = blockTimestamp + 1; + vm.warp(blockTimestamp); + + uint256 nonce = service.optOutNonces(operator, where); + uint48 deadline = uint48(blockTimestamp); + + bytes32 digest = computeOptOutDigest(service, operator, where, nonce, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + service.optOut(operator, where, deadline, signature); + + assertEq(service.isOptedIn(operator, where), false); + + assertEq(service.optOutNonces(operator, where), nonce + 1); + } + + function test_OptOutWithInvalidSignature() public { + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp; + blockTimestamp = blockTimestamp + 1_720_700_948; + vm.warp(blockTimestamp); + + service = new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); + + address operator = alice; + address where = bob; + + vm.startPrank(operator); + operatorRegistry.registerOperator(); + vm.stopPrank(); + + vm.startPrank(where); + networkRegistry.registerNetwork(); + vm.stopPrank(); + + vm.startPrank(operator); + service.optIn(where); + vm.stopPrank(); + + blockTimestamp = blockTimestamp + 1; + vm.warp(blockTimestamp); + + uint256 nonce = service.optOutNonces(operator, where); + uint48 deadline = uint48(blockTimestamp); + + bytes32 digest = computeOptOutDigest(service, operator, where, nonce, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(IOptInService.InvalidSignature.selector); + service.optOut(operator, where, deadline, signature); + } + + function test_OptOutWithExpiredDeadline() public { + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp; + blockTimestamp = blockTimestamp + 1_720_700_948; + vm.warp(blockTimestamp); + + service = new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); + + address operator = alice; + address where = bob; + + vm.startPrank(operator); + operatorRegistry.registerOperator(); + vm.stopPrank(); + + vm.startPrank(where); + networkRegistry.registerNetwork(); + vm.stopPrank(); + + vm.startPrank(operator); + service.optIn(where); + vm.stopPrank(); + + blockTimestamp = blockTimestamp + 1; + vm.warp(blockTimestamp); + + uint256 nonce = service.optOutNonces(operator, where); + uint48 deadline = uint48(blockTimestamp - 1); + + bytes32 digest = computeOptOutDigest(service, operator, where, nonce, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(IOptInService.ExpiredSignature.selector); + service.optOut(operator, where, deadline, signature); + } + + function computeOptInDigest( + IOptInService _service, + address who, + address where, + uint256 nonce, + uint48 deadline + ) internal view returns (bytes32) { + bytes32 OPT_IN_TYPEHASH = keccak256("OptIn(address who,address where,uint256 nonce,uint48 deadline)"); + bytes32 structHash = keccak256(abi.encode(OPT_IN_TYPEHASH, who, where, nonce, deadline)); + + bytes32 domainSeparator = _computeDomainSeparator(address(_service)); + + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function computeOptOutDigest( + IOptInService _service, + address who, + address where, + uint256 nonce, + uint48 deadline + ) internal view returns (bytes32) { + bytes32 OPT_OUT_TYPEHASH = keccak256("OptOut(address who,address where,uint256 nonce,uint48 deadline)"); + bytes32 structHash = keccak256(abi.encode(OPT_OUT_TYPEHASH, who, where, nonce, deadline)); + + bytes32 domainSeparator = _computeDomainSeparator(address(_service)); + + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function _computeDomainSeparator( + address _service + ) internal view returns (bytes32) { + bytes32 DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + (, string memory name, string memory version,,,,) = IERC5267(_service).eip712Domain(); + bytes32 NAME_HASH = keccak256(bytes(name)); + bytes32 VERSION_HASH = keccak256(bytes(version)); + uint256 chainId = block.chainid; + + return keccak256(abi.encode(DOMAIN_TYPEHASH, NAME_HASH, VERSION_HASH, chainId, _service)); + } } diff --git a/test/slasher/Slasher.t.sol b/test/slasher/Slasher.t.sol index 6d2766e2..a8415ac9 100644 --- a/test/slasher/Slasher.t.sol +++ b/test/slasher/Slasher.t.sol @@ -84,8 +84,10 @@ contract SlasherTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/slasher/VetoSlasher.t.sol b/test/slasher/VetoSlasher.t.sol index 8ffb0e77..3c72c7d6 100644 --- a/test/slasher/VetoSlasher.t.sol +++ b/test/slasher/VetoSlasher.t.sol @@ -87,8 +87,10 @@ contract VetoSlasherTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/vault/Vault.t.sol b/test/vault/Vault.t.sol index bcf8c122..b41f342a 100644 --- a/test/vault/Vault.t.sol +++ b/test/vault/Vault.t.sol @@ -77,8 +77,10 @@ contract VaultTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new Vault(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); diff --git a/test/vault/VaultTokenized.t.sol b/test/vault/VaultTokenized.t.sol index b08c0525..0737452f 100644 --- a/test/vault/VaultTokenized.t.sol +++ b/test/vault/VaultTokenized.t.sol @@ -78,8 +78,10 @@ contract VaultTokenizedTest is Test { operatorMetadataService = new MetadataService(address(operatorRegistry)); networkMetadataService = new MetadataService(address(networkRegistry)); networkMiddlewareService = new NetworkMiddlewareService(address(networkRegistry)); - operatorVaultOptInService = new OptInService(address(operatorRegistry), address(vaultFactory)); - operatorNetworkOptInService = new OptInService(address(operatorRegistry), address(networkRegistry)); + operatorVaultOptInService = + new OptInService(address(operatorRegistry), address(vaultFactory), "OperatorVaultOptInService"); + operatorNetworkOptInService = + new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); address vaultImpl = address(new VaultTokenized(address(delegatorFactory), address(slasherFactory), address(vaultFactory))); From 681827326616facc4f5655fd2d91239c9a300aa8 Mon Sep 17 00:00:00 2001 From: Kresh Date: Tue, 8 Oct 2024 16:31:03 +0400 Subject: [PATCH 2/2] feat: add signatures' invalidation --- src/contracts/service/OptInService.sol | 52 ++++++++++----------- src/interfaces/service/IOptInService.sol | 30 +++++++----- test/service/OptInService.t.sol | 58 ++++++++++++++++++------ 3 files changed, 89 insertions(+), 51 deletions(-) diff --git a/src/contracts/service/OptInService.sol b/src/contracts/service/OptInService.sol index 72e47694..d1134592 100644 --- a/src/contracts/service/OptInService.sol +++ b/src/contracts/service/OptInService.sol @@ -34,12 +34,7 @@ contract OptInService is StaticDelegateCallable, EIP712, IOptInService { /** * @inheritdoc IOptInService */ - mapping(address who => mapping(address where => uint256 nonce)) public optInNonces; - - /** - * @inheritdoc IOptInService - */ - mapping(address who => mapping(address where => uint256 nonce)) public optOutNonces; + mapping(address who => mapping(address where => uint256 nonce)) public nonces; mapping(address who => mapping(address where => Checkpoints.Trace208 value)) internal _isOptedIn; @@ -94,11 +89,7 @@ contract OptInService is StaticDelegateCallable, EIP712, IOptInService { uint48 deadline, bytes calldata signature ) external checkDeadline(deadline) { - if ( - !SignatureChecker.isValidSignatureNow( - who, _hash(true, who, where, optInNonces[who][where], deadline), signature - ) - ) { + if (!SignatureChecker.isValidSignatureNow(who, _hash(true, who, where, deadline), signature)) { revert InvalidSignature(); } @@ -123,17 +114,22 @@ contract OptInService is StaticDelegateCallable, EIP712, IOptInService { uint48 deadline, bytes calldata signature ) external checkDeadline(deadline) { - if ( - !SignatureChecker.isValidSignatureNow( - who, _hash(false, who, where, optOutNonces[who][where], deadline), signature - ) - ) { + if (!SignatureChecker.isValidSignatureNow(who, _hash(false, who, where, deadline), signature)) { revert InvalidSignature(); } _optOut(who, where); } + /** + * @inheritdoc IOptInService + */ + function increaseNonce( + address where + ) external { + _increaseNonce(msg.sender, where); + } + function _optIn(address who, address where) internal { if (!IRegistry(WHO_REGISTRY).isEntity(who)) { revert NotWho(); @@ -149,7 +145,7 @@ contract OptInService is StaticDelegateCallable, EIP712, IOptInService { _isOptedIn[who][where].push(Time.timestamp(), 1); - ++optInNonces[who][where]; + _increaseNonce(who, where); emit OptIn(who, where); } @@ -167,20 +163,24 @@ contract OptInService is StaticDelegateCallable, EIP712, IOptInService { _isOptedIn[who][where].push(Time.timestamp(), 0); - ++optOutNonces[who][where]; + _increaseNonce(who, where); emit OptOut(who, where); } - function _hash( - bool ifOptIn, - address who, - address where, - uint256 nonce, - uint48 deadline - ) internal view returns (bytes32) { + function _hash(bool ifOptIn, address who, address where, uint48 deadline) internal view returns (bytes32) { return _hashTypedDataV4( - keccak256(abi.encode(ifOptIn ? OPT_IN_TYPEHASH : OPT_OUT_TYPEHASH, who, where, nonce, deadline)) + keccak256( + abi.encode(ifOptIn ? OPT_IN_TYPEHASH : OPT_OUT_TYPEHASH, who, where, nonces[who][where], deadline) + ) ); } + + function _increaseNonce(address who, address where) internal { + unchecked { + ++nonces[who][where]; + } + + emit IncreaseNonce(who, where); + } } diff --git a/src/interfaces/service/IOptInService.sol b/src/interfaces/service/IOptInService.sol index 9ff258e9..d5fa42b8 100644 --- a/src/interfaces/service/IOptInService.sol +++ b/src/interfaces/service/IOptInService.sol @@ -24,6 +24,13 @@ interface IOptInService { */ event OptOut(address indexed who, address indexed where); + /** + * @notice Emitted when the nonce of a "who" to a "where" entity is increased. + * @param who address of the "who" + * @param where address of the "where" entity + */ + event IncreaseNonce(address indexed who, address indexed where); + /** * @notice Get the "who" registry's address. * @return address of the "who" registry @@ -60,20 +67,12 @@ interface IOptInService { function isOptedIn(address who, address where) external view returns (bool); /** - * @notice Get the opt-in nonce of a given "who" to a particular "where" entity. - * @param who address of the "who" - * @param where address of the "where" entity - * @return opt-in nonce - */ - function optInNonces(address who, address where) external view returns (uint256); - - /** - * @notice Get the opt-out nonce of a given "who" to a particular "where" entity. + * @notice Get the nonce of a given "who" to a particular "where" entity. * @param who address of the "who" * @param where address of the "where" entity - * @return opt-out nonce + * @return nonce */ - function optOutNonces(address who, address where) external view returns (uint256); + function nonces(address who, address where) external view returns (uint256); /** * @notice Opt-in a calling "who" to a particular "where" entity. @@ -108,4 +107,13 @@ interface IOptInService { * @param signature signature of the "who" */ function optOut(address who, address where, uint48 deadline, bytes calldata signature) external; + + /** + * @notice Increase the nonce of a given "who" to a particular "where" entity. + * @param where address of the "where" entity + * @dev It can be used to invalidate a given signature. + */ + function increaseNonce( + address where + ) external; } diff --git a/test/service/OptInService.t.sol b/test/service/OptInService.t.sol index f96d0116..f27b07c3 100644 --- a/test/service/OptInService.t.sol +++ b/test/service/OptInService.t.sol @@ -48,6 +48,7 @@ contract OperatorOptInServiceTest is Test { assertEq(service.WHERE_REGISTRY(), address(networkRegistry)); assertEq(service.isOptedInAt(alice, alice, 0, ""), false); assertEq(service.isOptedIn(alice, alice), false); + assertEq(service.nonces(alice, alice), 0); address operator = alice; address where = bob; @@ -294,10 +295,9 @@ contract OperatorOptInServiceTest is Test { networkRegistry.registerNetwork(); vm.stopPrank(); - uint256 nonce = service.optInNonces(operator, where); uint48 deadline = uint48(blockTimestamp); - bytes32 digest = computeOptInDigest(service, operator, where, nonce, deadline); + bytes32 digest = computeOptInDigest(service, operator, where, 0, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -306,7 +306,7 @@ contract OperatorOptInServiceTest is Test { assertEq(service.isOptedIn(operator, where), true); - assertEq(service.optInNonces(operator, where), nonce + 1); + assertEq(service.nonces(operator, where), 1); } function test_OptInWithInvalidSignature() public { @@ -327,10 +327,9 @@ contract OperatorOptInServiceTest is Test { networkRegistry.registerNetwork(); vm.stopPrank(); - uint256 nonce = service.optInNonces(operator, where); uint48 deadline = uint48(blockTimestamp); - bytes32 digest = computeOptInDigest(service, operator, where, nonce, deadline); + bytes32 digest = computeOptInDigest(service, operator, where, 0, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPrivateKey, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -357,10 +356,9 @@ contract OperatorOptInServiceTest is Test { networkRegistry.registerNetwork(); vm.stopPrank(); - uint256 nonce = service.optInNonces(operator, where); uint48 deadline = uint48(blockTimestamp - 1); - bytes32 digest = computeOptInDigest(service, operator, where, nonce, deadline); + bytes32 digest = computeOptInDigest(service, operator, where, 0, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -369,6 +367,41 @@ contract OperatorOptInServiceTest is Test { service.optIn(operator, where, deadline, signature); } + function test_IncreaseNonce() public { + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp; + blockTimestamp = blockTimestamp + 1_720_700_948; + vm.warp(blockTimestamp); + + service = new OptInService(address(operatorRegistry), address(networkRegistry), "OperatorNetworkOptInService"); + + address operator = alice; + address where = bob; + + vm.startPrank(operator); + operatorRegistry.registerOperator(); + vm.stopPrank(); + + vm.startPrank(where); + networkRegistry.registerNetwork(); + vm.stopPrank(); + + uint48 deadline = uint48(blockTimestamp); + + bytes32 digest = computeOptInDigest(service, operator, where, 0, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.startPrank(operator); + service.increaseNonce(where); + vm.stopPrank(); + + assertEq(service.nonces(operator, where), 1); + + vm.expectRevert(); + service.optIn(operator, where, deadline, signature); + } + function test_OptOutWithSignature() public { uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp * block.timestamp / block.timestamp; blockTimestamp = blockTimestamp + 1_720_700_948; @@ -394,10 +427,9 @@ contract OperatorOptInServiceTest is Test { blockTimestamp = blockTimestamp + 1; vm.warp(blockTimestamp); - uint256 nonce = service.optOutNonces(operator, where); uint48 deadline = uint48(blockTimestamp); - bytes32 digest = computeOptOutDigest(service, operator, where, nonce, deadline); + bytes32 digest = computeOptOutDigest(service, operator, where, 1, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -406,7 +438,7 @@ contract OperatorOptInServiceTest is Test { assertEq(service.isOptedIn(operator, where), false); - assertEq(service.optOutNonces(operator, where), nonce + 1); + assertEq(service.nonces(operator, where), 2); } function test_OptOutWithInvalidSignature() public { @@ -434,10 +466,9 @@ contract OperatorOptInServiceTest is Test { blockTimestamp = blockTimestamp + 1; vm.warp(blockTimestamp); - uint256 nonce = service.optOutNonces(operator, where); uint48 deadline = uint48(blockTimestamp); - bytes32 digest = computeOptOutDigest(service, operator, where, nonce, deadline); + bytes32 digest = computeOptOutDigest(service, operator, where, 1, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPrivateKey, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -471,10 +502,9 @@ contract OperatorOptInServiceTest is Test { blockTimestamp = blockTimestamp + 1; vm.warp(blockTimestamp); - uint256 nonce = service.optOutNonces(operator, where); uint48 deadline = uint48(blockTimestamp - 1); - bytes32 digest = computeOptOutDigest(service, operator, where, nonce, deadline); + bytes32 digest = computeOptOutDigest(service, operator, where, 1, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest); bytes memory signature = abi.encodePacked(r, s, v);