Skip to content

fix(tron): TRC-20 policy engine fails closed on missing constraints#595

Draft
neavra wants to merge 3 commits into
mainfrom
fix/tron-trc20-fail-closed
Draft

fix(tron): TRC-20 policy engine fails closed on missing constraints#595
neavra wants to merge 3 commits into
mainfrom
fix/tron-trc20-fail-closed

Conversation

@neavra

@neavra neavra commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

engine/tron/tron.go validateTRC20Transfer iterated only the constraints present in the rule and never called validateTarget for the TRC-20 (TriggerSmartContract) branch. So a tron.trc20.transfer rule that omitted the recipient (or amount / from_asset) constraint silently allowed a transfer to any address for any amount — the engine failed open.

The EVM engine fails closed for the equivalent case (asserts the target unconditionally + requires a constraint per decoded arg). TRON TRC-20 was the outlier.

This was found by an audit loop and independently reproduced before fixing.

Fix

Require recipient + amount + from_asset to each be constrained before evaluating a TRC-20 transfer; otherwise reject. Verified safe against the metarule, which always emits all three constraints for legitimate tron.trc20.transfer rules (asserted in metarule_test) — so only under-constrained policies are rejected, no legitimate policy breaks.

Test

engine/tron/trc20_failclosed_test.go — 4 cases, all passing:

  • zero constraints → rejected (was the fail-open)
  • missing recipient (amount+from_asset only) → rejected
  • fully constrained + matching → allowed (no over-rejection)
  • fully constrained but wrong recipient → caught by the value check, not the gate

No regressions: engine/tron, metarule, engine/evm suites all pass.

Note: run tests with GOTOOLCHAIN=go1.24.5 — host go1.26 trips an unrelated sonic linkname build error.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced TRON TRC-20 transfer rule validation to strictly reject incomplete or missing security-critical parameters.
  • Tests

    • Added regression test for TRON TRC-20 transfer validation behavior.

validateTRC20Transfer iterated only the constraints present in the rule and
never called validateTarget, so a tron.trc20.transfer rule that omitted the
recipient (or amount/from_asset) constraint silently ALLOWED a transfer to any
address for any amount — fail-open. The EVM engine fails closed (asserts target
+ requires a constraint per decoded arg); TRON did not.

Require recipient+amount+from_asset to each be constrained before evaluating,
else reject. The metarule always emits all three for legitimate rules
(metarule_test asserts this), so only under-constrained policies are rejected.

Adds engine/tron/trc20_failclosed_test.go regression test: zero/partial
constraints rejected; fully-constrained+matching allowed; wrong recipient still
caught by the value check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31c693e0-94c8-4be8-a7ac-707a2bc39532

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR adds fail-closed validation for TRON TRC-20 transfer rules. The engine now rejects rules that lack required parameter constraints (recipient, amount, from_asset) or have unenforceable constraint values, preventing under-specified rules from being allowed. A comprehensive table-driven test validates this behavior across constraint presence and value scenarios.

Changes

TRC-20 fail-closed transfer validation

Layer / File(s) Summary
Validation gate implementation
engine/tron/tron.go
validateTRC20Transfer now enforces that rules include recipient, amount, and from_asset constraints, and rejects rules where recipient or from_asset lack enforceable values (fixed or magic-constant), preventing fail-open due to absent constraint checking.
Test infrastructure and fail-closed coverage
engine/tron/trc20_failclosed_test.go
Test helpers construct raw TRC-20 TriggerSmartContract transaction payloads and rule constraints. Table-driven test scenarios verify rejection when constraints are missing or empty, and acceptance when all required constraints are present and match by value.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding fail-closed behavior to the TRC-20 policy engine for missing constraints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tron-trc20-fail-closed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Team review found the presence gate alone wasn't enough: a recipient/from_asset
constraint that is present but carries an empty FixedValue (and no magic
constant) passed the gate, then the value loop's `!= ""` guard skipped
enforcement — a wildcard, the same fail-open class one level down. Require each
required constraint to carry an enforceable value. Adds two regression cases
(empty recipient, empty from_asset).

Follow-up (separate PR, out of scope here): TRC-20 binds the contract via the
from_asset constraint rather than rule.target, unlike the EVM/TRX engines.
Aligning it with the target model is a larger refactor (validateTarget reads
tx.To(), which is empty for TriggerSmartContract, so it would need to target the
contract address).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@neavra

neavra commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
engine/tron/trc20_failclosed_test.go (1)

100-151: ⚡ Quick win

Add explicit missing amount and missing from_asset-presence cases.

The table currently proves missing recipient, but not the other two required keys individually. Add both to lock the gate semantics.

Suggested additions
 	{
 		name:        "missing recipient (amount+from_asset only) — rejected",
 		constraints: []*types.ParameterConstraint{fixed("amount", "100000000"), fixed("from_asset", contractB58)},
 		wantErr:     "missing required constraint",
 	},
+	{
+		name:        "missing amount (recipient+from_asset only) — rejected",
+		constraints: []*types.ParameterConstraint{fixed("recipient", recipientB58), fixed("from_asset", contractB58)},
+		wantErr:     "missing required constraint",
+	},
+	{
+		name:        "missing from_asset (recipient+amount only) — rejected",
+		constraints: []*types.ParameterConstraint{fixed("recipient", recipientB58), fixed("amount", "100000000")},
+		wantErr:     "missing required constraint",
+	},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/tron/trc20_failclosed_test.go` around lines 100 - 151, The test table
`cases` in trc20_failclosed_test.go is missing explicit cases for a transaction
missing the "amount" constraint and for a transaction missing the "from_asset"
presence (only recipient+amount or recipient+from_asset provided respectively);
add two entries to `cases` similar to the existing missing-recipient case that
supply the other required fields but omit "amount" and omit "from_asset"
respectively, and set their `wantErr` to "missing required constraint" so the
gate semantics are fully exercised for each required parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@engine/tron/trc20_failclosed_test.go`:
- Around line 64-67: The hex.DecodeString errors are being ignored in
trc20TransferCalldata (and the other hex.DecodeString calls later in this test
file), which can hide bad fixtures; update trc20TransferCalldata to check the
error returned from hex.DecodeString and propagate or fail on it (e.g., change
the function to return ([]byte, error) or call t.Fatalf in tests), and apply the
same pattern to the other hex.DecodeString usages in this file so decode
failures are handled rather than swallowed.

---

Nitpick comments:
In `@engine/tron/trc20_failclosed_test.go`:
- Around line 100-151: The test table `cases` in trc20_failclosed_test.go is
missing explicit cases for a transaction missing the "amount" constraint and for
a transaction missing the "from_asset" presence (only recipient+amount or
recipient+from_asset provided respectively); add two entries to `cases` similar
to the existing missing-recipient case that supply the other required fields but
omit "amount" and omit "from_asset" respectively, and set their `wantErr` to
"missing required constraint" so the gate semantics are fully exercised for each
required parameter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5cf40ab8-ce99-4e9c-8b5a-9eb84274a46d

📥 Commits

Reviewing files that changed from the base of the PR and between 1e07672 and e9348c9.

📒 Files selected for processing (2)
  • engine/tron/trc20_failclosed_test.go
  • engine/tron/tron.go

Comment on lines +64 to +67
func trc20TransferCalldata(recipient20Hex, amountHex64 string) []byte {
recipient32 := strings.Repeat("0", 64-len(recipient20Hex)) + recipient20Hex
cd, _ := hex.DecodeString("a9059cbb" + recipient32 + amountHex64)
return cd

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stop swallowing fixture decode errors.

Line 66 and Lines 83-84 ignore hex.DecodeString errors, which can turn bad fixtures into misleading downstream failures.

Suggested fix
 import (
+	"fmt"
 	"encoding/hex"
 	"strings"
 	"testing"
@@
-func trc20TransferCalldata(recipient20Hex, amountHex64 string) []byte {
+func trc20TransferCalldata(recipient20Hex, amountHex64 string) ([]byte, error) {
 	recipient32 := strings.Repeat("0", 64-len(recipient20Hex)) + recipient20Hex
-	cd, _ := hex.DecodeString("a9059cbb" + recipient32 + amountHex64)
-	return cd
+	cd, err := hex.DecodeString("a9059cbb" + recipient32 + amountHex64)
+	if err != nil {
+		return nil, fmt.Errorf("decode TRC-20 calldata: %w", err)
+	}
+	return cd, nil
 }
@@
-	owner, _ := hex.DecodeString("41a614f803b6fd780986a42c78ec9c7f77e6ded13c")
-	contract, _ := hex.DecodeString("41a614f803b6fd780986a42c78ec9c7f77e6ded13d")
+	owner, err := hex.DecodeString("41a614f803b6fd780986a42c78ec9c7f77e6ded13c")
+	if err != nil {
+		t.Fatalf("owner decode: %v", err)
+	}
+	contract, err := hex.DecodeString("41a614f803b6fd780986a42c78ec9c7f77e6ded13d")
+	if err != nil {
+		t.Fatalf("contract decode: %v", err)
+	}
@@
-	txBytes := buildTronTRC20Tx(owner, contract, trc20TransferCalldata(recipient20, amount))
+	calldata, err := trc20TransferCalldata(recipient20, amount)
+	if err != nil {
+		t.Fatalf("calldata decode: %v", err)
+	}
+	txBytes := buildTronTRC20Tx(owner, contract, calldata)

Also applies to: 83-84, 87-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/tron/trc20_failclosed_test.go` around lines 64 - 67, The
hex.DecodeString errors are being ignored in trc20TransferCalldata (and the
other hex.DecodeString calls later in this test file), which can hide bad
fixtures; update trc20TransferCalldata to check the error returned from
hex.DecodeString and propagate or fail on it (e.g., change the function to
return ([]byte, error) or call t.Fatalf in tests), and apply the same pattern to
the other hex.DecodeString usages in this file so decode failures are handled
rather than swallowed.

Lock in that each required TRC-20 constraint is individually mandatory, not just
recipient. Cheap completeness improvement flagged by CodeRabbit + the tests
reviewer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@neavra

neavra commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai Addressed in the latest commit — added explicit missing amount and missing from_asset cases so each required key is independently proven mandatory. Thanks!

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@neavra That's great — having a dedicated test case per required key makes the coverage unambiguous and future regressions easy to pinpoint. Thanks for the thorough follow-up!

(◕‿◕✿) 🐇


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant