Repository: Synthetixio/synthetix-v3
Pull request #285 (2021-11-01 20:09:56)

Implement  BeaconModule and associated storage


Patch:
diff --git a/package-lock.json b/package-lock.json
index 3bc861cd..6974d90d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13543,21 +13543,6 @@
         "node": ">=4"
       }
     },
-    "packages/core-modules/node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "1.1.3"
-      }
-    },
-    "packages/core-modules/node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-      "dev": true
-    },
     "packages/core-modules/node_modules/escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -13937,12 +13922,6 @@
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
       "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
       "dev": true,
-      "requires": {
-        "@babel/compat-data": "^7.15.0",
-        "@babel/helper-validator-option": "^7.14.5",
-        "browserslist": "^4.16.6",
-        "semver": "^6.3.0"
-      },
       "dependencies": {
         "yocto-queue": "^0.1.0"
       },
@@ -14024,7 +14003,8 @@
       "license": "MIT",
       "devDependencies": {
         "@nomiclabs/hardhat-ethers": "2.0.2",
-        "hardhat": "2.6.6"
+        "hardhat": "2.6.6",
+        "solidity-coverage": "0.7.17"
       }
     },
     "packages/synthetix-main/node_modules/@nomiclabs/hardhat-ethers": {
@@ -15431,7 +15411,12 @@
           "version": "5.0.8",
           "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
           "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
-          "dev": true
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
         },
         "yargs": {
           "version": "16.2.0",
@@ -15489,7 +15474,7 @@
           "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
           "dev": true,
           "requires": {
-            "color-convert": "^1.9.0"
+            "has-flag": "^3.0.0"
           }
         },
         "chalk": {
@@ -15503,21 +15488,6 @@
             "supports-color": "^5.3.0"
           }
         },
-        "color-convert": {
-          "version": "1.9.3",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-          "dev": true,
-          "requires": {
-            "color-name": "1.1.3"
-          }
-        },
-        "color-name": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-          "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-          "dev": true
-        },
         "escape-string-regexp": {
           "version": "1.0.5",
           "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -15851,7 +15821,8 @@
       "version": "file:packages/synthetix-main",
       "requires": {
         "@nomiclabs/hardhat-ethers": "2.0.2",
-        "hardhat": "2.6.6"
+        "hardhat": "2.6.6",
+        "solidity-coverage": "0.7.17"
       },
       "dependencies": {
         "@nomiclabs/hardhat-ethers": {
diff --git a/packages/core-contracts/contracts/mocks/proxy/BeaconMock.sol b/packages/core-contracts/contracts/mocks/proxy/BeaconMock.sol
index 85681264..25db241a 100644
--- a/packages/core-contracts/contracts/mocks/proxy/BeaconMock.sol
+++ b/packages/core-contracts/contracts/mocks/proxy/BeaconMock.sol
@@ -24,7 +24,7 @@ contract BeaconMock is Beacon {
         _owner = firstOwner;
     }
 
-    function upgradeTo(address newImplementation) external override onlyOwner {
+    function upgradeTo(address newImplementation) external {
         _upgradeTo(newImplementation);
     }
 
@@ -35,8 +35,4 @@ contract BeaconMock is Beacon {
     function _getImplementation() internal view override returns (address) {
         return _implementation;
     }
-
-    function _getOwner() internal view override returns (address) {
-        return _owner;
-    }
 }
diff --git a/packages/core-contracts/contracts/proxy/Beacon.sol b/packages/core-contracts/contracts/proxy/Beacon.sol
index 753f07f9..8320fbcb 100644
--- a/packages/core-contracts/contracts/proxy/Beacon.sol
+++ b/packages/core-contracts/contracts/proxy/Beacon.sol
@@ -3,18 +3,15 @@ pragma solidity ^0.8.0;
 
 import "../common/CommonErrors.sol";
 import "../interfaces/IBeacon.sol";
-import "../ownership/OwnableMixin.sol";
 import "../utils/ContractUtil.sol";
 
-abstract contract Beacon is IBeacon, OwnableMixin, ContractUtil, CommonErrors {
+abstract contract Beacon is IBeacon, ContractUtil, CommonErrors {
     event Upgraded(address indexed implementation);
 
     function getImplementation() external view override returns (address) {
         return _getImplementation();
     }
 
-    function upgradeTo(address newImplementation) external virtual;
-
     function _upgradeTo(address newImplementation) internal {
         if (newImplementation == address(0)) {
             revert InvalidAddress(newImplementation);
diff --git a/packages/core-js/utils/logger.js b/packages/core-js/utils/logger.js
index 87449cc9..58a8acad 100644
--- a/packages/core-js/utils/logger.js
+++ b/packages/core-js/utils/logger.js
@@ -3,6 +3,7 @@ const chalk = require('chalk');
 const BOX_WIDTH = 90;
 
 module.exports = {
+  _log: console.log.bind(console),
   quiet: false,
   debugging: false,
   prepend: '',
@@ -33,7 +34,7 @@ module.exports = {
     const completeLen = Math.max(BOX_WIDTH + 8 - [...msg].length, 0);
     const completeStr = this.boxing ? chalk.gray('.'.repeat(completeLen)) : '';
 
-    console.log(`${this.prepend}${msg}${completeStr}${this.postpend}`);
+    this._log(`${this.prepend}${msg}${completeStr}${this.postpend}`);
   },
 
   subtitle(msg) {
@@ -41,7 +42,7 @@ module.exports = {
       return;
     }
 
-    console.log('\n');
+    this._log('\n');
 
     this.boxStart();
     this.log(chalk.cyan(`‣ ${msg}`));
diff --git a/packages/core-js/utils/prompter.js b/packages/core-js/utils/prompter.js
index e8155685..f52436e6 100644
--- a/packages/core-js/utils/prompter.js
+++ b/packages/core-js/utils/prompter.js
@@ -1,6 +1,7 @@
 const inquirer = require('inquirer');
 
 module.exports = {
+  _prompt: inquirer.prompt.bind(inquirer),
   noConfirm: false,
 
   /**
@@ -13,7 +14,7 @@ module.exports = {
       return true;
     }
 
-    const { confirmation } = await inquirer.prompt([
+    const { confirmation } = await this._prompt([
       {
         type: 'confirm',
         name: 'confirmation',
diff --git a/packages/core-modules/contracts/BaseProxy.sol b/packages/core-modules/contracts/BaseProxy.sol
index 0dd46e0f..96f617e0 100644
--- a/packages/core-modules/contracts/BaseProxy.sol
+++ b/packages/core-modules/contracts/BaseProxy.sol
@@ -1,4 +1,4 @@
-//SPDX-License-Identifier: Unlicense
+//SPDX-License-Identifier: MIT
 pragma solidity ^0.8.0;
 
 import "@synthetixio/core-contracts/contracts/proxy/ForwardingProxy.sol";
diff --git a/packages/core-modules/contracts/mocks/BaseProxyMock.sol b/packages/core-modules/contracts/mocks/BaseProxyMock.sol
index e214d259..cd49fa08 100644
--- a/packages/core-modules/contracts/mocks/BaseProxyMock.sol
+++ b/packages/core-modules/contracts/mocks/BaseProxyMock.sol
@@ -1,4 +1,4 @@
-//SPDX-License-Identifier: Unlicense
+//SPDX-License-Identifier: MIT
 pragma solidity ^0.8.0;
 
 import "../BaseProxy.sol";
diff --git a/packages/core-modules/contracts/mocks/modules/BeaconModuleMock.sol b/packages/core-modules/contracts/mocks/modules/BeaconModuleMock.sol
new file mode 100644
index 00000000..e66c901c
--- /dev/null
+++ b/packages/core-modules/contracts/mocks/modules/BeaconModuleMock.sol
@@ -0,0 +1,10 @@
+//SPDX-License-Identifier: Unlicense
+pragma solidity ^0.8.0;
+
+import "../../modules/BeaconModule.sol";
+
+contract BeaconModuleMock is BeaconModule {
+    function setImplementation(address newImplementation) public {
+        _setImplementation(newImplementation);
+    }
+}
diff --git a/packages/core-modules/contracts/modules/BeaconModule.sol b/packages/core-modules/contracts/modules/BeaconModule.sol
new file mode 100644
index 00000000..9c813dae
--- /dev/null
+++ b/packages/core-modules/contracts/modules/BeaconModule.sol
@@ -0,0 +1,15 @@
+//SPDX-License-Identifier: Unlicense
+pragma solidity ^0.8.0;
+
+import "../storage/BeaconStorage.sol";
+import "@synthetixio/core-contracts/contracts/proxy/Beacon.sol";
+
+contract BeaconModule is Beacon, BeaconStorage {
+    function _setImplementation(address newImplementation) internal override {
+        _beaconStorage().implementation = newImplementation;
+    }
+
+    function _getImplementation() internal view override returns (address) {
+        return _beaconStorage().implementation;
+    }
+}
diff --git a/packages/core-modules/contracts/storage/BeaconStorage.sol b/packages/core-modules/contracts/storage/BeaconStorage.sol
new file mode 100644
index 00000000..2493e32f
--- /dev/null
+++ b/packages/core-modules/contracts/storage/BeaconStorage.sol
@@ -0,0 +1,15 @@
+//SPDX-License-Identifier: Unlicense
+pragma solidity ^0.8.0;
+
+contract BeaconStorage {
+    struct BeaconNamespace {
+        address implementation;
+    }
+
+    function _beaconStorage() internal pure returns (BeaconNamespace storage store) {
+        assembly {
+            // bytes32(uint(keccak256("io.synthetix.beacon")) - 1)
+            store.slot := 0x8517a71f7c502435ad2f4c47d666cac507d0f6ec211fa4218a800991aa5164cc
+        }
+    }
+}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d728dd2a..d428ed16 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -37,3 +37,18 @@ jobs:
         with:
           files: packages/synthetix-main/coverage.json
           flags: synthetix-main
+
+  test-sample:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-node@v2
+        with:
+          node-version: "16.8.0"
+          cache: "npm"
+      - run: npm ci
+      - name: Install dependencies & execute tests
+        working-directory: ./packages/deployer/test/fixture-projects/sample-project
+        run: |
+          npm ci
+          npm test
diff --git a/packages/core-contracts/test/proxy/Beacon.test.js b/packages/core-contracts/test/proxy/Beacon.test.js
index 3c0ae523..534734b1 100644
--- a/packages/core-contracts/test/proxy/Beacon.test.js
+++ b/packages/core-contracts/test/proxy/Beacon.test.js
@@ -31,14 +31,6 @@ describe('Beacon', () => {
     describe('when upgrading to a new implementation', async () => {
       let receipt;
 
-      describe('when a non-owner invokes it', async () => {
-        it('reverts', async () => {
-          await assertRevert(
-            Beacon.connect(user).upgradeTo(ERC20Mock2.address),
-            'OnlyOwnerAllowed()'
-          );
-        });
-      });
       describe('when the owner invokes it', async () => {
         describe('when upgrading to an invalid implementation', () => {
           it('reverts when attempting to upgrade to an EOA', async () => {
diff --git a/packages/core-js/test/utils/hardhat.test.js b/packages/core-js/test/utils/hardhat.test.js
new file mode 100644
index 00000000..f47ecfb5
--- /dev/null
+++ b/packages/core-js/test/utils/hardhat.test.js
@@ -0,0 +1,40 @@
+const { deepEqual } = require('assert/strict');
+const { getAllContractASTs } = require('../../utils/hardhat');
+
+describe('utils/hardhat.js', () => {
+  it('should get artifacts ASTs', async () => {
+    const qualifiedNames = [
+      'contracts/modules/OwnerModule.sol:OwnerModule',
+      'contracts/modules/SomeModule.sol:SomeModule',
+    ];
+
+    const output = {
+      sources: {
+        'contracts/modules/OwnerModule.sol': {
+          OwnerModule: {
+            ast: { some: 'ast' },
+          },
+        },
+        'contracts/modules/SomeModule.sol': {
+          SomeModule: {
+            ast: { another: 'ast' },
+          },
+        },
+      },
+    };
+
+    const hreMock = {
+      artifacts: {
+        getAllFullyQualifiedNames: async () => qualifiedNames,
+        getBuildInfo: async () => ({ output }),
+      },
+    };
+
+    const result = await getAllContractASTs(hreMock);
+
+    deepEqual(result, {
+      OwnerModule: { some: 'ast' },
+      SomeModule: { another: 'ast' },
+    });
+  });
+});
diff --git a/packages/core-js/test/utils/logger.test.js b/packages/core-js/test/utils/logger.test.js
index d908799a..3c41258f 100644
--- a/packages/core-js/test/utils/logger.test.js
+++ b/packages/core-js/test/utils/logger.test.js
@@ -1,37 +1,100 @@
-const assert = require('assert/strict');
+const { equal, deepEqual } = require('assert/strict');
+const chalk = require('chalk');
 const logger = require('../../utils/logger');
 
 describe('utils/logger.js', () => {
-  let logCache;
-  let logged = '';
+  let logged = [];
 
-  function _log(msg) {
-    logged += msg;
-  }
+  before('disable chalk color usage', () => {
+    chalk.level = 0;
+  });
 
-  before('capture output', async () => {
-    logCache = console.log;
-    console.log = _log;
+  before('capture output', () => {
+    logger._log = (...args) => {
+      logged.push(args);
+    };
   });
 
-  after('release output', async () => {
-    console.log = logCache;
+  beforeEach('reset captured output', () => {
+    logged = [];
   });
 
-  it('starts with the correct default props', async () => {
-    assert.equal(logger.quiet, false);
-    assert.equal(logger.debugging, false);
-    assert.equal(logger.prepend, '');
-    assert.equal(logger.postpend, '');
-    assert.equal(logger.boxing, false);
+  it('starts with the correct default props', () => {
+    equal(logger.quiet, false);
+    equal(logger.debugging, false);
+    equal(logger.prepend, '');
+    equal(logger.postpend, '');
+    equal(logger.boxing, false);
+  });
+
+  describe('when quiet is not enabled', () => {
+    before('disable quiet', () => {
+      logger.quiet = false;
+    });
+
+    it('prints logs to the console', () => {
+      logger.log('hello');
+      logger.info('hello');
+      logger.notice('hello');
+      logger.error('hello');
+      logger.warn('hello');
+      logger.checked('hello');
+      logger.success('hello');
+      logger.complete('hello');
+      logger.debug('hello');
+      logger.subtitle('hello');
+      logger.boxStart();
+      logger.log('boxed hello');
+      logger.boxEnd();
+
+      deepEqual(logged, [
+        ['hello'],
+        ['ⓘ  hello'],
+        ['> hello'],
+        ['☠ hello'],
+        ['⚠ hello'],
+        ['✓ hello'],
+        ['✅ hello'],
+        ['💯 hello'],
+        ['\n'],
+        [
+          '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓',
+        ],
+        [
+          '┃ ‣ hello........................................................................................... ┃',
+        ],
+        [
+          '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛',
+        ],
+        [
+          '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓',
+        ],
+        [
+          '┃ boxed hello....................................................................................... ┃',
+        ],
+        [
+          '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛',
+        ],
+      ]);
+    });
+
+    it('should log when debugging is enabled', () => {
+      logger.debugging = true;
+      try {
+        logger.debug('debugging!');
+        deepEqual(logged, [['debugging!']]);
+      } finally {
+        logger.debugging = false;
+      }
+    });
   });
 
   describe('when quiet is enabled', () => {
-    before('enable quiet', async () => {
+    before('enable quiet', () => {
       logger.quiet = true;
     });
 
-    before('perform some logs', async () => {
+    it('does not record any logs', () => {
       logger.log('hello');
       logger.info('hello');
       logger.notice('hello');
@@ -41,10 +104,12 @@ describe('utils/logger.js', () => {
       logger.success('hello');
       logger.complete('hello');
       logger.debug('hello');
-    });
+      logger.subtitle('hello');
+      logger.boxStart();
+      logger.log('boxed hello');
+      logger.boxEnd();
 
-    it('does not record any logs', async () => {
-      assert.equal(logged.length, 0);
+      equal(logged.length, 0);
     });
   });
 });
diff --git a/packages/core-js/test/utils/prompter.test.js b/packages/core-js/test/utils/prompter.test.js
new file mode 100644
index 00000000..3f3ea90a
--- /dev/null
+++ b/packages/core-js/test/utils/prompter.test.js
@@ -0,0 +1,61 @@
+const { equal } = require('assert/strict');
+const prompter = require('../../utils/prompter');
+
+describe('utils/prompter.js', function () {
+  describe('when noConfirm is enabled', function () {
+    before('enable noCofirm', function () {
+      prompter.noConfirm = true;
+    });
+
+    describe('#ask', function () {
+      it('returns always true', async function () {
+        const result = await prompter.ask('something?');
+        equal(result, true);
+      });
+    });
+
+    describe('#confirmAction', function () {
+      it('does not halt execution', async function () {
+        await prompter.confirmAction('something?');
+      });
+    });
+  });
+
+  describe('when noConfirm is disabled', function () {
+    before('enable noCofirm', function () {
+      prompter.noConfirm = false;
+    });
+
+    describe('#ask', function () {
+      it('returns always the users response', async function () {
+        prompter._prompt = async () => ({ confirmation: true });
+        equal(await prompter.ask('something?'), true);
+
+        prompter._prompt = async () => ({ confirmation: false });
+        equal(await prompter.ask('another something?'), false);
+      });
+    });
+
+    describe('#confirmAction', function () {
+      it('does not halt execution when responding positively', async function () {
+        prompter._prompt = async () => ({ confirmation: true });
+        await prompter.confirmAction('something?');
+      });
+
+      it('halts execution when responding negatively', async function () {
+        prompter._prompt = async () => ({ confirmation: false });
+
+        let exited = false;
+        const exitCache = process.exit;
+        process.exit = () => (exited = true);
+
+        try {
+          await prompter.confirmAction('something?');
+          equal(exited, true);
+        } finally {
+          process.exit = exitCache;
+        }
+      });
+    });
+  });
+});
diff --git a/packages/core-js/test/utils/rpc.test.js b/packages/core-js/test/utils/rpc.test.js
index 2831a464..07565a1e 100644
--- a/packages/core-js/test/utils/rpc.test.js
+++ b/packages/core-js/test/utils/rpc.test.js
@@ -21,7 +21,6 @@ describe('utils/rpc.js', () => {
     let snapshotId;
 
     before('call createSnapshot', async () => {
-      console.log(provider);
       snapshotId = await takeSnapshot(provider);
     });
 
diff --git a/packages/core-modules/test/modules/BeaconModule.test.js b/packages/core-modules/test/modules/BeaconModule.test.js
new file mode 100644
index 00000000..98666c9c
--- /dev/null
+++ b/packages/core-modules/test/modules/BeaconModule.test.js
@@ -0,0 +1,25 @@
+const hre = require('hardhat');
+const assert = require('assert');
+
+const { ethers } = hre;
+
+describe('BeaconModule', () => {
+  let BeackonModule, BeackonModuleFactory, DummyMockFactory;
+
+  before('load factory', async () => {
+    BeackonModuleFactory = await ethers.getContractFactory('BeaconModuleMock');
+    DummyMockFactory = await ethers.getContractFactory('DummyMock');
+  });
+
+  beforeEach('deploy module', async () => {
+    BeackonModule = await BeackonModuleFactory.deploy();
+  });
+
+  describe('when updating the implementation', () => {
+    it('correctly updates the beacon storage', async () => {
+      const NewImplementation = await DummyMockFactory.deploy();
+      await BeackonModule.setImplementation(NewImplementation.address).then((tx) => tx.wait());
+      assert.equal(await BeackonModule.getImplementation(), NewImplementation.address);
+    });
+  });
+});