Skip to content

Commit

Permalink
[MoveLib] Initial checkin of the governance contract (MystenLabs#1436)
Browse files Browse the repository at this point in the history
* [Move] Add Barebone governance contract

* Add treasury and initial coins

* Address feedback

* Add tests

* Address feedback

* Change quorum threshold pct to stake threshold
  • Loading branch information
lxfind authored Apr 28, 2022
1 parent c249345 commit 3ce600a
Show file tree
Hide file tree
Showing 7 changed files with 828 additions and 11 deletions.
55 changes: 55 additions & 0 deletions sui_programmability/framework/sources/Governance/Genesis.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

module Sui::Genesis {
use Std::Vector;

use Sui::Coin;
use Sui::SUI;
use Sui::SuiSystem;
use Sui::TxContext::TxContext;
use Sui::Validator;

/// The initial amount of SUI locked in the storage fund.
/// 10^14, an arbitrary number.
const INIT_STORAGE_FUND: u64 = 100000000000000;

/// Initial value of the lower-bound on the amount of stake required to become a validator.
const INIT_MIN_VALIDATOR_STAKE: u64 = 100000000000000;

/// Initial value of the upper-bound on the amount of stake allowed to become a validator.
const INIT_MAX_VALIDATOR_STAKE: u64 = 100000000000000000;

/// Initial value of the upper-bound on the number of validators.
const INIT_MAX_VALIDATOR_COUNT: u64 = 100;

/// Basic information of Validator1, as an example, all dummy values.
const VALIDATOR1_SUI_ADDRESS: address = @0x1234;
const VALIDATOR1_NAME: vector<u8> = b"Validator1";
const VALIDATOR1_IP_ADDRESS: vector<u8> = x"00FF00FF";
const VALIDATOR1_STAKE: u64 = 100000000000000;

/// This is a module initializer that runs during module publishing.
/// It will create a singleton SuiSystemState object, which contains
/// all the information we need in the system.
fun init(ctx: &mut TxContext) {
let treasury_cap = SUI::new(ctx);
let storage_fund = Coin::mint(INIT_STORAGE_FUND, &mut treasury_cap, ctx);
let validators = Vector::empty();
Vector::push_back(&mut validators, Validator::new(
VALIDATOR1_SUI_ADDRESS,
VALIDATOR1_NAME,
VALIDATOR1_IP_ADDRESS,
Coin::mint(VALIDATOR1_STAKE, &mut treasury_cap, ctx),
));
SuiSystem::create(
validators,
treasury_cap,
storage_fund,
INIT_MAX_VALIDATOR_COUNT,
INIT_MIN_VALIDATOR_STAKE,
INIT_MAX_VALIDATOR_STAKE,
ctx,
);
}
}
165 changes: 165 additions & 0 deletions sui_programmability/framework/sources/Governance/SuiSystem.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

module Sui::SuiSystem {
use Sui::Coin::{Self, Coin, TreasuryCap};
use Sui::ID::VersionedID;
use Sui::SUI::SUI;
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};
use Sui::Validator::{Self, Validator};
use Sui::ValidatorSet::{Self, ValidatorSet};

friend Sui::Genesis;

/// A list of system config parameters.
// TDOO: We will likely add more, a few potential ones:
// - the change in stake across epochs can be at most +/- x%
// - the change in the validator set across epochs can be at most x validators
struct SystemParameters has store {
/// Lower-bound on the amount of stake required to become a validator.
min_validator_stake: u64,
/// Upper-bound on the amount of stake allowed to become a validator.
max_validator_stake: u64,
/// Maximum number of validator candidates at any moment.
/// We do not allow the number of validators in any epoch to go above this.
max_validator_candidate_count: u64,
}

/// The top-level object containing all information of the Sui system.
struct SuiSystemState has key {
id: VersionedID,
/// The current epoch ID, starting from 0.
epoch: u64,
/// Contains all information about the validators.
validators: ValidatorSet,
/// The SUI treasury capability needed to mint SUI.
treasury_cap: TreasuryCap<SUI>,
/// The storage fund.
storage_fund: Coin<SUI>,
/// A list of system config parameters.
parameters: SystemParameters,
}

// ==== functions that can only be called by Genesis ====

/// Create a new SuiSystemState object and make it shared.
/// This function will be called only once in Genesis.
public(friend) fun create(
validators: vector<Validator>,
treasury_cap: TreasuryCap<SUI>,
storage_fund: Coin<SUI>,
max_validator_candidate_count: u64,
min_validator_stake: u64,
max_validator_stake: u64,
ctx: &mut TxContext,
) {
assert!(min_validator_stake < max_validator_stake, 0);
let state = SuiSystemState {
id: TxContext::new_id(ctx),
epoch: 0,
validators: ValidatorSet::new(validators),
treasury_cap,
storage_fund,
parameters: SystemParameters {
min_validator_stake,
max_validator_stake,
max_validator_candidate_count,
},
};
Transfer::share_object(state);
}

// ==== entry functions ====

/// Can be called by anyone who wishes to become a validator in the next epoch.
/// The `validator` object needs to be created before calling this.
/// The amount of stake in the `validator` object must meet the requirements.
// TODO: Does this need to go through a voting process? Any other criteria for
// someone to become a validator?
public(script) fun request_add_validator(
self: &mut SuiSystemState,
name: vector<u8>,
net_address: vector<u8>,
stake: Coin<SUI>,
ctx: &mut TxContext,
) {
assert!(
ValidatorSet::get_total_validator_candidate_count(&self.validators) < self.parameters.max_validator_candidate_count,
0
);
let stake_amount = Coin::value(&stake);
assert!(
stake_amount >= self.parameters.min_validator_stake
&& stake_amount <= self.parameters.max_validator_stake,
0
);
let validator = Validator::new(TxContext::sender(ctx), name, net_address, stake);
ValidatorSet::request_add_validator(&mut self.validators, validator);
}

/// A validator can call this function to request a removal in the next epoch.
/// We use the sender of `ctx` to look up the validator
/// (i.e. sender must match the sui_address in the validator).
/// At the end of the epoch, the `validator` object will be returned to the sui_address
/// of the validator.
public(script) fun request_remove_validator(
self: &mut SuiSystemState,
ctx: &mut TxContext,
) {
ValidatorSet::request_remove_validator(
&mut self.validators,
ctx,
)
}

/// A validator can request adding more stake. This will be processed at the end of epoch.
public(script) fun request_add_stake(
self: &mut SuiSystemState,
new_stake: Coin<SUI>,
ctx: &mut TxContext,
) {
ValidatorSet::request_add_stake(
&mut self.validators,
new_stake,
self.parameters.max_validator_stake,
ctx,
)
}

/// A validator can request to withdraw stake.
/// If the sender represents a pending validator (i.e. has just requested to become a validator
/// in the current epoch and hence is not active yet), the stake will be withdrawn immediately
/// and a coin with the withdraw amount will be sent to the validator's address.
/// If the sender represents an active validator, the request will be processed at the end of epoch.
public(script) fun request_withdraw_stake(
self: &mut SuiSystemState,
withdraw_amount: u64,
ctx: &mut TxContext,
) {
ValidatorSet::request_withdraw_stake(
&mut self.validators,
withdraw_amount,
self.parameters.min_validator_stake,
ctx,
)
}

/// This function should be called at the end of an epoch, and advances the system to the next epoch.
public(script) fun advance_epoch(
self: &mut SuiSystemState,
new_epoch: u64,
ctx: &mut TxContext,
) {
// Only an active validator can make a call to this function.
assert!(ValidatorSet::is_active_validator(&self.validators, TxContext::sender(ctx)), 0);

self.epoch = self.epoch + 1;
// Sanity check to make sure we are advancing to the right epoch.
assert!(new_epoch == self.epoch, 0);
ValidatorSet::advance_epoch(
&mut self.validators,
ctx,
)
}
}
157 changes: 157 additions & 0 deletions sui_programmability/framework/sources/Governance/Validator.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

module Sui::Validator {
use Std::ASCII::{Self, String};
use Std::Option::{Self, Option};
use Std::Vector;

use Sui::Coin::{Self, Coin};
use Sui::SUI::SUI;
use Sui::Transfer;
use Sui::TxContext::TxContext;

friend Sui::Genesis;
friend Sui::SuiSystem;
friend Sui::ValidatorSet;

#[test_only]
friend Sui::ValidatorTests;
#[test_only]
friend Sui::ValidatorSetTests;

struct Validator has store {
/// The Sui Address of the validator. This is the sender that created the Validator object,
/// and also the address to send validator/coins to during withdraws.
sui_address: address,
/// A unique human-readable name of this validator.
name: String,
/// The network address of the validator (could also contain extra info such as port, DNS and etc.).
net_address: vector<u8>,
/// The current active stake. This will not change during an epoch. It can only
/// be updated at the end of epoch.
stake: Coin<SUI>,
/// Pending stake deposits. It will be put into `stake` at the end of epoch.
pending_stake: Option<Coin<SUI>>,
/// Pending withdraw amount, processed at end of epoch.
pending_withdraw: u64,
}

public(friend) fun new(
sui_address: address,
name: vector<u8>,
net_address: vector<u8>,
stake: Coin<SUI>,
): Validator {
assert!(
Vector::length(&net_address) <= 100 || Vector::length(&name) <= 50,
0
);
Validator {
sui_address,
name: ASCII::string(name),
net_address,
stake,
pending_stake: Option::none(),
pending_withdraw: 0,
}
}

public(friend) fun destroy(self: Validator) {
let Validator {
sui_address,
name: _,
net_address: _,
stake,
pending_stake,
pending_withdraw,
} = self;
Transfer::transfer(stake, sui_address);
assert!(pending_withdraw == 0 && Option::is_none(&pending_stake), 0);
Option::destroy_none(pending_stake);
}

/// Add stake to an active validator. The new stake is added to the pending_stake field,
/// which will be processed at the end of epoch.
public(friend) fun request_add_stake(
self: &mut Validator,
new_stake: Coin<SUI>,
max_validator_stake: u64,
) {
let cur_stake = Coin::value(&self.stake);
if (Option::is_none(&self.pending_stake)) {
assert!(
cur_stake + Coin::value(&new_stake) <= max_validator_stake,
0
);
Option::fill(&mut self.pending_stake, new_stake)
} else {
let pending_stake = Option::extract(&mut self.pending_stake);
Coin::join(&mut pending_stake, new_stake);
assert!(
cur_stake + Coin::value(&pending_stake) <= max_validator_stake,
0
);
Option::fill(&mut self.pending_stake, pending_stake);
}
}

/// Withdraw stake from an active validator. Since it's active, we need
/// to add it to the pending withdraw amount and process it at the end
/// of epoch. We also need to make sure there is sufficient amount to withdraw.
public(friend) fun request_withdraw_stake(
self: &mut Validator,
withdraw_amount: u64,
min_validator_stake: u64,
) {
self.pending_withdraw = self.pending_withdraw + withdraw_amount;

let pending_stake_amount = if (Option::is_none(&self.pending_stake)) {
0
} else {
Coin::value(Option::borrow(&self.pending_stake))
};
let total_stake = Coin::value(&self.stake) + pending_stake_amount;
assert!(total_stake >= self.pending_withdraw + min_validator_stake, 0);
}

/// Process pending stake and pending withdraws.
public(friend) fun adjust_stake(self: &mut Validator, ctx: &mut TxContext) {
if (Option::is_some(&self.pending_stake)) {
let pending_stake = Option::extract(&mut self.pending_stake);
Coin::join(&mut self.stake, pending_stake);
};
if (self.pending_withdraw > 0) {
let coin = Coin::withdraw(&mut self.stake, self.pending_withdraw, ctx);
Coin::transfer(coin, self.sui_address);
self.pending_withdraw = 0;
}
}


public fun sui_address(self: &Validator): address {
self.sui_address
}

public fun stake_amount(self: &Validator): u64 {
Coin::value(&self.stake)
}

public fun pending_stake_amount(self: &Validator): u64 {
if (Option::is_some(&self.pending_stake)) {
Coin::value(Option::borrow(&self.pending_stake))
} else {
0
}
}

public fun pending_withdraw(self: &Validator): u64 {
self.pending_withdraw
}

public fun is_duplicate(self: &Validator, other: &Validator): bool {
self.sui_address == other.sui_address
|| self.name == other.name
|| self.net_address == other.net_address
}
}
Loading

0 comments on commit 3ce600a

Please sign in to comment.