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, 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 .with_unused_gas_weight(0)
83 .near_withdraw(U128(storage_deposit.as_yoctonear()))
84 .then(
85 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 .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 .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 .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 .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(); let used = env::promise_result_checked(0, MAX_RESULT_LENGTH).map_or(
162 if is_call {
163 amount.0
167 } else {
168 0
169 },
170 |value| {
171 if is_call {
172 serde_json::from_slice::<U128>(&value)
174 .unwrap_or_default()
175 .0
176 .min(amount.0)
177 } else if value.is_empty() {
178 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}