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