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