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