Skip to content

feat(precompiles): TIP-1026 add logoURI and createToken overload T5+#3745

Open
fgimenez wants to merge 12 commits intomainfrom
fgimenez/tip-1026-impl
Open

feat(precompiles): TIP-1026 add logoURI and createToken overload T5+#3745
fgimenez wants to merge 12 commits intomainfrom
fgimenez/tip-1026-impl

Conversation

@fgimenez
Copy link
Copy Markdown
Member

@fgimenez fgimenez commented Apr 28, 2026

ref #2996
ref https://linear.app/tempoxyz/issue/CHAIN-1142/tip-1026-optional-logouri-field-in-tip-20-token

Adds a logoURI field to TIP-20 tokens with a setLogoURI admin function and a 7-arg TIP20Factory.createToken Solidity overload, both gated behind T5. The URI is capped at 256 bytes and validated against a scheme allowlist
(https, http, ipfs, data, ASCII-case-insensitive); rejection surfaces as LogoURITooLong or InvalidLogoURI. Empty strings are valid: setLogoURI("") clears the URI and emits LogoURIUpdated; the factory overload skips both the slot write and the event when logoURI is empty (per spec).

The legacy 6-arg createToken selector (0x68130445) and the TokenCreated event topic0 are unchanged — pinned by a regression test. The factory does not introduce a factory-level event; at deploy time it emits the token's
LogoURIUpdated from the new token's address with updater = msg.sender.

@fgimenez fgimenez added the A-precompile Related to precompiles label Apr 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

⚠️ Changelog not found.

A changelog entry is required before merging. We've generated a suggested changelog based on your changes:

Preview
---
tempo-bench: patch
tempo-contracts: minor
tempo-node: patch
tempo-precompiles: minor
---

Added TIP-1026 token logo URI support: new `logoURI`/`setLogoURI` functions on TIP-20 tokens and a `createToken` factory overload that sets the logo atomically, both gated behind the T5 hardfork. Updated existing call sites to use the disambiguated `createToken_0` selector.

Add changelog to commit this to your branch.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

📊 Tempo Precompiles Coverage

precompiles

Coverage: 5722/7784 lines (73.51%)

File details
File Lines Coverage
src/account_keychain/dispatch.rs 30/68 44.12%
src/account_keychain/mod.rs 273/736 37.09%
src/address_registry/dispatch.rs 31/33 93.94%
src/address_registry/mod.rs 50/56 89.29%
src/error.rs 39/114 34.21%
src/ip_validation.rs 10/10 100.00%
src/lib.rs 181/216 83.80%
src/nonce/dispatch.rs 9/10 90.00%
src/nonce/mod.rs 46/61 75.41%
src/signature_verifier/dispatch.rs 19/20 95.00%
src/signature_verifier/mod.rs 13/17 76.47%
src/stablecoin_dex/dispatch.rs 92/93 98.92%
src/stablecoin_dex/mod.rs 855/918 93.14%
src/stablecoin_dex/order.rs 110/161 68.32%
src/stablecoin_dex/orderbook.rs 157/216 72.69%
src/storage/evm.rs 192/221 86.88%
src/storage/hashmap.rs 0/158 0.00%
src/storage/mod.rs 27/27 100.00%
src/storage/packing.rs 68/93 73.12%
src/storage/thread_local.rs 165/227 72.69%
src/storage/types/array.rs 0/72 0.00%
src/storage/types/bytes_like.rs 118/162 72.84%
src/storage/types/mapping.rs 27/48 56.25%
src/storage/types/mod.rs 67/91 73.63%
src/storage/types/primitives.rs 21/24 87.50%
src/storage/types/set.rs 28/192 14.58%
src/storage/types/slot.rs 55/81 67.90%
src/storage/types/vec.rs 101/246 41.06%
src/tip20/dispatch.rs 157/173 90.75%
src/tip20/mod.rs 633/739 85.66%
src/tip20/rewards.rs 238/252 94.44%
src/tip20/roles.rs 107/110 97.27%
src/tip20_factory/dispatch.rs 20/21 95.24%
src/tip20_factory/mod.rs 124/145 85.52%
src/tip403_registry/dispatch.rs 55/56 98.21%
src/tip403_registry/mod.rs 334/371 90.03%
src/tip_fee_manager/amm.rs 286/364 78.57%
src/tip_fee_manager/dispatch.rs 81/83 97.59%
src/tip_fee_manager/mod.rs 71/136 52.21%
src/validator_config/dispatch.rs 38/52 73.08%
src/validator_config/mod.rs 171/227 75.33%
src/validator_config_v2/dispatch.rs 71/73 97.26%
src/validator_config_v2/mod.rs 552/611 90.34%

contracts

Coverage: 1/269 lines (0.37%)

File details
File Lines Coverage
src/lib.rs 1/1 100.00%
src/precompiles/account_keychain.rs 0/40 0.00%
src/precompiles/address_registry.rs 0/12 0.00%
src/precompiles/nonce.rs 0/15 0.00%
src/precompiles/signature_verifier.rs 0/3 0.00%
src/precompiles/stablecoin_dex.rs 0/18 0.00%
src/precompiles/tip20.rs 0/74 0.00%
src/precompiles/tip20_factory.rs 0/12 0.00%
src/precompiles/tip403_registry.rs 0/24 0.00%
src/precompiles/tip_fee_manager.rs 0/18 0.00%
src/precompiles/validator_config.rs 0/13 0.00%
src/precompiles/validator_config_v2.rs 0/39 0.00%

Total: 5723/8053 lines (71.07%)

📦 Download full HTML report

@fgimenez
Copy link
Copy Markdown
Member Author

fgimenez commented May 5, 2026

/build-devnet

@fgimenez fgimenez marked this pull request as ready for review May 6, 2026 07:30
@fgimenez fgimenez added the cyclops Trigger Cyclops PR audit label May 6, 2026
Copy link
Copy Markdown

@tempoxyz-bot tempoxyz-bot left a comment

Choose a reason for hiding this comment

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

👁️ Cyclops Review

TIP-1026 implementation looks solid: T5 selector gating, role check, validate-then-deploy ordering in the factory, storage append-only, and legacy 6-arg createToken selector / TokenCreated topic preservation are all enforced and covered by unit + Solidity invariant tests. No exploitable security issues across three independent worker passes. The findings below are integration / hygiene risks, surfaced as inline comments where they map to the diff plus two issues against files outside the diff.


⚠️ [ISSUE] Solidity ↔ Rust storage layout test silently bypasses the new field (file not in diff)

Files: crates/precompiles/tests/storage_tests/solidity/testdata/tip20.sol and crates/precompiles/tests/storage_tests/solidity/precompiles.rs:143

The Rust TIP20Token struct now has logo_uri: String appended, but neither the Solidity reference fixture (testdata/tip20.sol) nor the layout_fields! enumeration in test_tip20_layout was updated. compare_layouts only iterates the field list it is given, so the layout-equivalence test passes without checking that logoURI lands on the same slot on both sides. This silently weakens the protection ZELLIC-23 was meant to give, and any future contributor inserting Solidity logoURI somewhere other than the end will desync the layout without a CI signal.

Recommended Fix: Add string logoURI; as the last field of tests/storage_tests/solidity/testdata/tip20.sol, and add logo_uri to the layout_fields! invocation in tests/storage_tests/solidity/precompiles.rs (both the test_tip20_layout site near line 163 and export_all_storage_constants near line 363).


⚠️ [ISSUE] tempo-contracts ships a SemVer-breaking Rust API rename without a major bump (file not in diff)

File: crates/contracts/Cargo.toml (publish = true, version 1.6.0)

alloy::sol! now sees two createToken overloads and renames the generated bindings to createToken_0Call / createToken_1Call (and factory.createToken_0(…) / factory.createToken_1(…) on the contract handle). The PR re-exports createToken_0Call as createTokenCall at tempo_contracts::precompiles, but consumers that imported via the inner path (tempo_contracts::precompiles::ITIP20Factory::createTokenCall) or used the alloy contract instance method factory.createToken(…) will fail to compile. This is a breaking change for downstream Rust consumers of the published crate; the version remains 1.6.0.

Recommended Fix: Bump tempo-contracts to 2.0.0 for the next release and note the rename in the changelog. Optionally add pub use createToken_0Call as createTokenCall inside ITIP20Factory to keep the inner-path import working.


Reviewer Callouts
  • data: scheme allowed. The validator only inspects the scheme prefix; everything past the first : is unchecked (NUL bytes, control chars, bidi marks, percent-encoded payloads, data:text/html,<script>…). This is the documented design — wallet/UI/explorer teams are responsible for sandboxing rendering. Worth one explicit confirmation that frontend treats data: URIs read from token storage as untrusted.
  • String storage shrink leaves orphan data. Overwriting a long URI with a shorter/empty one writes only the base slot; the keccak-derived data slots retain previous bytes. Reads remain correct (length is from the base slot), but any future “compact storage” or SSTORE-refund work that recomputes length from data slots would resurface stale bytes.
  • Gas pricing for setLogoURI / createTokenWithLogo. Charges only INPUT_PER_WORD_COST × ceil(calldata/32) plus SSTORE costs. Sanity-check that this matches comparable TIP-20 admin ops (setSupplyCap, changeTransferPolicyId).
  • Factory write_logo_uri skips the role check. Intentional (creator authoring initial state for a token they just deployed) and matches the threat model of the legacy 6-arg createToken. No new attack surface, but worth keeping in mind when reasoning about LogoURIUpdated.updater semantics.

Comment thread crates/precompiles/src/tip20/mod.rs Outdated
opted_in_supply: u128,
user_reward_info: Mapping<Address, UserRewardInfo>,

// TIP-1026: Token Logo URI (must remain at the end — storage is append-only)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is indeed valid, but a more generic problem not specific to this PR. in any case reordering of slots would be caught by snapshot reexecution, we can address compile-time checks separately. added logo_uri to the layout equivalence test here 42da069

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

assuming that they have always been unused, which i think it is the case, you can technically of the unused slots (i.e. _domain_separator)

Comment thread crates/precompiles/src/tip20_factory/mod.rs
Comment thread tips/verify/test/invariants/TIP1026.t.sol Outdated
@fgimenez fgimenez force-pushed the fgimenez/tip-1026-impl branch from 56211b4 to 42da069 Compare May 6, 2026 08:49
Comment thread crates/precompiles/src/tip20/mod.rs Outdated
opted_in_supply: u128,
user_reward_info: Mapping<Address, UserRewardInfo>,

// TIP-1026: Token Logo URI (must remain at the end — storage is append-only)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is indeed valid, but a more generic problem not specific to this PR. in any case reordering of slots would be caught by snapshot reexecution, we can address compile-time checks separately. added logo_uri to the layout equivalence test here 42da069

Comment thread crates/precompiles/src/tip20_factory/mod.rs
Comment thread tips/verify/test/invariants/TIP1026.t.sol Outdated
@fgimenez fgimenez added cyclops Trigger Cyclops PR audit and removed cyclops Trigger Cyclops PR audit labels May 6, 2026
Copy link
Copy Markdown

@tempoxyz-bot tempoxyz-bot left a comment

Choose a reason for hiding this comment

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

👁️ Cyclops Review — No actionable findings.

Three workers ran six independent passes (invariant tracing, blast-radius mapping, domain coverage A–D, and an invariant-breaker pass) over TIP-1026 (Token Logo URI) and all returned no findings. Validation (256-byte cap + scheme allowlist), T5 selector gating, append-only slot 18 layout, atomic factory creation, admin-gated setLogoURI, and the pinned legacy createToken selector (0x68130445) / TokenCreated topic0 all check out.

Reviewer Callouts
  • T5 activation timing: TempoHardfork::T5 still has None activation in crates/chainspec/src/hardfork.rs. Confirm the activation timestamp ships in lockstep with these selectors and that moderato exercises createTokenWithLogo before mainnet activation.
  • Storage-layout append-only contract: any future TIP-20 storage field MUST be appended after logo_uri (slot 18). The only safety net is the Solidity layout-equivalence test (crates/precompiles/tests/storage_tests/solidity/precompiles.rs).
  • createToken overload-ordering fragility: alloy disambiguates by declaration order (createToken_0 = legacy 6-arg, createToken_1 = 7-arg). Reordering the two declarations in crates/contracts/src/precompiles/tip20_factory.rs would silently swap dispatch routing. The selector-pin test test_create_token_selector_and_event_unchanged and the Foundry LEGACY_CREATE_TOKEN_SELECTOR invariant catch this — keep both.
  • Lossy UTF-8 decoding: alloy's SolType for String uses from_utf8_lossy, so malformed UTF-8 calldata is silently lossy-decoded (and may expand up to 3×). The length cap is enforced after decode so the on-chain invariant holds, but any future change using byte-exact string semantics (signed payloads, hash-keyed storage) should opt into abi_decode_validate.
  • Empty-URI semantics asymmetry: setLogoURI("") writes the slot and emits LogoURIUpdated(_, ""); createToken(..., "") skips both. Intentional and tested, but confirm downstream indexers don't rely on a deploy-time LogoURIUpdated for every token.
  • Tip20TokenMetadata (RPC simulate.rs) does not yet expose logoURI post-T5; dashboards relying on RPC for token metadata must query the precompile directly until that is added.
  • Stale long-string chunks on shrink: shrinking a previously-long logo_uri leaves old chunk slots at keccak256(18)+i non-zero (existing BytesLikeHandler pattern, also affects name/symbol/currency). Not exploitable since logoURI() reads only length chunks.

@fgimenez fgimenez force-pushed the fgimenez/tip-1026-impl branch from 7ea2fb8 to c627a36 Compare May 6, 2026 10:43
Comment thread crates/precompiles/src/tip20/mod.rs Outdated
Comment thread crates/precompiles/src/tip20/mod.rs Outdated
opted_in_supply: u128,
user_reward_info: Mapping<Address, UserRewardInfo>,

// TIP-1026: Token Logo URI (must remain at the end — storage is append-only)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

assuming that they have always been unused, which i think it is the case, you can technically of the unused slots (i.e. _domain_separator)

Comment thread crates/contracts/src/precompiles/tip20_factory.rs Outdated
Comment thread crates/precompiles/src/tip20/dispatch.rs Outdated
Comment thread crates/precompiles/src/tip20/dispatch.rs Outdated
Comment thread crates/precompiles/src/tip20/mod.rs Outdated
Comment thread crates/precompiles/src/tip20_factory/mod.rs Outdated
Comment thread crates/precompiles/src/tip20_factory/mod.rs Outdated
Comment thread crates/precompiles/src/tip20_factory/mod.rs Outdated
Comment thread crates/precompiles/src/tip20_factory/mod.rs Outdated
fgimenez and others added 3 commits May 6, 2026 16:29
@fgimenez
Copy link
Copy Markdown
Member Author

fgimenez commented May 6, 2026

assuming that they have always been unused, which i think it is the case, you can technically of the unused slots (i.e. _domain_separator)

good point, done here 553fb5c

@fgimenez fgimenez requested a review from 0xrusowsky May 7, 2026 08:10
@fgimenez fgimenez added cyclops Trigger Cyclops PR audit and removed cyclops Trigger Cyclops PR audit labels May 7, 2026
Copy link
Copy Markdown

@tempoxyz-bot tempoxyz-bot left a comment

Choose a reason for hiding this comment

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

👁️ Cyclops Review — No actionable findings.

TIP-1026 adds logoURI() / setLogoURI(string) (T5+, admin-gated) and a 7-arg createToken(...,string logoURI) factory overload, repurposing the previously-unused _domain_separator slot 5 as a Solidity string. Validation enforces a 256-byte cap and an ASCII-case-insensitive scheme allowlist (https/http/ipfs/data); empty strings clear. Legacy createToken selector 0x68130445 and TokenCreated topic are preserved via Solidity overload resolution (createToken_0/createToken_1). Two workers across four iterations independently reached NO_FINDING. The slot-5 reuse — the highest-risk change — is safe: the legacy writer was removed in PR #580 (2025-10-29) months before any live genesis (Moderato 2026-02-05, Presto 2026-02-12), domain_separator() is computed dynamically, initialize() never touches slot 5, genesis state leaves it unset, and a zero slot decodes deterministically to "" per Solidity short-string encoding.

Reviewer Callouts
  • Storage-slot reuse (slot 5): The "always 0 on pre-T5 tokens" invariant depends on no consensus path having ever written _domain_separator on a network whose state is carried forward. Verified for live chains, but a human should confirm there are no internal/dev/private snapshots being migrated forward where this might not hold. If any exist, a one-time T5-boundary normalization that explicitly zeroes slot 5 for every TIP-20 would be the safe play. An off-chain post-T5 sanity script is a cheap defense-in-depth.
  • write_logo_uri is pub(crate) and intentionally unguarded: The doc-comment is explicit ("caller's responsibility"), but a future maintainer adding a new caller could miss it. Consider renaming to write_logo_uri_unchecked or tightening visibility further (e.g., crate-private to tip20_factory only).
  • Pre-T5 layout coverage of genesis-installed tokens: test_logo_uri_pre_t5_deploy_post_t5_read_returns_empty exercises factory-deployed tokens. A genesis-installed token (e.g., pathUSD) would be worth covering explicitly to pin the assumption that genesis slot 5 stays zero.
  • Solidity layout-equivalence test toolchain: The tip20.layout.json test requires solc. Confirm CI runs the layout test with the right toolchain so any future struct reordering catches a mismatch immediately.
  • TIP-1026 factory error precedence: create_token_with_logo validates the URI before create_token, so LogoURITooLong/InvalidLogoURI masks would-be TokenAlreadyExists / InvalidQuoteToken reverts. Reasonable, but worth documenting if integrators rely on a specific revert reason.
  • Long-string state residue: When set_logo_uri replaces a long URI (≥32 bytes) with a shorter (or empty) value, residual chunks at keccak256(slot_5)+i past the new length are not freed. Symmetric to existing name/symbol behavior; not exploitable, but a future cleanup pass could call delete_bytes_like first.
  • Empty-string semantics asymmetry: setLogoURI("") validates and emits LogoURIUpdated; createTokenWithLogo skips both write and event when logoURI is empty. Intentional per spec, but indexers tracking logo updates must understand the deploy event is conditional on non-empty input.
  • Mainnet T5 activation: T5 has no mainnet activation block yet (crates/chainspec/src/hardfork.rs); behavior is dormant in production. Confirm nothing else in this PR depends on a specific mainnet T5 height.

@fgimenez
Copy link
Copy Markdown
Member Author

fgimenez commented May 7, 2026

/build-devnet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-precompile Related to precompiles cyclops Trigger Cyclops PR audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants