1use super::{ExecutableIntent, IntentEvent};
2use crate::{
3 DefuseError, Result,
4 accounts::AccountEvent,
5 amounts::Amounts,
6 engine::{Engine, Inspector, State, StateView},
7 events::DefuseEvent,
8 fees::Pips,
9 token_id::{TokenId, TokenIdType},
10};
11use defuse_num_utils::CheckedMulDiv;
12use impl_tools::autoimpl;
13use near_sdk::{AccountId, AccountIdRef, CryptoHash, near};
14use serde_with::{DisplayFromStr, serde_as};
15use std::{borrow::Cow, collections::BTreeMap};
16
17pub type TokenDeltas = Amounts<BTreeMap<TokenId, i128>>;
18
19#[near(serializers = [borsh, json])]
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21#[autoimpl(Deref using self.diff)]
22#[autoimpl(DerefMut using self.diff)]
23pub struct TokenDiff {
29 #[serde_as(as = "Amounts<BTreeMap<_, DisplayFromStr>>")]
30 pub diff: TokenDeltas,
31
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub memo: Option<String>,
34
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub referral: Option<AccountId>,
37}
38
39impl ExecutableIntent for TokenDiff {
40 fn execute_intent<S, I>(
41 self,
42 signer_id: &AccountIdRef,
43 engine: &mut Engine<S, I>,
44 intent_hash: CryptoHash,
45 ) -> Result<()>
46 where
47 S: State,
48 I: Inspector,
49 {
50 if self.diff.is_empty() {
51 return Err(DefuseError::InvalidIntent);
52 }
53
54 let protocol_fee = engine.state.fee();
55 let mut fees_collected: Amounts = Amounts::default();
56
57 for (token_id, delta) in &self.diff {
58 if *delta == 0 {
59 return Err(DefuseError::InvalidIntent);
60 }
61
62 engine
64 .state
65 .internal_apply_deltas(signer_id, [(token_id.clone(), *delta)])?;
66
67 if *delta < 0 {
69 let amount = delta.unsigned_abs();
70 let fee = Self::token_fee(token_id, amount, protocol_fee).fee_ceil(amount);
71
72 fees_collected
74 .add(token_id.clone(), fee)
75 .ok_or(DefuseError::BalanceOverflow)?;
76 }
77 }
78
79 engine.inspector.on_event(DefuseEvent::TokenDiff(
80 [IntentEvent::new(
81 AccountEvent::new(
82 signer_id,
83 TokenDiffEvent {
84 diff: Cow::Borrowed(&self),
85 fees_collected: fees_collected.clone(),
86 },
87 ),
88 intent_hash,
89 )]
90 .as_slice()
91 .into(),
92 ));
93
94 if !fees_collected.is_empty() {
96 engine
97 .state
98 .internal_add_balance(engine.state.fee_collector().into_owned(), fees_collected)?;
99 }
100
101 Ok(())
102 }
103}
104
105#[near(serializers = [json])]
106#[derive(Debug, Clone)]
107pub struct TokenDiffEvent<'a> {
109 #[serde(flatten)]
110 pub diff: Cow<'a, TokenDiff>,
111
112 #[serde_as(as = "Amounts<BTreeMap<_, DisplayFromStr>>")]
113 #[serde(skip_serializing_if = "Amounts::is_empty")]
114 pub fees_collected: Amounts,
115}
116
117impl TokenDiff {
118 #[inline]
121 pub fn closure(self, fee: Pips) -> Option<TokenDeltas> {
122 Self::closure_deltas(self.diff.into_inner(), fee)
123 }
124
125 #[inline]
128 pub fn closure_many(diffs: impl IntoIterator<Item = Self>, fee: Pips) -> Option<TokenDeltas> {
129 Self::closure_deltas(diffs.into_iter().flat_map(|d| d.diff.into_inner()), fee)
130 }
131
132 #[inline]
136 pub fn closure_deltas(
137 deltas: impl IntoIterator<Item = (TokenId, i128)>,
138 fee: Pips,
139 ) -> Option<TokenDeltas> {
140 deltas
141 .into_iter()
142 .try_fold(TokenDeltas::default(), |deltas, (token_id, delta)| {
144 let supply_delta = Self::supply_delta(&token_id, delta, fee)?;
145 deltas.with_apply_delta(token_id, supply_delta)
146 })?
147 .into_inner()
148 .into_iter()
149 .try_fold(TokenDeltas::default(), |deltas, (token_id, delta)| {
151 let closure = Self::closure_supply_delta(&token_id, delta, fee)?;
152 deltas.with_apply_delta(token_id, closure)
153 })
154 }
155
156 #[inline]
160 pub fn closure_delta(token_id: &TokenId, delta: i128, fee: Pips) -> Option<i128> {
161 Self::closure_supply_delta(token_id, Self::supply_delta(token_id, delta, fee)?, fee)
162 }
163
164 #[inline]
166 fn supply_delta(token_id: &TokenId, delta: i128, fee: Pips) -> Option<i128> {
167 if delta < 0 {
168 delta.checked_mul_div_ceil(
170 Self::token_fee(token_id, delta.unsigned_abs(), fee)
171 .invert()
172 .as_pips()
173 .into(),
174 Pips::MAX.as_pips().into(),
175 )
176 } else {
177 Some(delta)
179 }
180 }
181
182 #[inline]
186 pub fn closure_supply_delta(token_id: &TokenId, delta: i128, fee: Pips) -> Option<i128> {
187 let closure = delta.checked_neg()?;
188 if closure < 0 {
189 closure.checked_mul_div_euclid(
191 Pips::MAX.as_pips().into(),
192 Self::token_fee(token_id, delta.unsigned_abs(), fee)
193 .invert()
194 .as_pips()
195 .into(),
196 )
197 } else {
198 Some(closure)
200 }
201 }
202
203 #[inline]
204 pub fn token_fee(token_id: impl Into<TokenIdType>, amount: u128, fee: Pips) -> Pips {
205 let token_id = token_id.into();
206 match token_id {
207 TokenIdType::Nep141 => {}
208 TokenIdType::Nep245 if amount > 1 => {}
209 TokenIdType::Nep171 | TokenIdType::Nep245 => return Pips::ZERO,
211 #[cfg(feature = "imt")]
212 TokenIdType::Imt => {
213 if amount <= 1 {
214 return Pips::ZERO;
215 }
216 }
217 }
218 fee
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use itertools::Itertools;
225 use rstest::rstest;
226
227 use crate::token_id::{nep141::Nep141TokenId, nep171::Nep171TokenId, nep245::Nep245TokenId};
228
229 use super::*;
230
231 #[rstest]
232 #[test]
233 fn closure_delta(
234 #[values(
235 (Nep141TokenId::new("ft.near".parse::<AccountId>().unwrap()).into(), 1_000_000),
236 (Nep141TokenId::new("ft.near".parse::<AccountId>().unwrap()).into(), -1_000_000),
237 (Nep171TokenId::new("nft.near".parse::<AccountId>().unwrap(), "1".to_string()).into(), 1),
238 (Nep171TokenId::new("nft.near".parse::<AccountId>().unwrap(), "1".to_string()).into(), -1),
239 (Nep245TokenId::new("mt.near".parse::<AccountId>().unwrap(), "ft1".to_string()).into(), 1_000_000),
240 (Nep245TokenId::new("mt.near".parse::<AccountId>().unwrap(), "ft1".to_string()).into(), -1_000_000),
241 (Nep245TokenId::new("mt.near".parse::<AccountId>().unwrap(), "nft1".to_string()).into(), 1),
242 (Nep245TokenId::new("mt.near".parse::<AccountId>().unwrap(), "nft1".to_string()).into(), -1),
243 )]
244 token_delta: (TokenId, i128),
245 #[values(
246 Pips::ZERO,
247 Pips::ONE_PIP,
248 Pips::ONE_BIP,
249 Pips::ONE_PERCENT,
250 Pips::ONE_PERCENT * 50,
251 )]
252 fee: Pips,
253 ) {
254 let (token_id, delta) = token_delta;
255 let closure = TokenDiff::closure_delta(&token_id, delta, fee).unwrap();
256
257 assert_eq!(
258 TokenDiff::supply_delta(&token_id, delta, fee).unwrap()
259 + TokenDiff::supply_delta(&token_id, closure, fee).unwrap(),
260 0,
261 "invariant violated for {token_id}: delta: {delta}, closure: {closure}, fee: {fee}",
262 );
263 }
264
265 #[test]
266 fn closure_deltas_empty() {
267 assert!(
268 TokenDiff::closure_deltas(None, Pips::ONE_BIP)
269 .unwrap()
270 .is_empty()
271 );
272 }
273
274 #[rstest]
275 #[test]
276 fn closure_deltas_nonoverlapping(
277 #[values(
278 Pips::ZERO,
279 Pips::ONE_PIP,
280 Pips::ONE_BIP,
281 Pips::ONE_BIP * 12,
282 Pips::ONE_PERCENT,
283 Pips::ONE_PERCENT * 50,
284 )]
285 fee: Pips,
286 ) {
287 let [t1, t2, t3] = ["ft1", "ft2", "ft3"]
288 .map(|t| TokenId::from(Nep141TokenId::new(t.parse::<AccountId>().unwrap())));
289
290 for (d1, d2, d3) in [0, 1, -1, 50, -50, 100, -100, 300, -300, 10_000, -10_000]
291 .into_iter()
292 .tuple_combinations()
293 {
294 assert_eq!(
295 TokenDiff::closure_deltas(
296 [
297 TokenDeltas::default()
298 .with_apply_deltas([(t1.clone(), d1), (t2.clone(), d2)])
299 .unwrap(),
300 TokenDeltas::default()
301 .with_apply_deltas([(t3.clone(), d3)])
302 .unwrap(),
303 ]
304 .into_iter()
305 .flatten(),
306 fee
307 )
308 .unwrap(),
309 TokenDeltas::default()
310 .with_apply_deltas([
311 (t1.clone(), TokenDiff::closure_delta(&t1, d1, fee).unwrap()),
312 (t2.clone(), TokenDiff::closure_delta(&t2, d2, fee).unwrap()),
313 (t3.clone(), TokenDiff::closure_delta(&t3, d3, fee).unwrap()),
314 ])
315 .unwrap(),
316 "d1: {d1}, d2: {d2}, d3: {d3}"
317 );
318 }
319 }
320
321 #[rstest]
322 #[test]
323 fn arbitrage_means_somebody_looses(#[values(Pips::ZERO, Pips::ONE_BIP)] fee: Pips) {
324 let [t1, t2, t3] = ["ft1", "ft2", "ft3"]
325 .map(|t| TokenId::from(Nep141TokenId::new(t.parse::<AccountId>().unwrap())));
326
327 let closure = TokenDiff::closure_deltas(
328 [
329 TokenDeltas::default()
330 .with_apply_deltas([(t1.clone(), -100), (t2.clone(), 200)])
331 .unwrap(),
332 TokenDeltas::default()
333 .with_apply_deltas([(t2, -200), (t3.clone(), 300)])
334 .unwrap(),
335 TokenDeltas::default()
336 .with_apply_deltas([(t3, -300), (t1, 101)])
337 .unwrap(),
338 ]
339 .into_iter()
340 .flatten(),
341 fee,
342 )
343 .unwrap();
344 assert!(!closure.is_empty());
345 assert!(closure.into_inner().into_values().all(i128::is_negative));
346 }
347}