cw_stake_tracker/
lib.rs

1use cosmwasm_schema::{cw_serde, QueryResponses};
2use cosmwasm_std::{to_json_binary, Binary, StdResult, Storage, Timestamp, Uint128};
3use cw_wormhole::Wormhole;
4
5#[cfg(test)]
6mod tests;
7
8pub struct StakeTracker<'a> {
9    /// staked(t) := the total number of native tokens staked &
10    /// unbonding with validators at time t.
11    total_staked: Wormhole<'a, (), Uint128>,
12    /// validators(v, t) := the amount staked + amount unbonding with
13    /// validator v at time t.
14    ///
15    /// deps.api.addr_validate does not validate validator addresses,
16    /// so we're left with a string. in theory, as all of these
17    /// functions are called only _on_ (un)delegation, their
18    /// surrounding transactions should fail for invalid keys as the
19    /// staking module ought to error. this is checked in
20    /// `test_cw_vesting_staking` in
21    /// `ci/integration-tests/src/tests/cw_vesting_test.rs`.
22    validators: Wormhole<'a, String, Uint128>,
23    /// cardinality(t) := the # of validators with staked and/or
24    /// unbonding tokens at time t.
25    cardinality: Wormhole<'a, (), u64>,
26}
27
28#[cw_serde]
29#[derive(QueryResponses)]
30pub enum StakeTrackerQuery {
31    #[returns(::cosmwasm_std::Uint128)]
32    Cardinality { t: Timestamp },
33    #[returns(::cosmwasm_std::Uint128)]
34    TotalStaked { t: Timestamp },
35    #[returns(::cosmwasm_std::Uint128)]
36    ValidatorStaked { validator: String, t: Timestamp },
37}
38
39impl<'a> StakeTracker<'a> {
40    pub const fn new(
41        staked_prefix: &'a str,
42        validator_prefix: &'a str,
43        cardinality_prefix: &'a str,
44    ) -> Self {
45        Self {
46            total_staked: Wormhole::new(staked_prefix),
47            validators: Wormhole::new(validator_prefix),
48            cardinality: Wormhole::new(cardinality_prefix),
49        }
50    }
51
52    pub fn on_delegate(
53        &self,
54        storage: &mut dyn Storage,
55        t: Timestamp,
56        validator: String,
57        amount: Uint128,
58    ) -> StdResult<()> {
59        self.total_staked
60            .increment(storage, (), t.seconds(), amount)?;
61        let old = self
62            .validators
63            .load(storage, validator.clone(), t.seconds())?
64            .unwrap_or_default();
65        if old.is_zero() && !amount.is_zero() {
66            self.cardinality.increment(storage, (), t.seconds(), 1)?;
67        }
68        self.validators
69            .increment(storage, validator, t.seconds(), amount)?;
70        Ok(())
71    }
72
73    /// Makes note of a redelegation. Note, this only supports
74    /// redelegation of tokens that can be _immediately_
75    /// redelegated. The caller of this function should make a
76    /// `Delegation { delegator, validator: src }` query and ensure
77    /// that `amount <= resp.can_redelegate`.
78    pub fn on_redelegate(
79        &self,
80        storage: &mut dyn Storage,
81        t: Timestamp,
82        src: String,
83        dst: String,
84        amount: Uint128,
85    ) -> StdResult<()> {
86        let new = self
87            .validators
88            .decrement(storage, src, t.seconds(), amount)?;
89        if new.is_zero() {
90            self.cardinality.decrement(storage, (), t.seconds(), 1)?;
91        }
92        let new = self
93            .validators
94            .increment(storage, dst, t.seconds(), amount)?;
95        if new == amount {
96            self.cardinality.increment(storage, (), t.seconds(), 1)?;
97        }
98        Ok(())
99    }
100
101    pub fn on_undelegate(
102        &self,
103        storage: &mut dyn Storage,
104        t: Timestamp,
105        validator: String,
106        amount: Uint128,
107        unbonding_duration_seconds: u64,
108    ) -> StdResult<()> {
109        self.total_staked.decrement(
110            storage,
111            (),
112            t.seconds() + unbonding_duration_seconds,
113            amount,
114        )?;
115        let new = self.validators.decrement(
116            storage,
117            validator,
118            t.seconds() + unbonding_duration_seconds,
119            amount,
120        )?;
121        if new.is_zero() && !amount.is_zero() {
122            self.cardinality
123                .decrement(storage, (), t.seconds() + unbonding_duration_seconds, 1)?;
124        }
125        Ok(())
126    }
127
128    /// Registers a slash of bonded tokens.
129    ///
130    /// Invariants:
131    ///   1. amount is non-zero.
132    ///   2. the slash did indeed occur.
133    ///
134    /// Checking that these invariants are true is the responsibility
135    /// of the caller.
136    pub fn on_bonded_slash(
137        &self,
138        storage: &mut dyn Storage,
139        t: Timestamp,
140        validator: String,
141        amount: Uint128,
142    ) -> StdResult<()> {
143        enum Change {
144            /// Increment by one at (time: u64).
145            Inc(u64),
146            /// Decrement by one at (time: u64).
147            Dec(u64),
148        }
149
150        self.total_staked
151            .decrement(storage, (), t.seconds(), amount)?;
152
153        // tracks if the last value was non-zero after removing the
154        // slash amount. invariant (2) lets us initialize this to true
155        // as staked tokens are a prerequisite for slashing.
156        let mut was_nonzero = true;
157        // the set of times that the cardinality would have changed
158        // had the slash event been known.
159        let mut cardinality_changes = vec![];
160
161        // visit the history, update values to include the slashed
162        // amount, and make note of the changes to the cardinality
163        // history needed.
164        self.validators
165            .update(storage, validator, t.seconds(), &mut |staked, time| {
166                let new = staked - amount;
167                if new.is_zero() && was_nonzero {
168                    // the slash would have removed all staked tokens
169                    // at `time` => decrement the cardinality at `time`.
170                    cardinality_changes.push(Change::Dec(time));
171                    was_nonzero = false;
172                } else if !new.is_zero() && !was_nonzero {
173                    // the staked amount (including the slash) was
174                    // zero, and more tokens were staked, increment
175                    // the cardinality.
176                    cardinality_changes.push(Change::Inc(time));
177                    was_nonzero = true;
178                }
179                new
180            })?;
181
182        // we can't do these updates as part of the `update` call
183        // above as that would require two mutable references to
184        // storage.
185        for change in cardinality_changes {
186            match change {
187                Change::Inc(time) => self.cardinality.increment(storage, (), time, 1)?,
188                Change::Dec(time) => self.cardinality.decrement(storage, (), time, 1)?,
189            };
190        }
191
192        Ok(())
193    }
194
195    /// Registers a slash of unbonding tokens.
196    ///
197    /// Invariants:
198    ///   1. amount is non-zero.
199    ///   2. the slash did indeed occur.
200    ///
201    /// Checking that these invariants are true is the responsibility
202    /// of the caller.
203    pub fn on_unbonding_slash(
204        &self,
205        storage: &mut dyn Storage,
206        t: Timestamp,
207        validator: String,
208        amount: Uint128,
209    ) -> StdResult<()> {
210        // invariant (2) provides that a slash did occur at time `t`,
211        // and that the `amount` <= `total_unbonding`. As such, we
212        // know at some time `t' > t`, total_staked, and
213        // validator_staked are scheduled to decrease by an amount >=
214        // `amount`. this means that we can safely use
215        // `dangerously_update` as we are only adding an intermediate
216        // step to reach a future value (`staked - total_unbonding`).
217
218        self.total_staked
219            .dangerously_update(storage, (), t.seconds(), &mut |v, _| v - amount)?;
220        let new =
221            self.validators
222                .dangerously_update(storage, validator, t.seconds(), &mut |v, _| v - amount)?;
223        if new.is_zero() {
224            self.cardinality
225                .dangerously_update(storage, (), t.seconds(), &mut |v, _| v - 1)?;
226        }
227        Ok(())
228    }
229
230    /// Gets the total number of bonded and unbonding tokens across
231    /// all validators.
232    pub fn total_staked(&self, storage: &dyn Storage, t: Timestamp) -> StdResult<Uint128> {
233        self.total_staked
234            .load(storage, (), t.seconds())
235            .map(|v| v.unwrap_or_default())
236    }
237
238    /// Gets gets the number of tokens in the bonded or unbonding
239    /// state for validator `v`.
240    pub fn validator_staked(
241        &self,
242        storage: &dyn Storage,
243        t: Timestamp,
244        v: String,
245    ) -> StdResult<Uint128> {
246        self.validators
247            .load(storage, v, t.seconds())
248            .map(|v| v.unwrap_or_default())
249    }
250
251    /// Gets the number of validators for which there is a non-zero
252    /// number of tokens in the bonding or unbonding state for.
253    pub fn validator_cardinality(&self, storage: &dyn Storage, t: Timestamp) -> StdResult<u64> {
254        self.cardinality
255            .load(storage, (), t.seconds())
256            .map(|v| v.unwrap_or_default())
257    }
258
259    /// Provides a query interface for contracts that embed this stake
260    /// tracker and want to make its information part of their public
261    /// API.
262    pub fn query(&self, storage: &dyn Storage, msg: StakeTrackerQuery) -> StdResult<Binary> {
263        match msg {
264            StakeTrackerQuery::Cardinality { t } => to_json_binary(&Uint128::new(
265                self.validator_cardinality(storage, t)?.into(),
266            )),
267            StakeTrackerQuery::TotalStaked { t } => to_json_binary(&self.total_staked(storage, t)?),
268            StakeTrackerQuery::ValidatorStaked { validator, t } => {
269                to_json_binary(&self.validator_staked(storage, t, validator)?)
270            }
271        }
272    }
273}