Skip to main content

defuse/contract/tokens/nep245/
resolver.rs

1use std::borrow::Cow;
2
3use defuse_near_utils::{Lock, REFUND_MEMO, promise_result_checked_json_with_len};
4use defuse_nep245::{
5    ClearedApproval, MtEvent, MtTransferEvent, TokenId, resolver::MultiTokenResolver,
6};
7use near_sdk::{AccountId, json_types::U128, near, require};
8
9use crate::contract::{Contract, ContractExt};
10
11#[near]
12impl MultiTokenResolver for Contract {
13    #[private]
14    fn mt_resolve_transfer(
15        &mut self,
16        previous_owner_ids: Vec<AccountId>,
17        receiver_id: AccountId,
18        token_ids: Vec<TokenId>,
19        #[allow(unused_mut)] mut amounts: Vec<U128>,
20        approvals: Option<Vec<Option<Vec<ClearedApproval>>>>,
21    ) -> Vec<U128> {
22        require!(approvals.is_none(), "approvals are not supported");
23        require!(
24            !token_ids.is_empty()
25                && previous_owner_ids.len() == token_ids.len()
26                && amounts.len() == token_ids.len(),
27            "invalid args"
28        );
29
30        let mut refunds = promise_result_checked_json_with_len::<Vec<U128>>(0, amounts.len())
31            .ok()
32            .and_then(Result::ok)
33            .filter(|refund| refund.len() == amounts.len())
34            .unwrap_or_else(|| amounts.clone());
35
36        let sender_id = previous_owner_ids.first().cloned().unwrap();
37
38        for ((token_id, previous_owner_id), (amount, refund)) in token_ids
39            .iter()
40            .map(|token_id| token_id.parse().unwrap_or_else(|e| panic!("{e}")))
41            .zip(previous_owner_ids)
42            .zip(amounts.iter_mut().zip(&mut refunds))
43        {
44            require!(
45                sender_id == previous_owner_id,
46                "approvals are not supported"
47            );
48
49            refund.0 = refund.0.min(amount.0);
50            let Some(receiver) = self
51                .accounts
52                .get_mut(&receiver_id)
53                // NOTE: refunds from locked accounts are allowed to prevent
54                // senders from loss of funds.
55                //
56                // Receiver's account might have been locked between
57                // `mt_transfer_call()` and `mt_resolve_transfer()`, so that
58                // outgoing transfers are no longer allowed for this account.
59                // But here we distinguish between regular transfers and
60                // refunds, despite it would lead to `mt_transfer` event
61                // emitted with `old_owner_id` being the locked account.
62                //
63                // Locked receivers still won't be able to transfer funds in
64                // `<receiver_id>::on_mt_transfer()` implementation.
65                .map(Lock::as_inner_unchecked_mut)
66            else {
67                // receiver doesn't have an account, so nowhere to refund from
68                return amounts;
69            };
70            let receiver_balance = receiver.token_balances.amount_for(&token_id);
71            // refund maximum what we can
72            refund.0 = refund.0.min(receiver_balance);
73            if refund.0 == 0 {
74                // noting to refund
75                continue;
76            }
77
78            // withdraw refund
79            receiver
80                .token_balances
81                .sub(token_id.clone(), refund.0)
82                .unwrap();
83            // deposit refund
84            self.accounts
85                .get_or_create(previous_owner_id)
86                // refunds are allowed for locked accounts
87                .as_inner_unchecked_mut()
88                .token_balances
89                .add(token_id, refund.0)
90                .unwrap();
91
92            // update as used amount in-place
93            amount.0 -= refund.0;
94        }
95
96        let (refunded_token_ids, refunded_amounts): (Vec<_>, Vec<_>) = token_ids
97            .into_iter()
98            .zip(refunds)
99            .filter(|(_token_id, refund)| refund.0 > 0)
100            .unzip();
101
102        if !refunded_amounts.is_empty() {
103            MtEvent::MtTransfer(Cow::Borrowed(
104                [MtTransferEvent {
105                    authorized_id: None,
106                    old_owner_id: Cow::Borrowed(&receiver_id),
107                    new_owner_id: Cow::Borrowed(&sender_id),
108                    token_ids: refunded_token_ids.into(),
109                    amounts: refunded_amounts.into(),
110                    memo: Some(REFUND_MEMO.into()),
111                }]
112                .as_slice(),
113            ))
114            // NOTE: No need for `check_refund()` here since this IS the refund.
115            // The refund memo size was already accounted for in the original transfer.
116            .emit();
117        }
118
119        amounts
120    }
121}