Skip to main content

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