mantra-claimdrop-std 1.2.1

Common types for the claimdrop contract.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
use std::fmt::{Display, Formatter};

use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{ensure, Coin, Decimal, Timestamp, Uint128};
use cw_ownable::{cw_ownable_execute, cw_ownable_query};

use crate::error::ContractError;

/// Maximum length for campaign name
const MAX_NAME_LENGTH: usize = 200;
/// Maximum length for campaign description
const MAX_DESCRIPTION_LENGTH: usize = 2000;
/// Maximum length for campaign type
const MAX_TYPE_LENGTH: usize = 200;

#[cw_serde]
pub struct InstantiateMsg {
    /// Owner of the contract. If not set, it is the sender of the Instantiate message.
    pub owner: Option<String>,
    /// Optinal action in case the contract is instantiated via the claimdrop factory
    pub action: Option<CampaignAction>,
}

#[cw_ownable_execute]
#[cw_serde]
pub enum ExecuteMsg {
    /// Manages campaigns based on the action, defined by [CampaignAction].
    ManageCampaign { action: CampaignAction },
    /// Claims rewards from a campaign
    Claim {
        /// The receiver address of the claimed rewards. If not set, the sender of the message will be the receiver.
        /// This is useful for allowing a contract to do the claim operation on behalf of a user.
        receiver: Option<String>,
        /// The amount to claim. If not set, all available tokens will be claimed.
        amount: Option<Uint128>,
    },
    /// Adds a batch of addresses and their allocations. This can only be done before the campaign has started.
    AddAllocations {
        /// Vector of (address, amount) pairs
        allocations: Vec<(String, Uint128)>,
    },
    /// Replaces an address in the allocation list. This can only be done before the campaign has started.
    ReplaceAddress {
        /// The old address to replace
        old_address: String,
        /// The new address to use
        new_address: String,
    },
    /// Removes an address in the allocation list. This can only be done before the campaign has started.
    RemoveAddress {
        /// The address to remove
        address: String,
    },
    /// Blacklists or unblacklists an address. This can be done at any time.
    BlacklistAddress {
        /// The address to blacklist/unblacklist
        address: String,
        /// Whether to blacklist or unblacklist
        blacklist: bool,
    },
    /// Manages authorized wallets that can perform admin actions. Only the owner can manage authorized wallets.
    ManageAuthorizedWallets {
        /// Vector of addresses to authorize/unauthorize
        addresses: Vec<String>,
        /// Whether to authorize or unauthorize the addresses
        authorized: bool,
    },
    /// Sweep non-reward tokens from the contract (owner only)
    /// This allows retrieving any tokens accidentally sent to the contract
    /// that are not the campaign's reward denom
    Sweep {
        /// The denomination of the token to sweep
        denom: String,
        /// Optional amount to sweep. If not provided, sweeps entire balance
        amount: Option<Uint128>,
    },
}

#[cw_ownable_query]
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
    #[returns(CampaignResponse)]
    /// Get the airdrop campaign
    Campaign {},
    #[returns(RewardsResponse)]
    /// Get the rewards for a specific campaign and receiver address.
    Rewards {
        /// The address to get the rewards for.
        receiver: String,
    },
    #[returns(ClaimedResponse)]
    /// Get the total amount of tokens claimed on the campaign.
    Claimed {
        /// If provided, it will return the tokens claimed by the specified address.
        address: Option<String>,
        /// The address to start querying from. Used for paginating results.
        start_from: Option<String>,
        /// The maximum number of items to return. If not set, the default value is used. Used for paginating results.
        limit: Option<u16>,
    },
    #[returns(AllocationsResponse)]
    /// Get the allocation for an address
    Allocations {
        /// The address to get the allocation for, if provided
        address: Option<String>,
        /// The address to start querying from. Used for paginating results.
        start_after: Option<String>,
        /// The maximum number of items to return. If not set, the default value is used. Used for paginating results.
        limit: Option<u16>,
    },
    #[returns(BlacklistResponse)]
    /// Check if an address is blacklisted
    IsBlacklisted {
        /// The address to check
        address: String,
    },
    #[returns(AuthorizedResponse)]
    /// Check if an address is authorized (owner or authorized wallet)
    IsAuthorized {
        /// The address to check
        address: String,
    },
    #[returns(AuthorizedWalletsResponse)]
    /// Get authorized wallets with pagination
    AuthorizedWallets {
        /// The address to start querying from. Used for paginating results.
        start_after: Option<String>,
        /// The maximum number of items to return. Used for paginating results.
        limit: Option<u32>,
    },
}

#[cw_serde]
pub struct MigrateMsg {}

pub type CampaignResponse = Campaign;

/// Response to the Rewards query.
#[cw_serde]
pub struct RewardsResponse {
    /// The tokens that have been claimed by the address.
    pub claimed: Vec<Coin>,
    /// The total amount of tokens that is pending to be claimed by the address.
    pub pending: Vec<Coin>,
    /// The tokens that are available to be claimed by the address.
    pub available_to_claim: Vec<Coin>,
}

/// Response to the Claimed query.
#[cw_serde]
pub struct ClaimedResponse {
    /// Contains a vector with a tuple with (address, coin) that have been claimed
    pub claimed: Vec<(String, Coin)>,
}

/// Response to the Allocation query.
#[cw_serde]
pub struct AllocationsResponse {
    /// A vector with a tuple with (address, coin) that have been allocated.
    pub allocations: Vec<(String, Coin)>,
}

/// Response to the Blacklist query.
#[cw_serde]
pub struct BlacklistResponse {
    /// Whether the address is blacklisted
    pub is_blacklisted: bool,
}

/// Response to the IsAuthorized query.
#[cw_serde]
pub struct AuthorizedResponse {
    /// Whether the address is authorized (owner or authorized wallet)
    pub is_authorized: bool,
}

/// Response to the AuthorizedWallets query.
#[cw_serde]
pub struct AuthorizedWalletsResponse {
    /// List of authorized wallet addresses
    pub wallets: Vec<String>,
}

/// The campaign action that can be executed with the [ExecuteMsg::ManageCampaign] message.
#[cw_serde]
pub enum CampaignAction {
    /// Creates a new campaign
    CreateCampaign {
        /// The parameters to create a campaign with
        params: Box<CampaignParams>,
    },
    /// Closes the campaign
    CloseCampaign {},
}

/// Represents a campaign.
#[cw_serde]
pub struct Campaign {
    /// The campaign name
    pub name: String,
    /// The campaign description
    pub description: String,
    /// Campaign type. Value used by front ends.
    #[serde(rename = "type")]
    pub ty: String,
    /// The total amount of the reward asset that is intended to be allocated to the campaign
    pub total_reward: Coin,
    /// The amount of the reward asset that has been claimed
    pub claimed: Coin,
    /// The ways the reward is distributed, which are defined by the [DistributionType].
    /// The sum of the percentages must be 100.
    pub distribution_type: Vec<DistributionType>,
    /// The campaign start time (unix timestamp), in seconds
    pub start_time: u64,
    /// The campaign end time (unix timestamp), in seconds
    pub end_time: u64,
    /// The timestamp at which the campaign was closed, in seconds
    pub closed: Option<u64>,
}

impl Display for Campaign {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Campaign {{ name: {}, description: {}, type: {}, total_reward: {}, claimed: {}, distribution_type: {:?}, start_time: {}, end_time: {}, closed: {:?} }}",
            self.name,
            self.description,
            self.ty,
            self.total_reward,
            self.claimed,
            self.distribution_type,
            self.start_time,
            self.end_time,
            self.closed
        )
    }
}

impl Campaign {
    /// Creates a new campaign from the given parameters
    pub fn from_params(params: CampaignParams) -> Self {
        let reward_denom = params.total_reward.denom.clone();

        Campaign {
            name: params.name,
            description: params.description,
            ty: params.ty,
            total_reward: params.total_reward,
            claimed: Coin {
                denom: reward_denom,
                amount: Uint128::zero(),
            },
            distribution_type: params.distribution_type,
            start_time: params.start_time,
            end_time: params.end_time,
            closed: None,
        }
    }

    /// Checks if the campaign has started
    pub fn has_started(&self, current_time: &Timestamp) -> bool {
        current_time.seconds() >= self.start_time
    }

    /// Checks if the campaign has ended
    pub fn has_ended(&self, current_time: &Timestamp) -> bool {
        current_time.seconds() >= self.end_time
    }
}

/// Represents the parameters to create a campaign with.
#[cw_serde]
pub struct CampaignParams {
    /// The campaign name
    pub name: String,
    /// The campaign description
    pub description: String,
    /// Campaign type. Value used by front ends.
    #[serde(rename = "type")]
    pub ty: String,
    /// The total amount of the reward asset that is intended to be allocated to the campaign
    pub total_reward: Coin,
    /// The ways the reward is distributed, which are defined by the [DistributionType].
    /// The sum of the percentages must be 100.
    pub distribution_type: Vec<DistributionType>,
    /// The campaign start time (unix timestamp), in seconds
    pub start_time: u64,
    /// The campaign end timestamp (unix timestamp), in seconds
    pub end_time: u64,
    /// An optional label to be used for the instantiated claimdrop contract
    pub contract_label: String,
}

impl CampaignParams {
    /// Validates the campaign name and description
    pub fn validate_campaign_name_description(&self) -> Result<(), ContractError> {
        ensure!(
            !self.name.is_empty(),
            ContractError::InvalidCampaignParam {
                param: "name".to_string(),
                reason: "cannot be empty".to_string(),
            }
        );

        ensure!(
            self.name.len() <= MAX_NAME_LENGTH,
            ContractError::InvalidCampaignParam {
                param: "name".to_string(),
                reason: format!("cannot be longer than {} characters", MAX_NAME_LENGTH),
            }
        );

        ensure!(
            !self.description.is_empty(),
            ContractError::InvalidCampaignParam {
                param: "description".to_string(),
                reason: "cannot be empty".to_string(),
            }
        );

        ensure!(
            self.description.len() <= MAX_DESCRIPTION_LENGTH,
            ContractError::InvalidCampaignParam {
                param: "description".to_string(),
                reason: format!(
                    "cannot be longer than {} characters",
                    MAX_DESCRIPTION_LENGTH
                ),
            }
        );

        Ok(())
    }

    /// Validates the campaign type
    pub fn validate_campaign_type(&self) -> Result<(), ContractError> {
        ensure!(
            !self.ty.is_empty(),
            ContractError::InvalidCampaignParam {
                param: "type".to_string(),
                reason: "cannot be empty".to_string(),
            }
        );

        ensure!(
            self.ty.len() <= MAX_TYPE_LENGTH,
            ContractError::InvalidCampaignParam {
                param: "type".to_string(),
                reason: format!("cannot be longer than {} characters", MAX_TYPE_LENGTH),
            }
        );

        Ok(())
    }

    /// Validates the start and end times of a campaign
    pub fn validate_campaign_times(&self, current_time: Timestamp) -> Result<(), ContractError> {
        ensure!(
            self.start_time < self.end_time,
            ContractError::InvalidCampaignParam {
                param: "start_time".to_string(),
                reason: "cannot be greater or equal than end_time".to_string(),
            }
        );
        ensure!(
            self.start_time >= current_time.seconds(),
            ContractError::InvalidCampaignParam {
                param: "start_time".to_string(),
                reason: "cannot be less than the current time".to_string(),
            }
        );

        Ok(())
    }

    /// Ensures the distribution type parameters are correct
    pub fn validate_campaign_distribution(&self) -> Result<(), ContractError> {
        let mut total_percentage = Decimal::zero();

        ensure!(
            !self.distribution_type.is_empty() && self.distribution_type.len() <= 2,
            ContractError::InvalidCampaignParam {
                param: "distribution_type".to_string(),
                reason: "invalid number of distribution types, should be at least 1, maximum 2"
                    .to_string(),
            }
        );

        for dist in self.distribution_type.iter() {
            let (percentage, start_time, end_time, cliff_duration) = match dist {
                DistributionType::LinearVesting {
                    percentage,
                    start_time,
                    end_time,
                    cliff_duration,
                } => (percentage, start_time, Some(end_time), cliff_duration),
                DistributionType::LumpSum {
                    percentage,
                    start_time,
                } => (percentage, start_time, None, &None),
            };

            ensure!(
                percentage != Decimal::zero(),
                ContractError::ZeroDistributionPercentage
            );

            total_percentage = total_percentage.checked_add(*percentage)?;

            ensure!(
                *start_time >= self.start_time,
                ContractError::InvalidStartDistributionTime {
                    start_time: *start_time,
                    campaign_start_time: self.start_time,
                }
            );

            // validate the end time. Applies for the linear vesting distribution type only
            if let Some(end_time) = end_time {
                ensure!(
                    end_time > start_time,
                    ContractError::InvalidDistributionTimes {
                        start_time: *start_time,
                        end_time: *end_time,
                    }
                );

                ensure!(
                    *end_time <= self.end_time,
                    ContractError::InvalidEndDistributionTime {
                        end_time: *end_time,
                        campaign_end_time: self.end_time,
                    }
                );
            }

            // validate the cliff duration
            if let Some(cliff_duration) = cliff_duration {
                ensure!(
                    *cliff_duration > 0u64,
                    ContractError::InvalidCampaignParam {
                        param: "cliff_duration".to_string(),
                        reason: "cannot be zero".to_string(),
                    }
                );

                ensure!(
                    // it is safe to unwrap because this cliff validation only applies for linear vesting,
                    // which contains an end_time
                    *cliff_duration < end_time.unwrap() - start_time,
                    ContractError::InvalidCampaignParam {
                        param: "cliff_duration".to_string(),
                        reason: "cannot be greater or equal than the distribution duration"
                            .to_string(),
                    }
                );
            }
        }

        ensure!(
            total_percentage == Decimal::percent(100),
            ContractError::InvalidDistributionPercentage {
                expected: Decimal::percent(100),
                actual: total_percentage,
            }
        );

        Ok(())
    }

    /// Validates the total reward amount
    pub fn validate_rewards(&self) -> Result<(), ContractError> {
        ensure!(
            self.total_reward.amount > Uint128::zero(),
            ContractError::InvalidCampaignParam {
                param: "total_reward".to_string(),
                reason: "cannot be zero".to_string()
            }
        );

        Ok(())
    }
}

#[cw_serde]
pub enum DistributionType {
    /// The distribution is done in a linear vesting schedule
    LinearVesting {
        /// The percentage of the total reward to be distributed with a linear vesting schedule
        percentage: Decimal,
        /// The unix timestamp when this distribution type starts, in seconds
        start_time: u64,
        /// The unix timestamp when this distribution type ends, in seconds
        end_time: u64,
        /// The duration of the cliff, in seconds
        cliff_duration: Option<u64>,
    },
    /// The distribution is done in a single lump sum, i.e. no vesting period
    LumpSum {
        percentage: Decimal,
        /// The unix timestamp when this distribution type starts, in seconds
        start_time: u64,
    },
}

impl DistributionType {
    pub fn has_started(&self, current_time: &Timestamp) -> bool {
        let start_time = match self {
            DistributionType::LinearVesting { start_time, .. } => start_time,
            DistributionType::LumpSum { start_time, .. } => start_time,
        };

        current_time.seconds() >= *start_time
    }
}