defuse/contract/tokens/nep245/
withdraw.rs1#![allow(clippy::too_many_arguments)]
2
3use crate::{
4 contract::{Contract, ContractExt, Role, tokens::STORAGE_DEPOSIT_GAS},
5 tokens::nep245::{
6 MultiTokenForcedWithdrawer, MultiTokenWithdrawResolver, MultiTokenWithdrawer,
7 },
8};
9use defuse_core::{
10 DefuseError, Result,
11 engine::StateView,
12 intents::tokens::MtWithdraw,
13 token_id::{nep141::Nep141TokenId, nep245::Nep245TokenId},
14};
15use defuse_near_utils::{REFUND_MEMO, UnwrapOrPanic, UnwrapOrPanicError};
16use defuse_nep245::ext_mt_core;
17use defuse_wnear::{NEAR_WITHDRAW_GAS, ext_wnear};
18use near_contract_standards::storage_management::ext_storage_management;
19use near_plugins::{AccessControllable, Pausable, access_control_any, pause};
20use near_sdk::{
21 AccountId, Gas, NearToken, Promise, PromiseOrValue, assert_one_yocto, env, json_types::U128,
22 near, require, serde_json,
23};
24
25#[near]
26impl MultiTokenWithdrawer for Contract {
27 #[pause]
28 #[payable]
29 fn mt_withdraw(
30 &mut self,
31 token: AccountId,
32 receiver_id: AccountId,
33 token_ids: Vec<defuse_nep245::TokenId>,
34 amounts: Vec<U128>,
35 memo: Option<String>,
36 msg: Option<String>,
37 ) -> PromiseOrValue<Vec<U128>> {
38 assert_one_yocto();
39 self.internal_mt_withdraw(
40 self.ensure_auth_predecessor_id(),
41 MtWithdraw {
42 token,
43 receiver_id,
44 token_ids,
45 amounts,
46 memo,
47 msg,
48 storage_deposit: None,
49 min_gas: None,
50 },
51 false,
52 )
53 .unwrap_or_panic()
54 }
55}
56
57impl Contract {
58 pub(crate) fn internal_mt_withdraw(
59 &mut self,
60 owner_id: AccountId,
61 withdraw: MtWithdraw,
62 force: bool,
63 ) -> Result<PromiseOrValue<Vec<U128>>> {
64 if withdraw.token_ids.len() != withdraw.amounts.len() || withdraw.token_ids.is_empty() {
65 return Err(DefuseError::InvalidIntent);
66 }
67
68 self.withdraw(
69 &owner_id,
70 withdraw
71 .token_ids
72 .iter()
73 .cloned()
74 .map(|token_id| Nep245TokenId::new(withdraw.token.clone(), token_id))
75 .map(Into::into)
76 .zip(withdraw.amounts.iter().map(|a| a.0))
77 .chain(withdraw.storage_deposit.map(|amount| {
78 (
79 Nep141TokenId::new(self.wnear_id().into_owned()).into(),
80 amount.as_yoctonear(),
81 )
82 })),
83 Some("withdraw"),
84 force,
85 )?;
86
87 let is_call = withdraw.msg.is_some();
88 Ok(if let Some(storage_deposit) = withdraw.storage_deposit {
89 ext_wnear::ext(self.wnear_id.clone())
90 .with_attached_deposit(NearToken::from_yoctonear(1))
91 .with_static_gas(NEAR_WITHDRAW_GAS)
92 .with_unused_gas_weight(0)
94 .near_withdraw(U128(storage_deposit.as_yoctonear()))
95 .then(
96 Self::ext(env::current_account_id())
98 .with_static_gas(
99 Self::DO_MT_WITHDRAW_GAS
100 .checked_add(withdraw.min_gas())
101 .ok_or(DefuseError::GasOverflow)
102 .unwrap_or_panic(),
103 )
104 .do_mt_withdraw(withdraw.clone()),
105 )
106 } else {
107 Self::do_mt_withdraw(withdraw.clone())
108 }
109 .then(
110 Self::ext(env::current_account_id())
111 .with_static_gas(Self::mt_resolve_withdraw_gas(withdraw.token_ids.len()))
112 .with_unused_gas_weight(0)
114 .mt_resolve_withdraw(
115 withdraw.token,
116 owner_id,
117 withdraw.token_ids,
118 withdraw.amounts,
119 is_call,
120 ),
121 )
122 .into())
123 }
124
125 #[must_use]
126 fn mt_resolve_withdraw_gas(token_count: usize) -> Gas {
127 const MT_RESOLVE_WITHDRAW_PER_TOKEN_GAS: Gas = Gas::from_tgas(2);
129 const MT_RESOLVE_WITHDRAW_BASE_GAS: Gas = Gas::from_tgas(8);
130
131 let token_count: u64 = token_count.try_into().unwrap_or_panic_display();
132
133 MT_RESOLVE_WITHDRAW_BASE_GAS
134 .checked_add(
135 MT_RESOLVE_WITHDRAW_PER_TOKEN_GAS
136 .checked_mul(token_count)
137 .unwrap_or_panic(),
138 )
139 .unwrap_or_panic()
140 }
141}
142
143#[near]
144impl Contract {
145 const DO_MT_WITHDRAW_GAS: Gas = Gas::from_tgas(5)
146 .saturating_add(STORAGE_DEPOSIT_GAS);
149
150 #[private]
151 pub fn do_mt_withdraw(withdraw: MtWithdraw) -> Promise {
152 let min_gas = withdraw.min_gas();
153 let p = if let Some(storage_deposit) = withdraw.storage_deposit {
154 require!(
155 matches!(env::promise_result_checked(0, 0), Ok(data) if data.is_empty()),
156 "near_withdraw failed",
157 );
158
159 ext_storage_management::ext(withdraw.token)
160 .with_attached_deposit(storage_deposit)
161 .with_static_gas(STORAGE_DEPOSIT_GAS)
162 .with_unused_gas_weight(0)
164 .storage_deposit(Some(withdraw.receiver_id.clone()), None)
165 } else {
166 Promise::new(withdraw.token)
167 };
168
169 let p = ext_mt_core::ext_on(p)
170 .with_attached_deposit(NearToken::from_yoctonear(1))
171 .with_static_gas(min_gas)
172 .with_unused_gas_weight(1);
174 if let Some(msg) = withdraw.msg {
175 p.mt_batch_transfer_call(
176 withdraw.receiver_id,
177 withdraw.token_ids,
178 withdraw.amounts,
179 None,
180 withdraw.memo,
181 msg,
182 )
183 } else {
184 p.mt_batch_transfer(
185 withdraw.receiver_id,
186 withdraw.token_ids,
187 withdraw.amounts,
188 None,
189 withdraw.memo,
190 )
191 }
192 }
193}
194
195#[near]
196impl MultiTokenWithdrawResolver for Contract {
197 #[private]
198 fn mt_resolve_withdraw(
199 &mut self,
200 token: AccountId,
201 sender_id: AccountId,
202 token_ids: Vec<defuse_nep245::TokenId>,
203 amounts: Vec<U128>,
204 is_call: bool,
205 ) -> Vec<U128> {
206 require!(
207 token_ids.len() == amounts.len() && !amounts.is_empty(),
208 "invalid args"
209 );
210
211 let mut used =
212 env::promise_result_checked(0, Self::mt_on_transfer_max_result_len(amounts.len()))
213 .map_or_else(
214 |_err| {
215 if is_call {
216 amounts.clone()
220 } else {
221 vec![U128(0); amounts.len()]
222 }
223 },
224 |value| {
225 if is_call {
226 serde_json::from_slice::<Vec<U128>>(&value)
228 .ok()
229 .filter(|used| used.len() == amounts.len())
230 .unwrap_or_else(|| vec![U128(0); amounts.len()])
231 } else if value.is_empty() {
232 amounts.clone()
234 } else {
235 vec![U128(0); amounts.len()]
236 }
237 },
238 );
239
240 self.deposit(
241 sender_id,
242 token_ids
243 .into_iter()
244 .zip(amounts)
245 .zip(&mut used)
246 .filter_map(|((token_id, amount), used)| {
247 used.0 = used.0.min(amount.0);
249 let refund = amount.0.saturating_sub(used.0);
250 if refund > 0 {
251 Some((Nep245TokenId::new(token.clone(), token_id).into(), refund))
252 } else {
253 None
254 }
255 }),
256 Some(REFUND_MEMO),
257 )
258 .unwrap_or_panic();
259
260 used
261 }
262}
263
264#[near]
265impl MultiTokenForcedWithdrawer for Contract {
266 #[access_control_any(roles(Role::DAO, Role::UnrestrictedWithdrawer))]
267 #[payable]
268 fn mt_force_withdraw(
269 &mut self,
270 owner_id: AccountId,
271 token: AccountId,
272 receiver_id: AccountId,
273 token_ids: Vec<defuse_nep245::TokenId>,
274 amounts: Vec<U128>,
275 memo: Option<String>,
276 msg: Option<String>,
277 ) -> PromiseOrValue<Vec<U128>> {
278 assert_one_yocto();
279 self.internal_mt_withdraw(
280 owner_id,
281 MtWithdraw {
282 token,
283 receiver_id,
284 token_ids,
285 amounts,
286 memo,
287 msg,
288 storage_deposit: None,
289 min_gas: None,
290 },
291 true,
292 )
293 .unwrap_or_panic()
294 }
295}