Skip to content

fix: parseUnit macro divides by decimal count and rejects fractional input #656

Description

@Antrikshgwal

Summary

The parseUnit expression builtin divides the value by the raw decimal count instead of scaling by 10^decimals, and it cannot accept a fractional value at all. Both defects make it unusable for its documented purpose (building a fixed-point price threshold), and any task condition using it compares against a meaningless number.

Location

core/taskengine/macros/exp.go:118

func ParseUnit(val string, decimal uint) *big.Int {
	b, ok := ethmath.ParseBig256(val)
	if !ok {
		panic(fmt.Errorf("Parse error: %s", val))
	}

	r := big.NewInt(0)
	return r.Div(b, big.NewInt(int64(decimal)))   // <-- divides by `decimal` (e.g. 8), not 10^decimal
}

Registered as the user-facing parseUnit builtin at exp.go:206.

Defects

1. Wrong scaling. The name mirrors ethers.js parseUnits(value, decimals), the universal EVM convention, which returns value * 10^decimals. The implementation instead computes value / decimals (integer division by the raw count).

Call Current result Expected (value * 10^decimals)
parseUnit("2621", 8) 327 (2621 / 8) 262100000000
parseUnit("100", 6) 16 (100 / 6) 100000000

2. No fractional input. ethmath.ParseBig256 only parses integers/hex, so parseUnit("2621.99", 8) panics (Parse error: 2621.99). Price thresholds are inherently fractional, so this is exactly the input the builtin needs to accept.

Impact

parseUnit is exposed to users in task-condition expressions (documented use: a price threshold compared against chainlinkPrice(...), which returns an 8-decimal integer). Because the produced threshold is wrong by orders of magnitude — or panics outright on a fractional value — conditions fire when they shouldn't or never fire. Silent, incorrect automation behavior with no error surfaced.

Proposed fix

Reimplement ParseUnit to match ethers parseUnits:

  • scale by 10^decimals
  • accept a fractional component, right-padded to decimals
  • reject over-precision (fraction longer than decimals), negatives, and non-numeric input
func ParseUnit(val string, decimal uint) *big.Int {
	parts := strings.SplitN(val, ".", 2)
	whole, ok := new(big.Int).SetString(parts[0], 10)
	// ... scale whole by 10^decimals, add right-padded fraction, reject over-precision/negatives
}

No existing tests or callers depend on the current (broken) output — parseUnit has zero coverage and is only referenced in a commented-out example — so the fix is safe for anyone reasonably expecting ethers semantics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions