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
18pub(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(¤cy) && !force {
62 sh_warn!("{}", super::iso4217_warning_message(¤cy))?;
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}