oasis_runtime_sdk/modules/accounts/
fee.rs

1//! Fee manager.
2use std::collections::BTreeMap;
3
4use crate::types::{address::Address, token};
5
6/// The per-block fee manager that records what fees have been charged by the current transaction,
7/// how much should be refunded and what were all of the fee payments in the current block.
8///
9/// Note that the fee manager does not perform any state modifications by itself.
10#[derive(Clone, Default, Debug)]
11pub struct FeeManager {
12    /// Fees charged for the current transaction.
13    tx_fee: Option<TransactionFee>,
14    /// Fees charged in the current block.
15    block_fees: BTreeMap<token::Denomination, u128>,
16}
17
18/// Information about fees charged for the current transaction.
19#[derive(Clone, Default, Debug)]
20pub struct TransactionFee {
21    /// Transaction fee payer address.
22    payer: Address,
23    /// Denomination of the transaction fee.
24    denomination: token::Denomination,
25    /// Amount charged before transaction execution.
26    charged: u128,
27    /// Amount that should be refunded after transaction execution.
28    refunded: u128,
29}
30
31impl TransactionFee {
32    /// Denomination of the transaction fee.
33    pub fn denomination(&self) -> token::Denomination {
34        self.denomination.clone()
35    }
36
37    /// Transaction fee amount.
38    pub fn amount(&self) -> u128 {
39        self.charged.saturating_sub(self.refunded)
40    }
41
42    /// Transaction fee payer address.
43    pub fn payer(&self) -> Address {
44        self.payer
45    }
46}
47
48impl FeeManager {
49    /// Create a new per-block fee manager.
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Fees charged for the current transaction.
55    pub fn tx_fee(&self) -> Option<&TransactionFee> {
56        self.tx_fee.as_ref()
57    }
58
59    /// Record that a transaction fee has been charged.
60    ///
61    /// This method should only be called after the charged fee has been subtracted from the payer's
62    /// account (e.g. reflected in state).
63    pub fn record_fee(&mut self, payer: Address, amount: &token::BaseUnits) {
64        let tx_fee = self.tx_fee.get_or_insert_with(|| TransactionFee {
65            payer,
66            denomination: amount.denomination().clone(),
67            ..Default::default()
68        });
69
70        assert!(payer == tx_fee.payer, "transaction fee payer cannot change");
71        assert!(
72            amount.denomination() == &tx_fee.denomination,
73            "transaction fee denomination cannot change"
74        );
75
76        tx_fee.charged = tx_fee
77            .charged
78            .checked_add(amount.amount())
79            .expect("should never overflow");
80    }
81
82    /// Record that a portion of the previously charged transaction fee should be refunded.
83    pub fn record_refund(&mut self, amount: u128) {
84        if amount == 0 || self.tx_fee.is_none() {
85            return;
86        }
87
88        let tx_fee = self.tx_fee.as_mut().unwrap();
89        tx_fee.refunded = std::cmp::min(tx_fee.refunded.saturating_add(amount), tx_fee.charged);
90    }
91
92    /// Commit the currently open transaction fee by moving the final recorded amount into the fees
93    /// charged for the current block.
94    ///
95    /// Note that this does not perform any state modifications and the caller is assumed to apply
96    /// any updates after calling this method.
97    #[must_use = "fee updates should be applied after calling commit"]
98    pub fn commit_tx(&mut self) -> FeeUpdates {
99        let tx_fee = self.tx_fee.take().unwrap_or_default();
100        if tx_fee.amount() > 0 {
101            let block_fees = self
102                .block_fees
103                .entry(tx_fee.denomination.clone())
104                .or_default();
105
106            // Add to per-block accumulator.
107            *block_fees = block_fees
108                .checked_add(tx_fee.amount())
109                .expect("should never overflow");
110        }
111
112        FeeUpdates {
113            payer: tx_fee.payer,
114            refund: token::BaseUnits::new(tx_fee.refunded, tx_fee.denomination),
115        }
116    }
117
118    /// Commit the fees accumulated for the current block, returning the resulting map.
119    ///
120    /// Note that this does not perform any state modifications and the caller is assumed to apply
121    /// any updates after calling this method.
122    #[must_use = "accumulated fees should be applied after calling commit"]
123    pub fn commit_block(self) -> BTreeMap<token::Denomination, u128> {
124        self.block_fees
125    }
126}
127
128/// Fee updates to apply to state after `commit_tx`.
129///
130/// This assumes that the initial fee charge has already happened, see the description of
131/// `FeeManager::record_fee` for details.
132pub struct FeeUpdates {
133    /// Fee payer.
134    pub payer: Address,
135    /// Amount that should be refunded to fee payer.
136    pub refund: token::BaseUnits,
137}
138
139#[cfg(test)]
140mod test {
141    use super::*;
142
143    use crate::{
144        testing::keys,
145        types::token::{self, Denomination},
146    };
147
148    #[test]
149    fn test_basic_refund() {
150        let mut mgr = FeeManager::new();
151
152        assert!(mgr.tx_fee().is_none());
153
154        // First transaction with refund.
155        let fee = token::BaseUnits::new(1_000_000, Denomination::NATIVE);
156        mgr.record_fee(keys::alice::address(), &fee);
157
158        let tx_fee = mgr.tx_fee().expect("tx_fee should be set");
159        assert_eq!(tx_fee.payer(), keys::alice::address());
160        assert_eq!(&tx_fee.denomination(), fee.denomination());
161        assert_eq!(tx_fee.amount(), fee.amount());
162
163        mgr.record_refund(400_000);
164
165        let tx_fee = mgr.tx_fee().expect("tx_fee should be set");
166        assert_eq!(tx_fee.payer(), keys::alice::address());
167        assert_eq!(&tx_fee.denomination(), fee.denomination());
168        assert_eq!(tx_fee.amount(), 600_000, "should take refund into account");
169
170        let fee_updates = mgr.commit_tx();
171        assert_eq!(fee_updates.payer, keys::alice::address());
172        assert_eq!(
173            fee_updates.refund,
174            token::BaseUnits::new(400_000, Denomination::NATIVE)
175        );
176        assert!(mgr.tx_fee().is_none());
177
178        // Some more transactions.
179        mgr.record_fee(
180            keys::bob::address(),
181            &token::BaseUnits::new(50_000, Denomination::NATIVE),
182        );
183        let fee_updates = mgr.commit_tx();
184        assert_eq!(fee_updates.payer, keys::bob::address());
185        assert_eq!(
186            fee_updates.refund,
187            token::BaseUnits::new(0, Denomination::NATIVE)
188        );
189
190        mgr.record_fee(
191            keys::dave::address(),
192            &token::BaseUnits::new(25_000, "TEST".parse().unwrap()),
193        );
194        mgr.record_fee(
195            keys::dave::address(),
196            &token::BaseUnits::new(5_000, "TEST".parse().unwrap()),
197        );
198        let fee_updates = mgr.commit_tx();
199        assert_eq!(fee_updates.payer, keys::dave::address());
200        assert_eq!(
201            fee_updates.refund,
202            token::BaseUnits::new(0, "TEST".parse().unwrap())
203        );
204
205        let block_fees = mgr.commit_block();
206        assert_eq!(block_fees.len(), 2);
207        assert_eq!(block_fees[&Denomination::NATIVE], 650_000);
208        assert_eq!(block_fees[&"TEST".parse().unwrap()], 30_000);
209    }
210
211    #[test]
212    fn test_refund_without_charge() {
213        let mut mgr = FeeManager::new();
214
215        mgr.record_refund(1_000);
216        assert!(
217            mgr.tx_fee().is_none(),
218            "refund should not be recorded if no charge"
219        );
220
221        let fee_updates = mgr.commit_tx();
222        assert_eq!(fee_updates.payer, Default::default());
223        assert_eq!(
224            fee_updates.refund,
225            token::BaseUnits::new(0, Default::default())
226        );
227
228        let block_fees = mgr.commit_block();
229        assert!(block_fees.is_empty(), "there should be no recorded fees");
230    }
231
232    #[test]
233    #[should_panic(expected = "transaction fee payer cannot change")]
234    fn test_fail_payer_change() {
235        let mut mgr = FeeManager::new();
236
237        let fee = token::BaseUnits::new(1_000_000, Denomination::NATIVE);
238        mgr.record_fee(keys::alice::address(), &fee);
239        mgr.record_fee(keys::bob::address(), &fee); // Should panic.
240    }
241
242    #[test]
243    #[should_panic(expected = "transaction fee denomination cannot change")]
244    fn test_fail_denomination_change() {
245        let mut mgr = FeeManager::new();
246
247        let fee = token::BaseUnits::new(1_000_000, Denomination::NATIVE);
248        mgr.record_fee(keys::alice::address(), &fee);
249
250        let fee = token::BaseUnits::new(1_000_000, "TEST".parse().unwrap());
251        mgr.record_fee(keys::alice::address(), &fee); // Should panic.
252    }
253}