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, UnwrapOrPanic};
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, Gas, NearToken, Promise, PromiseOrValue, assert_one_yocto, env, json_types::U128,
20    near, require, serde_json,
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,
42                memo,
43                msg,
44                storage_deposit: None,
45                min_gas: None,
46            },
47            false,
48        )
49        .unwrap_or_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.0,
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_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, 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                matches!(env::promise_result_checked(0, 0), Ok(data) if data.is_empty()),
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(withdraw.receiver_id, withdraw.amount, withdraw.memo, msg)
143        } else {
144            p.ft_transfer(withdraw.receiver_id, withdraw.amount, withdraw.memo)
145        }
146    }
147}
148
149#[near]
150impl FungibleTokenWithdrawResolver for Contract {
151    #[private]
152    fn ft_resolve_withdraw(
153        &mut self,
154        token: AccountId,
155        sender_id: AccountId,
156        amount: U128,
157        is_call: bool,
158    ) -> U128 {
159        const MAX_RESULT_LENGTH: usize = "\"+340282366920938463463374607431768211455\"".len(); // u128::MAX
160
161        let used = env::promise_result_checked(0, MAX_RESULT_LENGTH).map_or(
162            if is_call {
163                // do not refund on failed `ft_transfer_call` due to
164                // NEP-141 vulnerability: `ft_resolve_transfer` fails to
165                // read result of `ft_on_transfer` due to insufficient gas
166                amount.0
167            } else {
168                0
169            },
170            |value| {
171                if is_call {
172                    // `ft_transfer_call` returns successfully transferred amount
173                    serde_json::from_slice::<U128>(&value)
174                        .unwrap_or_default()
175                        .0
176                        .min(amount.0)
177                } else if value.is_empty() {
178                    // `ft_transfer` returns empty result on success
179                    amount.0
180                } else {
181                    0
182                }
183            },
184        );
185
186        let refund = amount.0.saturating_sub(used);
187        if refund > 0 {
188            self.deposit(
189                sender_id,
190                [(Nep141TokenId::new(token).into(), refund)],
191                Some(REFUND_MEMO),
192            )
193            .unwrap_or_panic();
194        }
195
196        U128(used)
197    }
198}
199
200#[near]
201impl FungibleTokenForceWithdrawer for Contract {
202    #[access_control_any(roles(Role::DAO, Role::UnrestrictedWithdrawer))]
203    #[payable]
204    fn ft_force_withdraw(
205        &mut self,
206        owner_id: AccountId,
207        token: AccountId,
208        receiver_id: AccountId,
209        amount: U128,
210        memo: Option<String>,
211        msg: Option<String>,
212    ) -> PromiseOrValue<U128> {
213        assert_one_yocto();
214        self.internal_ft_withdraw(
215            owner_id,
216            FtWithdraw {
217                token,
218                receiver_id,
219                amount,
220                memo,
221                msg,
222                storage_deposit: None,
223                min_gas: None,
224            },
225            true,
226        )
227        .unwrap_or_panic()
228    }
229}