Skip to main content

ve3_shared/
msgs_bribe_manager.rs

1use crate::{
2  adapters::{asset_gauge::AssetGauge, global_config_adapter::ConfigExt},
3  constants::AT_ASSET_GAUGE,
4  error::SharedError,
5  helpers::{assets::Assets, time::Time},
6};
7use cosmwasm_schema::{cw_serde, QueryResponses};
8use cosmwasm_std::{Addr, QuerierWrapper, Uint128};
9use cw_asset::{Asset, AssetInfo, AssetInfoUnchecked, AssetUnchecked};
10
11#[cw_serde]
12pub struct InstantiateMsg {
13  pub global_config_addr: String,
14  pub whitelist: Vec<AssetInfoUnchecked>,
15  pub fee: AssetUnchecked,
16}
17
18#[cw_serde]
19pub enum BribeDistribution {
20  Func {
21    start: Option<u64>,
22    end: u64,
23    func_type: FuncType,
24  },
25  Next,
26  Specific(Vec<(u64, Uint128)>),
27}
28
29#[cw_serde]
30pub enum FuncType {
31  Linear,
32  Bezier,
33  EaseInOutCubic,
34  EaseInCubic,
35  EaseOutCubic,
36  Parametric,
37}
38
39#[cw_serde]
40pub struct Config {
41  pub whitelist: Vec<AssetInfo>,
42  pub allow_any: bool,
43  pub fee: Asset,
44  pub global_config_addr: Addr,
45}
46
47impl Config {
48  pub fn asset_gauge(&self, querier: &QuerierWrapper) -> Result<AssetGauge, SharedError> {
49    self.global_config().get_address(querier, AT_ASSET_GAUGE).map(AssetGauge)
50  }
51}
52
53#[cw_serde]
54#[derive(Default)]
55pub struct BribeBucket {
56  pub gauge: String,
57  pub asset: Option<AssetInfo>,
58  pub assets: Assets,
59}
60
61#[cw_serde]
62#[derive(Default)]
63pub struct BribeBuckets {
64  pub buckets: Vec<BribeBucket>,
65}
66
67impl BribeBuckets {
68  pub fn get(&mut self, gauge: &str, asset: &AssetInfo) -> &mut BribeBucket {
69    self.get_index(gauge, asset).1
70  }
71
72  pub fn get_index(&mut self, gauge: &str, asset: &AssetInfo) -> (usize, &mut BribeBucket) {
73    if let Some(i) =
74      self.buckets.iter().position(|a| a.gauge == gauge && a.asset.as_ref() == Some(asset))
75    {
76      (i, &mut self.buckets[i])
77    } else {
78      self.buckets.push(BribeBucket {
79        gauge: gauge.to_string(),
80        asset: Some(asset.clone()),
81        assets: Assets::default(),
82      });
83      let i = self.buckets.len() - 1;
84      (i, &mut self.buckets[i])
85    }
86  }
87
88  pub fn add(&mut self, gauge: &str, asset: &AssetInfo, bribe: &Asset) {
89    self.get(gauge, asset).assets.add(bribe);
90  }
91
92  pub fn remove(
93    &mut self,
94    gauge: &str,
95    asset: &AssetInfo,
96    bribe: &Asset,
97  ) -> Result<(), SharedError> {
98    let (index, bucket) = self.get_index(gauge, asset);
99    bucket.assets.remove(bribe)?;
100    if bucket.assets.is_empty() {
101      self.buckets.remove(index);
102    }
103    Ok(())
104  }
105
106  pub fn is_empty(&self) -> bool {
107    self.buckets.len() == 0
108  }
109}
110
111#[cw_serde]
112pub enum ExecuteMsg {
113  AddBribe {
114    bribe: AssetUnchecked,
115    gauge: String,
116    for_info: AssetInfoUnchecked,
117    distribution: BribeDistribution,
118  },
119
120  WithdrawBribes {
121    period: u64,
122  },
123
124  ClaimBribes {
125    periods: Option<Vec<u64>>,
126  },
127
128  // controller
129  WhitelistAssets(Vec<AssetInfoUnchecked>),
130  RemoveAssets(Vec<AssetInfoUnchecked>),
131
132  UpdateConfig {
133    fee: Option<AssetUnchecked>,
134    allow_any: Option<bool>,
135  },
136}
137
138#[cw_serde]
139#[derive(QueryResponses)]
140pub enum QueryMsg {
141  #[returns(Config)]
142  Config {},
143
144  #[returns(NextClaimPeriodResponse)]
145  NextClaimPeriod {
146    user: String,
147  },
148
149  #[returns(BribesResponse)]
150  Bribes {
151    period: Option<Time>,
152  },
153
154  #[returns(UserClaimableResponse)]
155  UserClaimable {
156    user: String,
157    periods: Option<Vec<u64>>,
158  },
159}
160
161#[cw_serde]
162pub struct NextClaimPeriodResponse {
163  pub period: u64,
164}
165
166pub type BribesResponse = BribeBuckets;
167
168#[cw_serde]
169pub struct UserClaimableResponse {
170  pub start: u64,
171  pub end: u64,
172  pub buckets: Vec<BribeBucket>,
173}
174
175#[cw_serde]
176pub struct MigrateMsg {}
177
178#[cfg(test)]
179mod test {
180  use cosmwasm_std::Addr;
181  use cw_asset::{Asset, AssetInfo};
182
183  use crate::{error::SharedError, msgs_bribe_manager::BribeBuckets};
184
185  use super::BribeBucket;
186
187  #[test]
188  fn test_bribe_bucket() {
189    let asset = AssetInfo::cw20(Addr::unchecked("ampLUNA"));
190    let gauge = "test".to_string();
191
192    let mut bucket = BribeBucket {
193      asset: Some(asset.clone()),
194      gauge: gauge.clone(),
195      assets: vec![].into(),
196    };
197
198    bucket.assets.add(&Asset::native("uluna", 1000u128));
199    bucket.assets.add(&Asset::native("uluna", 500u128));
200    bucket.assets.add(&Asset::cw20(Addr::unchecked("test"), 500u128));
201    bucket.assets.remove(&Asset::native("uluna", 1000u128)).unwrap();
202
203    assert_eq!(
204      bucket,
205      BribeBucket {
206        asset: Some(asset.clone()),
207        gauge: gauge.clone(),
208        assets: vec![
209          Asset::native("uluna", 500u128),
210          Asset::cw20(Addr::unchecked("test"), 500u128)
211        ]
212        .into()
213      }
214    );
215
216    bucket.assets.remove(&Asset::native("uluna", 500u128)).unwrap();
217
218    assert_eq!(
219      bucket,
220      BribeBucket {
221        asset: Some(asset.clone()),
222        gauge,
223        assets: vec![Asset::cw20(Addr::unchecked("test"), 500u128)].into()
224      }
225    );
226  }
227
228  #[test]
229  fn test_bribe_bucket_fail_not_existing() {
230    let asset = AssetInfo::cw20(Addr::unchecked("ampLUNA"));
231    let gauge = "test".to_string();
232
233    let mut bucket = BribeBucket {
234      asset: Some(asset.clone()),
235      gauge,
236      assets: vec![].into(),
237    };
238
239    let err = bucket.assets.remove(&Asset::native("uluna", 1000u128)).unwrap_err();
240
241    assert_eq!(
242      err,
243      SharedError::NotFound(format!("asset {0}", AssetInfo::native("uluna".to_string())))
244    )
245  }
246
247  #[test]
248  fn test_bribe_bucket_fail_insufficient_balance() {
249    let asset = AssetInfo::cw20(Addr::unchecked("ampLUNA"));
250    let gauge = "test".to_string();
251    let mut bucket = BribeBucket {
252      asset: Some(asset.clone()),
253      gauge,
254      assets: vec![].into(),
255    };
256
257    bucket.assets.add(&Asset::native("uluna", 500u128));
258    let err = bucket.assets.remove(&Asset::native("uluna", 1000u128)).unwrap_err();
259
260    assert_eq!(
261      err,
262      SharedError::InsufficientBalance(format!(
263        "existing: {0} withdrawing: {1}",
264        Asset::native("uluna", 500u128),
265        Asset::native("uluna", 1000u128)
266      ),)
267    )
268  }
269
270  #[test]
271  fn test_bribe_buckets() {
272    let mut bucket = BribeBuckets::default();
273
274    let asset = AssetInfo::cw20(Addr::unchecked("ampLUNA"));
275    let gauge = "test".to_string();
276
277    bucket.get(&gauge, &asset).assets.add(&Asset::native("uluna", 1000u128));
278    bucket.get(&gauge, &asset).assets.add(&Asset::native("uluna", 500u128));
279    bucket.get(&gauge, &asset).assets.add(&Asset::cw20(Addr::unchecked("test"), 500u128));
280    bucket.get(&gauge, &asset).assets.remove(&Asset::native("uluna", 1000u128)).unwrap();
281
282    assert_eq!(
283      bucket,
284      BribeBuckets {
285        buckets: vec![BribeBucket {
286          asset: Some(asset.clone()),
287          gauge: gauge.clone(),
288          assets: vec![
289            Asset::native("uluna", 500u128),
290            Asset::cw20(Addr::unchecked("test"), 500u128)
291          ]
292          .into()
293        }]
294      }
295    );
296
297    bucket.remove(&gauge, &asset, &Asset::native("uluna", 500u128)).unwrap();
298
299    assert_eq!(
300      bucket,
301      BribeBuckets {
302        buckets: vec![BribeBucket {
303          asset: Some(asset.clone()),
304          gauge: gauge.clone(),
305          assets: vec![Asset::cw20(Addr::unchecked("test"), 500u128)].into()
306        }]
307      }
308    );
309  }
310}