signet_evm/sys/
native.rs

1use crate::sys::{MintNativeSysLog, SysAction, SysBase};
2use alloy::{
3    consensus::{ReceiptEnvelope, TxEip1559, TxReceipt},
4    primitives::{utils::format_ether, Address, Log, U256},
5};
6use signet_extract::ExtractedEvent;
7use signet_types::{
8    constants::MINTER_ADDRESS,
9    primitives::{Transaction, TransactionSigned},
10    MagicSig,
11};
12use signet_zenith::Passage;
13use trevm::{
14    helpers::Ctx,
15    revm::{context::result::EVMError, Database, DatabaseCommit, Inspector},
16    Trevm, MIN_TRANSACTION_GAS,
17};
18
19const ETH_DECIMALS: u8 = 18;
20
21/// System transaction to mint native tokens.
22#[derive(Debug, Clone, Copy)]
23pub struct MintNative {
24    /// The address that will receive the minted tokens.
25    recipient: Address,
26
27    /// The host USD record for the mint.
28    decimals: u8,
29
30    /// The amount of native tokens to mint.
31    host_amount: U256,
32
33    /// The magic signature for the mint.
34    magic_sig: MagicSig,
35
36    /// The nonce of the mint transaction.
37    nonce: Option<u64>,
38    /// The rollup chain ID.
39    rollup_chain_id: u64,
40}
41
42impl MintNative {
43    /// Create a new [`MintNative`] instance from an [`ExtractedEvent`]
44    /// containing a [`Passage::EnterToken`] event.
45    pub fn new<R: TxReceipt<Log = Log>>(
46        event: &ExtractedEvent<'_, R, Passage::EnterToken>,
47        decimals: u8,
48    ) -> Self {
49        Self {
50            recipient: event.event.recipient(),
51            decimals,
52            host_amount: event.event.amount(),
53            magic_sig: event.magic_sig(),
54            nonce: None,
55            rollup_chain_id: event.rollup_chain_id(),
56        }
57    }
58
59    /// Create a new [`MintNative`] instance with a nonce.
60    pub fn new_with_nonce<R: TxReceipt<Log = Log>>(
61        event: &ExtractedEvent<'_, R, Passage::EnterToken>,
62        decimals: u8,
63        nonce: u64,
64    ) -> Self {
65        let mut mint = Self::new(event, decimals);
66        mint.populate_nonce(nonce);
67        mint
68    }
69
70    /// Create a new [`Log`] for the [`MintNative`] operation.
71    fn make_sys_log(&self) -> MintNativeSysLog {
72        MintNativeSysLog {
73            txHash: self.magic_sig.txid,
74            logIndex: self.magic_sig.event_idx as u64,
75            recipient: self.recipient,
76            amount: self.mint_amount(),
77        }
78    }
79
80    /// Convert the [`MintNative`] instance into a [`TransactionSigned`].
81    fn make_transaction(&self) -> TransactionSigned {
82        TransactionSigned::new_unhashed(
83            Transaction::Eip1559(TxEip1559 {
84                chain_id: self.rollup_chain_id,
85                nonce: self.nonce.expect("must be set"),
86                gas_limit: MIN_TRANSACTION_GAS,
87                max_fee_per_gas: 0,
88                max_priority_fee_per_gas: 0,
89                to: self.recipient.into(),
90                value: self.mint_amount(),
91                access_list: Default::default(),
92                input: Default::default(),
93            }),
94            self.magic_sig.into(),
95        )
96    }
97
98    /// Get the amount of native tokens to mint, adjusted for the decimals of
99    /// the host USD record.
100    pub fn mint_amount(&self) -> U256 {
101        let decimals = self.decimals;
102        adjust_decimals(self.host_amount, decimals, ETH_DECIMALS)
103    }
104}
105
106impl SysBase for MintNative {
107    fn name() -> &'static str {
108        "MintNative"
109    }
110
111    fn description(&self) -> String {
112        format!("Mint {} native tokens to {}", format_ether(self.mint_amount()), self.recipient)
113    }
114
115    fn has_nonce(&self) -> bool {
116        self.nonce.is_some()
117    }
118
119    fn populate_nonce(&mut self, nonce: u64) {
120        self.nonce = Some(nonce)
121    }
122
123    fn produce_transaction(&self) -> TransactionSigned {
124        self.make_transaction()
125    }
126
127    fn produce_log(&self) -> Log {
128        self.make_sys_log().into()
129    }
130
131    fn evm_sender(&self) -> Address {
132        MINTER_ADDRESS
133    }
134}
135
136impl SysAction for MintNative {
137    fn apply<Db, Insp, State>(
138        &self,
139        evm: &mut Trevm<Db, Insp, State>,
140    ) -> Result<(), EVMError<Db::Error>>
141    where
142        Db: Database + DatabaseCommit,
143        Insp: Inspector<Ctx<Db>>,
144    {
145        // Increase the balance of the recipient
146        evm.try_increase_balance_unchecked(self.recipient, self.mint_amount())
147            .map(drop)
148            .map_err(EVMError::Database)
149    }
150
151    fn produce_receipt(&self, cumulative_gas_used: u64) -> ReceiptEnvelope {
152        ReceiptEnvelope::Eip1559(
153            alloy::consensus::Receipt {
154                status: true.into(),
155                cumulative_gas_used: cumulative_gas_used.saturating_add(MIN_TRANSACTION_GAS),
156                logs: vec![self.make_sys_log().into()],
157            }
158            .with_bloom(),
159        )
160    }
161}
162
163/// Adjust the amount of tokens based on the decimals of the host USD record
164/// and the target decimals.
165///
166/// This is done by either dividing or multiplying the host amount
167/// by a power of 10, depending on whether the host decimals are greater than
168/// or less than the target decimals.
169fn adjust_decimals(amount: U256, decimals: u8, target_decimals: u8) -> U256 {
170    if target_decimals == 0 || decimals == 0 {
171        // If target decimals is 0, return the host amount unchanged
172        return amount;
173    }
174
175    if decimals > target_decimals {
176        let divisor_exp = decimals - target_decimals;
177        let divisor = U256::from(10u64).pow(U256::from(divisor_exp));
178        amount / divisor
179    } else {
180        let multiplier_exp = target_decimals.checked_sub(decimals).unwrap_or_default();
181        let multiplier = U256::from(10u64).pow(U256::from(multiplier_exp));
182        amount * multiplier
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use alloy::uint;
190
191    #[test]
192    fn mint_amount_math() {
193        uint! {
194            assert_eq!(adjust_decimals(10_U256.pow(6_U256), 6, 18), 10_U256.pow(18_U256));
195            assert_eq!(adjust_decimals(10_U256.pow(18_U256), 18, 6), 10_U256.pow(6_U256));
196
197            assert_eq!(adjust_decimals(10_U256.pow(6_U256), 6, 12), 10_U256.pow(12_U256));
198            assert_eq!(adjust_decimals(10_U256.pow(12_U256), 12, 6), 10_U256.pow(6_U256));
199
200            assert_eq!(adjust_decimals(10_U256.pow(18_U256), 18, 12), 10_U256.pow(12_U256));
201            assert_eq!(adjust_decimals(10_U256.pow(12_U256), 12, 18), 10_U256.pow(18_U256));
202
203            assert_eq!(adjust_decimals(10_U256.pow(6_U256), 6, 0), 10_U256.pow(6_U256));
204
205            assert_eq!(adjust_decimals(10_U256.pow(18_U256), 3, 6), 10_U256.pow(21_U256));
206            assert_eq!(adjust_decimals(10_U256.pow(21_U256), 6, 3), 10_U256.pow(18_U256));
207            assert_eq!(adjust_decimals(10_U256.pow(18_U256), 6, 3), 10_U256.pow(15_U256));
208        }
209    }
210}