croncat_manager/
balances.rs

1use cosmwasm_std::{
2    coins, from_binary, to_binary, Addr, BankMsg, Deps, DepsMut, MessageInfo, Order, Response,
3    StdError, StdResult, Storage, Uint128, WasmMsg,
4};
5use croncat_sdk_core::types::GasPrice;
6use croncat_sdk_manager::types::Config;
7use cw20::{Cw20Coin, Cw20CoinVerified, Cw20ExecuteMsg, Cw20ReceiveMsg};
8
9use crate::{
10    helpers::{check_if_sender_is_task_owner, check_ready_for_execution, gas_fee, get_tasks_addr},
11    msg::ReceiveMsg,
12    state::{AGENT_REWARDS, CONFIG, PAUSED, TASKS_BALANCES, TEMP_BALANCES_CW20, TREASURY_BALANCE},
13    ContractError,
14};
15
16pub(crate) fn add_user_cw20(
17    storage: &mut dyn Storage,
18    user_addr: &Addr,
19    cw20: &Cw20CoinVerified,
20) -> StdResult<Uint128> {
21    let new_bal = TEMP_BALANCES_CW20.update(
22        storage,
23        (user_addr, &cw20.address),
24        |bal| -> StdResult<Uint128> {
25            let bal = bal.unwrap_or_default();
26            Ok(bal.checked_add(cw20.amount)?)
27        },
28    )?;
29    Ok(new_bal)
30}
31
32pub(crate) fn sub_user_cw20(
33    storage: &mut dyn Storage,
34    user_addr: &Addr,
35    cw20: &Cw20CoinVerified,
36) -> Result<Uint128, ContractError> {
37    let current_balance = TEMP_BALANCES_CW20.may_load(storage, (user_addr, &cw20.address))?;
38    let new_bal = if let Some(bal) = current_balance {
39        bal.checked_sub(cw20.amount).map_err(StdError::overflow)?
40    } else {
41        return Err(ContractError::EmptyBalance {});
42    };
43
44    if new_bal.is_zero() {
45        TEMP_BALANCES_CW20.remove(storage, (user_addr, &cw20.address));
46    } else {
47        TEMP_BALANCES_CW20.save(storage, (user_addr, &cw20.address), &new_bal)?;
48    }
49    Ok(new_bal)
50}
51
52/// Adding agent and treasury rewards
53/// Refunding gas used by the agent for this task
54/// For example, if we have both `agent_fee`&`treasury_fee` set at 5% :
55/// 105% of gas cost goes to the agents (100% to cover gas used for this transaction and 5% as a reward)
56/// and remaining 5% goes to the treasury
57pub(crate) fn add_fee_rewards(
58    storage: &mut dyn Storage,
59    gas: u64,
60    gas_price: &GasPrice,
61    agent_addr: &Addr,
62    agent_fee: u16,
63    treasury_fee: u16,
64    reimburse_only: bool,
65) -> Result<(), ContractError> {
66    AGENT_REWARDS.update(
67        storage,
68        agent_addr,
69        |agent_balance| -> Result<_, ContractError> {
70            // Adding base gas and agent_fee here
71            let gas_fee = if reimburse_only {
72                gas
73            } else {
74                gas_fee(gas, agent_fee.into())? + gas
75            };
76            let amount: Uint128 = gas_price.calculate(gas_fee).unwrap().into();
77            Ok(agent_balance.unwrap_or_default().saturating_add(amount))
78        },
79    )?;
80
81    if !reimburse_only {
82        TREASURY_BALANCE.update(storage, |balance| -> Result<_, ContractError> {
83            let gas_fee = gas_fee(gas, treasury_fee.into())?;
84            let amount: Uint128 = gas_price.calculate(gas_fee).unwrap().into();
85            Ok(balance.saturating_add(amount))
86        })?;
87    }
88
89    Ok(())
90}
91
92// Contract methods
93
94/// Execute: Receive
95/// Message validated, to be sure about intention of transferred tokens
96/// Used by users before creating a task with cw20 send or transfer messages
97///
98/// Returns updated balances
99pub fn execute_receive_cw20(
100    deps: DepsMut,
101    info: MessageInfo,
102    wrapper: Cw20ReceiveMsg,
103) -> Result<Response, ContractError> {
104    let msg: ReceiveMsg = from_binary(&wrapper.msg)?;
105    let paused = PAUSED.load(deps.storage)?;
106    check_ready_for_execution(&info, paused)?;
107    let config = CONFIG.load(deps.storage)?;
108
109    let sender = deps.api.addr_validate(&wrapper.sender)?;
110    let coin_addr = info.sender;
111    if !config.cw20_whitelist.contains(&coin_addr) {
112        return Err(ContractError::NotSupportedCw20 {});
113    }
114
115    let cw20_verified = Cw20CoinVerified {
116        address: coin_addr,
117        amount: wrapper.amount,
118    };
119
120    match msg {
121        ReceiveMsg::RefillTempBalance {} => {
122            let user_cw20_balance = add_user_cw20(deps.storage, &sender, &cw20_verified)?;
123            Ok(Response::new()
124                .add_attribute("action", "receive_cw20")
125                .add_attribute("cw20_received", cw20_verified.to_string())
126                .add_attribute("user_cw20_balance", user_cw20_balance))
127        }
128        ReceiveMsg::RefillTaskBalance { task_hash } => {
129            // Check if sender is task owner
130            let tasks_addr = get_tasks_addr(&deps.querier, &config)?;
131            check_if_sender_is_task_owner(&deps.querier, &tasks_addr, &sender, &task_hash)?;
132
133            let mut task_balances = TASKS_BALANCES
134                .may_load(deps.storage, task_hash.as_bytes())?
135                .ok_or(ContractError::NoTaskHash {})?;
136            let mut balance = task_balances
137                .cw20_balance
138                .ok_or(ContractError::InvalidAttachedCoins {})?;
139            if balance.address != cw20_verified.address {
140                return Err(ContractError::InvalidAttachedCoins {});
141            }
142            balance.amount += cw20_verified.amount;
143            task_balances.cw20_balance = Some(balance);
144            TASKS_BALANCES.save(deps.storage, task_hash.as_bytes(), &task_balances)?;
145            Ok(Response::new()
146                .add_attribute("action", "receive_cw20")
147                .add_attribute("cw20_received", cw20_verified.to_string())
148                .add_attribute(
149                    "task_cw20_balance",
150                    task_balances.cw20_balance.unwrap().amount,
151                ))
152        }
153    }
154}
155
156pub fn execute_refill_task_cw20(
157    deps: DepsMut,
158    info: MessageInfo,
159    task_hash: String,
160    cw20: Cw20Coin,
161) -> Result<Response, ContractError> {
162    let paused = PAUSED.load(deps.storage)?;
163    check_ready_for_execution(&info, paused)?;
164    let config = CONFIG.load(deps.storage)?;
165
166    // check if sender is task owner
167    let tasks_addr = get_tasks_addr(&deps.querier, &config)?;
168    check_if_sender_is_task_owner(&deps.querier, &tasks_addr, &info.sender, &task_hash)?;
169
170    let cw20_verified = Cw20CoinVerified {
171        address: deps.api.addr_validate(&cw20.address)?,
172        amount: cw20.amount,
173    };
174
175    sub_user_cw20(deps.storage, &info.sender, &cw20_verified)?;
176    let mut task_balances = TASKS_BALANCES
177        .may_load(deps.storage, task_hash.as_bytes())?
178        .ok_or(ContractError::NoTaskHash {})?;
179    let mut balance = task_balances
180        .cw20_balance
181        .ok_or(ContractError::InvalidAttachedCoins {})?;
182    if balance.address != cw20_verified.address {
183        return Err(ContractError::InvalidAttachedCoins {});
184    }
185    balance.amount += cw20_verified.amount;
186    task_balances.cw20_balance = Some(balance);
187    TASKS_BALANCES.save(deps.storage, task_hash.as_bytes(), &task_balances)?;
188
189    Ok(Response::new()
190        .add_attribute("action", "refill_task_cw20")
191        .add_attribute("cw20_refilled", cw20_verified.to_string())
192        .add_attribute(
193            "task_cw20_balance",
194            task_balances.cw20_balance.unwrap().to_string(),
195        ))
196}
197
198/// Execute: WithdrawCw20WalletBalances
199/// Used by users to withdraw back their cw20 tokens
200///
201/// Returns updated balances
202///
203/// NOTE: During paused configuration, all funds will be temporarily locked.
204/// This is currently to safeguard all execution paths. All funds (not just user funds)
205/// are locked, until any pause concern has been addressed or finished. In many cases,
206/// this will occur for simple contract upgrades, but could be caused from DAO identified
207/// security risks. The pause-lock will be removed as future contract testing proves
208/// mature enough, deemed ready by DAO. We expect this to be several months post-launch.
209pub fn execute_user_withdraw(
210    deps: DepsMut,
211    info: MessageInfo,
212    limit: Option<u64>,
213) -> Result<Response, ContractError> {
214    let paused = PAUSED.load(deps.storage)?;
215    check_ready_for_execution(&info, paused)?;
216    let config = CONFIG.load(deps.storage)?;
217    let limit = limit.unwrap_or(config.limit);
218    let user_addr = info.sender;
219    let withdraws: Vec<Cw20CoinVerified> = TEMP_BALANCES_CW20
220        .prefix(&user_addr)
221        .range(deps.storage, None, None, Order::Ascending)
222        .take(limit as usize)
223        .map(|cw20_res| cw20_res.map(|(address, amount)| Cw20CoinVerified { address, amount }))
224        .collect::<StdResult<_>>()?;
225    if withdraws.is_empty() {
226        return Err(ContractError::EmptyBalance {});
227    }
228    // update user and croncat manager balances
229    for cw20 in withdraws.iter() {
230        sub_user_cw20(deps.storage, &user_addr, cw20)?;
231    }
232
233    let msgs = {
234        let mut msgs = Vec::with_capacity(withdraws.len());
235        for wd in withdraws {
236            msgs.push(WasmMsg::Execute {
237                contract_addr: wd.address.to_string(),
238                msg: to_binary(&Cw20ExecuteMsg::Transfer {
239                    recipient: user_addr.to_string(),
240                    amount: wd.amount,
241                })?,
242                funds: vec![],
243            });
244        }
245        msgs
246    };
247
248    Ok(Response::new()
249        .add_attribute("action", "user_withdraw")
250        .add_messages(msgs))
251}
252
253/// Execute: OwnerWithdraw
254/// Used by owner of the contract to move balances from the manager to treasury or owner address
255pub fn execute_owner_withdraw(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
256    let config = CONFIG.load(deps.storage)?;
257    if info.sender != config.owner_addr {
258        return Err(ContractError::Unauthorized {});
259    }
260    let address = config.treasury_addr.unwrap_or(config.owner_addr);
261
262    let withdraw = TREASURY_BALANCE.load(deps.storage)?;
263    TREASURY_BALANCE.save(deps.storage, &Uint128::zero())?;
264
265    if withdraw.is_zero() {
266        Err(ContractError::EmptyBalance {})
267    } else {
268        let bank_msg = BankMsg::Send {
269            to_address: address.into_string(),
270            amount: coins(withdraw.u128(), config.native_denom),
271        };
272        Ok(Response::new()
273            .add_attribute("action", "owner_withdraw")
274            .add_message(bank_msg))
275    }
276}
277
278pub fn execute_refill_native_balance(
279    deps: DepsMut,
280    info: MessageInfo,
281    task_hash: String,
282) -> Result<Response, ContractError> {
283    if PAUSED.load(deps.storage)? {
284        return Err(ContractError::ContractPaused {});
285    }
286    let config: Config = CONFIG.load(deps.storage)?;
287    // Check if sender is task owner
288    let tasks_addr = get_tasks_addr(&deps.querier, &config)?;
289    check_if_sender_is_task_owner(&deps.querier, &tasks_addr, &info.sender, &task_hash)?;
290
291    let mut task_balances = TASKS_BALANCES
292        .may_load(deps.storage, task_hash.as_bytes())?
293        .ok_or(ContractError::NoTaskHash {})?;
294
295    if info.funds.len() > 2 {
296        return Err(ContractError::InvalidAttachedCoins {});
297    }
298    for coin in info.funds {
299        if coin.denom == config.native_denom {
300            task_balances.native_balance += coin.amount
301        } else {
302            let mut ibc = task_balances
303                .ibc_balance
304                .ok_or(ContractError::InvalidAttachedCoins {})?;
305            if ibc.denom != coin.denom {
306                return Err(ContractError::InvalidAttachedCoins {});
307            }
308            ibc.amount += coin.amount;
309            task_balances.ibc_balance = Some(ibc);
310        }
311    }
312    TASKS_BALANCES.save(deps.storage, task_hash.as_bytes(), &task_balances)?;
313    Ok(Response::new().add_attribute("action", "refill_native_balance"))
314}
315
316/// Query: Cw20WalletBalances
317/// Used to get user's available cw20 coins balance that he can use to attach to the task balance
318/// Can be paginated
319///
320/// Returns list of cw20 balances
321pub fn query_users_balances(
322    deps: Deps,
323    address: String,
324    from_index: Option<u64>,
325    limit: Option<u64>,
326) -> StdResult<Vec<Cw20CoinVerified>> {
327    let config = CONFIG.load(deps.storage)?;
328    let addr = deps.api.addr_validate(&address)?;
329    let from_index = from_index.unwrap_or_default();
330    let limit = limit.unwrap_or(config.limit);
331
332    let cw20_balance = TEMP_BALANCES_CW20
333        .prefix(&addr)
334        .range(deps.storage, None, None, Order::Ascending)
335        .skip(from_index as usize)
336        .take(limit as usize)
337        .map(|balance_res| {
338            balance_res.map(|(addr, amount)| Cw20CoinVerified {
339                address: addr,
340                amount,
341            })
342        })
343        .collect::<StdResult<_>>()?;
344
345    Ok(cw20_balance)
346}