Skip to content

Commit f7f5605

Browse files
committed
Notification implementation
1 parent 237a115 commit f7f5605

File tree

12 files changed

+230
-13
lines changed

12 files changed

+230
-13
lines changed

Diff for: docs/autogen/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
- [INotifier](src/interfaces/UVN/L1/StakingMiddleware/INotifier.sol/interface.INotifier.md)
3434
- [IOperatorManager](src/interfaces/UVN/L1/StakingMiddleware/IOperatorManager.sol/interface.IOperatorManager.md)
3535
- [IProtocolRewardDistributor](src/interfaces/UVN/L1/StakingMiddleware/IProtocolRewardDistributor.sol/interface.IProtocolRewardDistributor.md)
36+
- [IService](src/interfaces/UVN/L1/StakingMiddleware/IService.sol/interface.IService.md)
3637
- [ISlashingManager](src/interfaces/UVN/L1/StakingMiddleware/ISlashingManager.sol/interface.ISlashingManager.md)
3738
- [IStakeManager](src/interfaces/UVN/L1/StakingMiddleware/IStakeManager.sol/interface.IStakeManager.md)
3839
- [IStakingMiddlewareParams](src/interfaces/UVN/L1/StakingMiddleware/IStakingMiddlewareParams.sol/interface.IStakingMiddlewareParams.md)

Diff for: docs/autogen/src/src/UVN/L1/StakingMiddleware/Notifier.sol/abstract.Notifier.md

+65-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Notifier
2-
[Git Source](https://github.com/Uniswap/unichain-contracts/blob/8c1be1ff6fd1da269c202b06cfb4eb0e104f04ef/src/UVN/L1/StakingMiddleware/Notifier.sol)
2+
[Git Source](https://github.com/Uniswap/unichain-contracts/blob/237a1154da63599fba331d8b41ad18f6d70fe59c/src/UVN/L1/StakingMiddleware/Notifier.sol)
33

44
**Inherits:**
55
[SlashingManager](/src/UVN/L1/StakingMiddleware/SlashingManager.sol/abstract.SlashingManager.md), ERC721, [INotifier](/src/interfaces/UVN/L1/StakingMiddleware/INotifier.sol/interface.INotifier.md)
@@ -8,6 +8,13 @@ This contract allows operators to mint ERC721 tokens to deposit into service con
88

99

1010
## State Variables
11+
### MIN_GAS
12+
13+
```solidity
14+
uint256 private constant MIN_GAS = 500_000;
15+
```
16+
17+
1118
### _uris
1219

1320
```solidity
@@ -23,6 +30,41 @@ mapping(address operator => string uri) private _uris;
2330
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) OperatorManager(name_);
2431
```
2532

33+
### _afterStake
34+
35+
36+
```solidity
37+
function _afterStake(address delegator, uint96 amount) internal virtual override;
38+
```
39+
40+
### _afterUnstake
41+
42+
43+
```solidity
44+
function _afterUnstake(address delegator, uint96 amount) internal virtual override;
45+
```
46+
47+
### _afterOperatorSelection
48+
49+
50+
```solidity
51+
function _afterOperatorSelection(address delegator, address operator) internal virtual override;
52+
```
53+
54+
### _afterOperatorUndelegationAnnouncement
55+
56+
57+
```solidity
58+
function _afterOperatorUndelegationAnnouncement(address delegator) internal virtual override;
59+
```
60+
61+
### _afterSlash
62+
63+
64+
```solidity
65+
function _afterSlash(address operator, uint256 remainingPercentage) internal virtual override;
66+
```
67+
2668
### mint
2769

2870
Allows an operator to mint a new token to deposit into service contracts
@@ -66,11 +108,31 @@ function transferFrom(address, address, uint256) public pure override;
66108
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override;
67109
```
68110

69-
### _isContract
111+
### _reportOperatorStakeUpdate
112+
113+
*reports the new operator stake to the service contract, triggered by a balance change through a delegator action*
114+
115+
*if requireSuccess is true, the function will revert if the call to the service contract fails*
116+
117+
*if requireSuccess is false, the function will not revert if the call to the service contract fails, this ensures that a delegator cannot be bricked by a malicious operator, they should always be able to undelegate from the operator to withdraw their stake. To ensure an honest undelegation can be processed by the recipient of the call, a minimum amount of gas is enforced.*
118+
119+
120+
```solidity
121+
function _reportOperatorStakeUpdate(address delegator, bool requireSuccess) internal;
122+
```
123+
124+
### _reportOperatorSlash
125+
126+
127+
```solidity
128+
function _reportOperatorSlash(address operator, uint256 remainingPercentage) internal;
129+
```
130+
131+
### _isServiceContract
70132

71133

72134
```solidity
73-
function _isContract(address account) private view returns (bool);
135+
function _isServiceContract(address account) private view returns (bool);
74136
```
75137

76138
### _toTokenId

Diff for: docs/autogen/src/src/UVN/L1/StakingMiddleware/SlashingManager.sol/abstract.SlashingManager.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SlashingManager
2-
[Git Source](https://github.com/Uniswap/unichain-contracts/blob/3cf601aa04842b039a5cf3b59e8450ff86ee0a21/src/UVN/L1/StakingMiddleware/SlashingManager.sol)
2+
[Git Source](https://github.com/Uniswap/unichain-contracts/blob/237a1154da63599fba331d8b41ad18f6d70fe59c/src/UVN/L1/StakingMiddleware/SlashingManager.sol)
33

44
**Inherits:**
55
[DelegatorAccessControl](/src/UVN/L1/StakingMiddleware/DelegatorAccessControl.sol/abstract.DelegatorAccessControl.md), [ISlashingManager](/src/interfaces/UVN/L1/StakingMiddleware/ISlashingManager.sol/interface.ISlashingManager.md)
@@ -244,7 +244,7 @@ function SLASHER_ROLE() public pure returns (bytes32);
244244

245245

246246
```solidity
247-
function _afterSlash(address operator, uint96 remainingPercentage) internal virtual;
247+
function _afterSlash(address operator, uint256 remainingPercentage) internal virtual;
248248
```
249249

250250
## Structs

Diff for: docs/autogen/src/src/interfaces/UVN/L1/StakingMiddleware/INotifier.sol/interface.INotifier.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# INotifier
2-
[Git Source](https://github.com/Uniswap/unichain-contracts/blob/8c1be1ff6fd1da269c202b06cfb4eb0e104f04ef/src/interfaces/UVN/L1/StakingMiddleware/INotifier.sol)
2+
[Git Source](https://github.com/Uniswap/unichain-contracts/blob/237a1154da63599fba331d8b41ad18f6d70fe59c/src/interfaces/UVN/L1/StakingMiddleware/INotifier.sol)
33

44
**Inherits:**
55
[ISlashingManager](/src/interfaces/UVN/L1/StakingMiddleware/ISlashingManager.sol/interface.ISlashingManager.md)
@@ -60,3 +60,19 @@ thrown when the recipient of a safe transfer is not the operator or a contract
6060
error InvalidRecipient();
6161
```
6262

63+
### WrappedError
64+
ERC-7751 error wrapping reverts by service contracts
65+
66+
67+
```solidity
68+
error WrappedError(address target, bytes4 selector, bytes reason, bytes details);
69+
```
70+
71+
### NotificationFailed
72+
details for wrapped error when a notification fails
73+
74+
75+
```solidity
76+
error NotificationFailed();
77+
```
78+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# IService
2+
[Git Source](https://github.com/Uniswap/unichain-contracts/blob/237a1154da63599fba331d8b41ad18f6d70fe59c/src/interfaces/UVN/L1/StakingMiddleware/IService.sol)
3+
4+
**Inherits:**
5+
IERC165
6+
7+
This interface is used by contracts that operators deposit their ERC-721 tokens into to operate for. It must implement the following functions in order to be notified of changes to operator's and delegator's stake.
8+
9+
10+
## Functions
11+
### reportOperatorStake
12+
13+
This function is called when a delegator's stake changes.
14+
15+
16+
```solidity
17+
function reportOperatorStake(address operator, uint96 newBalance, address delegator, uint96 newDelegatorStake)
18+
external;
19+
```
20+
**Parameters**
21+
22+
|Name|Type|Description|
23+
|----|----|-----------|
24+
|`operator`|`address`|The address of the operator.|
25+
|`newBalance`|`uint96`|The new balance of the operator.|
26+
|`delegator`|`address`|The address of the delegator.|
27+
|`newDelegatorStake`|`uint96`|The new stake of the delegator.|
28+
29+
30+
### reportOperatorSlash
31+
32+
This function is called when an operator is slashed.
33+
34+
35+
```solidity
36+
function reportOperatorSlash(address operator, uint256 remainingPercentage) external;
37+
```
38+
**Parameters**
39+
40+
|Name|Type|Description|
41+
|----|----|-----------|
42+
|`operator`|`address`|The address of the operator.|
43+
|`remainingPercentage`|`uint256`|The remaining percentage of the operator's stake.|
44+
45+

Diff for: docs/autogen/src/src/interfaces/UVN/L1/StakingMiddleware/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [INotifier](INotifier.sol/interface.INotifier.md)
77
- [IOperatorManager](IOperatorManager.sol/interface.IOperatorManager.md)
88
- [IProtocolRewardDistributor](IProtocolRewardDistributor.sol/interface.IProtocolRewardDistributor.md)
9+
- [IService](IService.sol/interface.IService.md)
910
- [ISlashingManager](ISlashingManager.sol/interface.ISlashingManager.md)
1011
- [IStakeManager](IStakeManager.sol/interface.IStakeManager.md)
1112
- [IStakingMiddlewareParams](IStakingMiddlewareParams.sol/interface.IStakingMiddlewareParams.md)

Diff for: src/UVN/L1/StakingMiddleware/Notifier.sol

+64-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.0;
33

44
import {INotifier} from '../../../interfaces/UVN/L1/StakingMiddleware/INotifier.sol';
5-
5+
import {IService} from '../../../interfaces/UVN/L1/StakingMiddleware/IService.sol';
66
import {OperatorManager} from './OperatorManager.sol';
77
import {SlashingManager} from './SlashingManager.sol';
88
import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol';
@@ -11,14 +11,41 @@ import {ERC721} from '@openzeppelin/contracts/token/ERC721/ERC721.sol';
1111
/// @title Notifier - Base contract for the Notifier
1212
/// @notice This contract allows operators to mint ERC721 tokens to deposit into service contracts they want to operate for. Whenever a delegator modifies their stake or the operator is slashed, the current owner of the token is notified (e.g., service contract). This allows the operator to participate in network upgrades by depositing their token into a new service contract. Additionally, it allows service contracts to implement arbitrary logic on deposits by requiring data to be sent alongside the token, implement their own migration logic, etc. Additionally, the operator can set a URI for their token where they can expose an endpoint to provide more information about themselves.
1313
abstract contract Notifier is SlashingManager, ERC721, INotifier {
14+
uint256 private constant MIN_GAS = 500_000;
15+
1416
mapping(address operator => string uri) private _uris;
1517

1618
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) OperatorManager(name_) {}
1719

20+
function _afterStake(address delegator, uint96 amount) internal virtual override {
21+
super._afterStake(delegator, amount);
22+
_reportOperatorStakeUpdate(delegator, true);
23+
}
24+
25+
function _afterUnstake(address delegator, uint96 amount) internal virtual override {
26+
super._afterUnstake(delegator, amount);
27+
_reportOperatorStakeUpdate(delegator, true);
28+
}
29+
30+
function _afterOperatorSelection(address delegator, address operator) internal virtual override {
31+
super._afterOperatorSelection(delegator, operator);
32+
_reportOperatorStakeUpdate(delegator, true);
33+
}
34+
35+
function _afterOperatorUndelegationAnnouncement(address delegator) internal virtual override {
36+
super._afterOperatorUndelegationAnnouncement(delegator);
37+
_reportOperatorStakeUpdate(delegator, false);
38+
}
39+
40+
function _afterSlash(address operator, uint256 remainingPercentage) internal virtual override {
41+
super._afterSlash(operator, remainingPercentage);
42+
_reportOperatorSlash(operator, remainingPercentage);
43+
}
44+
1845
/// @inheritdoc INotifier
1946
function mint() external {
2047
uint256 tokenId = _toTokenId(msg.sender);
21-
if (ownerOf(tokenId) != address(0)) revert AlreadyMinted();
48+
if (_ownerOf(tokenId) != address(0)) revert AlreadyMinted();
2249
_mint(msg.sender, tokenId);
2350
}
2451

@@ -42,17 +69,49 @@ abstract contract Notifier is SlashingManager, ERC721, INotifier {
4269

4370
/// @dev only allow transfers to contracts and the operator
4471
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override {
45-
if (to != _toAddress(tokenId) && !_isContract(to)) revert InvalidRecipient();
72+
if (to != _toAddress(tokenId) && !_isServiceContract(to)) revert InvalidRecipient();
4673
super.safeTransferFrom(from, to, tokenId, data);
4774
}
4875

49-
function _isContract(address account) private view returns (bool) {
76+
/// @dev reports the new operator stake to the service contract, triggered by a balance change through a delegator action
77+
/// @dev if requireSuccess is true, the function will revert if the call to the service contract fails
78+
/// @dev if requireSuccess is false, the function will not revert if the call to the service contract fails, this ensures that a delegator cannot be bricked by a malicious operator, they should always be able to undelegate from the operator to withdraw their stake. To ensure an honest undelegation can be processed by the recipient of the call, a minimum amount of gas is enforced.
79+
function _reportOperatorStakeUpdate(address delegator, bool requireSuccess) internal {
80+
address operator = delegates(delegator);
81+
if (operator == address(0)) return;
82+
// this reverts if the operator token is not minted
83+
address operatorHolder = _requireOwned(_toTokenId(operator));
84+
if (operatorHolder != operator) {
85+
uint256 minGas = requireSuccess ? gasleft() * 63 / 64 : MIN_GAS;
86+
try IService(operatorHolder).reportOperatorStake{gas: minGas}(
87+
operator, uint96(getVotes(operator)), delegator, _delegatorStake(delegator)
88+
) {} catch (bytes memory reason) {
89+
if (!requireSuccess) return;
90+
revert WrappedError(
91+
operatorHolder,
92+
IService.reportOperatorStake.selector,
93+
reason,
94+
abi.encodePacked(INotifier.NotificationFailed.selector)
95+
);
96+
}
97+
}
98+
}
99+
100+
function _reportOperatorSlash(address operator, uint256 remainingPercentage) internal {
101+
address operatorHolder = _ownerOf(_toTokenId(operator));
102+
if (operator != address(0) && operatorHolder != address(0) && operatorHolder != operator) {
103+
try IService(operatorHolder).reportOperatorSlash{gas: MIN_GAS}(operator, remainingPercentage) {} catch {}
104+
}
105+
}
106+
107+
function _isServiceContract(address account) private view returns (bool) {
50108
uint32 size;
51109
assembly {
52110
size := extcodesize(account)
53111
}
54112
// take into account 7702 accounts
55-
return size != 0 && size != 23;
113+
if (size == 0 || size == 23) return false;
114+
return IService(account).supportsInterface(type(IService).interfaceId);
56115
}
57116

58117
function _toTokenId(address owner) private pure returns (uint256) {

Diff for: src/UVN/L1/StakingMiddleware/SlashingManager.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ abstract contract SlashingManager is DelegatorAccessControl, ISlashingManager {
184184
});
185185
_slashingInstances[operator].push(instance);
186186
super._slashOperatorVotes(operator, remainingPercentage);
187-
_afterSlash(operator, uint96(remainingPercentage));
187+
_afterSlash(operator, remainingPercentage);
188188
}
189189

190190
/// @dev Overrides the `delegatorStake` function in `StakeManager` to reflect correct stake for a delegator accounting for slashing
@@ -279,5 +279,5 @@ abstract contract SlashingManager is DelegatorAccessControl, ISlashingManager {
279279
return keccak256('SLASHER_ROLE');
280280
}
281281

282-
function _afterSlash(address operator, uint96 remainingPercentage) internal virtual {}
282+
function _afterSlash(address operator, uint256 remainingPercentage) internal virtual {}
283283
}

Diff for: src/interfaces/UVN/L1/StakingMiddleware/INotifier.sol

+8
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ interface INotifier is ISlashingManager {
1111

1212
/// @notice Thrown when a token is already minted
1313
error AlreadyMinted();
14+
1415
/// @notice thrown when the `transferFrom` function is called
1516
error UnsafeTransfer();
17+
1618
/// @notice thrown when the recipient of a safe transfer is not the operator or a contract
1719
error InvalidRecipient();
1820

21+
/// @notice ERC-7751 error wrapping reverts by service contracts
22+
error WrappedError(address target, bytes4 selector, bytes reason, bytes details);
23+
24+
/// @notice details for wrapped error when a notification fails
25+
error NotificationFailed();
26+
1927
/// @notice Allows an operator to mint a new token to deposit into service contracts
2028
function mint() external;
2129

Diff for: src/interfaces/UVN/L1/StakingMiddleware/IService.sol

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol';
5+
6+
/// @title IService
7+
/// @notice This interface is used by contracts that operators deposit their ERC-721 tokens into to operate for. It must implement the following functions in order to be notified of changes to operator's and delegator's stake.
8+
interface IService is IERC165 {
9+
/// @notice This function is called when a delegator's stake changes.
10+
/// @param operator The address of the operator.
11+
/// @param newBalance The new balance of the operator.
12+
/// @param delegator The address of the delegator.
13+
/// @param newDelegatorStake The new stake of the delegator.
14+
function reportOperatorStake(address operator, uint96 newBalance, address delegator, uint96 newDelegatorStake)
15+
external;
16+
17+
/// @notice This function is called when an operator is slashed.
18+
/// @param operator The address of the operator.
19+
/// @param remainingPercentage The remaining percentage of the operator's stake.
20+
function reportOperatorSlash(address operator, uint256 remainingPercentage) external;
21+
}

Diff for: test/UVN/L1/StakingMiddleware.slashing.t.sol

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ contract StakingMiddlewareSlashingTest is Test {
4848
deposit(address(this), 1000);
4949
stakingMiddleware.depositIntoUniStaker(address(this));
5050
vm.prank(operator);
51+
stakingMiddleware.mint();
52+
vm.prank(operator);
5153
stakingMiddleware.setDelegationStatus(true);
5254
stakingMiddleware.delegate(operator);
5355
assertEq(stakingMiddleware.delegatorStake(address(this)), 1000, 'delegator stake does not match');

Diff for: test/UVN/L1/StakingMiddleware.t.sol

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ contract StakingMiddlewareTest is Test {
2525
stakeToken.approve(address(stakingMiddleware), 1000);
2626
stakingMiddleware.stake(1000);
2727
vm.prank(operator);
28+
stakingMiddleware.mint();
29+
vm.prank(operator);
2830
stakingMiddleware.setDelegationStatus(true);
2931
stakingMiddleware.delegate(operator);
3032
assertEq(stakingMiddleware.delegatorStake(address(this)), 1000);

0 commit comments

Comments
 (0)