Skip to main content

defuse/contract/tokens/nep245/
withdraw.rs

1#![allow(clippy::too_many_arguments)]
2
3use crate::{
4    contract::{Contract, ContractExt, Role, tokens::STORAGE_DEPOSIT_GAS},
5    tokens::nep245::{
6        MultiTokenForcedWithdrawer, MultiTokenWithdrawResolver, MultiTokenWithdrawer,
7    },
8};
9use defuse_core::{
10    DefuseError, Result,
11    engine::StateView,
12    intents::tokens::MtWithdraw,
13    token_id::{nep141::Nep141TokenId, nep245::Nep245TokenId},
14    tokens::BATCH_MAX_TOKENS,
15};
16use defuse_near_utils::{
17    REFUND_MEMO, promise_result_checked_json_with_len, promise_result_checked_void,
18};
19use defuse_nep245::ext_mt_core;
20use defuse_wnear::{NEAR_WITHDRAW_GAS, ext_wnear};
21use near_contract_standards::storage_management::ext_storage_management;
22use near_plugins::{AccessControllable, Pausable, access_control_any, pause};
23use near_sdk::{
24    AccountId, FunctionError, Gas, NearToken, Promise, PromiseOrValue, assert_one_yocto, env,
25    json_types::U128, near, require,
26};
27
28#[near]
29impl MultiTokenWithdrawer for Contract {
30    #[pause]
31    #[payable]
32    fn mt_withdraw(
33        &mut self,
34        token: AccountId,
35        receiver_id: AccountId,
36        token_ids: Vec<defuse_nep245::TokenId>,
37        amounts: Vec<U128>,
38        memo: Option<String>,
39        msg: Option<String>,
40    ) -> PromiseOrValue<Vec<U128>> {
41        assert_one_yocto();
42
43        self.internal_mt_withdraw(
44            self.ensure_auth_predecessor_id(),
45            MtWithdraw {
46                token,
47                receiver_id,
48                token_ids,
49                amounts: amounts.into_iter().map(Into::into).collect(),
50                memo,
51                msg,
52                storage_deposit: None,
53                min_gas: None,
54            },
55            false,
56        )
57        .unwrap_or_else(|err| err.panic())
58    }
59}
60
61impl Contract {
62    pub(crate) fn internal_mt_withdraw(
63        &mut self,
64        owner_id: AccountId,
65        withdraw: MtWithdraw,
66        force: bool,
67    ) -> Result<PromiseOrValue<Vec<U128>>> {
68        if withdraw.token_ids.len() != withdraw.amounts.len() || withdraw.token_ids.is_empty() {
69            return Err(DefuseError::InvalidIntent);
70        }
71
72        if withdraw.token_ids.len() > BATCH_MAX_TOKENS {
73            return Err(DefuseError::TooManyTokens(withdraw.token_ids.len()));
74        }
75
76        self.withdraw(
77            &owner_id,
78            withdraw
79                .token_ids
80                .iter()
81                .cloned()
82                .map(|token_id| Nep245TokenId::new(withdraw.token.clone(), token_id))
83                .map(Into::into)
84                .zip(withdraw.amounts.iter().copied())
85                .chain(withdraw.storage_deposit.map(|amount| {
86                    (
87                        Nep141TokenId::new(self.wnear_id().into_owned()).into(),
88                        amount.as_yoctonear(),
89                    )
90                })),
91            Some("withdraw"),
92            force,
93        )?;
94
95        let is_call = withdraw.msg.is_some();
96        Ok(if let Some(storage_deposit) = withdraw.storage_deposit {
97            ext_wnear::ext(self.wnear_id.clone())
98                .with_attached_deposit(NearToken::from_yoctonear(1))
99                .with_static_gas(NEAR_WITHDRAW_GAS)
100                // do not distribute remaining gas here
101                .with_unused_gas_weight(0)
102                .near_withdraw(U128(storage_deposit.as_yoctonear()))
103                .then(
104                    // schedule storage_deposit() only after near_withdraw() returns
105                    Self::ext(env::current_account_id())
106                        .with_static_gas(
107                            Self::DO_MT_WITHDRAW_GAS
108                                .checked_add(withdraw.min_gas())
109                                .ok_or(DefuseError::GasOverflow)
110                                .unwrap_or_else(|err| err.panic()),
111                        )
112                        .do_mt_withdraw(withdraw.clone()),
113                )
114        } else {
115            Self::do_mt_withdraw(withdraw.clone())
116        }
117        .then(
118            Self::ext(env::current_account_id())
119                .with_static_gas(Self::mt_resolve_withdraw_gas(withdraw.token_ids.len()))
120                // do not distribute remaining gas here
121                .with_unused_gas_weight(0)
122                .mt_resolve_withdraw(
123                    withdraw.token,
124                    owner_id,
125                    withdraw.token_ids,
126                    withdraw.amounts.into_iter().map(U128).collect(),
127                    is_call,
128                ),
129        )
130        .into())
131    }
132
133    #[must_use]
134    fn mt_resolve_withdraw_gas(token_count: usize) -> Gas {
135        // Values chosen to be similar to `MT_RESOLVE_TRANSFER_*` values
136        const MT_RESOLVE_WITHDRAW_PER_TOKEN_GAS: Gas = Gas::from_tgas(5);
137        const MT_RESOLVE_WITHDRAW_BASE_GAS: Gas = Gas::from_tgas(10);
138
139        let token_count: u64 = token_count.try_into().unwrap();
140
141        MT_RESOLVE_WITHDRAW_BASE_GAS
142            .checked_add(
143                MT_RESOLVE_WITHDRAW_PER_TOKEN_GAS
144                    .checked_mul(token_count)
145                    .ok_or(DefuseError::GasOverflow)
146                    .unwrap_or_else(|err| err.panic()),
147            )
148            .ok_or(DefuseError::GasOverflow)
149            .unwrap_or_else(|err| err.panic())
150    }
151}
152
153#[near]
154impl Contract {
155    const DO_MT_WITHDRAW_GAS: Gas = Gas::from_tgas(5)
156        // do_nft_withdraw() method is called externally
157        // only with storage_deposit
158        .saturating_add(STORAGE_DEPOSIT_GAS);
159
160    #[private]
161    pub fn do_mt_withdraw(withdraw: MtWithdraw) -> Promise {
162        let min_gas = withdraw.min_gas();
163        let p = if let Some(storage_deposit) = withdraw.storage_deposit {
164            require!(
165                promise_result_checked_void(0).is_ok(),
166                "near_withdraw failed",
167            );
168
169            ext_storage_management::ext(withdraw.token)
170                .with_attached_deposit(storage_deposit)
171                .with_static_gas(STORAGE_DEPOSIT_GAS)
172                // do not distribute remaining gas here
173                .with_unused_gas_weight(0)
174                .storage_deposit(Some(withdraw.receiver_id.clone()), None)
175        } else {
176            Promise::new(withdraw.token)
177        };
178
179        let p = ext_mt_core::ext_on(p)
180            .with_attached_deposit(NearToken::from_yoctonear(1))
181            .with_static_gas(min_gas)
182            // distribute remaining gas here
183            .with_unused_gas_weight(1);
184        let amounts: Vec<U128> = withdraw.amounts.into_iter().map(U128).collect();
185        if let Some(msg) = withdraw.msg {
186            p.mt_batch_transfer_call(
187                withdraw.receiver_id,
188                withdraw.token_ids,
189                amounts,
190                None,
191                withdraw.memo,
192                msg,
193            )
194        } else {
195            p.mt_batch_transfer(
196                withdraw.receiver_id,
197                withdraw.token_ids,
198                amounts,
199                None,
200                withdraw.memo,
201            )
202        }
203    }
204}
205
206#[near]
207impl MultiTokenWithdrawResolver for Contract {
208    #[private]
209    fn mt_resolve_withdraw(
210        &mut self,
211        token: AccountId,
212        sender_id: AccountId,
213        token_ids: Vec<defuse_nep245::TokenId>,
214        amounts: Vec<U128>,
215        is_call: bool,
216    ) -> Vec<U128> {
217        require!(
218            token_ids.len() == amounts.len() && !amounts.is_empty(),
219            "invalid args"
220        );
221
222        let mut used = if is_call {
223            // `mt_batch_transfer_call` returns successfully transferred amounts
224            match promise_result_checked_json_with_len::<Vec<U128>>(0, amounts.len()) {
225                Ok(Ok(used)) if used.len() == amounts.len() => used,
226                Ok(_) => vec![U128(0); amounts.len()],
227                // do not refund on failed `mt_batch_transfer_call` due to
228                // NEP-141 vulnerability: `mt_resolve_transfer` fails to
229                // read result of `mt_on_transfer` due to insufficient gas
230                Err(_) => amounts.clone(),
231            }
232        } else {
233            // `mt_batch_transfer` returns empty result on success
234            if promise_result_checked_void(0).is_ok() {
235                amounts.clone()
236            } else {
237                vec![U128(0); amounts.len()]
238            }
239        };
240
241        self.deposit(
242            sender_id,
243            token_ids
244                .into_iter()
245                .zip(amounts)
246                .zip(&mut used)
247                .filter_map(|((token_id, amount), used)| {
248                    // update min during iteration
249                    used.0 = used.0.min(amount.0);
250                    let refund = amount.0.saturating_sub(used.0);
251                    if refund > 0 {
252                        Some((Nep245TokenId::new(token.clone(), token_id).into(), refund))
253                    } else {
254                        None
255                    }
256                }),
257            Some(REFUND_MEMO),
258        )
259        .unwrap_or_else(|err| err.panic());
260
261        used
262    }
263}
264
265#[near]
266impl MultiTokenForcedWithdrawer for Contract {
267    #[access_control_any(roles(Role::DAO, Role::UnrestrictedWithdrawer))]
268    #[payable]
269    fn mt_force_withdraw(
270        &mut self,
271        owner_id: AccountId,
272        token: AccountId,
273        receiver_id: AccountId,
274        token_ids: Vec<defuse_nep245::TokenId>,
275        amounts: Vec<U128>,
276        memo: Option<String>,
277        msg: Option<String>,
278    ) -> PromiseOrValue<Vec<U128>> {
279        assert_one_yocto();
280
281        self.internal_mt_withdraw(
282            owner_id,
283            MtWithdraw {
284                token,
285                receiver_id,
286                token_ids,
287                amounts: amounts.into_iter().map(Into::into).collect(),
288                memo,
289                msg,
290                storage_deposit: None,
291                min_gas: None,
292            },
293            true,
294        )
295        .unwrap_or_else(|err| err.panic())
296    }
297}