Skip to content

Commit a1cb295

Browse files
committed
feat: First version of validatorFactory
1 parent a304688 commit a1cb295

File tree

3 files changed

+58
-214
lines changed

3 files changed

+58
-214
lines changed

foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ optimizer_runs = 200
77

88
# Remappings for OpenZeppelin contracts
99
remappings = [
10-
"@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/",
10+
"@openzeppelin/=node_modules/@openzeppelin/",
1111
"forge-std/=lib/forge-std/src/"
1212
]
1313

src/ValidatorFactory.sol

Lines changed: 49 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ pragma solidity ^0.8.20;
44
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
55
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
66
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7-
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
7+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
8+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
89
import "./ValidatorLogic.sol";
910

1011
/**
@@ -16,6 +17,7 @@ import "./ValidatorLogic.sol";
1617
* Manages staking positions for granular stake tracking.
1718
*/
1819
contract ValidatorFactory is ReentrancyGuard {
20+
using SafeERC20 for IERC20;
1921
// Events
2022
event ValidatorCreated(address indexed validator, address indexed proxy, uint256 stake);
2123
event ValidatorRemoved(address indexed validator);
@@ -41,6 +43,7 @@ contract ValidatorFactory is ReentrancyGuard {
4143
mapping(address => bool) public isValidator;
4244
address[] public validators;
4345

46+
// Custom errors
4447
error SenderNotValidator();
4548
error InsufficientStakeAmount();
4649
error AlreadyValidator();
@@ -65,189 +68,65 @@ contract ValidatorFactory is ReentrancyGuard {
6568
}
6669

6770
/**
68-
* @dev Create a new validator
69-
* @return proxy Address of the created proxy
71+
* @dev Stake tokens as a validator. Deploys proxy if not already present.
7072
*/
71-
function registerValidator(uint256 amount) external nonReentrant returns (address proxy) {
72-
if(amount < minimumStake) {
73-
revert InsufficientStakeAmount();
74-
}
75-
if(isValidator[msg.sender]) {
76-
revert AlreadyValidator();
77-
}
78-
if(validators.length >= maxValidators) {
79-
revert MaxValidatorsReached();
80-
}
81-
if(!stakingToken.safeTransferFrom(msg.sender, address(this), amount)) { //TODO: CREATE2
82-
revert TransferFailed();
83-
}
84-
85-
proxy = _registerValidator(amount);
86-
87-
emit ValidatorCreated(msg.sender, proxy, amount);
88-
}
73+
function stake(uint256 amount) external nonReentrant {
74+
require(amount >= minimumStake, "Stake below minimum");
75+
require(!isValidator[msg.sender], "Already validator");
76+
require(validators.length < maxValidators, "Max validators reached");
8977

90-
/**
91-
* @dev Create a new staking position for a validator
92-
* @param amount Amount to stake
93-
* @param description Description for the position
94-
* @return positionId ID of the created position
95-
*/
96-
function createStakingPosition(uint256 amount, string memory description) external nonReentrant onlyValidator returns (uint256 positionId) {
97-
if(amount == 0) {
98-
revert InsufficientStakeAmount();
99-
}
100-
101-
address proxy = validatorToProxy[msg.sender];
102-
103-
// Transfer tokens to factory first
104-
if(!stakingToken.safeTransferFrom(msg.sender, address(this), amount)) {
105-
revert TransferFailed();
106-
}
107-
108-
// Transfer to proxy
109-
stakingToken.safeTransfer(proxy, amount);
110-
111-
// Create position
112-
positionId = ValidatorLogic(proxy).createStakingPosition(amount, description);
113-
114-
emit StakingPositionCreated(msg.sender, positionId, amount, description);
115-
return positionId;
116-
}
78+
// Compute proxy address
79+
address proxy = computeProxyAddress(msg.sender, amount);
80+
// Pre-fund the proxy
81+
require(stakingToken.transferFrom(msg.sender, proxy, amount), "Transfer failed");
11782

118-
/**
119-
* @dev Increase stake in an existing position
120-
* @param positionId ID of the position to increase
121-
* @param amount Amount to add
122-
* @return newAmount New total amount in the position
123-
*/
124-
function increasePositionStake(uint256 positionId, uint256 amount) external nonReentrant onlyValidator returns (uint256 newAmount) {
125-
if(amount == 0) {
126-
revert InsufficientStakeAmount();
127-
}
128-
129-
address proxy = validatorToProxy[msg.sender];
130-
131-
// Transfer tokens to factory first
132-
if(!stakingToken.safeTransferFrom(msg.sender, address(this), amount)) {
133-
revert TransferFailed();
134-
}
135-
136-
// Transfer to proxy
137-
stakingToken.safeTransfer(proxy, amount);
138-
139-
// Increase position stake
140-
newAmount = ValidatorLogic(proxy).increasePositionStake(positionId, amount);
141-
142-
uint256 totalStake = getValidatorStake(msg.sender);
143-
emit StakeUpdated(msg.sender, totalStake - amount, totalStake);
144-
145-
return newAmount;
146-
}
147-
148-
/**
149-
* @dev Close a staking position (partial unstake)
150-
* @param positionId ID of the position to close
151-
* @return amount Amount that was in the position
152-
*/
153-
function closeStakingPosition(uint256 positionId) external nonReentrant onlyValidator returns (uint256 amount) {
154-
address proxy = validatorToProxy[msg.sender];
155-
156-
// Close position
157-
amount = ValidatorLogic(proxy).closeStakingPosition(positionId);
158-
159-
// Transfer tokens back to validator
160-
stakingToken.safeTransfer(msg.sender, amount);
161-
162-
emit StakingPositionClosed(msg.sender, positionId, amount);
163-
emit Unstaked(msg.sender, amount);
164-
165-
return amount;
166-
}
167-
168-
/**
169-
* @dev Get all active positions for a validator
170-
* @param validator Validator address
171-
* @return positions Array of active position IDs
172-
*/
173-
function getValidatorActivePositions(address validator) external view returns (uint256[] memory positions) {
174-
if(!isValidator[validator]) {
175-
return new uint256[](0);
83+
// Deploy proxy with CREATE2
84+
bytes memory data = abi.encodeWithSelector(
85+
ValidatorLogic.initialize.selector,
86+
msg.sender,
87+
address(stakingToken),
88+
amount
89+
);
90+
bytes32 salt = keccak256(abi.encodePacked(msg.sender));
91+
bytes memory bytecode = abi.encodePacked(
92+
type(BeaconProxy).creationCode,
93+
abi.encode(address(beacon), data)
94+
);
95+
assembly {
96+
let deployed := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
97+
if iszero(deployed) { revert(0, 0) }
17698
}
177-
178-
address proxy = validatorToProxy[validator];
179-
return ValidatorLogic(proxy).getValidatorActivePositions(validator);
99+
validatorToProxy[msg.sender] = proxy;
100+
isValidator[msg.sender] = true;
101+
validators.push(msg.sender);
102+
emit ValidatorCreated(msg.sender, proxy, amount);
180103
}
181104

182105
/**
183-
* @dev Get position details
184-
* @param validator Validator address
185-
* @param positionId ID of the position
186-
* @return position StakingPosition struct
106+
* @dev Unstake tokens as a validator. If the remaining stake is below minimum, remove validator and recursively unstake the rest.
187107
*/
188-
function getStakingPosition(address validator, uint256 positionId) external view returns (ValidatorLogic.StakingPosition memory position) {
189-
if(!isValidator[validator]) {
190-
revert PositionNotFound();
191-
}
192-
193-
address proxy = validatorToProxy[validator];
194-
return ValidatorLogic(proxy).getStakingPosition(positionId);
195-
}
196-
197-
function increaseStake(uint256 amount) external nonReentrant onlyValidator {
198-
uint256 totalStake = getValidatorStake(msg.sender);
108+
function unstake(uint256 amount) external nonReentrant onlyValidator {
199109
address proxy = validatorToProxy[msg.sender];
200-
201-
uint256 stakedAmount = ValidatorLogic(proxy).stake(amount);
202-
203-
emit StakeUpdated(msg.sender, totalStake, totalStake + stakedAmount);
204-
}
205-
206-
function decreaseStake(uint256 amount) external nonReentrant onlyValidator {
207-
uint256 totalStake = getValidatorStake(msg.sender);
208-
address proxy = validatorToProxy[msg.sender];
209-
210-
if(amount > totalStake) {
211-
revert AmountExceedsTotalStake();
110+
uint256 unstaked = ValidatorLogic(proxy).unstake(amount);
111+
require(stakingToken.transfer(msg.sender, unstaked), "Transfer failed");
112+
emit Unstaked(msg.sender, unstaked);
113+
114+
uint256 remainingStake = ValidatorLogic(proxy).getStakeAmount();
115+
if (remainingStake < minimumStake && remainingStake > 0) {
116+
// Unstake the residual below minimum
117+
uint256 residual = remainingStake;
118+
uint256 unstakedResidual = ValidatorLogic(proxy).unstake(residual);
119+
require(stakingToken.transfer(msg.sender, unstakedResidual), "Transfer failed");
120+
emit Unstaked(msg.sender, unstakedResidual);
121+
remainingStake = 0;
212122
}
213-
214-
(uint256 unstakedAmount, bool shouldRemove) = ValidatorLogic(proxy).unstake(amount);
215-
216-
if(shouldRemove) {
123+
if (remainingStake == 0) {
217124
_removeValidatorFromArray(msg.sender);
218125
isValidator[msg.sender] = false;
219126
emit ValidatorRemoved(msg.sender);
220127
}
221-
emit Unstaked(msg.sender, unstakedAmount);
222128
}
223129

224-
/**
225-
* @dev Slash validator stake (for malicious behavior)
226-
* @param validator Validator to slash
227-
* @param amount Amount to slash
228-
* @param reason Reason for slashing
229-
*/
230-
function slashValidator(address validator, uint256 amount, string memory reason) external onlyConsensusModule {
231-
if (!isValidator[validator]) {
232-
revert SenderNotValidator();
233-
}
234-
235-
address proxy = validatorToProxy[validator];
236-
uint256 currentStake = getValidatorStake(validator);
237-
238-
if (amount > currentStake) {
239-
amount = currentStake; // Slash entire stake if amount exceeds
240-
}
241-
242-
ValidatorLogic(proxy).slashStake(amount);
243-
244-
// Transfer slashed tokens to consensus module
245-
stakingToken.safeTransfer(consensusModule, amount);
246-
247-
emit ValidatorSlashed(validator, amount, reason);
248-
}
249-
250-
251130
/**
252131
* @dev Get top N validators by stake using optimized QuickSelect algorithm
253132
* @param count Number of validators to return
@@ -350,15 +229,6 @@ contract ValidatorFactory is ReentrancyGuard {
350229
return i;
351230
}
352231

353-
/**
354-
* @dev Get top N validators by stake (simplified version for backward compatibility)
355-
* @param count Number of validators to return
356-
* @return topValidators Array of validator addresses
357-
*/
358-
function getTopNValidatorsSimple(uint256 count) external view returns (address[] memory topValidators) {
359-
(topValidators, ) = getTopNValidators(count);
360-
}
361-
362232
/**
363233
* @dev Get all validators
364234
* @return allValidators Array of all validator addresses
@@ -389,7 +259,7 @@ contract ValidatorFactory is ReentrancyGuard {
389259
* @param validator Validator address
390260
* @return stakeAmount Stake amount
391261
*/
392-
function getValidatorStake(address validator) external view returns (uint256) {
262+
function getValidatorStake(address validator) public view returns (uint256) {
393263
if (!isValidator[validator]) return 0;
394264
address proxy = validatorToProxy[validator];
395265
return ValidatorLogic(proxy).getStakeAmount();
@@ -457,7 +327,7 @@ contract ValidatorFactory is ReentrancyGuard {
457327
abi.encode(address(beacon), data)
458328
);
459329

460-
address predicted = computeProxyAddress(msg.sender);
330+
address predicted = computeProxyAddress(msg.sender, _amount);
461331
stakingToken.safeTransferFrom(msg.sender, predicted, _amount);
462332

463333
assembly {

src/ValidatorLogic.sol

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ contract ValidatorLogic {
2626
error AlreadyInitialized();
2727
error InvalidOwner();
2828
error InsufficientStakeAmount();
29+
error InvalidAmount();
2930

3031
bytes32 private constant VALIDATOR_OWNER_SLOT = keccak256("validator.owner"); // address
3132
bytes32 private constant TOKEN_SLOT = keccak256("token"); // address
@@ -90,39 +91,15 @@ contract ValidatorLogic {
9091
emit ValidatorInitialized(_owner, _token, _stakeAmount);
9192
}
9293

93-
/**
94-
* @dev Create a new staking position
95-
* @param amount Amount to stake
96-
* @param description Optional description for the stake
97-
* @return positionId ID of the created position
98-
*/
99-
function createStakingPosition(uint256 amount, string memory description) external onlyFactory returns (uint256 positionId) {
100-
if(!StorageSlot.getBooleanSlot(IS_ACTIVE_SLOT).value) {
101-
revert ValidatorNotActive();
102-
}
103-
104-
positionId = _createStakingPosition(StorageSlot.getAddressSlot(VALIDATOR_OWNER_SLOT).value, amount, description);
105-
106-
// Update total stake
107-
uint256 currentStake = StorageSlot.getUint256Slot(STAKE_AMOUNT_SLOT).value;
108-
StorageSlot.getUint256Slot(STAKE_AMOUNT_SLOT).value = currentStake + amount;
109-
110-
emit StakeIncreased(StorageSlot.getAddressSlot(VALIDATOR_OWNER_SLOT).value, amount, currentStake + amount);
111-
return positionId;
112-
}
11394

11495
/**
115-
* @dev Increase validator stake (legacy function for backward compatibility)
96+
* @dev Increase validator stake
11697
* @param amount Amount to stake
11798
* @return stakedAmount Amount actually staked
11899
*/
119100
function stake(uint256 amount) external onlyFactory returns (uint256 stakedAmount) {
120-
if(amount == 0) {
121-
revert InvalidAmount();
122-
}
123-
if(!StorageSlot.getBooleanSlot(IS_ACTIVE_SLOT).value) {
124-
revert ValidatorNotActive();
125-
}
101+
if(amount == 0) revert InvalidAmount();
102+
if(!StorageSlot.getBooleanSlot(IS_ACTIVE_SLOT).value) revert ValidatorNotActive();
126103

127104
// Create new position
128105
uint256 positionId = _createStakingPosition(StorageSlot.getAddressSlot(VALIDATOR_OWNER_SLOT).value, amount, "Legacy stake");
@@ -145,9 +122,7 @@ contract ValidatorLogic {
145122
function unstake(uint256 amount) external onlyFactory returns (uint256 totalUnstaked) {
146123
uint256 stakeSlot = StorageSlot.getUint256Slot(STAKE_AMOUNT_SLOT).value;
147124

148-
if (amount > stakeSlot) {
149-
revert InsufficientStakeAmount();
150-
}
125+
if (amount > stakeSlot) revert InsufficientStakeAmount();
151126

152127
totalUnstaked = _unstake(getValidatorOwner(), amount, stakeSlot);
153128
// Update total stake
@@ -277,7 +252,7 @@ contract ValidatorLogic {
277252
* @dev Get validator owner
278253
* @return owner Validator owner address
279254
*/
280-
function getValidatorOwner() external view returns (address) {
255+
function getValidatorOwner() public view returns (address) {
281256
return StorageSlot.getAddressSlot(VALIDATOR_OWNER_SLOT).value;
282257
}
283258

@@ -306,7 +281,7 @@ contract ValidatorLogic {
306281
* @param description Position description
307282
* @return positionId ID of the created position
308283
*/
309-
function _createStakingPosition(address owner, uint256 amount, string memory description) internal returns (uint256 positionId) {
284+
function _createStakingPosition(address owner, uint256 amount) internal returns (uint256 positionId) {
310285
uint256 total = getTotalPositions();
311286
positionId = total + 1;
312287
setTotalPositions(positionId);
@@ -316,8 +291,7 @@ contract ValidatorLogic {
316291
amount: amount,
317292
timestamp: block.timestamp,
318293
bondingBlock: block.number,
319-
lastWithdrawalTimestamp: block.timestamp,
320-
description: description
294+
lastWithdrawalTimestamp: block.timestamp
321295
});
322296

323297
setStakingPosition(positionId, position);

0 commit comments

Comments
 (0)