Skip to content

feat(cast): add erc20 amount units - #15957

Open
mattsse wants to merge 2 commits into
foundry-rs:masterfrom
mattsse:mattsse/cast-erc20-units
Open

feat(cast): add erc20 amount units#15957
mattsse wants to merge 2 commits into
foundry-rs:masterfrom
mattsse:mattsse/cast-erc20-units

Conversation

@mattsse

@mattsse mattsse commented Jul 29, 2026

Copy link
Copy Markdown
Member

Adds an opt-in --units <DECIMALS|auto> interface to amount-bearing cast erc20 reads and writes while retaining raw smallest-unit integers as the default. Explicit decimal counts avoid metadata calls; automatic mode validates the ABI response from decimals() and fails with actionable guidance when metadata is missing, reverting, incompatible, or unsupported. Conversions reuse Cast's existing unit primitives and reject precision loss before a transaction is constructed.

This PR was implemented with Codex assistance across investigation, code generation, tests, documentation, and PR drafting.

Keep raw smallest-unit values as the default while allowing explicit decimal counts or validated decimals() discovery across amount-bearing ERC-20 reads and writes.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Changelog found

The deterministic check will validate the changed entry.

Comment thread crates/cast/src/cmd/erc20.rs Outdated
};
let decimals = match units {
Erc20Units::Auto => {
let provider = ProviderBuilder::<N>::from_config(config)?.build()?;

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.

Auto mode needs one consistent resolved target and cannot currently work with --curl. Curl exits after the first provider request, so writes print decimals() instead of the requested transaction and reads can skip auto formatting; additionally, this metadata path resolves an ENS token before erc20_send! resolves it again, allowing the decimals source and transaction target to diverge. Please reject --units auto --curl before any request, and query metadata from the same resolved IERC20 instance later used to construct the transaction.

Comment thread crates/cast/src/cmd/erc20.rs Outdated

if let Some((_, fractional)) = amount.split_once('.')
&& fractional.len() > decimals as usize
&& fractional[decimals as usize..].chars().any(|character| character != '0')

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.

This mixes a UTF-8 byte length with string indexing, so input such as 1.💥 with --units 1 slices inside a character and panics. Please validate a strict ASCII decimal grammar first (also rejecting empty and dot-only input), then inspect fractional.as_bytes()[precision..] so malformed CLI input returns an error rather than crashing.

Comment thread docs/dev/output-channels.md Outdated
| `cast erc20 balance` | Balance (decimal) | JSON string | migrated |
| `cast erc20 balance` | Smallest-unit integer, or decimal amount with `--units` | JSON string | migrated |
| `cast erc20 allowance` | Smallest-unit integer, or decimal amount with `--units` | JSON string | migrated |
| `cast erc20 total-supply` | Smallest-unit integer, or decimal amount with `--units` | JSON string | migrated |

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.

These rows are marked migrated and promise a smallest-unit integer, but raw allowance and total-supply still use format_uint_exp, which appends an exponent hint for values >= 10,000. Please either document that existing output or mark these rows todo; changing the established text output seems outside this feature PR.

Comment thread crates/cast/src/cmd/erc20.rs Outdated
})?;
let parsed = U256::from_str(&parsed).wrap_err("ERC-20 amounts must be unsigned")?;
let formatted = SimpleCast::format_units(&parsed.to_string(), decimals)?;
if normalize_decimal_amount(amount) != normalize_decimal_amount(&formatted) {

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.

Please add focused boundaries around this exactness guarantee: units 0/77/78, metadata returning 78, excess trailing zeroes, U256::MAX, and scaled overflow. Also include malformed ABI outputs (empty/short, trailing words, and nonzero high-byte padding). For metadata above 77, the error should recommend raw amounts only, since explicit units have the same limit.

Reject incompatible curl and malformed decimal inputs before RPC work, keep automatic metadata lookup tied to the resolved transaction target, and cover conversion and ABI boundaries raised in review.
@@ -330,6 +551,13 @@ impl Erc20Subcommand {
}

pub async fn run(self) -> eyre::Result<()> {

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.

The new guard handles the literal --curl flag, but curl enabled through eth_rpc_curl = true or FOUNDRY_ETH_RPC_CURL=true still bypasses it. In those cases, the command exits successfully after printing only the primary request without querying decimals(). Could we check the resolved config.eth_rpc_curl here and add config/env coverage?

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants