defuse/contract/tokens/nep171/
withdraw.rs

1use crate::{
2    contract::{Contract, ContractExt, Role, tokens::STORAGE_DEPOSIT_GAS},
3    tokens::nep171::{
4        NonFungibleTokenForceWithdrawer, NonFungibleTokenWithdrawResolver,
5        NonFungibleTokenWithdrawer,
6    },
7};
8use defuse_core::{
9    DefuseError, Result,
10    engine::StateView,
11    intents::tokens::NftWithdraw,
12    token_id::{nep141::Nep141TokenId, nep171::Nep171TokenId},
13};
14use defuse_near_utils::{REFUND_MEMO, UnwrapOrPanic};
15use defuse_wnear::{NEAR_WITHDRAW_GAS, ext_wnear};
16use near_contract_standards::{
17    non_fungible_token::{self, core::ext_nft_core},
18    storage_management::ext_storage_management,
19};
20use near_plugins::{AccessControllable, Pausable, access_control_any, pause};
21use near_sdk::{
22    AccountId, Gas, NearToken, Promise, PromiseOrValue, assert_one_yocto, env, json_types::U128,
23    near, require, serde_json,
24};
25use std::iter;
26
27#[near]
28impl NonFungibleTokenWithdrawer for Contract {
29    #[pause]
30    #[payable]
31    fn nft_withdraw(
32        &mut self,
33        token: AccountId,
34        receiver_id: AccountId,
35        token_id: non_fungible_token::TokenId,
36        memo: Option<String>,
37        msg: Option<String>,
38    ) -> PromiseOrValue<bool> {
39        assert_one_yocto();
40        self.internal_nft_withdraw(
41            self.ensure_auth_predecessor_id(),
42            NftWithdraw {
43                token,
44                receiver_id,
45                token_id,
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_nft_withdraw(
59        &mut self,
60        owner_id: AccountId,
61        withdraw: NftWithdraw,
62        force: bool,
63    ) -> Result<PromiseOrValue<bool>> {
64        self.withdraw(
65            &owner_id,
66            iter::once((
67                Nep171TokenId::new(withdraw.token.clone(), withdraw.token_id.clone()).into(),
68                1,
69            ))
70            .chain(withdraw.storage_deposit.map(|amount| {
71                (
72                    Nep141TokenId::new(self.wnear_id().into_owned()).into(),
73                    amount.as_yoctonear(),
74                )
75            })),
76            Some("withdraw"),
77            force,
78        )?;
79
80        let is_call = withdraw.is_call();
81        Ok(if let Some(storage_deposit) = withdraw.storage_deposit {
82            ext_wnear::ext(self.wnear_id.clone())
83                .with_attached_deposit(NearToken::from_yoctonear(1))
84                .with_static_gas(NEAR_WITHDRAW_GAS)
85                // do not distribute remaining gas here
86                .with_unused_gas_weight(0)
87                .near_withdraw(U128(storage_deposit.as_yoctonear()))
88                .then(
89                    // schedule storage_deposit() only after near_withdraw() returns
90                    Self::ext(env::current_account_id())
91                        .with_static_gas(
92                            Self::DO_NFT_WITHDRAW_GAS
93                                .checked_add(withdraw.min_gas())
94                                .ok_or(DefuseError::GasOverflow)
95                                .unwrap_or_panic(),
96                        )
97                        .do_nft_withdraw(withdraw.clone()),
98                )
99        } else {
100            Self::do_nft_withdraw(withdraw.clone())
101        }
102        .then(
103            Self::ext(env::current_account_id())
104                .with_static_gas(Self::NFT_RESOLVE_WITHDRAW_GAS)
105                // do not distribute remaining gas here
106                .with_unused_gas_weight(0)
107                .nft_resolve_withdraw(withdraw.token, owner_id, withdraw.token_id, is_call),
108        )
109        .into())
110    }
111}
112
113#[near]
114impl Contract {
115    const NFT_RESOLVE_WITHDRAW_GAS: Gas = Gas::from_tgas(5);
116    const DO_NFT_WITHDRAW_GAS: Gas = Gas::from_tgas(5)
117        // do_nft_withdraw() method is called externally
118        // only with storage_deposit
119        .saturating_add(STORAGE_DEPOSIT_GAS);
120
121    #[private]
122    pub fn do_nft_withdraw(withdraw: NftWithdraw) -> Promise {
123        let min_gas = withdraw.min_gas();
124        let p = if let Some(storage_deposit) = withdraw.storage_deposit {
125            require!(
126                matches!(env::promise_result_checked(0, 0), Ok(data) if data.is_empty()),
127                "near_withdraw failed",
128            );
129
130            ext_storage_management::ext(withdraw.token)
131                .with_attached_deposit(storage_deposit)
132                .with_static_gas(STORAGE_DEPOSIT_GAS)
133                // do not distribute remaining gas here
134                .with_unused_gas_weight(0)
135                .storage_deposit(Some(withdraw.receiver_id.clone()), None)
136        } else {
137            Promise::new(withdraw.token)
138        };
139
140        let p = ext_nft_core::ext_on(p)
141            .with_attached_deposit(NearToken::from_yoctonear(1))
142            .with_static_gas(min_gas)
143            // distribute remaining gas here
144            .with_unused_gas_weight(1);
145        if let Some(msg) = withdraw.msg {
146            p.nft_transfer_call(
147                withdraw.receiver_id,
148                withdraw.token_id,
149                None,
150                withdraw.memo,
151                msg,
152            )
153        } else {
154            p.nft_transfer(withdraw.receiver_id, withdraw.token_id, None, withdraw.memo)
155        }
156    }
157}
158
159#[near]
160impl NonFungibleTokenWithdrawResolver for Contract {
161    #[private]
162    fn nft_resolve_withdraw(
163        &mut self,
164        token: AccountId,
165        sender_id: AccountId,
166        token_id: non_fungible_token::TokenId,
167        is_call: bool,
168    ) -> bool {
169        const MAX_RESULT_LENGTH: usize = "false".len(); // `true` is shorter
170        let used = env::promise_result_checked(0, MAX_RESULT_LENGTH).map_or(
171            // do not refund on failed `nft_transfer_call` due to
172            // NEP-141 vulnerability: `nft_resolve_transfer` fails to
173            // read result of `nft_on_transfer` due to insufficient gas
174            is_call,
175            |value| {
176                if is_call {
177                    // `nft_transfer_call` returns true if token was successfully transferred
178                    serde_json::from_slice::<bool>(&value).unwrap_or_default()
179                } else {
180                    // `nft_transfer` returns empty result on success
181                    value.is_empty()
182                }
183            },
184        );
185
186        if !used {
187            self.deposit(
188                sender_id,
189                [(Nep171TokenId::new(token, token_id).into(), 1)],
190                Some(REFUND_MEMO),
191            )
192            .unwrap_or_panic();
193        }
194
195        used
196    }
197}
198
199#[near]
200impl NonFungibleTokenForceWithdrawer for Contract {
201    #[access_control_any(roles(Role::DAO, Role::UnrestrictedWithdrawer))]
202    #[payable]
203    fn nft_force_withdraw(
204        &mut self,
205        owner_id: AccountId,
206        token: AccountId,
207        receiver_id: AccountId,
208        token_id: non_fungible_token::TokenId,
209        memo: Option<String>,
210        msg: Option<String>,
211    ) -> PromiseOrValue<bool> {
212        assert_one_yocto();
213        self.internal_nft_withdraw(
214            owner_id,
215            NftWithdraw {
216                token,
217                receiver_id,
218                token_id,
219                memo,
220                msg,
221                storage_deposit: None,
222                min_gas: None,
223            },
224            true,
225        )
226        .unwrap_or_panic()
227    }
228}