diff --git a/actuator/src/main/java/org/tron/core/utils/AbiValidator.java b/actuator/src/main/java/org/tron/core/utils/AbiValidator.java new file mode 100644 index 00000000000..fe301029c38 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/utils/AbiValidator.java @@ -0,0 +1,189 @@ +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 BASE_TYPES = ImmutableSet.of( + "address", "bool", "string", "bytes", "function", "tuple", "trcToken"); + + 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()) { + throw new ContractValidateException(String.format( + "abi entry #%d: function must have a name", i)); + } + 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 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"; + } + String t = raw.trim(); + + 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()) { + return "shorthand uint/int is not allowed, use uintN/intN"; + } + 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"; + } +} diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..b947f08d600 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -209,6 +209,7 @@ import org.tron.core.store.StoreFactory; import org.tron.core.store.VotesStore; import org.tron.core.store.WitnessStore; +import org.tron.core.utils.AbiValidator; import org.tron.core.utils.TransactionUtil; import org.tron.core.vm.program.Program; import org.tron.core.zen.ShieldedTRC20ParametersBuilder; @@ -498,6 +499,7 @@ public TransactionCapsule createTransactionCapsule(com.google.protobuf.Message m if (percent < 0 || percent > 100) { throw new ContractValidateException("percent must be >= 0 and <= 100"); } + AbiValidator.validate(contract.getNewContract().getAbi()); } setTransaction(trx); return trx; diff --git a/framework/src/test/java/org/tron/common/runtime/TvmTestUtils.java b/framework/src/test/java/org/tron/common/runtime/TvmTestUtils.java index ec2cd5a5e02..0d254652eba 100644 --- a/framework/src/test/java/org/tron/common/runtime/TvmTestUtils.java +++ b/framework/src/test/java/org/tron/common/runtime/TvmTestUtils.java @@ -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,8 @@ private static SmartContract.ABI.Entry.EntryType getEntryType(String type) { return SmartContract.ABI.Entry.EntryType.Event; case "fallback": return SmartContract.ABI.Entry.EntryType.Fallback; + case "error": + return SmartContract.ABI.Entry.EntryType.Error; default: return SmartContract.ABI.Entry.EntryType.UNRECOGNIZED; } @@ -603,7 +606,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; } diff --git a/framework/src/test/java/org/tron/common/utils/PublicMethod.java b/framework/src/test/java/org/tron/common/utils/PublicMethod.java index 90a2aae3f76..24f9209884f 100644 --- a/framework/src/test/java/org/tron/common/utils/PublicMethod.java +++ b/framework/src/test/java/org/tron/common/utils/PublicMethod.java @@ -21,6 +21,8 @@ import org.tron.common.crypto.sm2.SM2Signer; import org.tron.common.utils.client.utils.TransactionUtils; import org.tron.core.Wallet; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.utils.AbiValidator; import org.tron.protos.Protocol; import org.tron.protos.contract.BalanceContract; import org.tron.protos.contract.SmartContractOuterClass; @@ -195,7 +197,13 @@ public static SmartContractOuterClass.SmartContract.ABI jsonStr2Abi(String jsonS abiBuilder.addEntrys(entryBuilder.build()); } - return abiBuilder.build(); + SmartContractOuterClass.SmartContract.ABI abi = abiBuilder.build(); + try { + AbiValidator.validate(abi); + } catch (ContractValidateException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + return abi; } /** constructor. */ diff --git a/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java b/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java new file mode 100644 index 00000000000..306c2570826 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java @@ -0,0 +1,96 @@ +package org.tron.core.services.http; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.tron.common.utils.client.utils.HttpMethed.createRequest; + +import javax.annotation.Resource; +import org.apache.http.client.methods.HttpPost; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; + +public class DeployContractServletTest extends BaseTest { + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + } + + @Resource + private DeployContractServlet deployContractServlet; + + private static final String OWNER_ADDRESS = "A099357684BC659F5166046B56C95A0E99F1265CBD"; + + private MockHttpServletResponse postWithAbi(String abi) { + String body = "{" + + "\"owner_address\":\"" + OWNER_ADDRESS + "\"," + + "\"name\":\"abi_validation_test\"," + + "\"bytecode\":\"00\"," + + "\"abi\":" + abi + + "}"; + MockHttpServletRequest request = createRequest(HttpPost.METHOD_NAME); + request.setContentType("application/json"); + request.setContent(body.getBytes(UTF_8)); + MockHttpServletResponse response = new MockHttpServletResponse(); + deployContractServlet.doPost(request, response); + return response; + } + + private static void assertRejected(MockHttpServletResponse response, String snippet) + throws Exception { + Assert.assertEquals(200, response.getStatus()); + String body = response.getContentAsString(); + Assert.assertTrue("expected error containing '" + snippet + "', got: " + body, + body.contains("Error") && body.contains(snippet)); + } + + @Test + public void rejectsShorthandUint() throws Exception { + String abi = "[{\"type\":\"function\",\"name\":\"foo\"," + + "\"inputs\":[{\"name\":\"x\",\"type\":\"uint\"}],\"outputs\":[]}]"; + assertRejected(postWithAbi(abi), "shorthand uint/int"); + } + + @Test + public void rejectsBadBytesN() throws Exception { + String abi = "[{\"type\":\"function\",\"name\":\"foo\"," + + "\"inputs\":[{\"name\":\"x\",\"type\":\"bytes33\"}],\"outputs\":[]}]"; + assertRejected(postWithAbi(abi), "bytesN size"); + } + + @Test + public void rejectsDuplicateFallback() throws Exception { + String abi = "[" + + "{\"type\":\"fallback\",\"stateMutability\":\"payable\"}," + + "{\"type\":\"fallback\",\"stateMutability\":\"payable\"}" + + "]"; + assertRejected(postWithAbi(abi), "only one fallback"); + } + + @Test + public void rejectsNonPayableReceive() throws Exception { + String abi = "[{\"type\":\"receive\",\"stateMutability\":\"nonpayable\"}]"; + assertRejected(postWithAbi(abi), "must be payable"); + } + + @Test + public void rejectsFixedFamily() throws Exception { + String abi = "[{\"type\":\"function\",\"name\":\"foo\"," + + "\"inputs\":[{\"name\":\"x\",\"type\":\"fixed128x18\"}],\"outputs\":[]}]"; + assertRejected(postWithAbi(abi), "fixed/ufixed"); + } + + @Test + public void acceptsTuplePermissively() throws Exception { + String abi = "[{\"type\":\"function\",\"name\":\"foo\"," + + "\"inputs\":[{\"name\":\"x\",\"type\":\"tuple\"}," + + "{\"name\":\"y\",\"type\":\"tuple[]\"}],\"outputs\":[]}]"; + MockHttpServletResponse response = postWithAbi(abi); + Assert.assertEquals(200, response.getStatus()); + String body = response.getContentAsString(); + Assert.assertFalse("tuple should not be rejected: " + body, body.contains("\"Error\"")); + } +} diff --git a/framework/src/test/java/org/tron/core/utils/AbiValidatorTest.java b/framework/src/test/java/org/tron/core/utils/AbiValidatorTest.java new file mode 100644 index 00000000000..7ecb31cbebd --- /dev/null +++ b/framework/src/test/java/org/tron/core/utils/AbiValidatorTest.java @@ -0,0 +1,284 @@ +package org.tron.core.utils; + +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +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 class AbiValidatorTest { + + @Test + public void emptyAbiIsAccepted() throws Exception { + AbiValidator.validate(null); + AbiValidator.validate(ABI.getDefaultInstance()); + } + + @Test + public void canonicalAbiIsAccepted() throws Exception { + ABI abi = ABI.newBuilder() + .addEntrys(function("transfer", + param("to", "address"), param("value", "uint256")) + .toBuilder().addOutputs(param("", "bool")).build()) + .addEntrys(event("Transfer", false, + indexedParam("from", "address"), + indexedParam("to", "address"), + param("value", "uint256"))) + .addEntrys(anonymousEvent("", + param("data", "bytes32"))) + .addEntrys(constructor(param("owner", "address"))) + .addEntrys(fallback()) + .addEntrys(receive()) + .addEntrys(function("mix", + param("a", "uint8"), param("b", "int256"), param("c", "bytes1"), + param("d", "bytes32"), param("e", "tuple"), param("f", "address[]"), + param("g", "uint256[3]"), param("h", "trcToken"))) + .build(); + AbiValidator.validate(abi); + } + + @Test + public void shorthandUintIsRejected() { + ABI abi = ABI.newBuilder().addEntrys( + function("foo", param("x", "uint"))).build(); + expect(abi, "shorthand uint/int"); + } + + @Test + public void shorthandIntIsRejected() { + ABI abi = ABI.newBuilder().addEntrys( + function("foo", param("x", "int"))).build(); + expect(abi, "shorthand uint/int"); + } + + @Test + public void invalidIntWidthIsRejected() { + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "uint7"))).build(), "integer width"); + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "uint257"))).build(), "integer width"); + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "uint12"))).build(), "integer width"); + } + + @Test + public void invalidBytesNIsRejected() { + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "bytes0"))).build(), "bytesN size"); + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "bytes33"))).build(), "bytesN size"); + } + + @Test + public void fixedFamilyIsRejected() { + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "fixed"))).build(), "fixed/ufixed"); + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "ufixed"))).build(), "fixed/ufixed"); + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "fixed128x18"))).build(), "fixed/ufixed"); + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "ufixed256x80"))).build(), "fixed/ufixed"); + } + + @Test + public void malformedArrayIsRejected() { + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "uint256[0]"))).build(), "array size must be positive"); + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "address[] storage"))).build(), "malformed array brackets"); + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "uint256[a]"))).build(), "malformed array brackets"); + } + + @Test + public void unknownBaseTypeIsRejected() { + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", "money"))).build(), "unknown base type"); + } + + @Test + public void emptyTypeIsRejected() { + expect(ABI.newBuilder().addEntrys( + function("foo", param("x", ""))).build(), "type must not be empty"); + } + + @Test + public void duplicateConstructorIsRejected() { + ABI abi = ABI.newBuilder() + .addEntrys(constructor()) + .addEntrys(constructor()) + .build(); + expect(abi, "only one constructor"); + } + + @Test + public void duplicateFallbackIsRejected() { + ABI abi = ABI.newBuilder() + .addEntrys(fallback()) + .addEntrys(fallback()) + .build(); + expect(abi, "only one fallback"); + } + + @Test + public void duplicateReceiveIsRejected() { + ABI abi = ABI.newBuilder() + .addEntrys(receive()) + .addEntrys(receive()) + .build(); + expect(abi, "only one receive"); + } + + @Test + public void receiveMustBePayable() { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Receive) + .setStateMutability(StateMutabilityType.Nonpayable)).build(); + expect(abi, "must be payable"); + } + + @Test + public void legacyPayableFlagSatisfiesReceive() throws Exception { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Receive) + .setPayable(true)).build(); + AbiValidator.validate(abi); + } + + @Test + public void fallbackWithIoIsRejected() { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Fallback) + .addInputs(param("x", "uint256"))).build(); + expect(abi, "fallback function must not have inputs or outputs"); + } + + @Test + public void receiveWithIoIsRejected() { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Receive) + .setStateMutability(StateMutabilityType.Payable) + .addOutputs(param("x", "uint256"))).build(); + expect(abi, "receive function must not have inputs or outputs"); + } + + @Test + public void unknownEntryTypeIsRejected() { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.UnknownEntryType)).build(); + expect(abi, "unknown entry type"); + } + + @Test + public void functionMissingNameIsRejected() { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Function)).build(); + expect(abi, "function must have a name"); + } + + @Test + public void nonAnonymousEventMissingNameIsRejected() { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Event)).build(); + expect(abi, "non-anonymous event must have a name"); + } + + @Test + public void anonymousEventMayOmitName() throws Exception { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Event) + .setAnonymous(true)).build(); + AbiValidator.validate(abi); + } + + @Test + public void errorMissingNameIsRejected() { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Error)).build(); + expect(abi, "error must have a name"); + } + + @Test + public void namedErrorIsAccepted() throws Exception { + ABI abi = ABI.newBuilder().addEntrys(Entry.newBuilder() + .setType(EntryType.Error) + .setName("InsufficientBalance") + .addInputs(param("required", "uint256"))).build(); + AbiValidator.validate(abi); + } + + @Test + public void errorMessageCarriesFieldPath() { + ABI abi = ABI.newBuilder() + .addEntrys(function("a", param("x", "address"))) + .addEntrys(function("b", param("x", "address"), param("y", "uint"))) + .build(); + ContractValidateException e = assertThrows(ContractValidateException.class, + () -> AbiValidator.validate(abi)); + String msg = e.getMessage(); + org.junit.Assert.assertTrue(msg, msg.contains("entry #1")); + org.junit.Assert.assertTrue(msg, msg.contains("inputs[1]")); + org.junit.Assert.assertTrue(msg, msg.contains("'uint'")); + } + + // helpers + + private static void expect(ABI abi, String snippet) { + ContractValidateException e = assertThrows(ContractValidateException.class, + () -> AbiValidator.validate(abi)); + org.junit.Assert.assertTrue( + "expected message to contain '" + snippet + "', got: " + e.getMessage(), + e.getMessage().contains(snippet)); + } + + private static Param param(String name, String type) { + return Param.newBuilder().setName(name).setType(type).build(); + } + + private static Param indexedParam(String name, String type) { + return Param.newBuilder().setName(name).setType(type).setIndexed(true).build(); + } + + private static Entry function(String name, Param... inputs) { + Entry.Builder b = Entry.newBuilder().setType(EntryType.Function).setName(name); + for (Param p : inputs) { + b.addInputs(p); + } + return b.build(); + } + + private static Entry event(String name, boolean anonymous, Param... inputs) { + Entry.Builder b = Entry.newBuilder().setType(EntryType.Event).setName(name) + .setAnonymous(anonymous); + for (Param p : inputs) { + b.addInputs(p); + } + return b.build(); + } + + private static Entry anonymousEvent(String name, Param... inputs) { + return event(name, true, inputs); + } + + private static Entry constructor(Param... inputs) { + Entry.Builder b = Entry.newBuilder().setType(EntryType.Constructor); + for (Param p : inputs) { + b.addInputs(p); + } + return b.build(); + } + + private static Entry fallback() { + return Entry.newBuilder().setType(EntryType.Fallback).build(); + } + + private static Entry receive() { + return Entry.newBuilder().setType(EntryType.Receive) + .setStateMutability(StateMutabilityType.Payable).build(); + } +}