Skip to content

Commit 259b00d

Browse files
authored
Merge pull request #1 from solendprotocol/hook
Weight-based delegation hook, change decrease_validator_stake to accept address, not validator index
2 parents 1aff518 + f9dd7cd commit 259b00d

File tree

6 files changed

+286
-4
lines changed

6 files changed

+286
-4
lines changed

contracts/sources/hooks/weight.move

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/// This module allows for the dynamic rebalancing of validator stakes based
2+
/// on a set of validator addresses and weights. Rebalance can be called permissionlessly
3+
module liquid_staking::weight {
4+
use sui_system::sui_system::{SuiSystemState};
5+
use liquid_staking::liquid_staking::{LiquidStakingInfo, AdminCap};
6+
use sui::vec_map::{Self, VecMap};
7+
use sui::bag::{Self, Bag};
8+
use liquid_staking::version::{Self, Version};
9+
10+
/* Constants */
11+
const CURRENT_VERSION: u16 = 1;
12+
13+
public struct WeightHook<phantom P> has key, store {
14+
id: UID,
15+
validator_addresses_and_weights: VecMap<address, u64>,
16+
total_weight: u64,
17+
admin_cap: AdminCap<P>,
18+
version: Version,
19+
extra_fields: Bag
20+
}
21+
22+
public struct WeightHookAdminCap<phantom P> has key, store {
23+
id: UID
24+
}
25+
26+
public fun new<P>(
27+
admin_cap: AdminCap<P>,
28+
ctx: &mut TxContext
29+
): (WeightHook<P>, WeightHookAdminCap<P>) {
30+
(
31+
WeightHook {
32+
id: object::new(ctx),
33+
validator_addresses_and_weights: vec_map::empty(),
34+
total_weight: 0,
35+
admin_cap,
36+
version: version::new(CURRENT_VERSION),
37+
extra_fields: bag::new(ctx)
38+
},
39+
WeightHookAdminCap { id: object::new(ctx) }
40+
)
41+
}
42+
43+
public fun set_validator_addresses_and_weights<P>(
44+
self: &mut WeightHook<P>,
45+
_: &WeightHookAdminCap<P>,
46+
validator_addresses_and_weights: VecMap<address, u64>,
47+
) {
48+
self.version.assert_version_and_upgrade(CURRENT_VERSION);
49+
self.validator_addresses_and_weights = validator_addresses_and_weights;
50+
51+
let mut total_weight = 0;
52+
self.validator_addresses_and_weights.keys().length().do!(|i| {
53+
let (_, weight) = self.validator_addresses_and_weights.get_entry_by_idx(i);
54+
total_weight = total_weight + *weight;
55+
});
56+
57+
self.total_weight = total_weight;
58+
}
59+
60+
public fun rebalance<P>(
61+
self: &mut WeightHook<P>,
62+
system_state: &mut SuiSystemState,
63+
liquid_staking_info: &mut LiquidStakingInfo<P>,
64+
ctx: &mut TxContext
65+
) {
66+
self.version.assert_version_and_upgrade(CURRENT_VERSION);
67+
if (self.total_weight == 0) {
68+
return
69+
};
70+
71+
liquid_staking_info.refresh(system_state, ctx);
72+
73+
let mut validator_addresses_and_weights = self.validator_addresses_and_weights;
74+
75+
// 1. add all validators that exist in lst_info.validators() to the validator_address_to_weight map if they don't already exist
76+
liquid_staking_info.storage().validators().do_ref!(|validator| {
77+
let validator_address = validator.validator_address();
78+
if (!validator_addresses_and_weights.contains(&validator_address)) {
79+
validator_addresses_and_weights.insert(validator_address, 0);
80+
};
81+
});
82+
83+
// 2. calculate current and target amounts of sui for each validator
84+
let (validator_addresses, validator_weights) = validator_addresses_and_weights.into_keys_values();
85+
86+
let total_sui_supply = liquid_staking_info.storage().total_sui_supply(); // we want to allocate the unaccrued spread fees as well
87+
88+
let validator_target_amounts = validator_weights.map!(|weight| {
89+
((total_sui_supply as u128) * (weight as u128) / (self.total_weight as u128)) as u64
90+
});
91+
92+
let validator_current_amounts = validator_addresses.map_ref!(|validator_address| {
93+
let validator_index = liquid_staking_info.storage().find_validator_index_by_address(*validator_address);
94+
if (validator_index >= liquid_staking_info.storage().validators().length()) {
95+
return 0
96+
};
97+
98+
let validator = liquid_staking_info.storage().validators().borrow(validator_index);
99+
validator.total_sui_amount()
100+
});
101+
102+
// 3. decrease the stake for validators that have more stake than the target amount
103+
validator_addresses.length().do!(|i| {
104+
if (validator_current_amounts[i] > validator_target_amounts[i]) {
105+
liquid_staking_info.decrease_validator_stake(
106+
&self.admin_cap,
107+
system_state,
108+
validator_addresses[i],
109+
validator_current_amounts[i] - validator_target_amounts[i],
110+
ctx
111+
);
112+
};
113+
});
114+
115+
// 4. increase the stake for validators that have less stake than the target amount
116+
validator_addresses.length().do!(|i| {
117+
if (validator_current_amounts[i] < validator_target_amounts[i]) {
118+
liquid_staking_info.increase_validator_stake(
119+
&self.admin_cap,
120+
system_state,
121+
validator_addresses[i],
122+
validator_target_amounts[i] - validator_current_amounts[i],
123+
ctx
124+
);
125+
};
126+
});
127+
}
128+
129+
public fun eject<P>(
130+
mut self: WeightHook<P>,
131+
admin_cap: WeightHookAdminCap<P>,
132+
): AdminCap<P> {
133+
self.version.assert_version_and_upgrade(CURRENT_VERSION);
134+
135+
let WeightHookAdminCap { id } = admin_cap;
136+
object::delete(id);
137+
138+
let WeightHook { id, admin_cap, extra_fields, .. } = self;
139+
extra_fields.destroy_empty();
140+
object::delete(id);
141+
142+
admin_cap
143+
}
144+
}

contracts/sources/liquid_staking.move

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module liquid_staking::liquid_staking {
1919
const EInvalidLstCreation: u64 = 0;
2020
const EMintInvariantViolated: u64 = 1;
2121
const ERedeemInvariantViolated: u64 = 2;
22+
const EValidatorNotFound: u64 = 3;
2223

2324
/* Constants */
2425
const CURRENT_VERSION: u16 = 1;
@@ -322,12 +323,15 @@ module liquid_staking::liquid_staking {
322323
self: &mut LiquidStakingInfo<P>,
323324
_: &AdminCap<P>,
324325
system_state: &mut SuiSystemState,
325-
validator_index: u64,
326+
validator_address: address,
326327
max_sui_amount: u64,
327328
ctx: &mut TxContext
328329
): u64 {
329330
self.refresh(system_state, ctx);
330331

332+
let validator_index = self.storage.find_validator_index_by_address(validator_address);
333+
assert!(validator_index < self.storage.validators().length(), EValidatorNotFound);
334+
331335
let sui_amount = self.storage.unstake_approx_n_sui_from_validator(
332336
system_state,
333337
validator_index,

contracts/sources/storage.move

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,19 @@ module liquid_staking::storage {
9595
self.total_sui_amount
9696
}
9797

98+
public(package) fun find_validator_index_by_address(self: &Storage, validator_address: address): u64 {
99+
let mut i = 0;
100+
while (i < self.validator_infos.length()) {
101+
if (self.validator_infos[i].validator_address == validator_address) {
102+
return i
103+
};
104+
105+
i = i + 1;
106+
};
107+
108+
i
109+
}
110+
98111
fun is_empty(self: &ValidatorInfo): bool {
99112
self.active_stake.is_none() && self.inactive_stake.is_none() && self.total_sui_amount == 0
100113
}

contracts/sources/version.move

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module liquid_staking::version {
66
const EIncorrectVersion: u64 = 0;
77

88
/// Capability object given to the pool creator
9-
public struct Version has store(u16)
9+
public struct Version has store, drop (u16)
1010

1111
public(package) fun new(
1212
version: u16,

contracts/tests/liquid_staking_tests.move

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ module liquid_staking::liquid_staking_tests {
364364
lst_info.decrease_validator_stake(
365365
&admin_cap,
366366
&mut system_state,
367-
1,
367+
@0x1,
368368
40 * MIST_PER_SUI,
369369
scenario.ctx()
370370
);
@@ -692,7 +692,7 @@ module liquid_staking::liquid_staking_tests {
692692
let unstaked_amount = lst_info.decrease_validator_stake(
693693
&admin_cap,
694694
&mut system_state,
695-
0,
695+
@0x0,
696696
unstake_amount,
697697
scenario.ctx()
698698
);

contracts/tests/weight_tests.move

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#[test_only]
2+
module liquid_staking::weight_tests {
3+
/* Tests */
4+
use sui::vec_map::{Self};
5+
use sui::test_scenario::{Self, Scenario};
6+
use sui::coin::{Self};
7+
use sui::address;
8+
use sui_system::governance_test_utils::{
9+
create_validators_with_stakes,
10+
create_sui_system_state_for_testing,
11+
advance_epoch_with_reward_amounts,
12+
};
13+
use sui_system::sui_system::{SuiSystemState};
14+
use sui_system::staking_pool::StakedSui;
15+
const MIST_PER_SUI: u64 = 1_000_000_000;
16+
use liquid_staking::fees::{Self};
17+
use liquid_staking::liquid_staking::{create_lst};
18+
use liquid_staking::weight::{Self, WeightHook};
19+
20+
public struct TEST has drop {}
21+
22+
#[test_only]
23+
public fun stake_with(validator_index: u64, amount: u64, scenario: &mut Scenario): StakedSui {
24+
scenario.next_tx(@0x0);
25+
26+
let mut system_state = scenario.take_shared<SuiSystemState>();
27+
28+
let ctx = scenario.ctx();
29+
30+
let staked_sui = system_state.request_add_stake_non_entry(
31+
coin::mint_for_testing(amount * MIST_PER_SUI, ctx),
32+
address::from_u256(validator_index as u256),
33+
ctx
34+
);
35+
36+
test_scenario::return_shared(system_state);
37+
scenario.next_tx(@0x0);
38+
39+
staked_sui
40+
}
41+
42+
#[test_only]
43+
fun setup_sui_system(scenario: &mut Scenario, stakes: vector<u64>) {
44+
let validators = create_validators_with_stakes(stakes, scenario.ctx());
45+
create_sui_system_state_for_testing(validators, 0, 0, scenario.ctx());
46+
47+
advance_epoch_with_reward_amounts(0, 0, scenario);
48+
}
49+
50+
51+
#[test]
52+
fun test_rebalance() {
53+
let mut scenario = test_scenario::begin(@0x0);
54+
55+
setup_sui_system(&mut scenario, vector[100, 100, 100]);
56+
scenario.next_tx(@0x0);
57+
58+
let (admin_cap, mut lst_info) = create_lst<TEST>(
59+
fees::new_builder(scenario.ctx()).to_fee_config(),
60+
coin::create_treasury_cap_for_testing(scenario.ctx()),
61+
scenario.ctx()
62+
);
63+
64+
let mut system_state = scenario.take_shared<SuiSystemState>();
65+
let sui = coin::mint_for_testing(100 * MIST_PER_SUI, scenario.ctx());
66+
let lst = lst_info.mint(&mut system_state, sui, scenario.ctx());
67+
68+
assert!(lst_info.total_lst_supply() == 100 * MIST_PER_SUI, 0);
69+
assert!(lst_info.storage().total_sui_supply() == 100 * MIST_PER_SUI, 0);
70+
71+
let (mut weight_hook, weight_hook_admin_cap) = weight::new(admin_cap, scenario.ctx());
72+
73+
weight_hook.set_validator_addresses_and_weights(
74+
&weight_hook_admin_cap,
75+
{
76+
let mut map = vec_map::empty();
77+
map.insert(address::from_u256(0), 100);
78+
map.insert(address::from_u256(1), 300);
79+
80+
map
81+
}
82+
);
83+
84+
weight_hook.rebalance(&mut system_state, &mut lst_info, scenario.ctx());
85+
86+
std::debug::print(&lst_info);
87+
88+
assert!(lst_info.storage().validators().borrow(0).total_sui_amount() == 25 * MIST_PER_SUI, 0);
89+
assert!(lst_info.storage().validators().borrow(1).total_sui_amount() == 75 * MIST_PER_SUI, 0);
90+
91+
weight_hook.set_validator_addresses_and_weights(
92+
&weight_hook_admin_cap,
93+
{
94+
let mut map = vec_map::empty();
95+
map.insert(address::from_u256(2), 100);
96+
97+
map
98+
}
99+
);
100+
weight_hook.rebalance(&mut system_state, &mut lst_info, scenario.ctx());
101+
102+
assert!(lst_info.storage().validators().borrow(0).total_sui_amount() == 0, 0);
103+
assert!(lst_info.storage().validators().borrow(1).total_sui_amount() == 0, 0);
104+
assert!(lst_info.storage().validators().borrow(2).total_sui_amount() == 100 * MIST_PER_SUI, 0);
105+
106+
// sharing to make sure shared object deletion actually works lol
107+
transfer::public_share_object(weight_hook);
108+
scenario.next_tx(@0x0);
109+
110+
let weight_hook = scenario.take_shared<WeightHook<TEST>>();
111+
let admin_cap = weight_hook.eject(weight_hook_admin_cap);
112+
113+
test_scenario::return_shared(system_state);
114+
115+
sui::test_utils::destroy(admin_cap);
116+
sui::test_utils::destroy(lst_info);
117+
sui::test_utils::destroy(lst);
118+
119+
scenario.end();
120+
}
121+
}

0 commit comments

Comments
 (0)