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