1use std::fmt::{Display, Formatter};
2
3use cosmwasm_schema::{cw_serde, QueryResponses};
4use cosmwasm_std::{ensure, Coin, Decimal, Timestamp, Uint128};
5use cw_ownable::{cw_ownable_execute, cw_ownable_query};
6
7use crate::error::ContractError;
8
9const MAX_NAME_LENGTH: usize = 200;
11const MAX_DESCRIPTION_LENGTH: usize = 2000;
13const MAX_TYPE_LENGTH: usize = 200;
15
16#[cw_serde]
17pub struct InstantiateMsg {
18 pub owner: Option<String>,
20 pub action: Option<CampaignAction>,
22}
23
24#[cw_ownable_execute]
25#[cw_serde]
26pub enum ExecuteMsg {
27 ManageCampaign { action: CampaignAction },
29 Claim {
31 receiver: Option<String>,
34 amount: Option<Uint128>,
36 },
37 AddAllocations {
39 allocations: Vec<(String, Uint128)>,
41 },
42 ReplaceAddress {
44 old_address: String,
46 new_address: String,
48 },
49 RemoveAddress {
51 address: String,
53 },
54 BlacklistAddress {
56 address: String,
58 blacklist: bool,
60 },
61 ManageAuthorizedWallets {
63 addresses: Vec<String>,
65 authorized: bool,
67 },
68 Sweep {
72 denom: String,
74 amount: Option<Uint128>,
76 },
77}
78
79#[cw_ownable_query]
80#[cw_serde]
81#[derive(QueryResponses)]
82pub enum QueryMsg {
83 #[returns(CampaignResponse)]
84 Campaign {},
86 #[returns(RewardsResponse)]
87 Rewards {
89 receiver: String,
91 },
92 #[returns(ClaimedResponse)]
93 Claimed {
95 address: Option<String>,
97 start_from: Option<String>,
99 limit: Option<u16>,
101 },
102 #[returns(AllocationsResponse)]
103 Allocations {
105 address: Option<String>,
107 start_after: Option<String>,
109 limit: Option<u16>,
111 },
112 #[returns(BlacklistResponse)]
113 IsBlacklisted {
115 address: String,
117 },
118 #[returns(AuthorizedResponse)]
119 IsAuthorized {
121 address: String,
123 },
124 #[returns(AuthorizedWalletsResponse)]
125 AuthorizedWallets {
127 start_after: Option<String>,
129 limit: Option<u32>,
131 },
132}
133
134#[cw_serde]
135pub struct MigrateMsg {}
136
137pub type CampaignResponse = Campaign;
138
139#[cw_serde]
141pub struct RewardsResponse {
142 pub claimed: Vec<Coin>,
144 pub pending: Vec<Coin>,
146 pub available_to_claim: Vec<Coin>,
148}
149
150#[cw_serde]
152pub struct ClaimedResponse {
153 pub claimed: Vec<(String, Coin)>,
155}
156
157#[cw_serde]
159pub struct AllocationsResponse {
160 pub allocations: Vec<(String, Coin)>,
162}
163
164#[cw_serde]
166pub struct BlacklistResponse {
167 pub is_blacklisted: bool,
169}
170
171#[cw_serde]
173pub struct AuthorizedResponse {
174 pub is_authorized: bool,
176}
177
178#[cw_serde]
180pub struct AuthorizedWalletsResponse {
181 pub wallets: Vec<String>,
183}
184
185#[cw_serde]
187pub enum CampaignAction {
188 CreateCampaign {
190 params: Box<CampaignParams>,
192 },
193 CloseCampaign {},
195}
196
197#[cw_serde]
199pub struct Campaign {
200 pub name: String,
202 pub description: String,
204 #[serde(rename = "type")]
206 pub ty: String,
207 pub total_reward: Coin,
209 pub claimed: Coin,
211 pub distribution_type: Vec<DistributionType>,
214 pub start_time: u64,
216 pub end_time: u64,
218 pub closed: Option<u64>,
220}
221
222impl Display for Campaign {
223 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
224 write!(
225 f,
226 "Campaign {{ name: {}, description: {}, type: {}, total_reward: {}, claimed: {}, distribution_type: {:?}, start_time: {}, end_time: {}, closed: {:?} }}",
227 self.name,
228 self.description,
229 self.ty,
230 self.total_reward,
231 self.claimed,
232 self.distribution_type,
233 self.start_time,
234 self.end_time,
235 self.closed
236 )
237 }
238}
239
240impl Campaign {
241 pub fn from_params(params: CampaignParams) -> Self {
243 let reward_denom = params.total_reward.denom.clone();
244
245 Campaign {
246 name: params.name,
247 description: params.description,
248 ty: params.ty,
249 total_reward: params.total_reward,
250 claimed: Coin {
251 denom: reward_denom,
252 amount: Uint128::zero(),
253 },
254 distribution_type: params.distribution_type,
255 start_time: params.start_time,
256 end_time: params.end_time,
257 closed: None,
258 }
259 }
260
261 pub fn has_started(&self, current_time: &Timestamp) -> bool {
263 current_time.seconds() >= self.start_time
264 }
265
266 pub fn has_ended(&self, current_time: &Timestamp) -> bool {
268 current_time.seconds() >= self.end_time
269 }
270}
271
272#[cw_serde]
274pub struct CampaignParams {
275 pub name: String,
277 pub description: String,
279 #[serde(rename = "type")]
281 pub ty: String,
282 pub total_reward: Coin,
284 pub distribution_type: Vec<DistributionType>,
287 pub start_time: u64,
289 pub end_time: u64,
291 pub contract_label: String,
293}
294
295impl CampaignParams {
296 pub fn validate_campaign_name_description(&self) -> Result<(), ContractError> {
298 ensure!(
299 !self.name.is_empty(),
300 ContractError::InvalidCampaignParam {
301 param: "name".to_string(),
302 reason: "cannot be empty".to_string(),
303 }
304 );
305
306 ensure!(
307 self.name.len() <= MAX_NAME_LENGTH,
308 ContractError::InvalidCampaignParam {
309 param: "name".to_string(),
310 reason: format!("cannot be longer than {} characters", MAX_NAME_LENGTH),
311 }
312 );
313
314 ensure!(
315 !self.description.is_empty(),
316 ContractError::InvalidCampaignParam {
317 param: "description".to_string(),
318 reason: "cannot be empty".to_string(),
319 }
320 );
321
322 ensure!(
323 self.description.len() <= MAX_DESCRIPTION_LENGTH,
324 ContractError::InvalidCampaignParam {
325 param: "description".to_string(),
326 reason: format!(
327 "cannot be longer than {} characters",
328 MAX_DESCRIPTION_LENGTH
329 ),
330 }
331 );
332
333 Ok(())
334 }
335
336 pub fn validate_campaign_type(&self) -> Result<(), ContractError> {
338 ensure!(
339 !self.ty.is_empty(),
340 ContractError::InvalidCampaignParam {
341 param: "type".to_string(),
342 reason: "cannot be empty".to_string(),
343 }
344 );
345
346 ensure!(
347 self.ty.len() <= MAX_TYPE_LENGTH,
348 ContractError::InvalidCampaignParam {
349 param: "type".to_string(),
350 reason: format!("cannot be longer than {} characters", MAX_TYPE_LENGTH),
351 }
352 );
353
354 Ok(())
355 }
356
357 pub fn validate_campaign_times(&self, current_time: Timestamp) -> Result<(), ContractError> {
359 ensure!(
360 self.start_time < self.end_time,
361 ContractError::InvalidCampaignParam {
362 param: "start_time".to_string(),
363 reason: "cannot be greater or equal than end_time".to_string(),
364 }
365 );
366 ensure!(
367 self.start_time >= current_time.seconds(),
368 ContractError::InvalidCampaignParam {
369 param: "start_time".to_string(),
370 reason: "cannot be less than the current time".to_string(),
371 }
372 );
373
374 Ok(())
375 }
376
377 pub fn validate_campaign_distribution(&self) -> Result<(), ContractError> {
379 let mut total_percentage = Decimal::zero();
380
381 ensure!(
382 !self.distribution_type.is_empty() && self.distribution_type.len() <= 2,
383 ContractError::InvalidCampaignParam {
384 param: "distribution_type".to_string(),
385 reason: "invalid number of distribution types, should be at least 1, maximum 2"
386 .to_string(),
387 }
388 );
389
390 for dist in self.distribution_type.iter() {
391 let (percentage, start_time, end_time, cliff_duration) = match dist {
392 DistributionType::LinearVesting {
393 percentage,
394 start_time,
395 end_time,
396 cliff_duration,
397 } => (percentage, start_time, Some(end_time), cliff_duration),
398 DistributionType::LumpSum {
399 percentage,
400 start_time,
401 } => (percentage, start_time, None, &None),
402 };
403
404 ensure!(
405 percentage != Decimal::zero(),
406 ContractError::ZeroDistributionPercentage
407 );
408
409 total_percentage = total_percentage.checked_add(*percentage)?;
410
411 ensure!(
412 *start_time >= self.start_time,
413 ContractError::InvalidStartDistributionTime {
414 start_time: *start_time,
415 campaign_start_time: self.start_time,
416 }
417 );
418
419 if let Some(end_time) = end_time {
421 ensure!(
422 end_time > start_time,
423 ContractError::InvalidDistributionTimes {
424 start_time: *start_time,
425 end_time: *end_time,
426 }
427 );
428
429 ensure!(
430 *end_time <= self.end_time,
431 ContractError::InvalidEndDistributionTime {
432 end_time: *end_time,
433 campaign_end_time: self.end_time,
434 }
435 );
436 }
437
438 if let Some(cliff_duration) = cliff_duration {
440 ensure!(
441 *cliff_duration > 0u64,
442 ContractError::InvalidCampaignParam {
443 param: "cliff_duration".to_string(),
444 reason: "cannot be zero".to_string(),
445 }
446 );
447
448 ensure!(
449 *cliff_duration < end_time.unwrap() - start_time,
452 ContractError::InvalidCampaignParam {
453 param: "cliff_duration".to_string(),
454 reason: "cannot be greater or equal than the distribution duration"
455 .to_string(),
456 }
457 );
458 }
459 }
460
461 ensure!(
462 total_percentage == Decimal::percent(100),
463 ContractError::InvalidDistributionPercentage {
464 expected: Decimal::percent(100),
465 actual: total_percentage,
466 }
467 );
468
469 Ok(())
470 }
471
472 pub fn validate_rewards(&self) -> Result<(), ContractError> {
474 ensure!(
475 self.total_reward.amount > Uint128::zero(),
476 ContractError::InvalidCampaignParam {
477 param: "total_reward".to_string(),
478 reason: "cannot be zero".to_string()
479 }
480 );
481
482 Ok(())
483 }
484}
485
486#[cw_serde]
487pub enum DistributionType {
488 LinearVesting {
490 percentage: Decimal,
492 start_time: u64,
494 end_time: u64,
496 cliff_duration: Option<u64>,
498 },
499 LumpSum {
501 percentage: Decimal,
502 start_time: u64,
504 },
505}
506
507impl DistributionType {
508 pub fn has_started(&self, current_time: &Timestamp) -> bool {
509 let start_time = match self {
510 DistributionType::LinearVesting { start_time, .. } => start_time,
511 DistributionType::LumpSum { start_time, .. } => start_time,
512 };
513
514 current_time.seconds() >= *start_time
515 }
516}