Skip to content

Commit dc885cb

Browse files
feat: Implement Allowlist contract for operator management
- Add Allowlist contract to replace TokenStaking functionality - Implement weight-based operator management without token staking - Add deployment and initialization scripts - Include consolidation script for operator reduction (20→4 operators) - Add comprehensive test coverage - Maintain compatibility with existing WalletRegistry interface
1 parent 30b0410 commit dc885cb

File tree

6 files changed

+1047
-12
lines changed

6 files changed

+1047
-12
lines changed

solidity/ecdsa/contracts/Allowlist.sol

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
1919
import "./WalletRegistry.sol";
2020

2121
/// @title Allowlist
22-
/// @notice The allowlist contract replaces the Threshold TokenStaking contract
23-
/// and is as an outcome of TIP-092 and TIP-100 governance decisions.
22+
/// @notice The allowlist contract replaces the Threshold TokenStaking contract.
2423
/// Staking tokens is no longer required to operate nodes. Beta stakers
25-
/// are selected by the DAO and operate the network based on the
26-
/// allowlist maintained by the DAO.
24+
/// operate the network based on the allowlist maintained by governance.
2725
/// @dev The allowlist contract maintains the maximum possible compatibility
2826
/// with the old TokenStaking contract interface, as utilized by the
2927
/// WalletRegistry contract.
@@ -160,8 +158,7 @@ contract Allowlist is Ownable2StepUpgradeable {
160158

161159
/// @notice Returns the current weight of the staking provider.
162160
/// @dev The function signature maintains compatibility with Threshold
163-
/// TokenStaking contract to minimize the TIP-092 impact on the
164-
/// WalletRegistry contract.
161+
/// TokenStaking contract interface.
165162
function authorizedStake(address stakingProvider, address)
166163
external
167164
view
@@ -170,11 +167,10 @@ contract Allowlist is Ownable2StepUpgradeable {
170167
return stakingProviders[stakingProvider].weight;
171168
}
172169

173-
/// @notice No-op stake seize operation. After TIP-092 tokens are not staked
170+
/// @notice No-op stake seize operation. Tokens are not staked
174171
/// so there is nothing to seize from.
175172
/// @dev The function signature maintains compatibility with Threshold
176-
/// TokenStaking contract to minimize the TIP-092 impact on the
177-
/// WalletRegistry contract.
173+
/// TokenStaking contract interface.
178174
function seize(
179175
uint96,
180176
uint256,
@@ -185,13 +181,12 @@ contract Allowlist is Ownable2StepUpgradeable {
185181
}
186182

187183
/// @notice Returns the stake owner, beneficiary, and authorizer roles for
188-
/// the given staking provider. After TIP-092 those roles are no
184+
/// the given staking provider. Those roles are no
189185
/// longer relevant as no tokens are staked. The owner is set to the
190186
/// allowlist owner, the beneficiary is the staking provider itself
191187
/// and the authorizer is the zero address.
192188
/// @dev The function signature maintains compatibility with Threshold
193-
/// TokenStaking contract to minimize the TIP-092 impact on the
194-
/// WalletRegistry contract.
189+
/// TokenStaking contract interface.
195190
function rolesOf(address stakingProvider)
196191
external
197192
view
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { HardhatRuntimeEnvironment } from "hardhat/types"
2+
import type { DeployFunction } from "hardhat-deploy/types"
3+
4+
const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
5+
const { getNamedAccounts, deployments, ethers, helpers } = hre
6+
7+
const { deployer, governance } = await getNamedAccounts()
8+
9+
// Get the WalletRegistry deployment - this will be replaced by Allowlist
10+
// but we still need it for initialization
11+
const WalletRegistry = await deployments.get("WalletRegistry")
12+
13+
// Deploy the Allowlist contract using upgradeable proxy pattern
14+
const [allowlist, proxyDeployment] = await helpers.upgrades.deployProxy(
15+
"Allowlist",
16+
{
17+
initializerArgs: [WalletRegistry.address],
18+
factoryOpts: {
19+
signer: await ethers.getSigner(deployer),
20+
},
21+
proxyOpts: {
22+
kind: "transparent",
23+
},
24+
}
25+
)
26+
27+
// Transfer ownership to governance if specified and different from deployer
28+
if (governance && governance !== deployer) {
29+
await helpers.ownable.transferOwnership("Allowlist", governance, deployer)
30+
}
31+
32+
// Log deployment information
33+
console.log(`Allowlist deployed at: ${allowlist.address}`)
34+
console.log(
35+
`Allowlist proxy admin: ${proxyDeployment.receipt.contractAddress}`
36+
)
37+
console.log(`Allowlist owner: ${await allowlist.owner()}`)
38+
39+
return true
40+
}
41+
42+
export default func
43+
44+
func.tags = ["Allowlist"]
45+
func.dependencies = ["WalletRegistry"]
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import type { HardhatRuntimeEnvironment } from "hardhat/types"
2+
import type { DeployFunction } from "hardhat-deploy/types"
3+
4+
const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
5+
const { getNamedAccounts, deployments, ethers } = hre
6+
const { deployer, governance } = await getNamedAccounts()
7+
8+
// Get contract instances
9+
const allowlistDeployment = await deployments.get("Allowlist")
10+
const walletRegistryDeployment = await deployments.get("WalletRegistry")
11+
const tokenStakingDeployment = await deployments.get("TokenStaking")
12+
13+
const allowlist = await ethers.getContractAt(
14+
"Allowlist",
15+
allowlistDeployment.address
16+
)
17+
const walletRegistry = await ethers.getContractAt(
18+
"WalletRegistry",
19+
walletRegistryDeployment.address
20+
)
21+
const tokenStaking = await ethers.getContractAt(
22+
"TokenStaking",
23+
tokenStakingDeployment.address
24+
)
25+
26+
// Get the governance signer (owner of Allowlist)
27+
const governanceSigner = await ethers.getSigner(governance || deployer)
28+
29+
console.log("Starting beta staker migration to Allowlist...")
30+
31+
// Query all existing beta stakers from WalletRegistry
32+
// We'll look for AuthorizationIncreased events to find all staking providers
33+
const authorizationFilter = walletRegistry.filters.AuthorizationIncreased()
34+
const authorizationEvents = await walletRegistry.queryFilter(
35+
authorizationFilter
36+
)
37+
38+
// Extract unique staking providers
39+
const stakingProviders = new Set<string>()
40+
for (const event of authorizationEvents) {
41+
if (event.args && event.args.stakingProvider) {
42+
stakingProviders.add(event.args.stakingProvider)
43+
}
44+
}
45+
46+
console.log(`Found ${stakingProviders.size} unique staking providers`)
47+
48+
// For each staking provider, get their current authorized stake and add to Allowlist
49+
const migrationResults = []
50+
51+
for (const stakingProvider of stakingProviders) {
52+
try {
53+
// Get current authorized stake from TokenStaking
54+
const authorizedStake = await tokenStaking.authorizedStake(
55+
stakingProvider,
56+
walletRegistry.address
57+
)
58+
59+
if (authorizedStake.gt(0)) {
60+
console.log(
61+
`Migrating staking provider ${stakingProvider} with weight ${ethers.utils.formatEther(
62+
authorizedStake
63+
)} T`
64+
)
65+
66+
// Add to Allowlist with current weight
67+
const tx = await allowlist
68+
.connect(governanceSigner)
69+
.addStakingProvider(stakingProvider, authorizedStake)
70+
await tx.wait()
71+
72+
migrationResults.push({
73+
stakingProvider,
74+
weight: authorizedStake,
75+
status: "success",
76+
})
77+
78+
console.log(`✓ Successfully migrated ${stakingProvider}`)
79+
} else {
80+
console.log(`Skipping ${stakingProvider} - no authorized stake`)
81+
migrationResults.push({
82+
stakingProvider,
83+
weight: authorizedStake,
84+
status: "skipped - no stake",
85+
})
86+
}
87+
} catch (error) {
88+
console.error(`✗ Failed to migrate ${stakingProvider}:`, error.message)
89+
migrationResults.push({
90+
stakingProvider,
91+
weight: "0",
92+
status: `failed: ${error.message}`,
93+
})
94+
}
95+
}
96+
97+
// Summary
98+
const successful = migrationResults.filter(
99+
(r) => r.status === "success"
100+
).length
101+
const failed = migrationResults.filter((r) =>
102+
r.status.startsWith("failed")
103+
).length
104+
const skipped = migrationResults.filter((r) =>
105+
r.status.startsWith("skipped")
106+
).length
107+
108+
console.log("\n=== Migration Summary ===")
109+
console.log(`Total staking providers found: ${stakingProviders.size}`)
110+
console.log(`Successfully migrated: ${successful}`)
111+
console.log(`Failed: ${failed}`)
112+
console.log(`Skipped: ${skipped}`)
113+
114+
// Save migration results to file for record keeping
115+
const fs = require("fs")
116+
const path = require("path")
117+
const resultsPath = path.join(__dirname, "../migration-results.json")
118+
fs.writeFileSync(
119+
resultsPath,
120+
JSON.stringify(
121+
{
122+
timestamp: new Date().toISOString(),
123+
network: hre.network.name,
124+
results: migrationResults,
125+
},
126+
null,
127+
2
128+
)
129+
)
130+
131+
console.log(`Migration results saved to: ${resultsPath}`)
132+
133+
if (failed > 0) {
134+
console.warn(
135+
`⚠️ Migration completed with ${failed} failures. Please review the results.`
136+
)
137+
} else {
138+
console.log("✅ Migration completed successfully!")
139+
}
140+
141+
return true
142+
}
143+
144+
export default func
145+
146+
func.tags = ["InitializeAllowlistWeights"]
147+
func.dependencies = ["Allowlist"]
148+
149+
// Only run this script when explicitly requested
150+
func.skip = async (hre) => !process.env.MIGRATE_ALLOWLIST_WEIGHTS

solidity/ecdsa/docs/Allowlist.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Allowlist Contract
2+
3+
The Allowlist contract replaces the TokenStaking contract, enabling governance-controlled beta staker selection without requiring token staking.
4+
5+
## Overview
6+
7+
The Allowlist contract maintains compatibility with the existing WalletRegistry interface while providing a simpler, governance-controlled mechanism for managing beta stakers. Instead of requiring tokens to be staked, governance directly controls which operators can participate in the network and their respective weights.
8+
9+
## Key Features
10+
11+
- **Governance Control**: Only the contract owner (governance) can add staking providers or modify their weights
12+
- **Weight Management**: Supports weight decrease operations with the same delay mechanism as TokenStaking
13+
- **WalletRegistry Compatible**: Implements the same interface methods used by WalletRegistry
14+
- **Beta Staker Consolidation**: Enables setting operator weights to zero for gradual consolidation
15+
- **No Token Requirements**: Removes the need for operators to stake tokens
16+
17+
## Contract Functions
18+
19+
### Core Management
20+
21+
#### `addStakingProvider(address stakingProvider, uint96 weight)`
22+
- **Access**: Owner only
23+
- **Purpose**: Add a new staking provider with specified weight
24+
- **Effects**: Calls `authorizationIncreased` on WalletRegistry
25+
- **Reverts**: If provider already exists with non-zero weight
26+
27+
#### `requestWeightDecrease(address stakingProvider, uint96 newWeight)`
28+
- **Access**: Owner only
29+
- **Purpose**: Request weight decrease for existing provider
30+
- **Effects**: Sets pending weight and calls `authorizationDecreaseRequested` on WalletRegistry
31+
- **Notes**: Can set weight to zero for consolidation
32+
33+
#### `approveAuthorizationDecrease(address stakingProvider)`
34+
- **Access**: WalletRegistry only
35+
- **Purpose**: Approve previously requested weight decrease
36+
- **Returns**: New weight value
37+
- **Effects**: Updates provider weight and clears pending value
38+
39+
### Compatibility Interface
40+
41+
#### `authorizedStake(address stakingProvider, address application)`
42+
- **Access**: Public view
43+
- **Purpose**: Return current weight for the provider
44+
- **Notes**: Second parameter (application) is ignored for compatibility
45+
46+
#### `seize(uint96 amount, uint256 rewardMultiplier, address notifier, address[] memory stakingProviders)`
47+
- **Access**: Public
48+
- **Purpose**: No-op seize operation (no tokens to seize)
49+
- **Effects**: Only emits `MaliciousBehaviorIdentified` event
50+
51+
#### `rolesOf(address stakingProvider)`
52+
- **Access**: Public view
53+
- **Returns**: (owner, stakingProvider, address(0))
54+
- **Purpose**: Return roles for compatibility
55+
56+
## Deployment Process
57+
58+
### 1. Deploy Allowlist Contract
59+
```bash
60+
npx hardhat deploy --tags Allowlist
61+
```
62+
63+
### 2. Initialize with Existing Beta Stakers
64+
```bash
65+
MIGRATE_ALLOWLIST_WEIGHTS=true npx hardhat deploy --tags InitializeAllowlistWeights
66+
```
67+
68+
### 3. Update WalletRegistry Integration
69+
Update WalletRegistry constructor to use Allowlist instead of TokenStaking.
70+
71+
## Beta Staker Consolidation Workflow
72+
73+
### Phase 1: Migration
74+
1. Deploy Allowlist contract
75+
2. Initialize with current beta staker weights from TokenStaking
76+
3. Verify all operators are properly migrated
77+
78+
### Phase 2: Consolidation
79+
1. Identify redundant operators for each entity (Boar, P2P, Staked.us)
80+
2. Use weight management script to set redundant operator weights to zero
81+
3. Monitor natural fund drainage as redemptions occur
82+
83+
### Phase 3: Cleanup
84+
1. Verify zero-weight operators have no remaining funds
85+
2. Coordinate with operators to shut down redundant nodes
86+
3. Confirm network operates correctly with ~20 operators instead of ~35
87+
88+
## Beta Staker Consolidation
89+
90+
#### Execute Full Consolidation
91+
```bash
92+
# Check current status
93+
npx hardhat run scripts/consolidate_beta_stakers.ts -- status --allowlist <address>
94+
95+
# Dry run to see what would happen
96+
npx hardhat run scripts/consolidate_beta_stakers.ts -- execute --allowlist <address> --dry-run
97+
98+
# Execute the consolidation (18 → 3 operators)
99+
npx hardhat run scripts/consolidate_beta_stakers.ts -- execute --allowlist <address>
100+
```
101+
102+
## Events
103+
104+
### `StakingProviderAdded(address indexed stakingProvider, uint96 weight)`
105+
Emitted when a new staking provider is added to the allowlist.
106+
107+
### `WeightDecreaseRequested(address indexed stakingProvider, uint96 oldWeight, uint96 newWeight)`
108+
Emitted when a weight decrease is requested.
109+
110+
### `WeightDecreaseFinalized(address indexed stakingProvider, uint96 oldWeight, uint96 newWeight)`
111+
Emitted when a weight decrease is approved and finalized.
112+
113+
### `MaliciousBehaviorIdentified(address notifier, address[] stakingProviders)`
114+
Emitted by the seize function for compatibility (no actual slashing occurs).
115+
116+
## Security Considerations
117+
118+
1. **Owner Control**: The contract owner has complete control over the operator set
119+
2. **No Slashing**: Misbehavior cannot be punished through token slashing
120+
3. **Weight Decreases**: Include delay mechanism to prevent sudden operator removal
121+
4. **Gradual Changes**: Consolidation should be done gradually to maintain network stability
122+
123+
## Integration Notes
124+
125+
- WalletRegistry calls remain the same, ensuring minimal integration changes
126+
- Authorization flow maintains existing delay mechanisms
127+
- Event emissions preserve compatibility with monitoring systems
128+
- No changes required to operator client software

0 commit comments

Comments
 (0)