@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.
- package/README.md +65 -0
- package/index.js +43 -0
- package/package.json +51 -0
- package/script/GenerateAlloc.s.sol +448 -0
- package/script/upgrades/DeployNewIPTokenStaking_V1_0_1.s.sol +42 -0
- package/script/utils/ChainIds.sol +12 -0
- package/script/utils/EIP1967Helper.sol +37 -0
- package/script/utils/InitializableHelper.sol +66 -0
- package/src/deploy/Create3.sol +62 -0
- package/src/interfaces/IIPTokenStaking.sol +303 -0
- package/src/interfaces/IUBIPool.sol +37 -0
- package/src/interfaces/IUpgradeEntrypoint.sol +33 -0
- package/src/libraries/Predeploys.sol +50 -0
- package/src/protocol/IPTokenStaking.sol +509 -0
- package/src/protocol/Secp256k1Verifier.sol +109 -0
- package/src/protocol/UBIPool.sol +98 -0
- package/src/protocol/UpgradeEntrypoint.sol +40 -0
- package/src/token/WIP.sol +90 -0
- package/test/data/ValidatorData.sol +122 -0
- package/test/deploy/Create3.t.sol +63 -0
- package/test/erc6551/Erc6551Registry.t.sol +33 -0
- package/test/secp256k/Secp256k1PubKeyVerifier.t.sol +50 -0
- package/test/stake/IPTokenStaking.t.sol +1022 -0
- package/test/timelock/Timelock.t.sol +136 -0
- package/test/token/WIP.t.sol +198 -0
- package/test/ubipool/UBIPool.t.sol +239 -0
- package/test/upgrade-entrypoint/UpgradeEntryPoint.t.sol +37 -0
- package/test/upgrades/PredeployUpgrades.t.sol +219 -0
- package/test/utils/Test.sol +170 -0
@@ -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
|
+
}
|