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: amounts.into_iter().map(Into::into).collect(),
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().copied())
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.into_iter().map(U128).collect(),
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        let amounts: Vec<U128> = withdraw.amounts.into_iter().map(U128).collect();
179        if let Some(msg) = withdraw.msg {
180            p.mt_batch_transfer_call(
181                withdraw.receiver_id,
182                withdraw.token_ids,
183                amounts,
184                None,
185                withdraw.memo,
186                msg,
187            )
188        } else {
189            p.mt_batch_transfer(
190                withdraw.receiver_id,
191                withdraw.token_ids,
192                amounts,
193                None,
194                withdraw.memo,
195            )
196        }
197    }
198}
199
200#[near]
201impl MultiTokenWithdrawResolver for Contract {
202    #[private]
203    fn mt_resolve_withdraw(
204        &mut self,
205        token: AccountId,
206        sender_id: AccountId,
207        token_ids: Vec<defuse_nep245::TokenId>,
208        amounts: Vec<U128>,
209        is_call: bool,
210    ) -> Vec<U128> {
211        require!(
212            token_ids.len() == amounts.len() && !amounts.is_empty(),
213            "invalid args"
214        );
215
216        let mut used = if is_call {
217            // `mt_batch_transfer_call` returns successfully transferred amounts
218            match promise_result_checked_json_with_len::<Vec<U128>>(0, amounts.len()) {
219                Ok(Ok(used)) if used.len() == amounts.len() => used,
220                Ok(_) => vec![U128(0); amounts.len()],
221                // do not refund on failed `mt_batch_transfer_call` due to
222                // NEP-141 vulnerability: `mt_resolve_transfer` fails to
223                // read result of `mt_on_transfer` due to insufficient gas
224                Err(_) => amounts.clone(),
225            }
226        } else {
227            // `mt_batch_transfer` returns empty result on success
228            if promise_result_checked_void(0).is_ok() {
229                amounts.clone()
230            } else {
231                vec![U128(0); amounts.len()]
232            }
233        };
234
235        self.deposit(
236            sender_id,
237            token_ids
238                .into_iter()
239                .zip(amounts)
240                .zip(&mut used)
241                .filter_map(|((token_id, amount), used)| {
242                    // update min during iteration
243                    used.0 = used.0.min(amount.0);
244                    let refund = amount.0.saturating_sub(used.0);
245                    if refund > 0 {
246                        Some((Nep245TokenId::new(token.clone(), token_id).into(), refund))
247                    } else {
248                        None
249                    }
250                }),
251            Some(REFUND_MEMO),
252        )
253        .unwrap_or_else(|err| err.panic());
254
255        used
256    }
257}
258
259#[near]
260impl MultiTokenForcedWithdrawer for Contract {
261    #[access_control_any(roles(Role::DAO, Role::UnrestrictedWithdrawer))]
262    #[payable]
263    fn mt_force_withdraw(
264        &mut self,
265        owner_id: AccountId,
266        token: AccountId,
267        receiver_id: AccountId,
268        token_ids: Vec<defuse_nep245::TokenId>,
269        amounts: Vec<U128>,
270        memo: Option<String>,
271        msg: Option<String>,
272    ) -> PromiseOrValue<Vec<U128>> {
273        assert_one_yocto();
274        self.internal_mt_withdraw(
275            owner_id,
276            MtWithdraw {
277                token,
278                receiver_id,
279                token_ids,
280                amounts: amounts.into_iter().map(Into::into).collect(),
281                memo,
282                msg,
283                storage_deposit: None,
284                min_gas: None,
285            },
286            true,
287        )
288        .unwrap_or_else(|err| err.panic())
289    }
290}