defuse/contract/tokens/nep141/
withdraw.rs1use 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 .with_unused_gas_weight(0)
84 .near_withdraw(U128(storage_deposit.as_yoctonear()))
85 .then(
86 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 .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 .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 .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 .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 match promise_result_checked_json::<U128>(0) {
163 Ok(Ok(used)) => used.0.min(amount.0),
164 Ok(Err(_deserialize_err)) => 0,
165 Err(_) => amount.0,
169 }
170 } else {
171 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}