@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.
@@ -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.365",
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.365",
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": "e499a31e1f0533416eb611c2c026a79b2dc1dead"
60
+ "gitHead": "ed5b2f3718b5da967d60c30fc589bdf20568f992"
61
61
  }