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