defuse/contract/tokens/nep141/
withdraw.rs

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