@piplabs/story-contracts 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @piplabs/story-contracts might be problematic. Click here for more details.

@@ -0,0 +1,509 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ pragma solidity 0.8.23;
3
+
4
+ import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
5
+ import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
6
+ import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
7
+ import { IIPTokenStaking } from "../interfaces/IIPTokenStaking.sol";
8
+ import { Secp256k1Verifier } from "./Secp256k1Verifier.sol";
9
+
10
+ /**
11
+ * @title IPTokenStaking
12
+ * @notice The deposit contract for IP token staked validators.
13
+ * @dev This contract is a sort of "bridge" to request validator related actions on the consensus chain.
14
+ * The response will happen on the consensus chain.
15
+ * Since most of the validator related actions are executed on the consensus chain, the methods in this contract
16
+ * must be considered requests and not final actions, a successful transaction here does not guarantee the success
17
+ * of the transaction on the consensus chain.
18
+ * NOTE: All $IP tokens staked to this contract will be burned (transferred to the zero address).
19
+ * The flow is as follows:
20
+ * 1. User calls a method in this contract, which will emit an event if checks pass.
21
+ * 2. Modules on the consensus chain are listening for these events and execute the corresponding logic
22
+ * (e.g. staking, create validator, etc.), minting tokens in CL if needed.
23
+ * 3. If the action fails in CL, for example staking on a validator that doesn't exist, the deposited $IP tokens will
24
+ * not be refunded to the user. Remember that the EL transaction of step 2 would not have reverted. So please be
25
+ * cautious when making transactions with this contract.
26
+ */
27
+ contract IPTokenStaking is IIPTokenStaking, Ownable2StepUpgradeable, ReentrancyGuardUpgradeable, Secp256k1Verifier {
28
+ using EnumerableSet for EnumerableSet.AddressSet;
29
+
30
+ /// @notice Maximum length of the validator moniker, in bytes.
31
+ uint256 public constant MAX_MONIKER_LENGTH = 70;
32
+
33
+ /// @notice Stake amount increments. Consensus Layer requires staking in increments of 1 gwei.
34
+ uint256 public constant STAKE_ROUNDING = 1 gwei;
35
+
36
+ /// @notice Default minimum fee charged for adding to CL storage
37
+ uint256 public immutable DEFAULT_MIN_FEE;
38
+
39
+ /// @notice The maximum size of the data field in the event logs.
40
+ uint256 public immutable MAX_DATA_LENGTH;
41
+
42
+ /// @notice Global minimum commission rate for validators
43
+ uint256 public minCommissionRate;
44
+
45
+ /// @notice Minimum amount required to stake.
46
+ uint256 public minStakeAmount;
47
+
48
+ /// @notice Minimum amount required to unstake.
49
+ uint256 public minUnstakeAmount;
50
+
51
+ /// @notice Counter to generate delegationIds for delegations with period.
52
+ /// @dev Starts in 1, since 0 is reserved for flexible delegations.
53
+ uint256 private _delegationIdCounter;
54
+
55
+ /// @notice The fee paid to update a validator (unjail, commission update, etc.)
56
+ uint256 public fee;
57
+
58
+ modifier chargesFee() {
59
+ require(msg.value == fee, "IPTokenStaking: Invalid fee amount");
60
+ payable(address(0x0)).transfer(msg.value);
61
+ _;
62
+ }
63
+
64
+ constructor(uint256 defaultMinFee, uint256 maxDataLength) {
65
+ require(defaultMinFee >= 1 gwei, "IPTokenStaking: Invalid default min fee");
66
+ DEFAULT_MIN_FEE = defaultMinFee;
67
+ MAX_DATA_LENGTH = maxDataLength;
68
+ _disableInitializers();
69
+ }
70
+
71
+ /// @notice Initializes the contract.
72
+ /// @dev Only callable once at proxy deployment.
73
+ /// @param args The initializer arguments.
74
+ function initialize(IIPTokenStaking.InitializerArgs calldata args) public initializer {
75
+ __ReentrancyGuard_init();
76
+ __Ownable_init(args.owner);
77
+ _setMinStakeAmount(args.minStakeAmount);
78
+ _setMinUnstakeAmount(args.minUnstakeAmount);
79
+ _setMinCommissionRate(args.minCommissionRate);
80
+ _setFee(args.fee);
81
+ }
82
+
83
+ /*//////////////////////////////////////////////////////////////////////////
84
+ // Admin Setters/Getters //
85
+ //////////////////////////////////////////////////////////////////////////*/
86
+
87
+ /// @dev Sets the minimum amount required to stake.
88
+ /// @param newMinStakeAmount The minimum amount required to stake.
89
+ function setMinStakeAmount(uint256 newMinStakeAmount) external onlyOwner {
90
+ _setMinStakeAmount(newMinStakeAmount);
91
+ }
92
+
93
+ /// @dev Sets the minimum amount required to withdraw.
94
+ /// @param newMinUnstakeAmount The minimum amount required to stake.
95
+ function setMinUnstakeAmount(uint256 newMinUnstakeAmount) external onlyOwner {
96
+ _setMinUnstakeAmount(newMinUnstakeAmount);
97
+ }
98
+
99
+ /// @notice Sets the fee charged for adding to CL storage.
100
+ /// @param newFee The new fee
101
+ function setFee(uint256 newFee) external onlyOwner {
102
+ _setFee(newFee);
103
+ }
104
+
105
+ /// @notice Sets the global minimum commission rate for validators.
106
+ /// @param newValue The new minimum commission rate.
107
+ function setMinCommissionRate(uint256 newValue) external onlyOwner {
108
+ _setMinCommissionRate(newValue);
109
+ }
110
+
111
+ /*//////////////////////////////////////////////////////////////////////////
112
+ // Internal setters //
113
+ //////////////////////////////////////////////////////////////////////////*/
114
+
115
+ /// @dev Sets the fee charged for adding to CL storage.
116
+ function _setFee(uint256 newFee) private {
117
+ require(newFee >= DEFAULT_MIN_FEE, "IPTokenStaking: Invalid min fee");
118
+ fee = newFee;
119
+ emit FeeSet(newFee);
120
+ }
121
+
122
+ /// @dev Sets the minimum amount required to stake.
123
+ /// @param newMinStakeAmount The minimum amount required to stake.
124
+ function _setMinStakeAmount(uint256 newMinStakeAmount) private {
125
+ minStakeAmount = newMinStakeAmount - (newMinStakeAmount % STAKE_ROUNDING);
126
+ require(minStakeAmount > 0, "IPTokenStaking: Zero min stake amount");
127
+ emit MinStakeAmountSet(minStakeAmount);
128
+ }
129
+
130
+ /// @dev Sets the minimum amount required to withdraw.
131
+ /// @param newMinUnstakeAmount The minimum amount required to stake.
132
+ function _setMinUnstakeAmount(uint256 newMinUnstakeAmount) private {
133
+ minUnstakeAmount = newMinUnstakeAmount - (newMinUnstakeAmount % STAKE_ROUNDING);
134
+ require(minUnstakeAmount > 0, "IPTokenStaking: Zero min unstake amount");
135
+ emit MinUnstakeAmountSet(minUnstakeAmount);
136
+ }
137
+
138
+ /// @dev Sets the minimum global commission rate for validators.
139
+ /// @param newValue The new minimum commission rate.
140
+ function _setMinCommissionRate(uint256 newValue) private {
141
+ require(newValue > 0, "IPTokenStaking: Zero min commission rate");
142
+ minCommissionRate = newValue;
143
+ emit MinCommissionRateChanged(newValue);
144
+ }
145
+
146
+ /*//////////////////////////////////////////////////////////////////////////
147
+ // Operator functions //
148
+ //////////////////////////////////////////////////////////////////////////*/
149
+
150
+ /// @notice Sets an operator for a delegator.
151
+ /// Calling this method will override any existing operator.
152
+ /// @param operator The operator address to add.
153
+ function setOperator(address operator) external payable chargesFee {
154
+ // Use unsetOperator to remove an operator, not setting to zero address
155
+ require(operator != address(0), "IPTokenStaking: zero input address");
156
+ emit SetOperator(msg.sender, operator);
157
+ }
158
+
159
+ /// @notice Removes current operator for a delegator.
160
+ function unsetOperator() external payable chargesFee {
161
+ emit UnsetOperator(msg.sender);
162
+ }
163
+
164
+ /*//////////////////////////////////////////////////////////////////////////
165
+ // Staking Configuration functions //
166
+ //////////////////////////////////////////////////////////////////////////*/
167
+
168
+ /// @notice Set/Update the withdrawal address that receives the withdrawals.
169
+ /// @param newWithdrawalAddress EVM address to receive the withdrawals.
170
+ function setWithdrawalAddress(address newWithdrawalAddress) external payable chargesFee {
171
+ require(newWithdrawalAddress != address(0), "IPTokenStaking: zero input address");
172
+ emit SetWithdrawalAddress({
173
+ delegator: msg.sender,
174
+ executionAddress: bytes32(uint256(uint160(newWithdrawalAddress))) // left-padded bytes32 of the address
175
+ });
176
+ }
177
+
178
+ /// @notice Set/Update the withdrawal address that receives the stake and reward withdrawals.
179
+ /// @dev To prevent spam, only delegators with stake can call this function with cool-down time.
180
+ /// @param newRewardsAddress EVM address to receive the stake and reward withdrawals.
181
+ function setRewardsAddress(address newRewardsAddress) external payable chargesFee {
182
+ require(newRewardsAddress != address(0), "IPTokenStaking: zero input address");
183
+ emit SetRewardAddress({
184
+ delegator: msg.sender,
185
+ executionAddress: bytes32(uint256(uint160(newRewardsAddress))) // left-padded bytes32 of the address
186
+ });
187
+ }
188
+
189
+ /*//////////////////////////////////////////////////////////////////////////
190
+ // Validator Creation //
191
+ //////////////////////////////////////////////////////////////////////////*/
192
+
193
+ /// @notice Entry point for creating a new validator with self delegation.
194
+ /// @dev The caller must provide the compressed public key that matches the expected EVM address.
195
+ /// Use this method to make sure the caller is the owner of the validator.
196
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
197
+ /// @param moniker The moniker of the validator.
198
+ /// @param commissionRate The commission rate of the validator.
199
+ /// @param maxCommissionRate The maximum commission rate of the validator.
200
+ /// @param maxCommissionChangeRate The maximum commission change rate of the validator.
201
+ /// @param supportsUnlocked Whether the validator supports unlocked staking.
202
+ /// @param data Additional data for the validator.
203
+ function createValidator(
204
+ bytes calldata validatorCmpPubkey,
205
+ string calldata moniker,
206
+ uint32 commissionRate,
207
+ uint32 maxCommissionRate,
208
+ uint32 maxCommissionChangeRate,
209
+ bool supportsUnlocked,
210
+ bytes calldata data
211
+ ) external payable verifyCmpPubkeyWithExpectedAddress(validatorCmpPubkey, msg.sender) nonReentrant {
212
+ _createValidator(
213
+ validatorCmpPubkey,
214
+ moniker,
215
+ commissionRate,
216
+ maxCommissionRate,
217
+ maxCommissionChangeRate,
218
+ supportsUnlocked,
219
+ data
220
+ );
221
+ }
222
+
223
+ /// @dev Validator is the delegator when creating a new validator (self-delegation).
224
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
225
+ /// @param moniker The moniker of the validator.
226
+ /// @param commissionRate The commission rate of the validator.
227
+ /// @param maxCommissionRate The maximum commission rate of the validator.
228
+ /// @param maxCommissionChangeRate The maximum commission change rate of the validator.
229
+ /// @param supportsUnlocked Whether the validator supports unlocked staking.
230
+ /// @param data Additional data for the validator.
231
+ function _createValidator(
232
+ bytes calldata validatorCmpPubkey,
233
+ string memory moniker,
234
+ uint32 commissionRate,
235
+ uint32 maxCommissionRate,
236
+ uint32 maxCommissionChangeRate,
237
+ bool supportsUnlocked,
238
+ bytes calldata data
239
+ ) internal {
240
+ (uint256 stakeAmount, uint256 remainder) = roundedStakeAmount(msg.value);
241
+ require(stakeAmount >= minStakeAmount, "IPTokenStaking: Stake amount under min");
242
+ require(commissionRate >= minCommissionRate, "IPTokenStaking: Commission rate under min");
243
+ require(commissionRate <= maxCommissionRate, "IPTokenStaking: Commission rate over max");
244
+ require(bytes(moniker).length <= MAX_MONIKER_LENGTH, "IPTokenStaking: Moniker length over max");
245
+ require(data.length <= MAX_DATA_LENGTH, "IPTokenStaking: Data length over max");
246
+
247
+ payable(address(0)).transfer(stakeAmount);
248
+ emit CreateValidator(
249
+ validatorCmpPubkey,
250
+ moniker,
251
+ stakeAmount,
252
+ commissionRate,
253
+ maxCommissionRate,
254
+ maxCommissionChangeRate,
255
+ supportsUnlocked ? 1 : 0,
256
+ msg.sender,
257
+ data
258
+ );
259
+ if (remainder > 0) {
260
+ _refundRemainder(remainder);
261
+ }
262
+ }
263
+
264
+ /*//////////////////////////////////////////////////////////////////////////
265
+ // Validator Config //
266
+ //////////////////////////////////////////////////////////////////////////*/
267
+
268
+ /// @notice Update the commission rate of a validator.
269
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
270
+ /// @param commissionRate The new commission rate of the validator.
271
+ function updateValidatorCommission(
272
+ bytes calldata validatorCmpPubkey,
273
+ uint32 commissionRate
274
+ ) external payable chargesFee verifyCmpPubkeyWithExpectedAddress(validatorCmpPubkey, msg.sender) {
275
+ require(commissionRate >= minCommissionRate, "IPTokenStaking: Commission rate under min");
276
+ emit UpdateValidatorCommission(validatorCmpPubkey, commissionRate);
277
+ }
278
+
279
+ /*//////////////////////////////////////////////////////////////////////////
280
+ // Token Staking //
281
+ //////////////////////////////////////////////////////////////////////////*/
282
+
283
+ /// @notice Entry point to stake (delegate) to the given validator. The consensus client (CL) is notified of
284
+ /// the deposit and manages the stake accounting and validator onboarding. Payer must be the delegator.
285
+ /// @dev Staking burns tokens in Execution Layer (EL). Unstaking (withdrawal) will trigger minting through
286
+ /// withdrawal queue.
287
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
288
+ /// @param stakingPeriod The staking period.
289
+ /// @param data Additional data for the stake.
290
+ /// @return delegationId The delegation ID, always 0 for flexible staking.
291
+ function stake(
292
+ bytes calldata validatorCmpPubkey,
293
+ IIPTokenStaking.StakingPeriod stakingPeriod,
294
+ bytes calldata data
295
+ ) external payable nonReentrant returns (uint256 delegationId) {
296
+ return _stake(msg.sender, validatorCmpPubkey, stakingPeriod, data);
297
+ }
298
+
299
+ /// @notice Entry point for staking IP token to stake to the given validator. The consensus chain is notified of
300
+ /// the stake and manages the stake accounting and validator onboarding. Payer can stake on behalf of another user,
301
+ /// who will be the beneficiary of the stake.
302
+ /// @dev Staking burns tokens in Execution Layer (EL). Unstaking (withdrawal) will trigger minting through
303
+ /// withdrawal queue.
304
+ /// @param delegator The delegator's address
305
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
306
+ /// @param stakingPeriod The staking period.
307
+ /// @param data Additional data for the stake.
308
+ /// @return delegationId The delegation ID, always 0 for flexible staking.
309
+ function stakeOnBehalf(
310
+ address delegator,
311
+ bytes calldata validatorCmpPubkey,
312
+ IIPTokenStaking.StakingPeriod stakingPeriod,
313
+ bytes calldata data
314
+ ) external payable nonReentrant returns (uint256 delegationId) {
315
+ return _stake(delegator, validatorCmpPubkey, stakingPeriod, data);
316
+ }
317
+
318
+ /// @dev Creates a validator (x/staking.MsgCreateValidator) if it does not exist. Then delegates the stake to the
319
+ /// validator (x/staking.MsgDelegate).
320
+ /// @param delegator The delegator's address
321
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
322
+ /// @param stakingPeriod The staking period.
323
+ /// @param data Additional data for the stake.
324
+ /// @return delegationId The delegation ID, always 0 for flexible staking.
325
+ function _stake(
326
+ address delegator,
327
+ bytes calldata validatorCmpPubkey,
328
+ IIPTokenStaking.StakingPeriod stakingPeriod,
329
+ bytes calldata data
330
+ ) internal verifyCmpPubkey(validatorCmpPubkey) returns (uint256) {
331
+ require(delegator != address(0), "IPTokenStaking: invalid delegator");
332
+ require(data.length <= MAX_DATA_LENGTH, "IPTokenStaking: Data length over max");
333
+ // This can't be tested from Foundry (Solidity), but can be triggered from js/rpc
334
+ require(stakingPeriod <= IIPTokenStaking.StakingPeriod.LONG, "IPTokenStaking: Invalid staking period");
335
+ (uint256 stakeAmount, uint256 remainder) = roundedStakeAmount(msg.value);
336
+ require(stakeAmount >= minStakeAmount, "IPTokenStaking: Stake amount under min");
337
+
338
+ uint256 delegationId = 0;
339
+ if (stakingPeriod != IIPTokenStaking.StakingPeriod.FLEXIBLE) {
340
+ delegationId = ++_delegationIdCounter;
341
+ }
342
+ emit Deposit(delegator, validatorCmpPubkey, stakeAmount, uint8(stakingPeriod), delegationId, msg.sender, data);
343
+ // We burn staked tokens
344
+ payable(address(0)).transfer(stakeAmount);
345
+
346
+ if (remainder > 0) {
347
+ _refundRemainder(remainder);
348
+ }
349
+
350
+ return delegationId;
351
+ }
352
+
353
+ /// @notice Entry point for redelegating the stake to another validator.
354
+ /// @dev For non flexible staking, your staking period will continue as is.
355
+ /// @dev For locked tokens, this will fail in CL if the validator doesn't support unlocked staking.
356
+ /// @param validatorSrcCmpPubkey 33 bytes compressed secp256k1 public key of source validator.
357
+ /// @param validatorDstCmpPubkey 33 bytes compressed secp256k1 public key of destination validator.
358
+ /// @param delegationId The delegation ID, 0 for flexible staking.
359
+ /// @param amount The amount of stake to redelegate.
360
+ function redelegate(
361
+ bytes calldata validatorSrcCmpPubkey,
362
+ bytes calldata validatorDstCmpPubkey,
363
+ uint256 delegationId,
364
+ uint256 amount
365
+ ) external payable chargesFee {
366
+ _redelegate(msg.sender, validatorSrcCmpPubkey, validatorDstCmpPubkey, delegationId, amount);
367
+ }
368
+
369
+ /// @notice Entry point for redelegating the stake to another validator on behalf of the delegator.
370
+ /// @dev For non flexible staking, your staking period will continue as is.
371
+ /// @dev For locked tokens, this will fail in CL if the validator doesn't support unlocked staking.
372
+ /// @dev Caller must be the operator for the delegator, set via `setOperator`. The operator check is done in CL, so
373
+ /// this method will succeed even if the caller is not the operator (but will fail in CL).
374
+ /// @param delegator The delegator's address
375
+ /// @param validatorSrcCmpPubkey 33 bytes compressed secp256k1 public key of source validator.
376
+ /// @param validatorDstCmpPubkey 33 bytes compressed secp256k1 public key of destination validator.
377
+ /// @param delegationId The delegation ID, 0 for flexible staking.
378
+ /// @param amount The amount of stake to redelegate.
379
+ function redelegateOnBehalf(
380
+ address delegator,
381
+ bytes calldata validatorSrcCmpPubkey,
382
+ bytes calldata validatorDstCmpPubkey,
383
+ uint256 delegationId,
384
+ uint256 amount
385
+ ) external payable chargesFee {
386
+ _redelegate(delegator, validatorSrcCmpPubkey, validatorDstCmpPubkey, delegationId, amount);
387
+ }
388
+
389
+ function _redelegate(
390
+ address delegator,
391
+ bytes calldata validatorSrcCmpPubkey,
392
+ bytes calldata validatorDstCmpPubkey,
393
+ uint256 delegationId,
394
+ uint256 amount
395
+ ) private verifyCmpPubkey(validatorSrcCmpPubkey) verifyCmpPubkey(validatorDstCmpPubkey) {
396
+ require(delegator != address(0), "IPTokenStaking: Invalid delegator");
397
+ require(
398
+ keccak256(validatorSrcCmpPubkey) != keccak256(validatorDstCmpPubkey),
399
+ "IPTokenStaking: Redelegating to same validator"
400
+ );
401
+ require(delegationId <= _delegationIdCounter, "IPTokenStaking: Invalid delegation id");
402
+ (uint256 stakeAmount, ) = roundedStakeAmount(amount);
403
+ require(stakeAmount >= minStakeAmount, "IPTokenStaking: Stake amount under min");
404
+
405
+ emit Redelegate(delegator, validatorSrcCmpPubkey, validatorDstCmpPubkey, delegationId, msg.sender, stakeAmount);
406
+ }
407
+
408
+ /// @notice Returns the rounded stake amount and the remainder.
409
+ /// @param rawAmount The raw stake amount.
410
+ /// @return amount The rounded stake amount.
411
+ /// @return remainder The remainder of the stake amount.
412
+ function roundedStakeAmount(uint256 rawAmount) public pure returns (uint256 amount, uint256 remainder) {
413
+ remainder = rawAmount % STAKE_ROUNDING;
414
+ amount = rawAmount - remainder;
415
+ }
416
+
417
+ /*//////////////////////////////////////////////////////////////////////////
418
+ // Unstake //
419
+ //////////////////////////////////////////////////////////////////////////*/
420
+
421
+ /// @notice Entry point for unstaking the previously staked token.
422
+ /// @dev Unstake (withdrawal) will trigger native minting, so token in this contract is considered as burned.
423
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
424
+ /// @param delegationId The delegation ID, 0 for flexible staking.
425
+ /// @param amount Token amount to unstake.
426
+ /// @param data Additional data for the unstake.
427
+ function unstake(
428
+ bytes calldata validatorCmpPubkey,
429
+ uint256 delegationId,
430
+ uint256 amount,
431
+ bytes calldata data
432
+ ) external payable chargesFee {
433
+ _unstake(msg.sender, validatorCmpPubkey, delegationId, amount, data);
434
+ }
435
+
436
+ /// @notice Entry point for unstaking the previously staked token on behalf of the delegator.
437
+ /// NOTE: If the amount is not divisible by STAKE_ROUNDING, it will be rounded down.
438
+ /// @dev Caller must be the operator for the delegator, set via `setOperator`. The operator check is done in CL, so
439
+ /// this method will succeed even if the caller is not the operator (but will fail in CL)
440
+ /// @param delegator The delegator's address
441
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
442
+ /// @param delegationId The delegation ID, 0 for flexible staking.
443
+ /// @param amount Token amount to unstake. This amount will be rounded to STAKE_ROUNDING.
444
+ /// @param data Additional data for the unstake.
445
+ function unstakeOnBehalf(
446
+ address delegator,
447
+ bytes calldata validatorCmpPubkey,
448
+ uint256 delegationId,
449
+ uint256 amount,
450
+ bytes calldata data
451
+ ) external payable chargesFee {
452
+ _unstake(delegator, validatorCmpPubkey, delegationId, amount, data);
453
+ }
454
+
455
+ function _unstake(
456
+ address delegator,
457
+ bytes calldata validatorCmpPubkey,
458
+ uint256 delegationId,
459
+ uint256 amount,
460
+ bytes calldata data
461
+ ) private verifyCmpPubkey(validatorCmpPubkey) {
462
+ require(delegationId <= _delegationIdCounter, "IPTokenStaking: Invalid delegation id");
463
+ (uint256 unstakeAmount, ) = roundedStakeAmount(amount);
464
+ require(unstakeAmount >= minUnstakeAmount, "IPTokenStaking: Unstake amount under min");
465
+ require(data.length <= MAX_DATA_LENGTH, "IPTokenStaking: Data length over max");
466
+
467
+ emit Withdraw(delegator, validatorCmpPubkey, unstakeAmount, delegationId, msg.sender, data);
468
+ }
469
+
470
+ /*//////////////////////////////////////////////////////////////////////////
471
+ // Unjail //
472
+ //////////////////////////////////////////////////////////////////////////*/
473
+
474
+ /// @notice Requests to unjail the validator. Caller must pay a fee to prevent spamming.
475
+ /// Fee must be exact amount.
476
+ /// @param data Additional data for the unjail.
477
+ function unjail(
478
+ bytes calldata validatorCmpPubkey,
479
+ bytes calldata data
480
+ ) external payable chargesFee verifyCmpPubkeyWithExpectedAddress(validatorCmpPubkey, msg.sender) {
481
+ require(data.length <= MAX_DATA_LENGTH, "IPTokenStaking: Data length over max");
482
+ emit Unjail(msg.sender, validatorCmpPubkey, data);
483
+ }
484
+
485
+ /// @notice Requests to unjail the validator on behalf of the delegator.
486
+ /// @dev Must be an approved operator for the delegator.
487
+ /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key of validator.
488
+ /// @param data Additional data for the unjail.
489
+ function unjailOnBehalf(
490
+ bytes calldata validatorCmpPubkey,
491
+ bytes calldata data
492
+ ) external payable chargesFee verifyCmpPubkey(validatorCmpPubkey) {
493
+ require(data.length <= MAX_DATA_LENGTH, "IPTokenStaking: Data length over max");
494
+ emit Unjail(msg.sender, validatorCmpPubkey, data);
495
+ }
496
+
497
+ /*//////////////////////////////////////////////////////////////////////////
498
+ // Helpers //
499
+ //////////////////////////////////////////////////////////////////////////*/
500
+
501
+ /// @dev Refunds the remainder of the stake amount to the msg sender.
502
+ /// WARNING: Methods using this function should have nonReentrant modifier
503
+ /// to prevent potential reentrancy attacks.
504
+ /// @param remainder The remainder of the stake amount.
505
+ function _refundRemainder(uint256 remainder) private {
506
+ (bool success, ) = msg.sender.call{ value: remainder }("");
507
+ require(success, "IPTokenStaking: Failed to refund remainder");
508
+ }
509
+ }
@@ -0,0 +1,109 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ pragma solidity 0.8.23;
3
+
4
+ import { EllipticCurve } from "elliptic-curve-solidity/contracts/EllipticCurve.sol";
5
+
6
+ /**
7
+ * @title Secp256k1Verifier
8
+ * @notice Utility functions for secp256k1 public key verification
9
+ */
10
+ abstract contract Secp256k1Verifier {
11
+ /// @notice Curve parameter a
12
+ uint256 public constant AA = 0;
13
+
14
+ /// @notice Curve parameter b
15
+ uint256 public constant BB = 7;
16
+
17
+ /// @notice Prime field modulus
18
+ uint256 public constant PP = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F;
19
+
20
+ /// @notice Verifies that the syntax of the given public key is a 33 byte compressed secp256k1 public key.
21
+ modifier verifyCmpPubkey(bytes calldata cmpPubkey) {
22
+ bytes memory uncmpPubkey = _uncompressPublicKey(cmpPubkey);
23
+ _verifyUncmpPubkey(uncmpPubkey);
24
+ _;
25
+ }
26
+
27
+ /// @notice Verifies that the given 33 byte compressed secp256k1 public key is valid and
28
+ /// matches the expected EVM address.
29
+ modifier verifyCmpPubkeyWithExpectedAddress(bytes calldata cmpPubkey, address expectedAddress) {
30
+ bytes memory uncmpPubkey = _uncompressPublicKey(cmpPubkey);
31
+ _verifyUncmpPubkey(uncmpPubkey);
32
+ require(
33
+ _uncmpPubkeyToAddress(uncmpPubkey) == expectedAddress,
34
+ "Secp256k1Verifier: Invalid pubkey derived address"
35
+ );
36
+ _;
37
+ }
38
+
39
+ /// @notice Verifies that the given public key is a 33 byte compressed secp256k1 public key on the curve.
40
+ /// @param cmpPubkey The compressed 33-byte public key to validate
41
+ function _verifyCmpPubkey(bytes memory cmpPubkey) internal pure {
42
+ bytes memory uncmpPubkey = _uncompressPublicKey(cmpPubkey);
43
+ _verifyUncmpPubkey(uncmpPubkey);
44
+ }
45
+
46
+ /// @notice Uncompress a compressed 33-byte Secp256k1 public key.
47
+ /// @dev Uses EllipticCurve.deriveY to recover the Y coordinate
48
+ function _uncompressPublicKey(bytes memory cmpPubkey) internal pure returns (bytes memory) {
49
+ require(cmpPubkey.length == 33, "Secp256k1Verifier: Invalid cmp pubkey length");
50
+ require(cmpPubkey[0] == 0x02 || cmpPubkey[0] == 0x03, "Secp256k1Verifier: Invalid cmp pubkey prefix");
51
+
52
+ // Extract X coordinate
53
+ uint256 x;
54
+ assembly {
55
+ x := mload(add(cmpPubkey, 0x21))
56
+ }
57
+ uint8 prefix = uint8(cmpPubkey[0]);
58
+ // Derive Y coordinate
59
+ uint256 y = EllipticCurve.deriveY(prefix, x, AA, BB, PP);
60
+
61
+ // Construct uncompressed key
62
+ bytes memory uncmpPubkey = new bytes(65);
63
+ uncmpPubkey[0] = 0x04;
64
+ assembly {
65
+ mstore(add(uncmpPubkey, 0x21), x)
66
+ mstore(add(uncmpPubkey, 0x41), y)
67
+ }
68
+ return uncmpPubkey;
69
+ }
70
+
71
+ /// @notice Verifies that the given public key is a 65 byte uncompressed secp256k1 public key on the curve.
72
+ /// @param uncmpPubkey The uncompressed 65-byte public key to validate
73
+ function _verifyUncmpPubkey(bytes memory uncmpPubkey) internal pure {
74
+ require(uncmpPubkey.length == 65, "Secp256k1Verifier: Invalid uncmp pubkey length");
75
+ require(uncmpPubkey[0] == 0x04, "Secp256k1Verifier: Invalid uncmp pubkey prefix");
76
+
77
+ // Extract x and y coordinates
78
+ uint256 x;
79
+ uint256 y;
80
+ assembly {
81
+ x := mload(add(uncmpPubkey, 0x21))
82
+ y := mload(add(uncmpPubkey, 0x41))
83
+ }
84
+
85
+ // Verify the derived point lies on the curve
86
+ require(EllipticCurve.isOnCurve(x, y, AA, BB, PP), "Secp256k1Verifier: pubkey not on curve");
87
+ }
88
+
89
+ /// @notice Converts the given public key to an EVM address.
90
+ /// @dev Assume all calls to this function passes in the uncompressed public key.
91
+ /// @param uncmpPubkey 65 bytes uncompressed secp256k1 public key, with prefix 04.
92
+ /// @return address The EVM address derived from the public key.
93
+ function _uncmpPubkeyToAddress(bytes memory uncmpPubkey) internal pure returns (address) {
94
+ // Create a new bytes memory array with length 64 (65-1 to skip prefix)
95
+ bytes memory pubkeyNoPrefix = new bytes(64);
96
+
97
+ // Copy bytes after prefix using assembly
98
+ assembly {
99
+ // Copy 64 bytes starting from position 1 of input
100
+ // to position 0 of output
101
+ let srcPtr := add(add(uncmpPubkey, 0x20), 1) // Skip first byte
102
+ let destPtr := add(pubkeyNoPrefix, 0x20)
103
+ mstore(destPtr, mload(srcPtr))
104
+ mstore(add(destPtr, 0x20), mload(add(srcPtr, 0x20)))
105
+ }
106
+
107
+ return address(uint160(uint256(keccak256(pubkeyNoPrefix))));
108
+ }
109
+ }