defuse_core/intents/
token_diff.rs

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)]
23/// The user declares the will to have a set of changes done to set of tokens. For example,
24/// a simple trade of 100 of token A for 200 of token B, can be represented by `TokenDiff`
25/// of {"A": -100, "B": 200} (this format is just for demonstration purposes).
26/// In general, the user can submit multiple changes with many tokens,
27/// not just token A for token B.
28pub 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            // add delta to signer's account
63            engine
64                .state
65                .internal_apply_deltas(signer_id, [(token_id.clone(), *delta)])?;
66
67            // take fees only from negative deltas (i.e. token_in)
68            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                // collect fee
73                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        // deposit fees to collector
95        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)]
107/// An event emitted when a `TokenDiff` intent is executed.
108pub 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    /// Returns [`TokenDiff`] closure to successfully execute `self`
119    /// assuming given `fee`
120    #[inline]
121    pub fn closure(self, fee: Pips) -> Option<TokenDeltas> {
122        Self::closure_deltas(self.diff.into_inner(), fee)
123    }
124
125    /// Returns [`TokenDiff`] closure to successfully execute given set
126    /// of distinct [`TokenDiff`] assuming given `fee`
127    #[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    /// Returns closure for deltas that should be given in a single
133    /// [`TokenDiff`] to successfully execute given set of distinct `deltas`
134    /// assuming given `fee`
135    #[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            // collect total supply deltas
143            .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            // calculate closures from total supply deltas
150            .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    /// Returns closure for delta that should be given in a single
157    /// [`TokenDiff`] to successfully execute [`TokenDiff`] with given
158    /// `delta` on the same token assuming given `fee`.
159    #[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    /// Returns total supply delta from token delta
165    #[inline]
166    fn supply_delta(token_id: &TokenId, delta: i128, fee: Pips) -> Option<i128> {
167        if delta < 0 {
168            // fee is taken only on negative deltas (i.e. token_in)
169            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            // token_out
178            Some(delta)
179        }
180    }
181
182    /// Returns closure for total supply delta that should be given in
183    /// a single [`TokenDiff`] to successfully execute [`TokenDiff`] with
184    /// given `delta` on the same token assuming given `fee`.
185    #[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            // fee is taken only on negative deltas (i.e. token_in)
190            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            // token_out
199            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            // do not take fees on NFTs and MTs with |delta| <= 1
210            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}