Skip to content

Commit

Permalink
Rename contracts to Liquid and NonLiquid
Browse files Browse the repository at this point in the history
  • Loading branch information
DrZoltanFazekas committed Oct 24, 2024
1 parent f0cfcb6 commit ea3a8c3
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 87 deletions.
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Liquid Staking
# Delegated Staking

This repo contains the contracts and scripts needed to activate a validator that users can stake ZIL with. When delegating stake, users receive a non-rebasing **liquid staking token** (LST) that anyone can send to the validator's delegation contract later on to withdraw the staked ZIL plus the corresponding share of the validator rewards.
This repo contains the contracts and scripts needed to activate a validator that users can stake ZIL with. Currently, there are two variants of the contracts. When delegating stake to the liquid variant, users receive a non-rebasing **liquid staking token** (LST) that anyone can send to the validator's delegation contract later on to withdraw the staked ZIL plus the corresponding share of the validator rewards. When delegating stake to the non-liquid variant, the delegator can withdraw rewards.

Install Foundry (https://book.getfoundry.sh/getting-started/installation) and the OpenZeppelin contracts before proceeding with the deployment:
```
Expand All @@ -9,11 +9,15 @@ forge install OpenZeppelin/openzeppelin-contracts --no-commit
```

## Contract Deployment
The delegation contract is used by delegators to stake and unstake ZIL with the respective validator. It acts as the validator node's control address and interacts with the `Deposit` system contract. `Delegation` is the initial implementation of the delegation contract that creates a `NonRebasingLST` contract when it is initialized. `DelegationV2` implements staking, unstaking and other features.
The delegation contract is used by delegators to stake and unstake ZIL with the respective validator. It acts as the validator node's control address and interacts with the `Deposit` system contract.

`BaseDelegation` is an abstract contract that concrete implementations inherit from.

`LiquidDelegation` is the initial implementation of the liquid staking variant of the delegation contract that creates a `NonRebasingLST` contract when it is initialized. `LiquidDelegationV2` implements all other features. `NonLiquidDelegation` is the initial implementation of the non-liquid staking variant of the delegation contract that allows delegators to withdraw rewards. The following sections describe the usage of the liquid staking variant; the non-liquid staking variant will be added later.

The delegation contract shall be deployed and upgraded by the account with the private key that was used to run the validator node and was used to generate its BLS keypair and peer id. Make sure the `PRIVATE_KEY` environment variable is set accordingly.

To deploy `Delegation` run
To deploy `LiquidDelegation` run
```bash
forge script script/deploy_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy
```
Expand All @@ -28,7 +32,7 @@ You will see an output like this:

You will need the proxy address from the above output in all commands below.

To upgrade the contract to `DelegationV2`, run
To upgrade the contract to `LiquidDelegationV2`, run
```bash
forge script script/upgrade_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2
```
Expand All @@ -46,22 +50,28 @@ The output will look like this:

Now or at a later time you can set the commission on the rewards the validator earns to e.g. 10% as follows:
```bash
forge script script/commission_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint16)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000
forge script script/commission_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint16, bool)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000 false
```

The output will contain the following information:
```
Running version: 2
LST address: 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83
Old commission rate: 0.0%
Commission rate: 0.0%
New commission rate: 10.0%
```

Note that the commission rate is specified as an integer to be devided by the `DENOMINATOR` which can be retrieved from the delegation contract:
Note that the commission rate is specified as an integer to be divided by the `DENOMINATOR` which can be retrieved from the delegation contract:
```bash
cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "DENOMINATOR()(uint256)" --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g'
```

Once the validator is activated and starts earning rewards, commissions are transferred automatically to the validator node's account. To collect the outstanding commissions that haven't been transferred yet, run
```bash
forge script script/commission_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint16, bool)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 10001 true
```
using any value for the second argument that is larger than the `DENOMINATOR` to leave the commission percentage unchanged and `true` for the third argument.

## Validator Activation
If you node's account has enough ZIL for the minimum stake required, you can activate your node as a validator with a deposit of e.g. 10 million ZIL. Run
```bash
Expand Down
4 changes: 2 additions & 2 deletions script/claim_Delegation.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
pragma solidity ^0.8.26;

import {Script} from "forge-std/Script.sol";
import {DelegationV2} from "src/DelegationV2.sol";
import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol";
import "forge-std/console.sol";

contract Claim is Script {
function run(address payable proxy) external {

address staker = msg.sender;

DelegationV2 delegation = DelegationV2(
LiquidDelegationV2 delegation = LiquidDelegationV2(
proxy
);

Expand Down
34 changes: 22 additions & 12 deletions script/commission_Delegation.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ pragma solidity ^0.8.26;

import {Script} from "forge-std/Script.sol";
import {NonRebasingLST} from "src/NonRebasingLST.sol";
import {DelegationV2} from "src/DelegationV2.sol";
import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol";
import {Console} from "src/Console.sol";
import "forge-std/console.sol";

contract Stake is Script {
function run(address payable proxy, uint16 commissionNumerator) external {
function run(address payable proxy, uint16 commissionNumerator, bool collectCommission) external {

uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

DelegationV2 delegation = DelegationV2(
proxy
);
LiquidDelegationV2 delegation = LiquidDelegationV2(
proxy
);

console.log("Running version: %s",
delegation.version()
Expand All @@ -25,18 +25,28 @@ contract Stake is Script {
address(lst)
);

Console.log("Old commission rate: %s.%s%s%%",
Console.log("Commission rate: %s.%s%s%%",
delegation.getCommissionNumerator(),
2
);

vm.broadcast(deployerPrivateKey);
if (commissionNumerator <= delegation.DENOMINATOR()) {
vm.broadcast(deployerPrivateKey);

delegation.setCommissionNumerator(commissionNumerator);
delegation.setCommissionNumerator(commissionNumerator);

Console.log("New commission rate: %s.%s%s%%",
delegation.getCommissionNumerator(),
2
);
Console.log("New commission rate: %s.%s%s%%",
delegation.getCommissionNumerator(),
2
);
}

if (collectCommission) {
vm.broadcast(deployerPrivateKey);

delegation.collectCommission();

console.log("Outstanding commission transferred");
}
}
}
8 changes: 4 additions & 4 deletions script/deploy_Delegation.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.26;

import {Script} from "forge-std/Script.sol";
import {Delegation} from "src/Delegation.sol";
import {LiquidDelegation} from "src/LiquidDelegation.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/console.sol";

Expand All @@ -15,11 +15,11 @@ contract Deploy is Script {
vm.startBroadcast(deployerPrivateKey);

address implementation = address(
new Delegation()
new LiquidDelegation()
);

bytes memory initializerCall = abi.encodeWithSelector(
Delegation.initialize.selector,
LiquidDelegation.initialize.selector,
owner
);

Expand All @@ -33,7 +33,7 @@ contract Deploy is Script {
implementation
);

Delegation delegation = Delegation(
LiquidDelegation delegation = LiquidDelegation(
proxy
);

Expand Down
4 changes: 2 additions & 2 deletions script/stake_Delegation.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.26;

import {Script} from "forge-std/Script.sol";
import {NonRebasingLST} from "src/NonRebasingLST.sol";
import {DelegationV2} from "src/DelegationV2.sol";
import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol";
import "forge-std/console.sol";

contract Stake is Script {
Expand All @@ -13,7 +13,7 @@ contract Stake is Script {
address owner = vm.addr(deployerPrivateKey);
address staker = msg.sender;

DelegationV2 delegation = DelegationV2(
LiquidDelegationV2 delegation = LiquidDelegationV2(
proxy
);

Expand Down
4 changes: 2 additions & 2 deletions script/unstake_Delegation.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.26;

import {Script} from "forge-std/Script.sol";
import {NonRebasingLST} from "src/NonRebasingLST.sol";
import {DelegationV2} from "src/DelegationV2.sol";
import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol";
import "forge-std/console.sol";

contract Unstake is Script {
Expand All @@ -13,7 +13,7 @@ contract Unstake is Script {
address owner = vm.addr(deployerPrivateKey);
address staker = msg.sender;

DelegationV2 delegation = DelegationV2(
LiquidDelegationV2 delegation = LiquidDelegationV2(
proxy
);

Expand Down
16 changes: 8 additions & 8 deletions script/upgrade_Delegation.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
pragma solidity ^0.8.26;

import {Script} from "forge-std/Script.sol";
import {Delegation} from "src/Delegation.sol";
import {DelegationV2} from "src/DelegationV2.sol";
import {LiquidDelegation} from "src/LiquidDelegation.sol";
import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol";
import "forge-std/console.sol";

contract Upgrade is Script {
Expand All @@ -12,7 +12,7 @@ contract Upgrade is Script {
address owner = vm.addr(deployerPrivateKey);
console.log("Signer is %s", owner);

Delegation oldDelegation = Delegation(
LiquidDelegation oldDelegation = LiquidDelegation(
proxy
);

Expand All @@ -27,25 +27,25 @@ contract Upgrade is Script {
vm.startBroadcast(deployerPrivateKey);

address payable newImplementation = payable(
new DelegationV2()
new LiquidDelegationV2()
);

console.log("New implementation deployed: %s",
newImplementation
);

bytes memory reinitializerCall = abi.encodeWithSelector(
DelegationV2.reinitialize.selector
LiquidDelegationV2.reinitialize.selector
);

oldDelegation.upgradeToAndCall(
newImplementation,
reinitializerCall
);

DelegationV2 newDelegation = DelegationV2(
proxy
);
LiquidDelegationV2 newDelegation = LiquidDelegationV2(
proxy
);

console.log("Upgraded to version: %s",
newDelegation.version()
Expand Down
106 changes: 106 additions & 0 deletions src/BaseDelegation.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.26;

import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "src/NonRebasingLST.sol";

library WithdrawalQueue {

//TODO: add it to the variables and implement a getter and an onlyOwner setter
// since a governance vote can change the unbonding period anytime or fetch
// it from the deposit contract
uint256 public constant UNBONDING_PERIOD = 30; //approx. 30s, used only for testing

struct Item {
uint256 blockNumber;
uint256 amount;
}

struct Fifo {
uint256 first;
uint256 last;
mapping(uint256 => Item) items;
}

function queue(Fifo storage fifo, uint256 amount) internal {
fifo.items[fifo.last] = Item(block.number + UNBONDING_PERIOD, amount);
fifo.last++;
}

function dequeue(Fifo storage fifo) internal returns(Item memory result) {
require(fifo.first < fifo.last, "queue empty");
result = fifo.items[fifo.first];
delete fifo.items[fifo.first];
fifo.first++;
}

function ready(Fifo storage fifo, uint256 index) internal view returns(bool) {
return index < fifo.last && fifo.items[index].blockNumber <= block.number;
}

function ready(Fifo storage fifo) internal view returns(bool) {
return ready(fifo, fifo.first);
}
}

// the contract is supposed to be deployed with the node's signer account
abstract contract BaseDelegation is Initializable, PausableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable {

using WithdrawalQueue for WithdrawalQueue.Fifo;

/// @custom:storage-location erc7201:zilliqa.storage.BaseDelegation
struct BaseStorage {
bytes blsPubKey;
bytes peerId;
uint256 commissionNumerator;
mapping(address => WithdrawalQueue.Fifo) withdrawals;
uint256 totalWithdrawals;
}

// keccak256(abi.encode(uint256(keccak256("zilliqa.storage.BaseDelegation")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant BaseStorageLocation = 0xc8ff0e571ef581b660c1651f85bbac921a40f9489bd04631c07fa723c13c6000;

function _getBaseStorage() private pure returns (BaseStorage storage $) {
assembly {
$.slot := BaseStorageLocation
}
}

uint256 public constant MIN_DELEGATION = 100 ether;
address public constant DEPOSIT_CONTRACT = 0x000000000000000000005a494C4445504F534954;
uint256 public constant DENOMINATOR = 10_000;

//TODO: check
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function version() public view returns(uint64) {
return _getInitializedVersion();
}

//TODO: check
function initialize(address initialOwner) initializer public {
__Pausable_init();
__Ownable_init(initialOwner);
__Ownable2Step_init();
__UUPSUpgradeable_init();
}

//TODO: check
function __BaseStorage_init() internal onlyInitializing {
__BaseStorage_init_unchained();
}

//TODO: check
function __BaseStorage_init_unchained() internal onlyInitializing {
}

//TODO: check
function _authorizeUpgrade(address newImplementation) internal onlyOwner override {}

}
Loading

0 comments on commit ea3a8c3

Please sign in to comment.