@towns-protocol/contracts 0.0.365 → 0.0.366
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.
- package/docs/staking_state_machine.md +256 -0
- package/package.json +3 -3
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Staking Position State Machine
|
|
2
|
+
|
|
3
|
+
This document describes the lifecycle of a staking position in the RewardsDistributionV2 contract.
|
|
4
|
+
|
|
5
|
+
## State-Determining Fields
|
|
6
|
+
|
|
7
|
+
A deposit's state is determined by two key properties (once it exists):
|
|
8
|
+
|
|
9
|
+
- **deposit.delegatee**: Address the stake is delegated to (operator or space)
|
|
10
|
+
- **deposit.pendingWithdrawal**: Amount waiting to be withdrawn
|
|
11
|
+
|
|
12
|
+
Note: DepositIds are assigned sequentially starting from 0 when stake() is called. The "Non-existent" state refers to the period before calling stake() to create the deposit.
|
|
13
|
+
|
|
14
|
+
## States
|
|
15
|
+
|
|
16
|
+
### 1. Non-existent
|
|
17
|
+
- **Condition**: Before calling stake()/permitAndStake()/stakeOnBehalf()
|
|
18
|
+
- **Description**: No deposit has been created yet; depositId will be assigned upon staking
|
|
19
|
+
|
|
20
|
+
### 2. Active
|
|
21
|
+
- **Condition**: `delegatee != address(0)` AND `pendingWithdrawal == 0`
|
|
22
|
+
- **Description**: Stake is actively delegated to an operator or space, earning rewards
|
|
23
|
+
- **Key behaviors**:
|
|
24
|
+
- Earning staking rewards based on `deposit.beneficiary`
|
|
25
|
+
- Delegated voting power goes to `deposit.delegatee`
|
|
26
|
+
- Can increase stake, redelegate, or change beneficiary
|
|
27
|
+
|
|
28
|
+
### 3. Withdrawal Initiated
|
|
29
|
+
- **Condition**: `delegatee == address(0)` AND `pendingWithdrawal > 0`
|
|
30
|
+
- **Description**: User has initiated withdrawal, stake is no longer active
|
|
31
|
+
- **Key behaviors**:
|
|
32
|
+
- No longer earning rewards
|
|
33
|
+
- Delegation has been removed
|
|
34
|
+
- Funds held in proxy, waiting for final withdrawal
|
|
35
|
+
- Can redelegate to restake the pending amount
|
|
36
|
+
|
|
37
|
+
### 4. Withdrawn
|
|
38
|
+
- **Condition**: `delegatee == address(0)` AND `pendingWithdrawal == 0` AND deposit exists
|
|
39
|
+
- **Description**: Withdrawal completed, deposit shell remains but inactive
|
|
40
|
+
- **Key behaviors**:
|
|
41
|
+
- No active stake or pending withdrawals
|
|
42
|
+
- Deposit record persists in storage
|
|
43
|
+
- Cannot increase stake (would fail delegatee validation)
|
|
44
|
+
- Could theoretically redelegate with 0 amount
|
|
45
|
+
|
|
46
|
+
## State Transitions
|
|
47
|
+
|
|
48
|
+
### From Non-existent → Active
|
|
49
|
+
|
|
50
|
+
**Functions**: `stake()`, `permitAndStake()`, `stakeOnBehalf()`
|
|
51
|
+
|
|
52
|
+
Creates a new deposit with:
|
|
53
|
+
- New depositId assigned (sequential, starting from 0)
|
|
54
|
+
- Delegatee set to specified operator/space
|
|
55
|
+
- Beneficiary set for rewards
|
|
56
|
+
- Stake amount transferred to new delegation proxy
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Active → Active (State Preserved)
|
|
61
|
+
|
|
62
|
+
#### Increase Stake
|
|
63
|
+
|
|
64
|
+
**Functions**: `increaseStake()`, `permitAndIncreaseStake()`
|
|
65
|
+
|
|
66
|
+
Adds more funds to existing position:
|
|
67
|
+
- Validates existing delegatee is still valid
|
|
68
|
+
- Increases stake amount
|
|
69
|
+
- Updates earning power for rewards
|
|
70
|
+
|
|
71
|
+
#### Redelegate
|
|
72
|
+
|
|
73
|
+
**Function**: `redelegate()` (when `pendingWithdrawal == 0`)
|
|
74
|
+
|
|
75
|
+
Changes delegation target:
|
|
76
|
+
- Validates new delegatee is operator or space
|
|
77
|
+
- Updates rewards with old commission rate
|
|
78
|
+
- Sets new delegatee and commission rate
|
|
79
|
+
- Calls `DelegationProxy.redelegate()` to update on-chain delegation
|
|
80
|
+
|
|
81
|
+
#### Change Beneficiary
|
|
82
|
+
|
|
83
|
+
**Function**: `changeBeneficiary()`
|
|
84
|
+
|
|
85
|
+
Changes who receives rewards:
|
|
86
|
+
- Validates delegatee is still valid
|
|
87
|
+
- Settles existing rewards
|
|
88
|
+
- Transfers earning power to new beneficiary
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### Active → Withdrawal Initiated
|
|
93
|
+
|
|
94
|
+
**Function**: `initiateWithdraw()`
|
|
95
|
+
|
|
96
|
+
Begins withdrawal process:
|
|
97
|
+
- Calls internal withdraw logic that:
|
|
98
|
+
- Sets `delegatee = address(0)`
|
|
99
|
+
- Moves stake amount to `pendingWithdrawal`
|
|
100
|
+
- Claims any pending rewards
|
|
101
|
+
- For external deposits: Calls `DelegationProxy.redelegate(address(0))`
|
|
102
|
+
- For self-owned deposits: Sets `pendingWithdrawal = 0` (immediate withdrawal)
|
|
103
|
+
|
|
104
|
+
**Special Case**: If `owner == address(this)`, the deposit goes directly to Withdrawn state.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Withdrawal Initiated → Withdrawn
|
|
109
|
+
|
|
110
|
+
**Function**: `withdraw()`
|
|
111
|
+
|
|
112
|
+
Completes withdrawal:
|
|
113
|
+
- Validates `pendingWithdrawal > 0`
|
|
114
|
+
- Validates `owner != address(this)` (self-owned deposits cannot withdraw)
|
|
115
|
+
- Sets `pendingWithdrawal = 0`
|
|
116
|
+
- Transfers tokens from proxy to owner
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### Withdrawal Initiated → Active
|
|
121
|
+
|
|
122
|
+
**Function**: `redelegate()` (when `pendingWithdrawal > 0`)
|
|
123
|
+
|
|
124
|
+
Restakes pending withdrawal:
|
|
125
|
+
- Validates new delegatee is operator or space
|
|
126
|
+
- Calls `increaseStake()` with `pendingWithdrawal` amount
|
|
127
|
+
- Sets `deposit.delegatee = newDelegatee`
|
|
128
|
+
- Sets `deposit.pendingWithdrawal = 0`
|
|
129
|
+
- Calls `DelegationProxy.redelegate()` to update delegation
|
|
130
|
+
|
|
131
|
+
This is a special recovery mechanism allowing users to restake without completing withdrawal.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## State Transition Diagram
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
┌─────────────┐
|
|
139
|
+
│ │
|
|
140
|
+
│ Non-existent│
|
|
141
|
+
│ │
|
|
142
|
+
└──────┬──────┘
|
|
143
|
+
│ stake(), permitAndStake(), stakeOnBehalf()
|
|
144
|
+
▼
|
|
145
|
+
┌─────────────────────────────────────────┐
|
|
146
|
+
│ │
|
|
147
|
+
│ Active │
|
|
148
|
+
│ • Earning rewards │
|
|
149
|
+
│ • Delegated to operator/space │
|
|
150
|
+
│ │◄────┐
|
|
151
|
+
└──┬──────────┬───────────────────────┬──┘ │
|
|
152
|
+
│ │ │ │
|
|
153
|
+
│ │ │ │
|
|
154
|
+
│ │ increaseStake() │ │
|
|
155
|
+
│ │ permitAndIncreaseStake() │
|
|
156
|
+
│ │ redelegate() │ │
|
|
157
|
+
│ │ changeBeneficiary() │ │
|
|
158
|
+
│ └───────────────────────┘ │
|
|
159
|
+
│ │
|
|
160
|
+
│ initiateWithdraw() │
|
|
161
|
+
▼ │
|
|
162
|
+
┌─────────────────────────────────────────┐ │
|
|
163
|
+
│ │ │
|
|
164
|
+
│ Withdrawal Initiated │ │
|
|
165
|
+
│ • No rewards │ │
|
|
166
|
+
│ • Funds in proxy, pending withdrawal │ │
|
|
167
|
+
│ │ │
|
|
168
|
+
└──┬──────────────────────────────────┬───┘ │
|
|
169
|
+
│ │ │
|
|
170
|
+
│ withdraw() │ │
|
|
171
|
+
│ │ │
|
|
172
|
+
▼ │ │
|
|
173
|
+
┌─────────────────────────────────────────┐ │
|
|
174
|
+
│ │ │
|
|
175
|
+
│ Withdrawn │ │
|
|
176
|
+
│ • Empty deposit │ │
|
|
177
|
+
│ • No active stake │ │
|
|
178
|
+
│ │ │
|
|
179
|
+
└─────────────────────────────────────────┘ │
|
|
180
|
+
│
|
|
181
|
+
redelegate() ──────────────┘
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Function Availability by State
|
|
185
|
+
|
|
186
|
+
| Function | Non-existent | Active | Withdrawal Initiated | Withdrawn |
|
|
187
|
+
|--------------------------|--------------|--------------------------|----------------------|--------------|
|
|
188
|
+
| stake() | ✅ Creates | ❌ | ❌ | ❌ |
|
|
189
|
+
| permitAndStake() | ✅ Creates | ❌ | ❌ | ❌ |
|
|
190
|
+
| stakeOnBehalf() | ✅ Creates | ❌ | ❌ | ❌ |
|
|
191
|
+
| increaseStake() | ❌ | ✅ | ❌ | ❌ |
|
|
192
|
+
| permitAndIncreaseStake() | ❌ | ✅ | ❌ | ❌ |
|
|
193
|
+
| redelegate() | ❌ | ✅ Same state | ✅ → Active | ⚠️ Edge case |
|
|
194
|
+
| changeBeneficiary() | ❌ | ✅ | ❌ | ❌ |
|
|
195
|
+
| initiateWithdraw() | ❌ | ✅ → Withdrawal Initiated | ❌ | ❌ |
|
|
196
|
+
| withdraw() | ❌ | ❌ | ✅ → Withdrawn | ❌ |
|
|
197
|
+
|
|
198
|
+
## Special Cases
|
|
199
|
+
|
|
200
|
+
### Self-Owned Deposits (owner == address(this))
|
|
201
|
+
|
|
202
|
+
When a deposit is owned by the contract itself:
|
|
203
|
+
|
|
204
|
+
- **No delegation proxy** is used
|
|
205
|
+
- **initiateWithdraw()**: Sets `pendingWithdrawal = 0` immediately (goes straight to Withdrawn)
|
|
206
|
+
- **withdraw()**: Reverts with `RewardsDistribution__CannotWithdrawFromSelf`
|
|
207
|
+
|
|
208
|
+
This is used internally by the protocol for special staking mechanisms.
|
|
209
|
+
|
|
210
|
+
### Redelegate with Pending Withdrawal
|
|
211
|
+
|
|
212
|
+
When `redelegate()` is called on a deposit in "Withdrawal Initiated" state:
|
|
213
|
+
|
|
214
|
+
- Instead of calling `staking.redelegate()`, it calls `staking.increaseStake()`
|
|
215
|
+
- Treats the `pendingWithdrawal` as a new stake increase
|
|
216
|
+
- Manually sets `deposit.delegatee` to the new delegatee
|
|
217
|
+
- Manually sets `deposit.pendingWithdrawal = 0`
|
|
218
|
+
- Effectively "cancels" the withdrawal and restakes to a new delegatee
|
|
219
|
+
|
|
220
|
+
This provides a recovery path without requiring users to complete withdrawal and create a new deposit.
|
|
221
|
+
|
|
222
|
+
## Validation Rules
|
|
223
|
+
|
|
224
|
+
### Owner Validation
|
|
225
|
+
- All state-changing functions require `msg.sender == deposit.owner` (via `_revertIfNotDepositOwner()`)
|
|
226
|
+
|
|
227
|
+
### Delegatee Validation
|
|
228
|
+
- `increaseStake()`, `redelegate()`, `changeBeneficiary()`: Require delegatee to be a valid operator or space
|
|
229
|
+
- Validation happens via `_revertIfNotOperatorOrSpace(delegatee)`
|
|
230
|
+
|
|
231
|
+
### Amount Validation
|
|
232
|
+
- `withdraw()`: Requires `pendingWithdrawal > 0`
|
|
233
|
+
|
|
234
|
+
## Implementation Notes
|
|
235
|
+
|
|
236
|
+
### Reward Settlement
|
|
237
|
+
- Most state transitions trigger reward settlement via the internal `StakingRewards` library
|
|
238
|
+
- `_sweepSpaceRewardsIfNecessary()` is called to transfer space delegation rewards to operators
|
|
239
|
+
|
|
240
|
+
### Delegation Proxy Lifecycle
|
|
241
|
+
- Proxy is deployed when first stake is created (for non-self-owned deposits)
|
|
242
|
+
- Proxy persists through the entire deposit lifecycle
|
|
243
|
+
- Proxy's delegation target is updated via `redelegate()` calls
|
|
244
|
+
- Tokens are transferred from/to proxy during stake/withdrawal operations
|
|
245
|
+
|
|
246
|
+
### Commission Rates
|
|
247
|
+
- Commission rate is fetched at the time of each operation
|
|
248
|
+
- For space delegatees, the commission rate of their active operator is used
|
|
249
|
+
- Rate changes don't affect existing positions until next state transition
|
|
250
|
+
|
|
251
|
+
## References
|
|
252
|
+
|
|
253
|
+
- Main contract: `RewardsDistributionV2.sol` lines 94-242
|
|
254
|
+
- Base implementation: `RewardsDistributionBase.sol`
|
|
255
|
+
- Storage library: `StakingRewards.sol`
|
|
256
|
+
- Proxy implementation: `DelegationProxy.sol`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.366",
|
|
4
4
|
"packageManager": "yarn@3.8.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build-types": "bash scripts/build-contract-types.sh",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@layerzerolabs/oapp-evm": "^0.3.2",
|
|
36
36
|
"@openzeppelin/merkle-tree": "^1.0.8",
|
|
37
37
|
"@prb/test": "^0.6.4",
|
|
38
|
-
"@towns-protocol/prettier-config": "^0.0.
|
|
38
|
+
"@towns-protocol/prettier-config": "^0.0.366",
|
|
39
39
|
"@typechain/ethers-v5": "^11.1.2",
|
|
40
40
|
"@wagmi/cli": "^2.2.0",
|
|
41
41
|
"forge-std": "github:foundry-rs/forge-std#v1.10.0",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"publishConfig": {
|
|
58
58
|
"access": "public"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "ed5b2f3718b5da967d60c30fc589bdf20568f992"
|
|
61
61
|
}
|