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