-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(vm): add ABI semantic validation for /wallet/deploycontract #6703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
1c4c06d
ed66c1e
a1368ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| package org.tron.core.utils; | ||
|
|
||
| import com.google.common.collect.ImmutableSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
| import org.tron.core.exception.ContractValidateException; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.EntryType; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.Param; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.StateMutabilityType; | ||
|
|
||
| public final class AbiValidator { | ||
|
|
||
| private static final Pattern INT_TYPE = Pattern.compile("^(u?int)(\\d*)$"); | ||
| private static final Pattern BYTES_N_TYPE = Pattern.compile("^bytes(\\d+)$"); | ||
| private static final Pattern ARRAY_SUFFIX = Pattern.compile("\\[(\\d*)]$"); | ||
|
|
||
| private static final Set<String> BASE_TYPES = ImmutableSet.of( | ||
| "address", "bool", "string", "bytes", "function", "tuple", "trcToken", | ||
| // Legacy Solidity alias for bytes1; pre-0.8 source could emit this in | ||
| // hand-written or older-tool ABIs. Modern solc canonicalizes to bytes1. | ||
| "byte"); | ||
|
|
||
| private AbiValidator() { | ||
| } | ||
|
|
||
| public static void validate(ABI abi) throws ContractValidateException { | ||
| if (abi == null || abi.getEntrysCount() == 0) { | ||
| return; | ||
| } | ||
|
|
||
| int constructorCount = 0; | ||
| int fallbackCount = 0; | ||
| int receiveCount = 0; | ||
|
|
||
| for (int i = 0; i < abi.getEntrysCount(); i++) { | ||
| Entry entry = abi.getEntrys(i); | ||
| EntryType type = entry.getType(); | ||
|
|
||
| if (type == EntryType.UnknownEntryType || type == EntryType.UNRECOGNIZED) { | ||
| throw new ContractValidateException( | ||
| String.format("abi entry #%d: unknown entry type", i)); | ||
| } | ||
|
|
||
| switch (type) { | ||
| case Constructor: | ||
| constructorCount++; | ||
| break; | ||
| case Fallback: | ||
| fallbackCount++; | ||
| if (entry.getInputsCount() > 0 || entry.getOutputsCount() > 0) { | ||
| throw new ContractValidateException(String.format( | ||
| "abi entry #%d: fallback function must not have inputs or outputs", i)); | ||
| } | ||
| break; | ||
| case Receive: | ||
| receiveCount++; | ||
| if (entry.getInputsCount() > 0 || entry.getOutputsCount() > 0) { | ||
| throw new ContractValidateException(String.format( | ||
| "abi entry #%d: receive function must not have inputs or outputs", i)); | ||
| } | ||
| if (entry.getStateMutability() != StateMutabilityType.Payable && !entry.getPayable()) { | ||
| throw new ContractValidateException(String.format( | ||
| "abi entry #%d: receive function must be payable", i)); | ||
| } | ||
| break; | ||
| case Function: | ||
| if (entry.getName().isEmpty()) { | ||
| // Pre-0.4.16 solc emitted fallback as `type: function, name: "" `. | ||
| // Tolerate the legacy shape (no inputs/outputs) and count it | ||
| // toward fallbackCount so duplicate fallbacks are still caught. | ||
| if (entry.getInputsCount() > 0 || entry.getOutputsCount() > 0) { | ||
| throw new ContractValidateException(String.format( | ||
| "abi entry #%d: function must have a name", i)); | ||
| } | ||
| fallbackCount++; | ||
| } | ||
| break; | ||
| case Event: | ||
| if (entry.getName().isEmpty() && !entry.getAnonymous()) { | ||
| throw new ContractValidateException(String.format( | ||
| "abi entry #%d: non-anonymous event must have a name", i)); | ||
| } | ||
| break; | ||
| case Error: | ||
| if (entry.getName().isEmpty()) { | ||
| throw new ContractValidateException(String.format( | ||
| "abi entry #%d: error must have a name", i)); | ||
| } | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
|
|
||
| validateParams(i, "inputs", entry.getInputsList()); | ||
| validateParams(i, "outputs", entry.getOutputsList()); | ||
| } | ||
|
|
||
| if (constructorCount > 1) { | ||
| throw new ContractValidateException("abi: only one constructor is allowed"); | ||
| } | ||
| if (fallbackCount > 1) { | ||
| throw new ContractValidateException("abi: only one fallback function is allowed"); | ||
| } | ||
| if (receiveCount > 1) { | ||
| throw new ContractValidateException("abi: only one receive function is allowed"); | ||
| } | ||
| } | ||
|
|
||
| private static void validateParams(int entryIdx, String side, List<Param> params) | ||
| throws ContractValidateException { | ||
| for (int j = 0; j < params.size(); j++) { | ||
| String type = params.get(j).getType(); | ||
| String reason = checkType(type); | ||
| if (reason != null) { | ||
| throw new ContractValidateException(String.format( | ||
| "abi entry #%d %s[%d] type '%s': %s", entryIdx, side, j, type, reason)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Returns null when the type is acceptable, otherwise a short failure reason. | ||
| private static String checkType(String raw) { | ||
| if (raw == null || raw.isEmpty()) { | ||
| return "type must not be empty"; | ||
| } | ||
| if (!raw.equals(raw.trim())) { | ||
| return "type must not contain leading/trailing whitespace"; | ||
| } | ||
| String t = raw; | ||
|
|
||
| while (true) { | ||
| Matcher m = ARRAY_SUFFIX.matcher(t); | ||
| if (!m.find()) { | ||
| break; | ||
| } | ||
| String n = m.group(1); | ||
| if (!n.isEmpty()) { | ||
| long size; | ||
| try { | ||
| size = Long.parseLong(n); | ||
| } catch (NumberFormatException nfe) { | ||
| return "malformed array size"; | ||
| } | ||
| if (size <= 0) { | ||
| return "array size must be positive"; | ||
| } | ||
| } | ||
| t = t.substring(0, t.length() - m.group().length()); | ||
| } | ||
|
|
||
| if (t.indexOf('[') >= 0 || t.indexOf(']') >= 0) { | ||
| return "malformed array brackets"; | ||
| } | ||
|
|
||
| if (BASE_TYPES.contains(t)) { | ||
| return null; | ||
| } | ||
|
|
||
| Matcher mi = INT_TYPE.matcher(t); | ||
| if (mi.matches()) { | ||
| String width = mi.group(2); | ||
| if (width.isEmpty()) { | ||
| // Solidity aliases: uint = uint256, int = int256. Hand-written and | ||
| // pre-canonicalization ABIs may carry the shorthand; modern solc | ||
| // already emits the explicit width. | ||
| return null; | ||
| } | ||
| int w; | ||
| try { | ||
| w = Integer.parseInt(width); | ||
| } catch (NumberFormatException nfe) { | ||
| return "invalid integer width"; | ||
| } | ||
| if (w < 8 || w > 256 || (w % 8) != 0) { | ||
| return "integer width must be a multiple of 8 in [8, 256]"; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| Matcher mb = BYTES_N_TYPE.matcher(t); | ||
| if (mb.matches()) { | ||
| int n; | ||
| try { | ||
| n = Integer.parseInt(mb.group(1)); | ||
| } catch (NumberFormatException nfe) { | ||
| return "invalid bytesN size"; | ||
| } | ||
| if (n < 1 || n > 32) { | ||
| return "bytesN size must be in [1, 32]"; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| if (t.startsWith("fixed") || t.startsWith("ufixed")) { | ||
| return "fixed/ufixed types are not supported"; | ||
| } | ||
|
|
||
| return "unknown base type"; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| import org.tron.core.exception.ReceiptCheckErrException; | ||
| import org.tron.core.exception.VMIllegalException; | ||
| import org.tron.core.store.StoreFactory; | ||
| import org.tron.core.utils.AbiValidator; | ||
| import org.tron.core.vm.repository.Repository; | ||
| import org.tron.core.vm.repository.RepositoryImpl; | ||
|
|
||
|
|
@@ -489,6 +490,10 @@ private static SmartContract.ABI.Entry.EntryType getEntryType(String type) { | |
| return SmartContract.ABI.Entry.EntryType.Event; | ||
| case "fallback": | ||
| return SmartContract.ABI.Entry.EntryType.Fallback; | ||
| case "receive": | ||
| return SmartContract.ABI.Entry.EntryType.Receive; | ||
| case "error": | ||
| return SmartContract.ABI.Entry.EntryType.Error; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great call adding Minor follow-up: both this helper and
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. Adding |
||
| default: | ||
| return SmartContract.ABI.Entry.EntryType.UNRECOGNIZED; | ||
| } | ||
|
|
@@ -541,7 +546,8 @@ public static SmartContract.ABI jsonStr2Abi(String jsonStr) { | |
| logger.error("No type!"); | ||
| return null; | ||
| } | ||
| if (!type.equalsIgnoreCase("fallback") && null == inputs) { | ||
| if (!type.equalsIgnoreCase("fallback") && !type.equalsIgnoreCase("receive") | ||
| && null == inputs) { | ||
| logger.error("No inputs!"); | ||
| return null; | ||
| } | ||
|
|
@@ -603,7 +609,13 @@ public static SmartContract.ABI jsonStr2Abi(String jsonStr) { | |
| abiBuilder.addEntrys(entryBuilder.build()); | ||
| } | ||
|
|
||
| return abiBuilder.build(); | ||
| SmartContract.ABI abi = abiBuilder.build(); | ||
| try { | ||
| AbiValidator.validate(abi); | ||
| } catch (ContractValidateException e) { | ||
| throw new IllegalArgumentException(e.getMessage(), e); | ||
| } | ||
| return abi; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package org.tron.common.runtime; | ||
|
|
||
| import static org.junit.Assert.assertEquals; | ||
|
|
||
| import org.junit.Test; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.EntryType; | ||
|
|
||
| public class TvmTestUtilsTest { | ||
|
|
||
| @Test | ||
| public void jsonStr2AbiAcceptsReceiveEntry() { | ||
| String json = "[{\"stateMutability\":\"payable\",\"type\":\"receive\"}]"; | ||
| ABI abi = TvmTestUtils.jsonStr2Abi(json); | ||
| assertEquals(1, abi.getEntrysCount()); | ||
| Entry entry = abi.getEntrys(0); | ||
| assertEquals(EntryType.Receive, entry.getType()); | ||
| assertEquals(0, entry.getInputsCount()); | ||
| assertEquals(0, entry.getOutputsCount()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package org.tron.common.utils; | ||
|
|
||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertTrue; | ||
|
|
||
| import org.junit.Test; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry; | ||
| import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.EntryType; | ||
|
|
||
| public class PublicMethodTest { | ||
|
|
||
| @Test | ||
| public void jsonStr2AbiAcceptsReceiveEntry() { | ||
| String json = "[{\"stateMutability\":\"payable\",\"type\":\"receive\"}]"; | ||
| ABI abi = PublicMethod.jsonStr2Abi(json); | ||
| assertEquals(1, abi.getEntrysCount()); | ||
| Entry entry = abi.getEntrys(0); | ||
| assertEquals(EntryType.Receive, entry.getType()); | ||
| assertEquals(0, entry.getInputsCount()); | ||
| assertEquals(0, entry.getOutputsCount()); | ||
| } | ||
|
|
||
| @Test | ||
| public void getEntryTypeMapsReceive() { | ||
| assertEquals(EntryType.Receive, PublicMethod.getEntryType("receive")); | ||
| } | ||
|
|
||
| @Test | ||
| public void getEntryTypeUnknownStaysUnrecognized() { | ||
| assertTrue(PublicMethod.getEntryType("weirdo") == EntryType.UNRECOGNIZED); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is validate(ABI abi) too strict? It may reject legitimate ABIs generated by older or non-standard compilers.