Skip to content

Commit

Permalink
Refactor for supporting multiple validators
Browse files Browse the repository at this point in the history
  • Loading branch information
DrZoltanFazekas committed Jan 10, 2025
1 parent 047a5dd commit 1708e3a
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 48 deletions.
140 changes: 117 additions & 23 deletions src/BaseDelegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ abstract contract BaseDelegation is IDelegation, PausableUpgradeable, Ownable2St

using WithdrawalQueue for WithdrawalQueue.Fifo;

struct Validator {
bytes blsPubKey;
uint256 futureStake;
address rewardAddress;
address controlAddress;
}

/// @custom:storage-location erc7201:zilliqa.storage.BaseDelegation
struct BaseDelegationStorage {
bytes blsPubKey;
bytes peerId;
Validator[] validators;
uint256 commissionNumerator;
mapping(address => WithdrawalQueue.Fifo) withdrawals;
uint256 totalWithdrawals;
bool activated;
}

// keccak256(abi.encode(uint256(keccak256("zilliqa.storage.BaseDelegation")) - 1)) & ~bytes32(uint256(0xff))
Expand Down Expand Up @@ -54,15 +61,71 @@ abstract contract BaseDelegation is IDelegation, PausableUpgradeable, Ownable2St

function _authorizeUpgrade(address newImplementation) internal onlyOwner virtual override {}

function _migrate(bytes calldata blsPubKey) internal onlyOwner virtual {
function _join(bytes calldata blsPubKey, address controlAddress) internal onlyOwner virtual {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
require(!_isActivated() && address(this).balance == 0, "validator can not be migrated");
$.blsPubKey = blsPubKey;
(bool success, bytes memory data) = DEPOSIT_CONTRACT.call(abi.encodeWithSignature("getPeerId(bytes)", blsPubKey));
require(success, "peer id could not be retrieved");
$.peerId = data;
//TODO: remove next line if _join() works for the initial migration too, otherwise uncomment
//require(_isActivated(), "there is no other validator yet");
//TODO: check that there is no validator with the same blsPubKey already

(bool success, bytes memory data) = DEPOSIT_CONTRACT.call(abi.encodeWithSignature("getFutureStake(bytes)", blsPubKey));
require(success, "future stake could not be retrieved");
uint256 futureStake = abi.decode(data, (uint256));

(success, data) = DEPOSIT_CONTRACT.call(abi.encodeWithSignature("getRewardAddress(bytes)", blsPubKey));
require(success, "reward address could not be retrieved");
address rewardAddress = abi.decode(data, (address));

// the control address should have been set to this contract
// by the original control address otherwise the call will fail
(success, ) = DEPOSIT_CONTRACT.call(abi.encodeWithSignature("setRewardAddress(bytes,address)", blsPubKey, address(this)));
require(success, "reward address could not be changed");

$.validators.push(Validator(
blsPubKey,
futureStake,
rewardAddress,
controlAddress
));
}

function leave(bytes calldata blsPubKey) public virtual {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
for (uint256 i = 0; i < $.validators.length; i++)
if (keccak256($.validators[i].blsPubKey) == keccak256(blsPubKey)) {
require(msg.sender == $.validators[i].controlAddress, "only the control address can initiate leaving");
//TODO: call the deposit contract's setRewardAddress() function to restore $.validators[i].rewardAddress
//TODO: call the deposit contract's setControlAddress() function to restore $.validators[i].controlAddress
if (i < $.validators.length - 1)
$.validators[i] = $.validators[$.validators.length - 1];
delete $.validators[$.validators.length - 1];
}
}

function validators() public view returns(Validator[] memory) {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
return $.validators;
}

function _migrate(bytes calldata blsPubKey) internal onlyOwner virtual {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();

//TODO: why is this check necessary? if it's not, we could simple use join for the first
// migration too (after removing the requirement isActivated there)
//
// if it was already activated then it would most likely have non-zero balance due to the rewards accrued
// then it's the case of joining anyway
//
// if it was not yet activated but had a non-zero balance then
// this balance would be seen as new rewards in _rewards()
// therefore we need to set
// $.totalRewards = int256(getRewards());
// instead of requiring it to be zero
//
require(!_isActivated() && address(this).balance == 0, "validator can not be migrated");
$.activated = true;

//TODO: replace address(0) with the original control address
_join(blsPubKey, address(0));
}

function migrate(bytes calldata blsPubKey) public virtual;
Expand All @@ -74,9 +137,14 @@ abstract contract BaseDelegation is IDelegation, PausableUpgradeable, Ownable2St
uint256 depositAmount
) internal virtual {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
require($.blsPubKey.length == 0, "deposit already performed");
$.blsPubKey = blsPubKey;
$.peerId = peerId;
require(!_isActivated(), "deposit already performed");
$.activated = true;
$.validators.push(Validator(
blsPubKey,
depositAmount,
owner(),
owner()
));
(bool success, ) = DEPOSIT_CONTRACT.call{
value: depositAmount
}(
Expand Down Expand Up @@ -106,6 +174,10 @@ abstract contract BaseDelegation is IDelegation, PausableUpgradeable, Ownable2St
function _increaseDeposit(uint256 amount) internal virtual {
// topup the deposit only if already activated as a validator
if (_isActivated()) {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
//TODO: increase all validators' deposit proportionally once https://github.com/Zilliqa/zq2/issues/2057 is fixed
// until then we increase only the last validator's deposit
$.validators[$.validators.length - 1].futureStake += amount;
(bool success, ) = DEPOSIT_CONTRACT.call{
value: amount
}(
Expand All @@ -118,6 +190,11 @@ abstract contract BaseDelegation is IDelegation, PausableUpgradeable, Ownable2St
function _decreaseDeposit(uint256 amount) internal virtual {
// unstake the deposit only if already activated as a validator
if (_isActivated()) {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
//TODO: decrease all validators' deposit proportionally once https://github.com/Zilliqa/zq2/issues/2057 is fixed
// until then we decrease only the last validator's deposit
$.validators[$.validators.length - 1].futureStake -= amount;
//TODO: if the validator's futureStake is zero then force it to leave
(bool success, ) = DEPOSIT_CONTRACT.call(
abi.encodeWithSignature("unstake(uint256)",
amount
Expand All @@ -130,16 +207,20 @@ abstract contract BaseDelegation is IDelegation, PausableUpgradeable, Ownable2St
function _withdrawDeposit() internal virtual {
// withdraw the unstaked deposit only if already activated as a validator
if (_isActivated()) {
//TODO: withdraw all validators' unstaked deposits once https://github.com/Zilliqa/zq2/issues/2057 is fixed
// until then we withdraw only the last validator's unstaked deposit
(bool success, ) = DEPOSIT_CONTRACT.call(
abi.encodeWithSignature("withdraw()")
);
require(success, "deposit withdrawal failed");
}
}

// return if the first validator has been deposited already
// otherwise we are supposed to be in the fundraising phase
function _isActivated() internal virtual view returns(bool) {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
return $.blsPubKey.length > 0;
return $.activated;
}

function getCommissionNumerator() public virtual view returns(uint256) {
Expand Down Expand Up @@ -216,24 +297,37 @@ abstract contract BaseDelegation is IDelegation, PausableUpgradeable, Ownable2St
return $.totalWithdrawals;
}

function getRewards() public virtual view returns(uint256) {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
function getRewards() public virtual view returns(uint256 total) {
if (!_isActivated())
return 0;
(bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall(
abi.encodeWithSignature("getRewardAddress(bytes)", $.blsPubKey)
);
require(success, "could not retrieve reward address");
address rewardAddress = abi.decode(data, (address));
return rewardAddress.balance;
// currently all validators have the same reward address,
// which is the address of this delegation contract
total = address(this).balance;
/* if the validators had separate vault contracts as reward address
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
for (uint256 i = 0; i < $.validators.length; i++) {
(bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall(
abi.encodeWithSignature("getRewardAddress(bytes)", $.validators[i].blsPubKey)
);
require(success, "could not retrieve reward address");
address rewardAddress = abi.decode(data, (address));
//TODO: only if no other validator had the same reward address otherwise we add its balance twice
total += rewardAddress.balance;
}
*/
}

function getStake() public virtual view returns(uint256) {
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
function getStake() public virtual view returns(uint256 total) {
if (!_isActivated())
return address(this).balance;
BaseDelegationStorage storage $ = _getBaseDelegationStorage();
for (uint256 i = 0; i < $.validators.length; i++)
total += $.validators[i].futureStake;
}

function getStake(bytes calldata blsPubKey) public virtual view returns(uint256) {
(bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall(
abi.encodeWithSignature("getFutureStake(bytes)", $.blsPubKey)
abi.encodeWithSignature("getFutureStake(bytes)", blsPubKey)
);
require(success, "could not retrieve staked amount");
return abi.decode(data, (uint256));
Expand Down
58 changes: 41 additions & 17 deletions src/LiquidDelegationV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,22 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation {

// called by the node's owner who deployed this contract
// to turn the already deposited validator node into a staking pool
//TODO: rename to join() and adjust the readme
function migrate(bytes calldata blsPubKey) public override onlyOwner {
_migrate(blsPubKey);

//TODO: check if _isActivated() otherwise getRewards() would return 0 and taxedRewards could be greater
// deduct the commission from the yet untaxed rewards before calculating the number of shares
taxRewards();

//TODO: test what happens if this is called when the lst supply is non-zero
// i.e. this is not the first staking
_stake(getStake(blsPubKey));
/*TODO: remove
LiquidDelegationStorage storage $ = _getLiquidDelegationStorage();
require(NonRebasingLST($.lst).totalSupply() == 0, "stake already delegated");
NonRebasingLST($.lst).mint(owner(), getStake());
*/
}

// called by the node's owner who deployed this contract
Expand All @@ -71,48 +82,58 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation {
}

// called by the node's owner who deployed this contract
// with at least the minimum stake to deposit the node
// with at least the minimum stake to deposit a node
// as a validator before any stake is delegated to it
function depositFirst(
bytes calldata blsPubKey,
bytes calldata peerId,
bytes calldata signature
) public override payable onlyOwner {
//TODO: test what happens if this is called when the lst supply is non-zero
// i.e. this is not the first staking
_stake(msg.value);
/*TODO: remove
LiquidDelegationStorage storage $ = _getLiquidDelegationStorage();
require(NonRebasingLST($.lst).totalSupply() == 0, "stake already delegated");
NonRebasingLST($.lst).mint(owner(), msg.value);
*/
_deposit(
blsPubKey,
peerId,
signature,
msg.value
);
LiquidDelegationStorage storage $ = _getLiquidDelegationStorage();
require(NonRebasingLST($.lst).totalSupply() == 0, "stake already delegated");
NonRebasingLST($.lst).mint(owner(), msg.value);
}

function stake() public override payable whenNotPaused {
require(msg.value >= MIN_DELEGATION, "delegated amount too low");
uint256 shares;
LiquidDelegationStorage storage $ = _getLiquidDelegationStorage();
// deduct commission from the rewards only if already activated as a validator
// otherwise getRewards() returns 0 but taxedRewards would be greater than 0
// if we are in the fundraising phase getRewards() would return 0 and taxedRewards would be greater
// i.e. the commission calculated in taxRewards() would be negative, therefore
if (_isActivated()) {
// the delegated amount is temporarily part of the rewards as it's in the balance
// add to the taxed rewards to avoid commission and remove it again after taxing
// the amount just delegated is temporarily part of the rewards since it was added to the balance
// therefore add it to the taxed rewards too to avoid commission and remove it again after taxing
$.taxedRewards += msg.value;
// before calculating the shares deduct the commission from the yet untaxed rewards
// deduct the commission from the yet untaxed rewards before calculating the number of shares
taxRewards();
$.taxedRewards -= msg.value;
}
_stake(msg.value);
_increaseDeposit(msg.value);
}

function _stake(uint256 value) internal {
require(value >= MIN_DELEGATION, "delegated amount too low");
uint256 shares;
LiquidDelegationStorage storage $ = _getLiquidDelegationStorage();
uint256 depositedStake = getStake();
if (NonRebasingLST($.lst).totalSupply() == 0)
// if the validator hasn't deposited yet, the formula for calculating the shares would divide by zero, therefore
shares = msg.value;
// if no validator deposited yet the formula for calculating the shares would divide by zero, hence
shares = value;
else
// otherwise depositedStake is greater than zero even if the deposit hasn't been activated yet
shares = NonRebasingLST($.lst).totalSupply() * msg.value / (depositedStake + $.taxedRewards);
shares = NonRebasingLST($.lst).totalSupply() * value / (depositedStake + $.taxedRewards);
NonRebasingLST($.lst).mint(_msgSender(), shares);
_increaseDeposit(msg.value);
emit Staked(_msgSender(), msg.value, abi.encode(shares));
emit Staked(_msgSender(), value, abi.encode(shares));
}

function unstake(uint256 shares) public override whenNotPaused returns(uint256 amount) {
Expand Down Expand Up @@ -168,7 +189,9 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation {
taxRewards();
// withdraw the unstaked deposit once the unbonding period is over
_withdrawDeposit();
$.taxedRewards -= total;
// prevent underflow if there is nothing to withdraw hence taxedRewards is zero
if (_isActivated())
$.taxedRewards -= total;
(bool success, ) = _msgSender().call{
value: total
}("");
Expand All @@ -177,6 +200,7 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation {
}

function stakeRewards() public override onlyOwner {
require(_isActivated(), "No validator activated and rewards earned yet");
_stakeRewards();
}

Expand Down
20 changes: 12 additions & 8 deletions src/NonLiquidDelegationV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation {
// the amount that has already been withdrawn from the
// constantly growing rewards accrued since the last staking
mapping(address => uint256) withdrawnAfterLastStaking;
// balance of the reward address minus the
// rewards accrued since the last staking
// the balance of the reward address without the rewards
// accrued since the last staking
int256 totalRewards;
}

Expand Down Expand Up @@ -116,14 +116,18 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation {
event RewardPaid(address indexed delegator, uint256 reward);

// called by the node's owner who deployed this contract
// to turn the already deposited validator node into a staking pool
// to add an already deposited validator node to the staking pool
//TODO: rename to join() and adjust the readme
function migrate(bytes calldata blsPubKey) public override onlyOwner {
_migrate(blsPubKey);
NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage();
require($.stakings.length == 0, "stake already delegated");
// the owner's deposit must also be recorded as staking otherwise
// the owner would not benefit from the rewards accrued by the deposit
_append(int256(getStake()));

//TODO: uncomment or remove the next 2 lines depending on whether migration works without it
//NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage();
//require($.stakings.length == 0, "stake already delegated");

// the node's deposit must also be recorded as staking otherwise
// its owner would not benefit from the rewards accrued due to that deposit
_append(int256(getStake(blsPubKey)));
}

// called by the node's owner who deployed this contract
Expand Down

0 comments on commit 1708e3a

Please sign in to comment.