Skip to main content

cast/cmd/tip20/
create.rs

1#![allow(clippy::too_many_arguments)]
2
3use crate::tx::{SendTxOpts, TxParams};
4use alloy_ens::NameOrAddress;
5use alloy_network::{Network, TransactionBuilder};
6use alloy_primitives::B256;
7use alloy_provider::Provider;
8use alloy_rpc_types::TransactionInputKind;
9use alloy_sol_types::{SolCall, SolError};
10use alloy_transport::{RpcError, TransportErrorKind};
11use foundry_cli::utils::LoadConfig;
12use foundry_common::provider::ProviderBuilder;
13use tempo_alloy::TempoNetwork;
14use tempo_contracts::precompiles::{
15    TIP20_FACTORY_ADDRESS, UnknownFunctionSelector, createTokenWithLogoCall, is_iso4217_currency,
16};
17
18/// Returns a warning message for non-ISO 4217 currency codes used in TIP-20 token creation.
19pub(crate) fn iso4217_warning_message(currency: &str) -> String {
20    let hyperlink = |url: &str| format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\");
21    let tip20_docs = hyperlink("https://docs.tempo.xyz/protocol/tip20/overview");
22    let iso_docs = hyperlink("https://www.iso.org/iso-4217-currency-codes.html");
23
24    format!(
25        "\"{currency}\" is not a recognized ISO 4217 currency code.\n\
26         \n\
27         If the token you are trying to deploy is a fiat-backed stablecoin, Tempo strongly\n\
28         recommends that the currency code field be the ISO-4217 currency code of the fiat\n\
29         currency your token tracks (e.g. \"USD\", \"EUR\", \"GBP\").\n\
30         \n\
31         The currency field is IMMUTABLE after token creation and affects fee payment\n\
32         eligibility, DEX routing, and quote token pairing. Only \"USD\"-denominated tokens\n\
33         can be used to pay transaction fees on Tempo.\n\
34         \n\
35         Learn more:\n  \
36         - Tempo TIP-20 docs: {tip20_docs}\n  \
37         - ISO 4217 standard: {iso_docs}"
38    )
39}
40
41pub(super) async fn run(
42    name: String,
43    symbol: String,
44    currency: String,
45    quote_token: NameOrAddress,
46    admin: NameOrAddress,
47    salt: B256,
48    logo_uri: Option<String>,
49    force: bool,
50    send_tx: SendTxOpts,
51    tx_opts: TxParams,
52) -> eyre::Result<()> {
53    if let Some(logo_uri) = logo_uri.as_deref() {
54        super::logo::validate_logo_uri(logo_uri)?;
55    }
56
57    let (signer, tempo_access_key) = super::resolve_tip20_signer(&send_tx, &tx_opts).await?;
58
59    let config = send_tx.eth.rpc.load_config()?;
60
61    if !is_iso4217_currency(&currency) && !force {
62        sh_warn!("{}", super::iso4217_warning_message(&currency))?;
63        let response: String = foundry_common::prompt!("\nContinue anyway? [y/N] ")?;
64        if !matches!(response.trim(), "y" | "Y") {
65            sh_status!("Aborted.")?;
66            return Ok(());
67        }
68    }
69
70    let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
71    let quote_token_addr = quote_token.resolve(&provider).await?;
72    let admin_addr = admin.resolve(&provider).await?;
73
74    let (sig, mut args) = match logo_uri {
75        Some(logo_uri) => {
76            let tx = create_logo_call_request(createTokenWithLogoCall {
77                name: name.clone(),
78                symbol: symbol.clone(),
79                currency: currency.clone(),
80                quoteToken: quote_token_addr,
81                admin: admin_addr,
82                salt,
83                logoURI: logo_uri.clone(),
84            });
85            ensure_t5_create_logo_supported(&provider, &tx).await?;
86            (
87                "createToken(string,string,string,address,address,bytes32,string)",
88                vec![
89                    name,
90                    symbol,
91                    currency,
92                    quote_token_addr.to_string(),
93                    admin_addr.to_string(),
94                    salt.to_string(),
95                    logo_uri,
96                ],
97            )
98        }
99        None => (
100            "createToken(string,string,string,address,address,bytes32)",
101            vec![
102                name,
103                symbol,
104                currency,
105                quote_token_addr.to_string(),
106                admin_addr.to_string(),
107                salt.to_string(),
108            ],
109        ),
110    };
111    super::send_tip20_transaction(
112        NameOrAddress::Address(TIP20_FACTORY_ADDRESS),
113        sig,
114        std::mem::take(&mut args),
115        send_tx,
116        tx_opts,
117        signer,
118        tempo_access_key,
119    )
120    .await?;
121
122    Ok(())
123}
124
125fn create_logo_call_request(
126    call: createTokenWithLogoCall,
127) -> <TempoNetwork as Network>::TransactionRequest {
128    let mut tx = <TempoNetwork as Network>::TransactionRequest::default();
129    tx.set_kind(TIP20_FACTORY_ADDRESS.into());
130    tx.set_input_kind(call.abi_encode(), TransactionInputKind::Both);
131    tx
132}
133
134async fn ensure_t5_create_logo_supported<P>(
135    provider: &P,
136    tx: &<TempoNetwork as Network>::TransactionRequest,
137) -> eyre::Result<()>
138where
139    P: Provider<TempoNetwork>,
140{
141    match provider.call(tx.clone()).await {
142        Ok(_) => Ok(()),
143        Err(err) if is_t5_create_logo_unknown_selector(&err) => {
144            eyre::bail!(
145                "--logo-uri requires a T5-compatible TIP20Factory; the configured RPC rejected the 7-arg createToken selector 0x5323d222"
146            )
147        }
148        Err(_) => Ok(()),
149    }
150}
151
152fn is_t5_create_logo_unknown_selector(err: &RpcError<TransportErrorKind>) -> bool {
153    let Some(data) = err
154        .as_error_resp()
155        .and_then(|error| error.data.as_ref())
156        .and_then(|data| serde_json::from_str::<alloy_primitives::Bytes>(data.get()).ok())
157    else {
158        return false;
159    };
160
161    is_t5_create_logo_unknown_selector_data(data.as_ref())
162}
163
164fn is_t5_create_logo_unknown_selector_data(data: &[u8]) -> bool {
165    data == UnknownFunctionSelector { selector: createTokenWithLogoCall::SELECTOR.into() }
166        .abi_encode()
167        .as_slice()
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use alloy_primitives::{address, b256, bytes};
174    use tempo_contracts::precompiles::createTokenCall;
175
176    #[test]
177    fn legacy_create_selector_is_preserved() {
178        let calldata = createTokenCall {
179            name: "US Dollar Coin".to_string(),
180            symbol: "USDC".to_string(),
181            currency: "USD".to_string(),
182            quoteToken: address!("0000000000000000000000000000000000000001"),
183            admin: address!("0000000000000000000000000000000000000002"),
184            salt: b256!("0000000000000000000000000000000000000000000000000000000000000003"),
185        }
186        .abi_encode();
187
188        assert_eq!(&calldata[..4], bytes!("68130445").as_ref());
189    }
190
191    #[test]
192    fn t5_create_selector_includes_logo_uri_overload() {
193        let calldata = createTokenWithLogoCall {
194            name: "US Dollar Coin".to_string(),
195            symbol: "USDC".to_string(),
196            currency: "USD".to_string(),
197            quoteToken: address!("0000000000000000000000000000000000000001"),
198            admin: address!("0000000000000000000000000000000000000002"),
199            salt: b256!("0000000000000000000000000000000000000000000000000000000000000003"),
200            logoURI: "https://example.com/logo.png".to_string(),
201        }
202        .abi_encode();
203
204        assert_ne!(&calldata[..4], bytes!("68130445").as_ref());
205        assert_eq!(&calldata[..4], createTokenWithLogoCall::SELECTOR.as_ref());
206    }
207
208    #[test]
209    fn detects_t5_create_logo_unknown_selector_revert_data() {
210        let data = UnknownFunctionSelector { selector: createTokenWithLogoCall::SELECTOR.into() }
211            .abi_encode();
212
213        assert!(is_t5_create_logo_unknown_selector_data(&data));
214    }
215}