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