Skip to main content

foundry_primitives/transaction/
celo.rs

1//! CIP-64 transaction encoding. Execution-time fee currency accounting is not implemented.
2
3use alloy_consensus::{
4    SignableTransaction, Transaction, TxEip1559, Typed2718,
5    transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
6};
7use alloy_eips::{eip2718::IsTyped2718, eip2930::AccessList, eip7702::SignedAuthorization};
8use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256};
9use alloy_rlp::{BufMut, Decodable, Encodable};
10use serde::{Deserialize, Serialize};
11
12/// Celo dynamic fee transaction type introduced by CIP-64.
13pub const CIP64_TX_TYPE: u8 = 0x7b;
14
15/// An EIP-1559-shaped transaction with a signed fee currency field.
16#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct TxCip64 {
19    /// Ordinary EIP-1559 transaction fields.
20    #[serde(flatten)]
21    pub inner: TxEip1559,
22    /// Currency used to pay transaction fees, or native CELO when absent.
23    #[serde(default)]
24    pub fee_currency: Option<Address>,
25}
26
27impl RlpEcdsaEncodableTx for TxCip64 {
28    fn rlp_encoded_fields_length(&self) -> usize {
29        self.inner.rlp_encoded_fields_length()
30            + self.fee_currency.map_or(1, |address| address.length())
31    }
32
33    fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
34        self.inner.rlp_encode_fields(out);
35        match self.fee_currency {
36            Some(address) => address.encode(out),
37            None => out.put_u8(alloy_rlp::EMPTY_STRING_CODE),
38        }
39    }
40}
41
42impl RlpEcdsaDecodableTx for TxCip64 {
43    const DEFAULT_TX_TYPE: u8 = CIP64_TX_TYPE;
44
45    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
46        let inner = TxEip1559::rlp_decode_fields(buf)?;
47        let fee_currency = match buf.first() {
48            Some(&alloy_rlp::EMPTY_STRING_CODE) => {
49                *buf = &buf[1..];
50                None
51            }
52            _ => Some(Address::decode(buf)?),
53        };
54        Ok(Self { inner, fee_currency })
55    }
56}
57
58impl Typed2718 for TxCip64 {
59    fn ty(&self) -> u8 {
60        CIP64_TX_TYPE
61    }
62}
63
64impl IsTyped2718 for TxCip64 {
65    fn is_type(type_id: u8) -> bool {
66        type_id == CIP64_TX_TYPE
67    }
68}
69
70impl SignableTransaction<Signature> for TxCip64 {
71    fn set_chain_id(&mut self, chain_id: ChainId) {
72        self.inner.chain_id = chain_id;
73    }
74
75    fn encode_for_signing(&self, out: &mut dyn BufMut) {
76        out.put_u8(CIP64_TX_TYPE);
77        self.rlp_encode(out);
78    }
79
80    fn payload_len_for_signature(&self) -> usize {
81        self.rlp_encoded_length() + 1
82    }
83}
84
85impl Transaction for TxCip64 {
86    fn chain_id(&self) -> Option<ChainId> {
87        self.inner.chain_id()
88    }
89
90    fn nonce(&self) -> u64 {
91        self.inner.nonce()
92    }
93
94    fn gas_limit(&self) -> u64 {
95        self.inner.gas_limit()
96    }
97
98    fn gas_price(&self) -> Option<u128> {
99        self.inner.gas_price()
100    }
101
102    fn max_fee_per_gas(&self) -> u128 {
103        self.inner.max_fee_per_gas()
104    }
105
106    fn max_priority_fee_per_gas(&self) -> Option<u128> {
107        self.inner.max_priority_fee_per_gas()
108    }
109
110    fn max_fee_per_blob_gas(&self) -> Option<u128> {
111        self.inner.max_fee_per_blob_gas()
112    }
113
114    fn priority_fee_or_price(&self) -> u128 {
115        self.inner.priority_fee_or_price()
116    }
117
118    fn is_dynamic_fee(&self) -> bool {
119        self.inner.is_dynamic_fee()
120    }
121
122    fn kind(&self) -> TxKind {
123        self.inner.kind()
124    }
125
126    fn is_create(&self) -> bool {
127        self.inner.is_create()
128    }
129
130    fn value(&self) -> U256 {
131        self.inner.value()
132    }
133
134    fn input(&self) -> &Bytes {
135        self.inner.input()
136    }
137
138    fn access_list(&self) -> Option<&AccessList> {
139        self.inner.access_list()
140    }
141
142    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
143        self.inner.blob_versioned_hashes()
144    }
145
146    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
147        self.inner.authorization_list()
148    }
149
150    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
151        self.inner.effective_gas_price(base_fee)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::{FoundryTransactionRequest, FoundryTxEnvelope, FoundryTypedTx};
159    use alloy_eips::eip2718::{Decodable2718, Encodable2718};
160    use alloy_primitives::{address, hex};
161    use serde_json::json;
162
163    #[test]
164    fn cip64_encoding_and_signing_payload() {
165        let tx = TxCip64 {
166            inner: TxEip1559 {
167                chain_id: 1,
168                max_priority_fee_per_gas: 1,
169                max_fee_per_gas: 2,
170                gas_limit: 21_000,
171                to: address!("1111111111111111111111111111111111111111").into(),
172                ..Default::default()
173            },
174            fee_currency: Some(address!("2222222222222222222222222222222222222222")),
175        };
176        // CIP-64 appends feeCurrency after accessList, before the signature fields.
177        assert_eq!(
178            tx.encoded_for_signing(),
179            hex!(
180                "7bf4018001028252089411111111111111111111111111111111111111118080c0942222222222222222222222222222222222222222"
181            )
182        );
183        let signing_hash = tx.signature_hash();
184        let signed = tx.clone().into_signed(Signature::new(U256::ONE, U256::from(2), false));
185        let envelope = FoundryTxEnvelope::Celo(signed);
186        let encoded = envelope.encoded_2718();
187        assert_eq!(
188            encoded,
189            hex!(
190                "7bf7018001028252089411111111111111111111111111111111111111118080c0942222222222222222222222222222222222222222800102"
191            )
192        );
193        assert_eq!(FoundryTxEnvelope::decode_2718(&mut encoded.as_slice()).unwrap(), envelope);
194        let mut native = tx;
195        native.fee_currency = None;
196        assert_ne!(native.signature_hash(), signing_hash);
197        let native = FoundryTxEnvelope::Celo(native.into_signed(Signature::new(
198            U256::ONE,
199            U256::from(2),
200            false,
201        )));
202        assert_eq!(
203            FoundryTxEnvelope::decode_2718(&mut native.encoded_2718().as_slice()).unwrap(),
204            native
205        );
206    }
207
208    #[test]
209    fn cip64_request_classification_and_conflicts() {
210        let currency = "0x2222222222222222222222222222222222222222";
211        let request = json!({"feeCurrency":currency,"chainId":"0x1","nonce":"0x0","gas":"0x5208","maxFeePerGas":"0x2","maxPriorityFeePerGas":"0x1","to":"0x1111111111111111111111111111111111111111"});
212        let parsed: FoundryTransactionRequest = serde_json::from_value(request.clone()).unwrap();
213        let typed = parsed.build_typed_tx().unwrap();
214        assert!(matches!(typed, FoundryTypedTx::Celo(_)));
215        assert_eq!(
216            serde_json::to_value(FoundryTransactionRequest::from(typed)).unwrap()["feeCurrency"],
217            currency
218        );
219        for (field, value) in [
220            ("type", json!("0x2")),
221            ("gasPrice", json!("0x1")),
222            ("authorizationList", json!([])),
223            ("blobVersionedHashes", json!([])),
224            ("feeToken", json!(currency)),
225        ] {
226            let mut invalid = request.clone();
227            invalid[field] = value;
228            assert!(
229                serde_json::from_value::<FoundryTransactionRequest>(invalid).is_err(),
230                "{field}"
231            );
232        }
233        let native: FoundryTransactionRequest =
234            serde_json::from_value(json!({"type":"0x7b"})).unwrap();
235        assert!(matches!(native, FoundryTransactionRequest::Celo(_)));
236        let ordinary: FoundryTransactionRequest =
237            serde_json::from_value(json!({"feeCurrency":null})).unwrap();
238        assert!(ordinary.is_ethereum());
239    }
240}