Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/tempo-bench/src/cmd/max_tps/dex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ where
let factory = ITIP20Factory::new(TIP20_FACTORY_ADDRESS, provider.clone());
let salt = alloy::primitives::B256::random();
let receipt = factory
.createToken(
.createToken_0(
"Test".to_owned(),
"TEST".to_owned(),
"USD".to_owned(),
Expand Down
16 changes: 16 additions & 0 deletions crates/contracts/src/precompiles/tip20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ crate::sol! {
function supplyCap() external view returns (uint256);
function paused() external view returns (bool);
function transferPolicyId() external view returns (uint64);
function logoURI() external view returns (string memory);
function setLogoURI(string calldata newLogoURI) external;
function burnBlocked(address from, uint256 amount) external;
function mintWithMemo(address to, uint256 amount, bytes32 memo) external;
function burnWithMemo(uint256 amount, bytes32 memo) external;
Expand Down Expand Up @@ -149,6 +151,7 @@ crate::sol! {
event QuoteTokenUpdate(address indexed updater, address indexed newQuoteToken);
event RewardDistributed(address indexed funder, uint256 amount);
event RewardRecipientSet(address indexed holder, address indexed recipient);
event LogoURIUpdated(address indexed updater, string newLogoURI);

// Errors
error InsufficientBalance(uint256 available, uint256 required, address token);
Expand All @@ -170,6 +173,8 @@ crate::sol! {
error InvalidTransferPolicyId();
error PermitExpired();
error InvalidSignature();
error LogoURITooLong();
error InvalidLogoURI();
}
}

Expand Down Expand Up @@ -310,6 +315,17 @@ impl TIP20Error {
pub const fn invalid_signature() -> Self {
Self::InvalidSignature(ITIP20::InvalidSignature {})
}

/// Error when logoURI exceeds 256 bytes (TIP-1026)
pub const fn logo_uri_too_long() -> Self {
Self::LogoURITooLong(ITIP20::LogoURITooLong {})
}

/// Error when logoURI is not a syntactically valid URI or its scheme is
/// not in the protocol allowlist (TIP-1026).
pub const fn invalid_logo_uri() -> Self {
Self::InvalidLogoURI(ITIP20::InvalidLogoURI {})
}
}

#[cfg(test)]
Expand Down
17 changes: 17 additions & 0 deletions crates/contracts/src/precompiles/tip20_factory.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
pub use ITIP20Factory::{
ITIP20FactoryErrors as TIP20FactoryError, ITIP20FactoryEvents as TIP20FactoryEvent,
createToken_0Call as createTokenCall, createToken_1Call as createTokenWithLogoCall,
};
use alloy_primitives::Address;

crate::sol! {
#[derive(Debug, PartialEq, Eq)]
#[sol(abi)]
#[allow(clippy::too_many_arguments)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]

interface ITIP20Factory {
error AddressReserved();
error AddressNotReserved();
Expand All @@ -23,6 +25,21 @@ crate::sol! {
bytes32 salt
) external returns (address);

/// @notice Creates a token and sets its logoURI atomically (TIP-1026).
/// @dev Solidity overload of `createToken` with an additional `logoURI` argument.
/// Reverts with `LogoURITooLong` if `bytes(logoURI).length > 256`, or
/// with `InvalidLogoURI` if `logoURI` is non-empty and either has no
/// parseable scheme or its scheme is not in the allow-list.
function createToken(
string memory name,
string memory symbol,
string memory currency,
address quoteToken,
address admin,
bytes32 salt,
string memory logoURI
) external returns (address);

function isTIP20(address token) public view returns (bool);

function getTokenAddress(address sender, bytes32 salt) public pure returns (address);
Expand Down
2 changes: 1 addition & 1 deletion crates/node/tests/it/block_building.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ where

// Create token
let salt = B256::random();
let create_tx = factory.createToken(
let create_tx = factory.createToken_0(
"Test".to_string(),
"TEST".to_string(),
"USD".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion crates/node/tests/it/tip20_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async fn test_create_token() -> eyre::Result<()> {
let balance = provider.get_account_info(caller).await?.balance;
assert_eq!(balance, U256::ZERO);
let receipt = factory
.createToken(
.createToken_0(
"Test".to_string(),
"TEST".to_string(),
"USD".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion crates/node/tests/it/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ where
let factory = ITIP20Factory::new(TIP20_FACTORY_ADDRESS, provider.clone());
let salt = B256::random();
let receipt = factory
.createToken(
.createToken_0(
"Test".to_string(),
"TEST".to_string(),
"USD".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion crates/precompiles/src/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ impl TIP20Setup {
let salt = self.salt.unwrap_or_else(B256::random);
let token_address = factory.create_token(
admin,
tip20_factory::ITIP20Factory::createTokenCall {
tip20_factory::createTokenCall {
name: name.to_string(),
symbol: symbol.to_string(),
currency,
Expand Down
78 changes: 75 additions & 3 deletions crates/precompiles/src/tip20/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ const T2_ADDED: &[[u8; 4]] = &[
ITIP20::DOMAIN_SEPARATORCall::SELECTOR,
];

/// Selectors added at T5: TIP-1026 Token Logo URI.
const T5_ADDED: &[[u8; 4]] = &[
ITIP20::logoURICall::SELECTOR,
ITIP20::setLogoURICall::SELECTOR,
];

/// Decoded call variant — either a TIP-20 token call or a role-management call.
enum TIP20Call {
TIP20(ITIP20Calls),
Expand Down Expand Up @@ -57,7 +63,10 @@ impl Precompile for TIP20Token {

dispatch_call(
calldata,
&[SelectorSchedule::new(TempoHardfork::T2).with_added(T2_ADDED)],
&[
SelectorSchedule::new(TempoHardfork::T2).with_added(T2_ADDED),
SelectorSchedule::new(TempoHardfork::T5).with_added(T5_ADDED),
],
TIP20Call::decode,
|call| match call {
// Metadata functions (no calldata decoding needed)
Expand Down Expand Up @@ -85,6 +94,9 @@ impl Precompile for TIP20Token {
TIP20Call::TIP20(ITIP20Calls::paused(_)) => {
metadata::<ITIP20::pausedCall>(|| self.paused())
}
TIP20Call::TIP20(ITIP20Calls::logoURI(_)) => {
metadata::<ITIP20::logoURICall>(|| self.logo_uri())
}

// View functions
TIP20Call::TIP20(ITIP20Calls::balanceOf(call)) => {
Expand Down Expand Up @@ -128,6 +140,9 @@ impl Precompile for TIP20Token {
TIP20Call::TIP20(ITIP20Calls::setSupplyCap(call)) => {
mutate_void(call, msg_sender, |s, c| self.set_supply_cap(s, c))
}
TIP20Call::TIP20(ITIP20Calls::setLogoURI(call)) => {
mutate_void(call, msg_sender, |s, c| self.set_logo_uri(s, c))
}
TIP20Call::TIP20(ITIP20Calls::pause(call)) => {
mutate_void(call, msg_sender, |s, c| self.pause(s, c))
}
Expand Down Expand Up @@ -755,8 +770,8 @@ mod tests {
use crate::test_util::{assert_full_coverage, check_selector_coverage};
use tempo_contracts::precompiles::{IRolesAuth::IRolesAuthCalls, ITIP20::ITIP20Calls};

// Use T2 hardfork so T2-gated selectors (permit, nonces, DOMAIN_SEPARATOR) are active
let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
// Use T5 hardfork so all selectors are active.
let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
let admin = Address::random();

StorageCtx::enter(&mut storage, || {
Expand All @@ -779,6 +794,63 @@ mod tests {
})
}

#[test]
fn test_logo_uri_selectors_gated_behind_t5() -> eyre::Result<()> {
// Pre-T5: logoURI/setLogoURI should return unknown selector.
let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
let admin = Address::random();

StorageCtx::enter(&mut storage, || {
let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;

// logoURI selector is gated
let logo_uri_calldata = ITIP20::logoURICall {}.abi_encode();
let result = token.call(&logo_uri_calldata, admin)?;
assert!(result.is_revert());
assert!(UnknownFunctionSelector::abi_decode(&result.bytes).is_ok());

// setLogoURI selector is gated
let set_logo_uri_calldata = ITIP20::setLogoURICall {
newLogoURI: "https://example.com/icon.svg".to_string(),
}
.abi_encode();
let result = token.call(&set_logo_uri_calldata, admin)?;
assert!(result.is_revert());
assert!(UnknownFunctionSelector::abi_decode(&result.bytes).is_ok());

Ok(())
})
}

#[test]
fn test_logo_uri_pre_t5_deploy_post_t5_read_returns_empty() -> eyre::Result<()> {
let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
let admin = Address::random();
let token_address = StorageCtx::enter(&mut storage, || -> eyre::Result<Address> {
let token = TIP20Setup::create("Test", "TST", admin).apply()?;
Ok(token.address())
})?;

// Activate T5; the token deployed under T4 is now read under T5.
storage.set_spec(TempoHardfork::T5);

StorageCtx::enter(&mut storage, || {
let mut token = TIP20Token::from_address(token_address)?;

// Direct accessor: empty by default for pre-T5-deployed tokens.
assert_eq!(token.logo_uri()?, "");

// ABI-level: the previously-gated selector now dispatches and returns "".
let calldata = ITIP20::logoURICall {}.abi_encode();
let result = token.call(&calldata, admin)?;
assert!(!result.is_revert(), "logoURI() must succeed post-T5");
let decoded = ITIP20::logoURICall::abi_decode_returns(&result.bytes)?;
assert_eq!(decoded, "");

Ok(())
})
}

#[test]
fn test_permit_selectors_gated_behind_t2() -> eyre::Result<()> {
// Pre-T2: permit/nonces/DOMAIN_SEPARATOR should return unknown selector
Expand Down
Loading
Loading