Skip to main content

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, promise_result_checked_json, promise_result_checked_void};
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, FunctionError, Gas, NearToken, Promise, PromiseOrValue, assert_one_yocto, env,
23    json_types::U128, near, require,
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_else(|err| err.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_else(|err| err.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                promise_result_checked_void(0).is_ok(),
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        let used = if is_call {
170            // `nft_transfer_call` returns true if token was successfully transferred
171            match promise_result_checked_json::<bool>(0) {
172                Ok(Ok(used)) => used,
173                Ok(Err(_deserialization_err)) => false,
174                // do not refund on failed `nft_transfer_call` due to
175                // NEP-141 vulnerability: `nft_resolve_transfer` fails to
176                // read result of `nft_on_transfer` due to insufficient gas
177                Err(_) => true,
178            }
179        } else {
180            // `nft_transfer` returns empty result on success
181            promise_result_checked_void(0).is_ok()
182        };
183
184        if !used {
185            self.deposit(
186                sender_id,
187                [(Nep171TokenId::new(token, token_id).into(), 1)],
188                Some(REFUND_MEMO),
189            )
190            .unwrap_or_else(|err| err.panic());
191        }
192
193        used
194    }
195}
196
197#[near]
198impl NonFungibleTokenForceWithdrawer for Contract {
199    #[access_control_any(roles(Role::DAO, Role::UnrestrictedWithdrawer))]
200    #[payable]
201    fn nft_force_withdraw(
202        &mut self,
203        owner_id: AccountId,
204        token: AccountId,
205        receiver_id: AccountId,
206        token_id: non_fungible_token::TokenId,
207        memo: Option<String>,
208        msg: Option<String>,
209    ) -> PromiseOrValue<bool> {
210        assert_one_yocto();
211        self.internal_nft_withdraw(
212            owner_id,
213            NftWithdraw {
214                token,
215                receiver_id,
216                token_id,
217                memo,
218                msg,
219                storage_deposit: None,
220                min_gas: None,
221            },
222            true,
223        )
224        .unwrap_or_else(|err| err.panic())
225    }
226}