Repository: protofire/solhint
Pull request #541 (2024-01-09 20:32:26)

GC: mutlitoken1155 rule
Search the imports to find an ERC721 contract and suggest using 1155


Patch:
diff --git a/conf/rulesets/solhint-all.js b/conf/rulesets/solhint-all.js
index 8da5af3..f3b9577 100644
--- a/conf/rulesets/solhint-all.js
+++ b/conf/rulesets/solhint-all.js
@@ -25,6 +25,7 @@ module.exports = Object.freeze({
       },
     ],
     'constructor-syntax': 'warn',
+    'gas-multitoken1155': 'warn',
     'comprehensive-interface': 'warn',
     quotes: ['error', 'double'],
     'const-name-snakecase': 'warn',
diff --git a/docs/rules.md b/docs/rules.md
index 4bded6c..707f45f 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -25,6 +25,13 @@ title:       "Rule Index of Solhint"
 | [constructor-syntax](./rules/best-practises/constructor-syntax.md)       | Constructors should use the new constructor keyword.                                                                                       |              |            |
         
 
+## Gas Consumption Rules
+
+| Rule Id                                                             | Error                                               | Recommended | Deprecated |
+| ------------------------------------------------------------------- | --------------------------------------------------- | ----------- | ---------- |
+| [gas-multitoken1155](./rules/gas-consumption/gas-multitoken1155.md) | ERC1155 is a cheaper non-fungible token than ERC721 |             |            |
+        
+
 ## Miscellaneous
 
 | Rule Id                                                                     | Error                                                                                                                                  | Recommended  | Deprecated |
diff --git a/docs/rules/gas-consumption/gas-multitoken1155.md b/docs/rules/gas-consumption/gas-multitoken1155.md
new file mode 100644
index 0000000..7b0b6b9
--- /dev/null
+++ b/docs/rules/gas-consumption/gas-multitoken1155.md
@@ -0,0 +1,38 @@
+---
+warning:     "This is a dynamically generated file. Do not edit manually."
+layout:      "default"
+title:       "gas-multitoken1155 | Solhint"
+---
+
+# gas-multitoken1155
+![Category Badge](https://img.shields.io/badge/-Gas%20Consumption%20Rules-informational)
+![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow)
+
+## Description
+ERC1155 is a cheaper non-fungible token than ERC721
+
+## Options
+This rule accepts a string option of rule severity. Must be one of "error", "warn", "off". Default to warn.
+
+### Example Config
+```json
+{
+  "rules": {
+    "gas-multitoken1155": "warn"
+  }
+}
+```
+
+### Notes
+- [source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b&utm_campaign=42ccb5d8-c2cc-4416-b661-8eec8368f72b&utm_source=so&utm_medium=mail&utm_content=40a3d3be-d07d-479e-af1d-6b2ef1b950da&cid=9619984a-b43c-4002-ba71-820fd72bb83a#viewer-8v8t9) of the rule initiciative
+
+## Examples
+This rule does not have examples.
+
+## Version
+This rule is introduced in the latest version.
+
+## Resources
+- [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/gas-consumption/gas-multitoken1155.js)
+- [Document source](https://github.com/protofire/solhint/tree/master/docs/rules/gas-consumption/gas-multitoken1155.md)
+- [Test cases](https://github.com/protofire/solhint/tree/master/test/rules/gas-consumption/gas-multitoken1155.js)
diff --git a/lib/rules/gas-consumption/gas-multitoken1155.js b/lib/rules/gas-consumption/gas-multitoken1155.js
new file mode 100644
index 0000000..bd1410b
--- /dev/null
+++ b/lib/rules/gas-consumption/gas-multitoken1155.js
@@ -0,0 +1,45 @@
+const BaseChecker = require('../base-checker')
+
+const ruleId = 'gas-multitoken1155'
+const meta = {
+  type: 'gas-consumption',
+
+  docs: {
+    description: 'ERC1155 is a cheaper non-fungible token than ERC721',
+    category: 'Gas Consumption Rules',
+    notes: [
+      {
+        note: '[source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b&utm_campaign=42ccb5d8-c2cc-4416-b661-8eec8368f72b&utm_source=so&utm_medium=mail&utm_content=40a3d3be-d07d-479e-af1d-6b2ef1b950da&cid=9619984a-b43c-4002-ba71-820fd72bb83a#viewer-8v8t9) of the rule initiciative',
+      },
+    ],
+  },
+
+  isDefault: false,
+  recommended: false,
+  defaultSetup: 'warn',
+
+  schema: null,
+}
+
+class GasMultitoken1155 extends BaseChecker {
+  constructor(reporter) {
+    super(reporter, ruleId, meta)
+  }
+
+  hasERC721String(path) {
+    // Convert both the input string and the substring to lowercase for case-insensitive comparison
+    const lowercaseInput = path.toLowerCase()
+    const lowercaseSubstring = 'erc721'
+
+    // Check if the lowercase input string contains the lowercase substring
+    return lowercaseInput.includes(lowercaseSubstring)
+  }
+
+  ImportDirective(node) {
+    if (this.hasERC721String(node.path)) {
+      this.error(node, 'GC: ERC721 import found - Use of ERC1155 recommended')
+    }
+  }
+}
+
+module.exports = GasMultitoken1155
diff --git a/lib/rules/gas-consumption/index.js b/lib/rules/gas-consumption/index.js
new file mode 100644
index 0000000..c117a72
--- /dev/null
+++ b/lib/rules/gas-consumption/index.js
@@ -0,0 +1,6 @@
+const GasMultitoken1155 = require('./gas-multitoken1155')
+
+// module.exports = function checkers(reporter, config, tokens) {
+module.exports = function checkers(reporter, config) {
+  return [new GasMultitoken1155(reporter, config)]
+}
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 7e8a3b5..c423db5 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -6,6 +6,7 @@ const order = require('./order/index')
 const bestPractises = require('./best-practises/index')
 const deprecations = require('./deprecations/index')
 const miscellaneous = require('./miscellaneous/index')
+const gasConsumption = require('./gas-consumption/index')
 const configObject = require('../config')
 const { validSeverityMap } = require('../config/config-validator')
 
@@ -64,6 +65,7 @@ function coreRules(meta) {
     ...naming(reporter, config),
     ...order(reporter, tokens),
     ...security(reporter, config, inputSrc),
+    ...gasConsumption(reporter, config),
   ]
 }
 
diff --git a/package.json b/package.json
index faa8a81..2864a4b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "solhint",
-  "version": "4.1.1",
+  "version": "4.2.0",
   "description": "Solidity Code Linter",
   "main": "lib/index.js",
   "keywords": [
@@ -18,7 +18,7 @@
   "scripts": {
     "exec-tests": "nyc mocha --recursive && nyc report --reporter=text-lcov | coveralls",
     "test:coverage": "npm run exec-tests",
-    "test": "mocha --recursive --timeout 10000",
+    "test": "mocha --recursive",
     "lint": "eslint .",
     "generate-rulesets": "node scripts/generate-rulesets.js && prettier --write conf/rulesets",
     "docs": "node scripts/generate-rule-docs.js",


Test patch:
diff --git a/e2e/package.json b/e2e/package.json
index 282f9ed..41185fa 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -4,7 +4,7 @@
   "description": "E2E tests for solhint",
   "main": "index.js",
   "scripts": {
-    "test": "mocha test.js formatters-test.js autofix-test.js"
+    "test": "mocha test.js formatters-test.js autofix-test.js --timeout 10000"
   },
   "author": "",
   "license": "MIT",
diff --git a/test/rules/gas-consumption/gas-multitoken1155.js b/test/rules/gas-consumption/gas-multitoken1155.js
new file mode 100644
index 0000000..28597bc
--- /dev/null
+++ b/test/rules/gas-consumption/gas-multitoken1155.js
@@ -0,0 +1,63 @@
+const assert = require('assert')
+const linter = require('../../../lib/index')
+
+const ERROR_MSG = 'GC: ERC721 import found - Use of ERC1155 recommended'
+
+describe('Linter - gas-multitoken1155', () => {
+  it('should raise error', () => {
+    const code = `
+    // SPDX-License-Identifier: Apache-2.0
+    pragma solidity ^0.8.4;
+
+    import '@openzeppelin/contracts/token/ERC722/ERC722.sol';
+    import '@openzeppelin/contracts/token/erc721.sol';
+    import '@openzeppelin/contracts/token/ERC721.sol';
+
+    contract A {}
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'gas-multitoken1155': 'error' },
+    })
+
+    assert.equal(report.errorCount, 2)
+    assert.equal(report.messages[0].message, ERROR_MSG)
+    assert.equal(report.messages[1].message, ERROR_MSG)
+  })
+
+  it('should raise error', () => {
+    const code = `
+    // SPDX-License-Identifier: Apache-2.0
+    pragma solidity ^0.8.4;
+
+    import '@openzeppelin/contracts/token/ERC721/ERC722.sol';
+
+    contract A {}
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'gas-multitoken1155': 'error' },
+    })
+
+    assert.equal(report.errorCount, 1)
+    assert.equal(report.messages[0].message, ERROR_MSG)
+  })
+
+  it('should NOT raise error', () => {
+    const code = `
+    // SPDX-License-Identifier: Apache-2.0
+    pragma solidity ^0.8.4;
+
+    import '@openzeppelin/contracts/token/ERC722/ERC722.sol';
+    import '@openzeppelin/contracts/token/erc20.sol';
+    
+    contract A {}
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'gas-multitoken1155': 'error' },
+    })
+
+    assert.equal(report.errorCount, 0)
+  })
+})